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}