/* * 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 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.Map; import java.util.Vector; /** * <p>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. </p> * * <p> * 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. * </p> * * <p> * The sample below includes JSON from <a href="https://anapioficeandfire.com/">https://anapioficeandfire.com/</a> * generated by the query <a href="http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3">http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3</a>: * </p> * <script src="https://gist.github.com/codenameone/f9fdacaac12583cd2eed.js"></script> * <img src="https://www.codenameone.com/img/developer-guide/json-parsing.png" alt="JSON Parsing Result"> * * * <p>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.</p> * <script src="https://gist.github.com/codenameone/22efe9e04e2b8986dfc3.js"></script> * * @author Shai Almog */ public class JSONParser implements JSONParseCallback { /** * Indicates that the parser will generate long objects and not just doubles for numeric values * @return the useLongsDefault */ public static boolean isUseLongs() { return useLongsDefault; } /** * Indicates that the parser will generate long objects and not just doubles for numeric values * @param aUseLongsDefault the useLongsDefault to set */ public static void setUseLongs(boolean aUseLongsDefault) { useLongsDefault = aUseLongsDefault; } /** * Indicates that the parser will include null values in the parsed output * @return the includeNullsDefault */ public static boolean isIncludeNulls() { return includeNullsDefault; } /** * Indicates that the parser will include null values in the parsed output * @param aIncludeNullsDefault the includeNullsDefault to set */ public static void setIncludeNulls(boolean aIncludeNullsDefault) { includeNullsDefault = aIncludeNullsDefault; } 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 static boolean includeNullsDefault; private boolean modern; private Map<String, Object> state; private java.util.List<Object> parseStack; private String currentKey; 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') { 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') { 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(useLongsDefault) { 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(useLongsDefault) { 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(useLongsDefault) { 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(); } } /** * <p> * Parses the given input stream into this object and returns the parse tree.<br> * 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. * </p> * <p> * The sample below includes JSON from <a href="https://anapioficeandfire.com/">https://anapioficeandfire.com/</a> * generated by the query <a href="http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3">http://www.anapioficeandfire.com/api/characters?page=5&pageSize=3</a>: * </p> * <script src="https://gist.github.com/codenameone/f9fdacaac12583cd2eed.js"></script> * <img src="https://www.codenameone.com/img/developer-guide/json-parsing.png" alt="JSON Parsing Result"> * * @param i the reader * @return the parse tree as a hashtable * @throws IOException if thrown by the stream */ public Map<String, Object> parseJSON(Reader i) throws IOException { modern = true; state = new LinkedHashMap<String, Object>(); parseStack = new ArrayList<Object>(); currentKey = null; 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<String, Object> parse(Reader i) throws IOException { modern = false; state = new Hashtable(); parseStack = new Vector(); currentKey = null; parse(i, this); return (Hashtable<String, Object>)state; } private boolean isStackHash() { return parseStack.get(parseStack.size() - 1) instanceof Map; } private Map getStackHash() { return (Map) parseStack.get(parseStack.size() - 1); } private java.util.List<Object> getStackVec() { return (java.util.List<Object>) 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); } /** * {@inheritDoc} */ public void startArray(String arrayName) { java.util.List<Object> currentVector; Map newOne; if(modern) { currentVector = new ArrayList<Object>(); } else { currentVector = new Vector<Object>(); } // 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 || isIncludeNulls()) { 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 keyValue(String key, String value) { } /** * {@inheritDoc} */ public boolean isAlive() { return true; } }