All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.codename1.io.JSONParser Maven / Gradle / Ivy

There is a newer version: 7.0.161
Show newest version
/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores
 * CA 94065 USA or visit www.oracle.com if you need additional information or
 * have any questions.
 */
package com.codename1.io;

import com.codename1.processing.Result;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * 

Fast and dirty parser for JSON content on the web, it essentially returns * a {@link java.util.Map} object containing the object fields mapped to their values. If the value is * a nested object a nested {@link java.util.Map}/{@link java.util.List} is returned.

* *

* The {@code JSONParser} returns a {@code Map} which is great if the root object is a {@code Map} but in * some cases its a list of elements (as is the case above). In this case a special case {@code "root"} element is * created to contain the actual list of elements. See the sample below for exact usage of this. *

* *

* The sample below includes JSON from https://anapioficeandfire.com/ * generated by the query http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3: *

* * JSON Parsing Result * * *

The sample code below fetches a page of data from the nestoria housing listing API as a list of Map elements. * You can see instructions on how to display the data in the {@link com.codename1.components.InfiniteScrollAdapter} * class.

* * * @author Shai Almog */ public class JSONParser implements JSONParseCallback { /** * Checks whether JSONParser instances will use longs to represent numeric values by default. This is just a * global default setting. You should use {@link #isUseLongsInstance() } to check the status for a particular * JSONParser object. * * @return the useLongsDefault * @deprecated Use {@link #isUseLongsInstance() } to check whether the current JSONParser uses longs. */ public static boolean isUseLongs() { return useLongsDefault; } /** * Checks to see if this parser generates long objects and not just doubles for numeric values. * @return */ public boolean isUseLongsInstance() { return useLongs; } /** * Indicates that the parser will generate long objects and not just doubles for numeric values. * *

Warning: This method will affect ALL JSONParser instances in the application. Prefer to use {@link #setUseLongsInstance(boolean) } * to only affect the behaviour of the particular JSONParser instance.

* @param aUseLongsDefault the useLongsDefault to set * @deprecated Use {@link #setUseLongsInstance(boolean) } */ public static void setUseLongs(boolean aUseLongsDefault) { useLongsDefault = aUseLongsDefault; } /** * Sets the current JSONParser instance to use longs instead of doubles for numeric values. Prefer this to the static {@link #setUseLongs(boolean) } * so that it doesn't disrupt libraries that may depend on JSONParser. * @param longs True to use * @since 7.0 */ public void setUseLongsInstance(boolean longs) { useLongs = longs; } /** * Checks the default setting for {@link #isIncludeNullsInstance() }. * @return the includeNullsDefault The global default setting for {@link #isIncludeNullsInstance() }. * @deprecated Use {@link #isIncludeNullsInstance() } instead. */ public static boolean isIncludeNulls() { return includeNullsDefault; } /** * Sets the global default settings for {@link #isIncludeNullsInstance() }. * @param aIncludeNullsDefault the includeNullsDefault to set * @deprecated Use {@link #setIncludeNullsInstance(boolean) } instead. */ public static void setIncludeNulls(boolean aIncludeNullsDefault) { includeNullsDefault = aIncludeNullsDefault; } /** * Sets whether to include null values in parsed content. * @param include True to include null values in parsed content. * @since 7.0 */ public void setIncludeNullsInstance(boolean include) { includeNulls = include; } /** * Checks whether this parser will include null values in parsed content. * * @return True if null values are included in parsed content. * @since 7.0 */ public boolean isIncludeNullsInstance() { return includeNulls; } /** * Global default setting for {@link #isUseBooleanInstance() }. * @return the useBooleanDefault * @deprecated Use {@link #isUseBooleanInstance() } instead. */ public static boolean isUseBoolean() { return useBooleanDefault; } /** * Indicates that the parser will generate Boolean objects and not just Strings for boolean values * @return True if the parser will generate Boolean objects and not just Strings for boolean values. * @since 7.0 */ public boolean isUseBooleanInstance() { return useBoolean; } /** * Indicates that the parser will generate Boolean objects and not just Strings for boolean values * @param useBoolean True to generate Boolean objects and not just Strings for boolean values. * @since 7.0 */ public void setUseBooleanInstance(boolean useBoolean) { this.useBoolean = useBoolean; } /** * Sets the global default value for {@link #isUseBooleanInstance() } * @param aUseBooleanDefault the useBooleanDefault to set * @deprecated Use {@link #setUseBooleanInstance(boolean) } instead. */ public static void setUseBoolean(boolean aUseBooleanDefault) { useBooleanDefault = aUseBooleanDefault; } static class ReaderClass { char[] buffer; int buffOffset; int buffSize = -1; int read(Reader is) throws IOException { int c = -1; if(buffer == null) { buffer = new char[8192]; } if(buffSize < 0 || buffOffset >= buffSize) { buffSize = is.read(buffer, 0, buffer.length); if(buffSize < 0) { return -1; } buffOffset = 0; } c = buffer[buffOffset]; buffOffset ++; return c; } } private static boolean useLongsDefault; private boolean useLongs = useLongsDefault; private static boolean useLongs(JSONParseCallback callback) { if (callback instanceof JSONParser) { return ((JSONParser)callback).isUseLongsInstance(); } return useLongsDefault; } /** * Indicates that the parser will generate Boolean objects and not just Strings for boolean values */ private static boolean useBooleanDefault; private boolean useBoolean = useBooleanDefault; private static boolean useBoolean(JSONParseCallback callback) { if (callback instanceof JSONParser) { return ((JSONParser)callback).isUseBooleanInstance(); } return useBooleanDefault; } private static boolean includeNullsDefault; private boolean includeNulls = includeNullsDefault; private boolean modern; private Map state; private java.util.List parseStack; private String currentKey; /** * If strict is set to false, then the parser will attempt to sanitize the JSON * input before parsing. I.e. it will accept invalid JSON, such as unquoted keys, etc.. */ private boolean strict = true; static class KeyStack extends Vector { protected String peek() { return (String)elementAt(0); } protected void push(String key) { insertElementAt(key, 0); } protected String pop() { if (isEmpty()) { return null; } String key = peek(); removeElementAt(0); return key; } }; /** * Static method! Parses the given input stream and fires the data into the given callback. * * @param i the reader * @param callback a generic callback to receive the parse events * @throws IOException if thrown by the stream */ public static void parse(Reader i, JSONParseCallback callback) throws IOException { boolean quoteMode = false; ReaderClass rc = new ReaderClass(); rc.buffOffset = 0; rc.buffSize = -1; int row = 1; int column = 1; StringBuilder currentToken = new StringBuilder(); KeyStack blocks = new KeyStack(); String currentBlock = ""; String lastKey = null; try { while (callback.isAlive()) { int currentChar = rc.read(i); if (currentChar < 0) { return; } char c = (char) currentChar; if(c == '\n') { row++; column = 0; } else { column++; } if (quoteMode) { switch (c) { case '"': String v = currentToken.toString(); callback.stringToken(v); if (lastKey != null) { callback.keyValue(lastKey, v); lastKey = null; } else { lastKey = v; } currentToken.setLength(0); quoteMode = false; continue; case '\\': c = (char) rc.read(i); if (c == 'u') { String unicode = "" + ((char) rc.read(i)) + ((char) rc.read(i)) + ((char) rc.read(i)) + ((char) rc.read(i)); try { c = (char) Integer.parseInt(unicode, 16); } catch (NumberFormatException err) { // problem in parsing the u notation! Log.e(err); System.out.println("Error in parsing \\u" + unicode); } } else { switch(c) { case 'n': currentToken.append('\n'); continue; case 't': currentToken.append('\t'); continue; case 'r': currentToken.append('\r'); continue; } } currentToken.append(c); continue; } currentToken.append(c); } else { switch (c) { case 'n': // check for null char u = (char) rc.read(i); char l = (char) rc.read(i); char l2 = (char) rc.read(i); if (u == 'u' && l == 'l' && l2 == 'l') { // this is null callback.stringToken(null); if (lastKey != null) { callback.keyValue(lastKey, null); lastKey = null; } } else { // parsing error.... Log.p("Expected null for key value while parsing JSON token at row: " + row + " column: " + column + " buffer: " + currentToken.toString()); } continue; case 't': // check for true char a1 = (char) rc.read(i); char a2 = (char) rc.read(i); char a3 = (char) rc.read(i); if (a1 == 'r' && a2 == 'u' && a3 == 'e') { if(useBoolean(callback)) { callback.booleanToken(true); } else { callback.stringToken("true"); } if (lastKey != null) { callback.keyValue(lastKey, "true"); lastKey = null; } } else { // parsing error.... Log.p("Expected true for key value while parsing JSON token at row: " + row + " column: " + column + " buffer: " + currentToken.toString()); } continue; case 'f': // this can either be the start of "false" or the end of a // fraction number... if (currentToken.length() > 0) { currentToken.append('f'); continue; } // check for false char b1 = (char) rc.read(i); char b2 = (char) rc.read(i); char b3 = (char) rc.read(i); char b4 = (char) rc.read(i); if (b1 == 'a' && b2 == 'l' && b3 == 's' && b4 == 'e') { if(useBoolean(callback)) { callback.booleanToken(false); } else { callback.stringToken("false"); } if (lastKey != null) { callback.keyValue(lastKey, "false"); lastKey = null; } } else { // parsing error.... Log.p("Expected false for key value while parsing JSON token at row: " + row + " column: " + column + " buffer: " + currentToken.toString()); } continue; case '{': if (lastKey == null) { if (blocks.size() == 0) { lastKey = "root"; } else { lastKey = blocks.peek(); } } blocks.push(lastKey); callback.startBlock(lastKey); lastKey = null; continue; case '}': if (currentToken.length() > 0) { try { String ct = currentToken.toString(); if(useLongs(callback)) { if(ct.indexOf('.') > -1) { callback.numericToken(Double.parseDouble(ct)); } else { callback.longToken(Long.parseLong(ct)); } } else { callback.numericToken(Double.parseDouble(ct)); } if (lastKey != null) { callback.keyValue(lastKey, currentToken.toString()); lastKey = null; currentToken.setLength(0); } } catch (NumberFormatException err) { Log.e(err); // this isn't a number! } } currentBlock = blocks.pop(); callback.endBlock(currentBlock); lastKey = null; continue; case '[': blocks.push(lastKey); callback.startArray(lastKey); lastKey = null; continue; case ']': if (currentToken.length() > 0) { try { String ct = currentToken.toString(); if(useLongs(callback)) { if(ct.indexOf('.') > -1) { callback.numericToken(Double.parseDouble(ct)); } else { callback.longToken(Long.parseLong(ct)); } } else { callback.numericToken(Double.parseDouble(ct)); } if (lastKey != null) { callback.keyValue(lastKey, currentToken.toString()); lastKey = null; } } catch (NumberFormatException err) { // this isn't a number! } } currentToken.setLength(0); currentBlock = blocks.pop(); callback.endArray(currentBlock); lastKey = null; continue; case ' ': case '\r': case '\t': case '\n': // whitespace continue; case '"': quoteMode = true; continue; case ':': case ',': if (currentToken.length() > 0) { try { String ct = currentToken.toString(); if(useLongs(callback)) { if(ct.indexOf('.') > -1) { callback.numericToken(Double.parseDouble(ct)); } else { callback.longToken(Long.parseLong(ct)); } } else { callback.numericToken(Double.parseDouble(ct)); } if (lastKey != null) { callback.keyValue(lastKey, currentToken.toString()); lastKey = null; } } catch (NumberFormatException err) { // this isn't a number! } } currentToken.setLength(0); continue; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-': case '.': case 'x': case 'd': case 'l': case 'e': case 'E': currentToken.append(c); continue; } } } } catch (Exception err) { Log.e(err); Log.p("Exception during JSON parsing at row: " + row + " column: " + column + " buffer: " + currentToken.toString()); /*System.out.println(); int current = i.read(); while(current >= 0) { System.out.print((char)current); current = i.read(); }*/ i.close(); } } /** *

* Parses the given input stream into this object and returns the parse tree.
* The {@code JSONParser} returns a {@code Map} which is great if the root object is a {@code Map} but in * some cases its a list of elements (as is the case above). In this case a special case {@code "root"} element is * created to contain the actual list of elements. See the sample below for exact usage of this. *

*

* The sample below includes JSON from https://anapioficeandfire.com/ * generated by the query http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3: *

* * JSON Parsing Result * * @param i the reader * @return the parse tree as a hashtable * @throws IOException if thrown by the stream */ public Map parseJSON(Reader i) throws IOException { modern = true; state = new LinkedHashMap(); parseStack = new ArrayList(); currentKey = null; if (!strict) { i = new CharArrayReader(JSONSanitizer.sanitize(Util.readToString(i)).toCharArray()); } parse(i, this); return state; } /** * Parses the given input stream into this object and returns the parse tree * * @param i the reader * @return the parse tree as a hashtable * @throws IOException if thrown by the stream * @deprecated use the new parseJSON instead */ public Hashtable parse(Reader i) throws IOException { modern = false; state = new Hashtable(); parseStack = new Vector(); currentKey = null; if (!strict) { String cleaned = JSONSanitizer.sanitize(Util.readToString(i)); i = new CharArrayReader(cleaned.toCharArray()); } parse(i, this); return (Hashtable)state; } private boolean isStackHash() { return parseStack.size() > 0 && parseStack.get(parseStack.size() - 1) instanceof Map; } private Map getStackHash() { return (Map) parseStack.get(parseStack.size() - 1); } private java.util.List getStackVec() { return (java.util.List) parseStack.get(parseStack.size() - 1); } /** * {@inheritDoc} */ public void startBlock(String blockName) { if (parseStack.size() == 0) { parseStack.add(state); } else { Map newOne; if(modern) { newOne = new LinkedHashMap(); } else { newOne = new Hashtable(); } if (isStackHash()) { getStackHash().put(currentKey, newOne); currentKey = null; } else { getStackVec().add(newOne); } parseStack.add(newOne); } } /** * {@inheritDoc} */ public void endBlock(String blockName) { parseStack.remove(parseStack.size() - 1); } /** * Checks if this JSON parser is in strict mode. When in strict mode, which is the default, * only valid JSON will be parsed. If strict mode is disabled, then it will attempt to * sanitize the JSON input before parsing. This can be handy if you want to parse structures * that are *almost* JSON. E.g. non-quoted keys, single-quotes on strings, unquoted strings. Etc. * * @return True if strict mode is enabled. * @since 7.0 * @see #setStrict(boolean) */ public boolean isStrict() { return strict; } /** * Enables or disables strict mode. Default is true. *

When strict mode is disabled, the parser will sanitize the JSON input before parsing. The effect * is that it will be able to parse input that is json-ish.

*

Non-Strict Input

* The sanitizer takes JSON like content, and interprets it as JS eval * would. Specifically, it deals with these non-standard constructs. *
    *
  • {@code '...'} Single quoted strings are converted to JSON strings. *
  • {@code \xAB} Hex escapes are converted to JSON unicode escapes. *
  • {@code \012} Octal escapes are converted to JSON unicode escapes. *
  • {@code 0xAB} Hex integer literals are converted to JSON decimal * numbers. *
  • {@code 012} Octal integer literals are converted to JSON decimal * numbers. *
  • {@code +.5} Decimal numbers are coerced to JSON's stricter format. *
  • {@code [0,,2]} Elisions in arrays are filled with {@code null}. *
  • {@code [1,2,3,]} Trailing commas are removed. *
  • {foo:"bar"} Unquoted property names are quoted. *
  • //comments JS style line and block comments are removed. *
  • (...) Grouping parentheses are removed. *
* * @param strict True to enable strict mode, false to disable it. * @see #isStrict() */ public void setStrict(boolean strict) { this.strict = strict; } /** * {@inheritDoc} */ public void startArray(String arrayName) { java.util.List currentVector; Map newOne; if(modern) { currentVector = new ArrayList(); } else { currentVector = new Vector(); } // the root of the JSON is an array, we need to wrap it in an assignment if (parseStack.size() == 0) { parseStack.add(state); currentKey = "root"; } if (isStackHash()) { getStackHash().put(currentKey, currentVector); currentKey = null; } else { getStackVec().add(currentVector); } parseStack.add(currentVector); } /** * {@inheritDoc} */ public void endArray(String arrayName) { parseStack.remove(parseStack.size() - 1); } /** * {@inheritDoc} */ public void stringToken(String tok) { if (isStackHash()) { if (currentKey == null) { currentKey = tok; } else { if (tok != null || isIncludeNullsInstance()) { getStackHash().put(currentKey, tok); } currentKey = null; } } else { getStackVec().add(tok); } } /** * {@inheritDoc} */ public void numericToken(double tok) { if (isStackHash()) { getStackHash().put(currentKey, new Double(tok)); currentKey = null; } else { getStackVec().add(new Double(tok)); } } /** * {@inheritDoc} */ public void longToken(long tok) { if (isStackHash()) { getStackHash().put(currentKey, new Long(tok)); currentKey = null; } else { getStackVec().add(new Long(tok)); } } /** * {@inheritDoc} */ public void booleanToken(boolean tok) { if (isStackHash()) { getStackHash().put(currentKey, tok); currentKey = null; } else { getStackVec().add(tok); } } /** * {@inheritDoc} */ public void keyValue(String key, String value) { } /** * {@inheritDoc} */ public boolean isAlive() { return true; } /** * Static method to convert the given {@link java.util.Map} to a valid JSON * representation. The values allowed types are: {@link java.lang.Number}, {@link java.lang.String}, {@link java.lang.Boolean}, * {@link java.util.List}, {@link java.util.Map} or null. * * Limited whitespace is inserted be make the resulting JSON string more * readable. * * Simple example of usage: *
{@code
     * Map person = new LinkedHashMap<>();
     * person.put("firstName", "Paco");
     * person.put("lastName", "Bellz");
     * person.put("isAlive", true);
     * person.put("age", 35);
     * person.put("weight (kg)", 70.7);
     *
     * Log.p("--- mapToJson() test");
     * Log.p("\n" + mapToJson(person));
     * }
* * The output will be: *
{@code
     * {
     *  "firstName": "Paco",
     *  "lastName": "Bellz",
     *  "isAlive": true,
     *  "age": 35,
     *  "weight (kg)": 70.7
     *}
     *}
* * More complex example of usage: *
{@code
     * Map phoneNumber1 = new LinkedHashMap<>();
     * phoneNumber1.put("home", "212 555-1234");
     * Map phoneNumber2 = new LinkedHashMap<>();
     * phoneNumber2.put("office", "646 555-4567");
     * Map phoneNumber3 = new LinkedHashMap<>();
     * phoneNumber3.put("mobile", "123 456-7890");
     * Map phoneNumber4 = new LinkedHashMap<>();
     * phoneNumber4.put("mobile", "06124578965");
     * ArrayList phoneNumbers = new ArrayList();
     * ArrayList phoneNumbers2 = new ArrayList();
     * phoneNumbers.add(phoneNumber1);
     * phoneNumbers.add(phoneNumber2);
     * phoneNumbers.add(phoneNumber3);
     * phoneNumbers2.add(phoneNumber4);
     * Map address1 = new LinkedHashMap<>();
     * address1.put("streetAddress", "53, London Street");
     * address1.put("city", "Paris");
     * address1.put("state", "FR");
     * address1.put("postalCode", "54856");
     * Map address2 = new LinkedHashMap<>();
     * address2.put("streetAddress", "21 2nd Street");
     * address2.put("city", "New York");
     * address2.put("state", "NY");
     * address2.put("postalCode", "10021-3100");
     * Map secondPerson = new LinkedHashMap<>();
     * secondPerson.put("firstName", "Gioia");
     * secondPerson.put("lastName", "Mia");
     * secondPerson.put("isAlive", true);
     * secondPerson.put("age", 34);
     * secondPerson.put("weight (kg)", 60.2);
     * secondPerson.put("address", address2);
     * address2.put("phoneNumbers", phoneNumbers2);
     * Map firstPerson = new LinkedHashMap<>();
     * firstPerson.put("firstName", "Paco");
     * firstPerson.put("lastName", "Bellz");
     * firstPerson.put("isAlive", true);
     * firstPerson.put("age", 35);
     * firstPerson.put("weight (kg)", 70.7);
     * firstPerson.put("address", address1);
     * firstPerson.put("partner", secondPerson);
     * firstPerson.put("children", new ArrayList());
     * firstPerson.put("extraInfo", null);
     * firstPerson.put("phoneNumbers", phoneNumbers);
     * List friends = new ArrayList();
     * friends.add("Paul");
     * friends.add("Karl");
     * friends.add("Mary");
     * firstPerson.put("onVacation", false);
     * firstPerson.put("friends", friends);
     *
     * Log.p("--- mapToJson() test");
     * Log.p("\n" + mapToJson(firstPerson));
     * }
* * The output will be: *
{@code
     * {
     *  "firstName": "Paco",
     *  "lastName": "Bellz",
     *  "isAlive": true,
     *  "age": 35,
     *  "weight (kg)": 70.7,
     *  "address": {
     *    "streetAddress": "53, London Street",
     *    "city": "Paris",
     *    "state": "FR",
     *    "postalCode": "54856"
     *  },
     *  "partner": {
     *    "firstName": "Gioia",
     *    "lastName": "Mia",
     *    "isAlive": true,
     *    "age": 34,
     *    "weight (kg)": 60.2,
     *    "address": {
     *      "streetAddress": "21 2nd Street",
     *      "city": "New York",
     *      "state": "NY",
     *      "postalCode": "10021-3100",
     *      "phoneNumbers": [{"mobile": "06124578965"}]
     *    }
     *  },
     *  "children": [],
     *  "extraInfo": null,
     *  "phoneNumbers": [
     *    {"home": "212 555-1234"},
     *    {"office": "646 555-4567"},
     *    {"mobile": "123 456-7890"}
     *  ],
     *  "onVacation": false,
     *  "friends": [
     *    "Paul",
     *    "Karl",
     *    "Mary"
     *  ]
     *}
     *}
* * @param map The map to be converted to a JSON string * @return The JSON string */ public static String mapToJson(Map map) { return Result.fromContent(map).toString(); } }