    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package com.orsoncharts.data;
    034
    035import java.io.IOException;
    036import java.io.StringWriter;
    037import java.io.Writer;
    038import java.io.Reader;
    039import java.io.StringReader;
    040import java.util.ArrayList;
    041import java.util.LinkedHashMap;
    042import java.util.List;
    043import java.util.Map;
    044import com.orsoncharts.data.category.StandardCategoryDataset3D;
    045import com.orsoncharts.util.json.JSONValue;
    046import com.orsoncharts.util.json.parser.JSONParser;
    047import com.orsoncharts.util.json.parser.ParseException;
    048import com.orsoncharts.util.ArgChecks;
    049import com.orsoncharts.util.json.parser.ContainerFactory;
    050import com.orsoncharts.data.xyz.XYZDataset;
    051import com.orsoncharts.data.xyz.XYZSeries;
    052import com.orsoncharts.data.xyz.XYZSeriesCollection;
    053
    054/**
    055 * Utility methods for interchange between datasets ({@link KeyedValues},
    056 * {@link KeyedValues3D} and {@link XYZDataset}) and JSON format strings.
    057 *
    058 * @since 1.3
    059 */
    060public class JSONUtils {
    061
    062    /**
    063     * Parses the supplied JSON string into a {@link KeyedValues} instance.
    064     * <br><br>
    065     * Implementation note:  this method returns an instance of
    066     * {@link StandardPieDataset3D}).
    067     *
    068     * @param json  the JSON string ({@code null} not permitted).
    069     *
    070     * @return A {@link KeyedValues} instance.
    071     */
    072    public static KeyedValues<? extends Number> readKeyedValues(String json) {
    073        ArgChecks.nullNotPermitted(json, "json");
    074        StringReader in = new StringReader(json);
    075        KeyedValues<? extends Number> result;
    076        try {
    077            result = readKeyedValues(in);
    078        } catch (IOException ex) {
    079            // not for StringReader
    080            result = null;
    081        }
    082        return result;
    083    }
    084
    085    /**
    086     * Parses characters from the supplied reader and returns the corresponding
    087     * {@link KeyedValues} instance.
    088     * <br><br>
    089     * Implementation note:  this method returns an instance of
    090     * {@link StandardPieDataset3D}).
    091     *
    092     * @param reader  the reader ({@code null} not permitted).
    093     *
    094     * @return A {@code KeyedValues} instance.
    095     *
    096     * @throws IOException if there is an I/O problem.
    097     */
    098    public static KeyedValues<? extends Number> readKeyedValues(Reader reader)
    099            throws IOException {
    100        ArgChecks.nullNotPermitted(reader, "reader");
    101        try {
    102            JSONParser parser = new JSONParser();
    103            // parse with custom containers (to preserve item order)
    104            List list = (List) parser.parse(reader, createContainerFactory());
    105            StandardPieDataset3D result = new StandardPieDataset3D();
    106            for (Object item : list) {
    107                List itemAsList = (List) item;
    108                result.add((Comparable) itemAsList.get(0),
    109                        (Number) itemAsList.get(1));
    110            }
    111            return result;
    112        } catch (ParseException ex) {
    113            throw new RuntimeException(ex);
    114        }
    115    }
    116
    117    /**
    118     * Returns a string containing the data in JSON format.  The format is
    119     * an array of arrays, where each sub-array represents one data value.
    120     * The sub-array should contain two items, first the item key as a string
    121     * and second the item value as a number.  For example:
    122     * {@code [["Key A", 1.0], ["Key B", 2.0]]}
    123     * <br><br>
    124     * Note that this method can be used with instances of {@link PieDataset3D}.
    125     *
    126     * @param data  the data ({@code null} not permitted).
    127     *
    128     * @return A string in JSON format.
    129     */
    130    public static String writeKeyedValues(KeyedValues<? extends Number> data) {
    131        ArgChecks.nullNotPermitted(data, "data");
    132        StringWriter sw = new StringWriter();
    133        try {
    134            writeKeyedValues(data, sw);
    135        } catch (IOException ex) {
    136            throw new RuntimeException(ex);
    137        }
    138        return sw.toString();
    139    }
    140
    141    /**
    142     * Writes the data in JSON format to the supplied writer.
    143     * <br><br>
    144     * Note that this method can be used with instances of {@link PieDataset3D}.
    145     *
    146     * @param data  the data ({@code null} not permitted).
    147     * @param writer  the writer ({@code null} not permitted).
    148     *
    149     * @throws IOException if there is an I/O problem.
    150     */
    151    public static void writeKeyedValues(KeyedValues<? extends Number> data,
    152            Writer writer) throws IOException {
    153        ArgChecks.nullNotPermitted(data, "data");
    154        ArgChecks.nullNotPermitted(writer, "writer");
    155        writer.write("[");
    156        boolean first = true;
    157        for (Comparable<?> key : data.getKeys()) {
    158            if (!first) {
    159                writer.write(", ");
    160            } else {
    161                first = false;
    162            }
    163            writer.write("[");
    164            writer.write(JSONValue.toJSONString(key.toString()));
    165            writer.write(", ");
    166            writer.write(JSONValue.toJSONString(data.getValue(key)));
    167            writer.write("]");
    168        }
    169        writer.write("]");
    170    }
    171
    172    /**
    173     * Reads a data table from a JSON format string.
    174     *
    175     * @param json  the string ({@code null} not permitted).
    176     *
    177     * @return A data table.
    178     */
    179    public static KeyedValues2D<? extends Number> readKeyedValues2D(
    180            String json) {
    181        ArgChecks.nullNotPermitted(json, "json");
    182        StringReader in = new StringReader(json);
    183        KeyedValues2D<? extends Number> result;
    184        try {
    185            result = readKeyedValues2D(in);
    186        } catch (IOException ex) {
    187            // not for StringReader
    188            result = null;
    189        }
    190        return result;
    191    }
    192
    193    /**
    194     * Reads a data table from a JSON format string coming from the specified
    195     * reader.
    196     *
    197     * @param reader  the reader ({@code null} not permitted).
    198     *
    199     * @return A data table.
    200     *
    201     * @throws java.io.IOException if there is an I/O problem.
    202     */
    203    public static KeyedValues2D<? extends Number> readKeyedValues2D(
    204            Reader reader) throws IOException {
    205
    206        JSONParser parser = new JSONParser();
    207        try {
    208            Map map = (Map) parser.parse(reader, createContainerFactory());
    209            DefaultKeyedValues2D result = new DefaultKeyedValues2D();
    210            if (map.isEmpty()) {
    211                return result;
    212            }
    213
    214            // read the keys
    215            Object keysObj = map.get("columnKeys");
    216            List<?> keys = null;
    217            if (keysObj instanceof List<?>) {
    218                keys = (List<?>) keysObj;
    219            } else {
    220                if (keysObj == null) {
    221                    throw new RuntimeException("No 'columnKeys' defined.");
    222                } else {
    223                    throw new RuntimeException("Please check the 'columnKeys', "
    224                            + "the format does not parse to a list.");
    225                }
    226            }
    227
    228            Object dataObj = map.get("rows");
    229            if (dataObj instanceof List) {
    230                List<?> rowList = (List<?>) dataObj;
    231                // each entry in the map has the row key and an array of
    232                // values (the length should match the list of keys above
    233                for (Object rowObj : rowList) {
    234                    processRow(rowObj, keys, result);
    235                }
    236            } else { // the 'data' entry is not parsing to a list
    237                if (dataObj == null) {
    238                    throw new RuntimeException("No 'rows' section defined.");
    239                } else {
    240                    throw new RuntimeException("Please check the 'rows' "
    241                            + "entry, the format does not parse to a list of "
    242                            + "rows.");
    243                }
    244            }
    245            return result;
    246        } catch (ParseException ex) {
    247            throw new RuntimeException(ex);
    248        }
    249    }
    250
    251    /**
    252     * Processes an entry for one row in a KeyedValues2D.
    253     *
    254     * @param rowObj  the series object.
    255     * @param columnKeys  the required column keys.
    256     */
    257    static void processRow(Object rowObj, List<?> columnKeys,
    258            DefaultKeyedValues2D dataset) {
    259
    260        if (!(rowObj instanceof List)) {
    261            throw new RuntimeException("Check the 'data' section it contains "
    262                    + "a row that does not parse to a list.");
    263        }
    264
    265        // we expect the row data object to be an array containing the
    266        // rowKey and rowValueArray entries, where rowValueArray
    267        // should have the same number of entries as the columnKeys
    268        List rowList = (List) rowObj;
    269        Object rowKey = rowList.get(0);
    270        Object rowDataObj = rowList.get(1);
    271        if (!(rowDataObj instanceof List)) {
    272            throw new RuntimeException("Please check the row entry for "
    273                    + rowKey + " because it is not parsing to a list (of "
    274                    + "rowKey and rowDataValues items.");
    275        }
    276        List<?> rowData = (List<?>) rowDataObj;
    277        if (rowData.size() != columnKeys.size()) {
    278            throw new RuntimeException("The values list for series "
    279                    + rowKey + " does not contain the correct number of "
    280                    + "entries to match the columnKeys.");
    281        }
    282
    283        for (int c = 0; c < rowData.size(); c++) {
    284            Object columnKey = columnKeys.get(c);
    285            dataset.setValue(objToDouble(rowData.get(c)),
    286                    rowKey.toString(), columnKey.toString());
    287        }
    288    }
    289
    290    /**
    291     * Writes a data table to a string in JSON format.
    292     *
    293     * @param data  the data ({@code null} not permitted).
    294     *
    295     * @return The string.
    296     */
    297    public static String writeKeyedValues2D(
    298            KeyedValues2D<? extends Number> data) {
    299        ArgChecks.nullNotPermitted(data, "data");
    300        StringWriter sw = new StringWriter();
    301        try {
    302            writeKeyedValues2D(data, sw);
    303        } catch (IOException ex) {
    304            throw new RuntimeException(ex);
    305        }
    306        return sw.toString();
    307    }
    308
    309    /**
    310     * Writes the data in JSON format to the supplied writer.
    311     *
    312     * @param data  the data ({@code null} not permitted).
    313     * @param writer  the writer ({@code null} not permitted).
    314     *
    315     * @throws IOException if there is an I/O problem.
    316     */
    317    public static void writeKeyedValues2D(KeyedValues2D<? extends Number> data,
    318            Writer writer) throws IOException {
    319        ArgChecks.nullNotPermitted(data, "data");
    320        ArgChecks.nullNotPermitted(writer, "writer");
    321        List<Comparable<?>> columnKeys = data.getColumnKeys();
    322        List<Comparable<?>> rowKeys = data.getRowKeys();
    323        writer.write("{");
    324        if (!columnKeys.isEmpty()) {
    325            writer.write("\"columnKeys\": [");
    326            boolean first = true;
    327            for (Comparable<?> columnKey : columnKeys) {
    328                if (!first) {
    329                    writer.write(", ");
    330                } else {
    331                    first = false;
    332                }
    333                writer.write(JSONValue.toJSONString(columnKey.toString()));
    334            }
    335            writer.write("]");
    336        }
    337        if (!rowKeys.isEmpty()) {
    338            writer.write(", \"rows\": [");
    339            boolean firstRow = true;
    340            for (Comparable<?> rowKey : rowKeys) {
    341                if (!firstRow) {
    342                    writer.write(", [");
    343                } else {
    344                    writer.write("[");
    345                    firstRow = false;
    346                }
    347                // write the row data
    348                writer.write(JSONValue.toJSONString(rowKey.toString()));
    349                writer.write(", [");
    350                boolean first = true;
    351                for (Comparable<?> columnKey : columnKeys) {
    352                    if (!first) {
    353                        writer.write(", ");
    354                    } else {
    355                        first = false;
    356                    }
    357                    writer.write(JSONValue.toJSONString(data.getValue(rowKey,
    358                            columnKey)));
    359                }
    360                writer.write("]]");
    361            }
    362            writer.write("]");
    363        }
    364        writer.write("}");
    365    }
    366
    367    /**
    368     * Parses the supplied string and (if possible) creates a
    369     * {@link KeyedValues3D} instance.
    370     *
    371     * @param json  the JSON string ({@code null} not permitted).
    372     *
    373     * @return A {@code KeyedValues3D} instance.
    374     */
    375    public static KeyedValues3D<? extends Number> readKeyedValues3D(
    376            String json) {
    377        StringReader in = new StringReader(json);
    378        KeyedValues3D<? extends Number> result;
    379        try {
    380            result = readKeyedValues3D(in);
    381        } catch (IOException ex) {
    382            // not for StringReader
    383            result = null;
    384        }
    385        return result;
    386    }
    387
    388    /**
    389     * Parses character data from the reader and (if possible) creates a
    390     * {@link KeyedValues3D} instance.  This method will read back the data
    391     * written by {@link JSONUtils#writeKeyedValues3D(
    392     * com.orsoncharts.data.KeyedValues3D, java.io.Writer) }.
    393     *
    394     * @param reader  the reader ({@code null} not permitted).
    395     *
    396     * @return A {@code KeyedValues3D} instance.
    397     *
    398     * @throws IOException if there is an I/O problem.
    399     */
    400    public static KeyedValues3D<? extends Number> readKeyedValues3D(
    401            Reader reader) throws IOException {
    402        JSONParser parser = new JSONParser();
    403        try {
    404            Map map = (Map) parser.parse(reader, createContainerFactory());
    405            StandardCategoryDataset3D result = new StandardCategoryDataset3D();
    406            if (map.isEmpty()) {
    407                return result;
    408            }
    409
    410            // read the row keys, we'll use these to validate the row keys
    411            // supplied with the data
    412            Object rowKeysObj = map.get("rowKeys");
    413            List<?> rowKeys;
    414            if (rowKeysObj instanceof List<?>) {
    415                rowKeys = (List<?>) rowKeysObj;
    416            } else {
    417                if (rowKeysObj == null) {
    418                    throw new RuntimeException("No 'rowKeys' defined.");
    419                } else {
    420                    throw new RuntimeException("Please check the 'rowKeys', "
    421                            + "the format does not parse to a list.");
    422                }
    423            }
    424
    425            // read the column keys, the data is provided later in rows that
    426            // should have the same number of entries as the columnKeys list
    427            Object columnKeysObj = map.get("columnKeys");
    428            List<?> columnKeys;
    429            if (columnKeysObj instanceof List<?>) {
    430                columnKeys = (List<?>) columnKeysObj;
    431            } else {
    432                if (columnKeysObj == null) {
    433                    throw new RuntimeException("No 'columnKeys' defined.");
    434                } else {
    435                    throw new RuntimeException("Please check the 'columnKeys', "
    436                            + "the format does not parse to a list.");
    437                }
    438            }
    439
    440            // the data object should be a list of data series
    441            Object dataObj = map.get("data");
    442            if (dataObj instanceof List) {
    443                List<?> seriesList = (List<?>) dataObj;
    444                // each entry in the map has the series name as the key, and
    445                // the value is a map of row data (rowKey, list of values)
    446                for (Object seriesObj : seriesList) {
    447                    processSeries(seriesObj, rowKeys, columnKeys, result);
    448                }
    449            } else { // the 'data' entry is not parsing to a list
    450                if (dataObj == null) {
    451                    throw new RuntimeException("No 'data' section defined.");
    452                } else {
    453                    throw new RuntimeException("Please check the 'data' "
    454                            + "entry, the format does not parse to a list of "
    455                            + "series.");
    456                }
    457            }
    458            return result;
    459        } catch (ParseException ex) {
    460            throw new RuntimeException(ex);
    461        }
    462    }
    463
    464    /**
    465     * Processes an entry for one series.
    466     *
    467     * @param seriesObj  the series object.
    468     * @param rowKeys  the expected row keys.
    469     * @param columnKeys  the required column keys.
    470     */
    471    static void processSeries(Object seriesObj, List<?> rowKeys,
    472            List<?> columnKeys, StandardCategoryDataset3D dataset) {
    473
    474        if (!(seriesObj instanceof Map)) {
    475            throw new RuntimeException("Check the 'data' section it contains "
    476                    + "a series that does not parse to a map.");
    477        }
    478
    479        // we expect the series data object to be a map of
    480        // rowKey ==> rowValueArray entries, where rowValueArray
    481        // should have the same number of entries as the columnKeys
    482        Map seriesMap = (Map) seriesObj;
    483        Object seriesKey = seriesMap.get("seriesKey");
    484        Object seriesRowsObj = seriesMap.get("rows");
    485        if (!(seriesRowsObj instanceof Map)) {
    486            throw new RuntimeException("Please check the series entry for "
    487                    + seriesKey + " because it is not parsing to a map (of "
    488                    + "rowKey -> rowDataValues items.");
    489        }
    490        Map<?, ?> seriesData = (Map<?, ?>) seriesRowsObj;
    491        for (Object rowKey : seriesData.keySet()) {
    492            if (!rowKeys.contains(rowKey)) {
    493                throw new RuntimeException("The row key " + rowKey + " is not "
    494                        + "listed in the rowKeys entry.");
    495            }
    496            Object rowValuesObj = seriesData.get(rowKey);
    497            if (!(rowValuesObj instanceof List<?>)) {
    498                throw new RuntimeException("Please check the entry for series "
    499                        + seriesKey + " and row " + rowKey + " because it "
    500                        + "does not parse to a list of values.");
    501            }
    502            List<?> rowValues = (List<?>) rowValuesObj;
    503            if (rowValues.size() != columnKeys.size()) {
    504                throw new RuntimeException("The values list for series "
    505                        + seriesKey + " and row " + rowKey + " does not "
    506                        + "contain the correct number of entries to match "
    507                        + "the columnKeys.");
    508            }
    509            for (int c = 0; c < rowValues.size(); c++) {
    510                Object columnKey = columnKeys.get(c);
    511                dataset.addValue(objToDouble(rowValues.get(c)),
    512                        seriesKey.toString(), rowKey.toString(),
    513                        columnKey.toString());
    514            }
    515        }
    516    }
    517
    518    /**
    519     * Returns a string containing the data in JSON format.
    520     *
    521     * @param dataset  the data ({@code null} not permitted).
    522     *
    523     * @return A string in JSON format.
    524     */
    525    public static String writeKeyedValues3D(
    526            KeyedValues3D<? extends Number> dataset) {
    527        ArgChecks.nullNotPermitted(dataset, "dataset");
    528        StringWriter sw = new StringWriter();
    529        try {
    530            writeKeyedValues3D(dataset, sw);
    531        } catch (IOException ex) {
    532            throw new RuntimeException(ex);
    533        }
    534        return sw.toString();
    535    }
    536
    537    /**
    538     * Writes the dataset in JSON format to the supplied writer.
    539     *
    540     * @param dataset  the dataset ({@code null} not permitted).
    541     * @param writer  the writer ({@code null} not permitted).
    542     *
    543     * @throws IOException if there is an I/O problem.
    544     */
    545    public static void writeKeyedValues3D(
    546            KeyedValues3D<? extends Number> dataset, Writer writer)
    547            throws IOException {
    548        ArgChecks.nullNotPermitted(dataset, "dataset");
    549        ArgChecks.nullNotPermitted(writer, "writer");
    550
    551        writer.write("{");
    552        if (!dataset.getColumnKeys().isEmpty()) {
    553            writer.write("\"columnKeys\": [");
    554            boolean first = true;
    555            for (Comparable<?> key : dataset.getColumnKeys()) {
    556                if (!first) {
    557                    writer.write(", ");
    558                } else {
    559                    first = false;
    560                }
    561                writer.write(JSONValue.toJSONString(key.toString()));
    562            }
    563            writer.write("], ");
    564        }
    565
    566        // write the row keys
    567        if (!dataset.getRowKeys().isEmpty()) {
    568            writer.write("\"rowKeys\": [");
    569            boolean first = true;
    570            for (Comparable<?> key : dataset.getRowKeys()) {
    571                if (!first) {
    572                    writer.write(", ");
    573                } else {
    574                    first = false;
    575                }
    576                writer.write(JSONValue.toJSONString(key.toString()));
    577            }
    578            writer.write("], ");
    579        }
    580
    581        // write the data which is zero, one or many data series
    582        // a data series has a 'key' and a 'rows' attribute
    583        // the 'rows' attribute is a Map from 'rowKey' -> array of data values
    584        if (dataset.getSeriesCount() != 0) {
    585            writer.write("\"series\": [");
    586            boolean first = true;
    587            for (Comparable<?> seriesKey : dataset.getSeriesKeys()) {
    588                if (!first) {
    589                    writer.write(", ");
    590                } else {
    591                    first = false;
    592                }
    593                writer.write("{\"seriesKey\": ");
    594                writer.write(JSONValue.toJSONString(seriesKey.toString()));
    595                writer.write(", \"rows\": [");
    596
    597                boolean firstRow = true;
    598                for (Comparable<?> rowKey : dataset.getRowKeys()) {
    599                    if (countForRowInSeries(dataset, seriesKey, rowKey) > 0) {
    600                        if (!firstRow) {
    601                            writer.write(", [");
    602                        } else {
    603                            writer.write("[");
    604                            firstRow = false;
    605                        }
    606                        // write the row values
    607                        writer.write(JSONValue.toJSONString(rowKey.toString())
    608                                + ", [");
    609                        for (int c = 0; c < dataset.getColumnCount(); c++) {
    610                            Comparable columnKey = dataset.getColumnKey(c);
    611                            if (c != 0) {
    612                                writer.write(", ");
    613                            }
    614                            writer.write(JSONValue.toJSONString(
    615                                    dataset.getValue(seriesKey, rowKey,
    616                                    columnKey)));
    617                        }
    618                        writer.write("]]");
    619                    }
    620                }
    621                writer.write("]}");
    622            }
    623            writer.write("]");
    624        }
    625        writer.write("}");
    626    }
    627
    628    /**
    629     * Returns the number of non-{@code null} entries for the specified
    630     * series and row.
    631     *
    632     * @param data  the dataset ({@code null} not permitted).
    633     * @param seriesKey  the series key ({@code null} not permitted).
    634     * @param rowKey  the row key ({@code null} not permitted).
    635     *
    636     * @return The count.
    637     */
    638    private static int countForRowInSeries(KeyedValues3D<?> data,
    639            Comparable<?> seriesKey, Comparable<?> rowKey) {
    640        ArgChecks.nullNotPermitted(data, "data");
    641        ArgChecks.nullNotPermitted(seriesKey, "seriesKey");
    642        ArgChecks.nullNotPermitted(rowKey, "rowKey");
    643        int seriesIndex = data.getSeriesIndex(seriesKey);
    644        if (seriesIndex < 0) {
    645            throw new IllegalArgumentException("Series not found: "
    646                    + seriesKey);
    647        }
    648        int rowIndex = data.getRowIndex(rowKey);
    649        if (rowIndex < 0) {
    650            throw new IllegalArgumentException("Row not found: " + rowKey);
    651        }
    652        int count = 0;
    653        for (int c = 0; c < data.getColumnCount(); c++) {
    654            Number n = (Number) data.getValue(seriesIndex, rowIndex, c);
    655            if (n != null) {
    656                count++;
    657            }
    658        }
    659        return count;
    660    }
    661
    662    /**
    663     * Parses the string and (if possible) creates an {XYZDataset} instance
    664     * that represents the data.  This method will read back the data that
    665     * is written by
    666     * {@link #writeXYZDataset(com.orsoncharts.data.xyz.XYZDataset)}.
    667     *
    668     * @param json  a JSON formatted string ({@code null} not permitted).
    669     *
    670     * @return A dataset.
    671     *
    672     * @see #writeXYZDataset(com.orsoncharts.data.xyz.XYZDataset)
    673     */
    674    public static XYZDataset readXYZDataset(String json) {
    675        ArgChecks.nullNotPermitted(json, "json");
    676        StringReader in = new StringReader(json);
    677        XYZDataset result;
    678        try {
    679            result = readXYZDataset(in);
    680        } catch (IOException ex) {
    681            // not for StringReader
    682            result = null;
    683        }
    684        return result;
    685    }
    686
    687    /**
    688     * Parses character data from the reader and (if possible) creates an
    689     * {XYZDataset} instance that represents the data.
    690     *
    691     * @param reader  a reader ({@code null} not permitted).
    692     *
    693     * @return A dataset.
    694     *
    695     * @throws IOException if there is an I/O problem.
    696     */
    697    public static XYZDataset readXYZDataset(Reader reader) throws IOException {
    698        JSONParser parser = new JSONParser();
    699        XYZSeriesCollection result = new XYZSeriesCollection();
    700        try {
    701            List<?> list = (List<?>) parser.parse(reader,
    702                    createContainerFactory());
    703            // each entry in the array should be a series array (where the
    704            // first item is the series name and the next value is an array
    705            // (of arrays of length 3) containing the data items
    706            for (Object seriesArray : list) {
    707                if (seriesArray instanceof List) {
    708                    List<?> seriesList = (List<?>) seriesArray;
    709                    XYZSeries series = createSeries(seriesList);
    710                    result.add(series);
    711                } else {
    712                    throw new RuntimeException(
    713                            "Input for a series did not parse to a list.");
    714                }
    715            }
    716        } catch (ParseException ex) {
    717            throw new RuntimeException(ex);
    718        }
    719        return result;
    720    }
    721
    722    /**
    723     * Returns a string containing the dataset in JSON format.
    724     *
    725     * @param dataset  the dataset ({@code null} not permitted).
    726     *
    727     * @return A string in JSON format.
    728     */
    729    public static String writeXYZDataset(XYZDataset dataset) {
    730        StringWriter sw = new StringWriter();
    731        try {
    732            writeXYZDataset(dataset, sw);
    733        } catch (IOException ex) {
    734            throw new RuntimeException(ex);
    735        }
    736        return sw.toString();
    737    }
    738
    739    /**
    740     * Writes the dataset in JSON format to the supplied writer.
    741     *
    742     * @param dataset  the data ({@code null} not permitted).
    743     * @param writer  the writer ({@code null} not permitted).
    744     *
    745     * @throws IOException if there is an I/O problem.
    746     */
    747    public static void writeXYZDataset(XYZDataset dataset, Writer writer)
    748            throws IOException {
    749        writer.write("[");
    750        boolean first = true;
    751        for (Comparable<?> seriesKey : dataset.getSeriesKeys()) {
    752            if (!first) {
    753                writer.write(", [");
    754            } else {
    755                writer.write("[");
    756                first = false;
    757            }
    758            writer.write(JSONValue.toJSONString(seriesKey.toString()));
    759            writer.write(", [");
    760            int seriesIndex = dataset.getSeriesIndex(seriesKey);
    761            int itemCount = dataset.getItemCount(seriesIndex);
    762            for (int i = 0; i < itemCount; i++) {
    763                if (i != 0) {
    764                    writer.write(", ");
    765                }
    766                writer.write("[");
    767                writer.write(JSONValue.toJSONString(Double.valueOf(
    768                        dataset.getX(seriesIndex, i))));
    769                writer.write(", ");
    770                writer.write(JSONValue.toJSONString(Double.valueOf(
    771                        dataset.getY(seriesIndex, i))));
    772                writer.write(", ");
    773                writer.write(JSONValue.toJSONString(Double.valueOf(
    774                        dataset.getZ(seriesIndex, i))));
    775                writer.write("]");
    776            }
    777            writer.write("]]");
    778        }
    779        writer.write("]");
    780    }
    781
    782    /**
    783     * Converts an arbitrary object to a double.
    784     *
    785     * @param obj  an object ({@code null} permitted).
    786     *
    787     * @return A double primitive (possibly Double.NaN).
    788     */
    789    private static double objToDouble(Object obj) {
    790        if (obj == null) {
    791            return Double.NaN;
    792        }
    793        if (obj instanceof Number) {
    794            return ((Number) obj).doubleValue();
    795        }
    796        double result = Double.NaN;
    797        try {
    798            result = Double.valueOf(obj.toString());
    799        } catch (Exception e) {
    800
    801        }
    802        return result;
    803    }
    804
    805    /**
    806     * Creates an {@link XYZSeries} from the supplied list.  The list is
    807     * coming from the JSON parser and should contain the series name as the
    808     * first item, and a list of data items as the second item.  The list of
    809     * data items should be a list of lists (
    810     *
    811     * @param sArray  the series array.
    812     *
    813     * @return A data series.
    814     */
    815    private static XYZSeries createSeries(List<?> sArray) {
    816        Comparable<?> key = (Comparable<?>) sArray.get(0);
    817        List<?> dataItems = (List<?>) sArray.get(1);
    818        XYZSeries series = new XYZSeries(key);
    819        for (Object item : dataItems) {
    820            if (item instanceof List<?>) {
    821                List<?> xyz = (List<?>) item;
    822                if (xyz.size() != 3) {
    823                    throw new RuntimeException(
    824                            "A data item should contain three numbers, "
    825                            + "but we have " + xyz);
    826                }
    827                double x = objToDouble(xyz.get(0));
    828                double y = objToDouble(xyz.get(1));
    829                double z = objToDouble(xyz.get(2));
    830                series.add(x, y, z);
    831
    832            } else {
    833                throw new RuntimeException(
    834                        "Expecting a data item (x, y, z) for series " + key
    835                        + " but found " + item + ".");
    836            }
    837        }
    838        return series;
    839    }
    840
    841    /**
    842     * Returns a custom container factory for the JSON parser.  We create this
    843     * so that the collections respect the order of elements.
    844     *
    845     * @return The container factory.
    846     */
    847    private static ContainerFactory createContainerFactory() {
    848        return new ContainerFactory() {
    849            @Override
    850            public Map createObjectContainer() {
    851                return new LinkedHashMap();
    852            }
    853
    854            @Override
    855            public List creatArrayContainer() {
    856                return new ArrayList();
    857            }
    858        };
    859    }
    860
    861}