001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
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 org.jfree.chart3d.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 org.jfree.chart3d.util.json.JSONValue;
045import org.jfree.chart3d.util.json.parser.JSONParser;
046import org.jfree.chart3d.util.json.parser.ParseException;
047import org.jfree.chart3d.internal.Args;
048import org.jfree.chart3d.util.json.parser.ContainerFactory;
049import org.jfree.chart3d.data.category.StandardCategoryDataset3D;
050import org.jfree.chart3d.data.xyz.XYZDataset;
051import org.jfree.chart3d.data.xyz.XYZSeries;
052import org.jfree.chart3d.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        Args.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        Args.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<>();
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        Args.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        Args.nullNotPermitted(data, "data");
155        Args.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        Args.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<>();
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        Args.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        Args.nullNotPermitted(data, "data");
326        Args.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     * org.jfree.chart3d.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        Args.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        Args.nullNotPermitted(dataset, "dataset");
557        Args.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        Args.nullNotPermitted(data, "data");
652        Args.nullNotPermitted(seriesKey, "seriesKey");
653        Args.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(org.jfree.chart3d.data.xyz.XYZDataset)}.
678     * 
679     * @param json  a JSON formatted string ({@code null} not permitted).
680     * 
681     * @return A dataset.
682     * 
683     * @see #writeXYZDataset(org.jfree.chart3d.data.xyz.XYZDataset) 
684     */
685    public static XYZDataset<String> readXYZDataset(String json) {
686        Args.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<>();
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(
781                        dataset.getX(seriesIndex, i)));
782                writer.write(", ");
783                writer.write(JSONValue.toJSONString(
784                        dataset.getY(seriesIndex, i)));
785                writer.write(", ");
786                writer.write(JSONValue.toJSONString(
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.parseDouble(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}