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