/* * Copyright 2003-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.forgerock.openicf.connectors.scriptedcrest; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.http.HttpEntity; import org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport; import org.codehaus.groovy.runtime.IOGroovyMethods; import org.forgerock.json.resource.QueryResult; import groovy.json.JsonException; import groovy.json.JsonLexer; import groovy.json.JsonToken; import groovy.lang.Closure; import static groovy.json.JsonTokenType.*; /** * JSON slurper which parses text or reader content into a data structure of lists and maps. * <p/> * Example usage: <code><pre> * def slurper = new JsonSlurper() * def result = slurper.parseText('{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}') * <p/> * assert result.person.name == "Guillaume" * assert result.person.age == 33 * assert result.person.pets.size() == 2 * assert result.person.pets[0] == "dog" * assert result.person.pets[1] == "cat" * </pre></code> * * @author Guillaume Laforge * @since 1.8.0 */ public class StreamingJsonSlurper { /** * Parse a JSON data structure from content from a reader * * @param reader * reader over a JSON content * @return a data structure of lists and maps */ public Object parse(Reader reader, Closure handler, Closure<Object> callback) { Object content = null; JsonLexer lexer = new JsonLexer(reader); JsonToken token = lexer.nextToken(); if (token.getType() == OPEN_CURLY) { content = parseObject(lexer, handler); } else if (token.getType() == OPEN_BRACKET) { parseArray(lexer, handler); } else { throw new JsonException("A JSON payload should start with " + OPEN_CURLY.getLabel() + " or " + OPEN_BRACKET.getLabel() + ".\n" + "Instead, '" + token.getText() + "' was found " + "on line: " + token.getStartLine() + ", " + "column: " + token.getStartColumn()); } if (null != callback) { callback.call(lexer, content); } return content; } public Object parse(HttpEntity entity, Closure handler, Closure<Object> callback) { Reader reader = null; try { return parse(IOGroovyMethods.newReader(entity.getContent()), handler, callback); } catch (IOException ioe) { throw new JsonException("Unable to process Entity Content", ioe); } finally { if (reader != null) { DefaultGroovyMethodsSupport.closeWithWarning(reader); } } } /** * Parse an array from the lexer * * @param lexer * the lexer * @return a list of JSON values */ @SuppressWarnings("unchecked") private List parseArray(JsonLexer lexer, Closure handler) { List content = new ArrayList(); JsonToken currentToken; for (; ; ) { currentToken = lexer.nextToken(); if (currentToken == null) { throw new JsonException("Expected a value on line: " + lexer.getReader().getLine() + ", " + "column: " + lexer.getReader().getColumn() + ".\n" + "But got an unterminated array."); } if (currentToken.getType() == OPEN_CURLY) { if (null != handler) { handler.call(parseObject(lexer, null)); } else { content.add(parseObject(lexer, handler)); } } else if (currentToken.getType() == OPEN_BRACKET) { content.add(parseArray(lexer, handler)); } else if (currentToken.getType().ordinal() >= NULL.ordinal()) { content.add(currentToken.getValue()); } else if (currentToken.getType() == CLOSE_BRACKET) { return content; } else { throw new JsonException("Expected a value, an array, or an object " + "on line: " + currentToken.getStartLine() + ", " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } currentToken = lexer.nextToken(); if (currentToken == null) { throw new JsonException("Expected " + CLOSE_BRACKET.getLabel() + " " + "or " + COMMA.getLabel() + " " + "on line: " + lexer.getReader().getLine() + ", " + "column: " + lexer.getReader().getColumn() + ".\n" + "But got an unterminated array."); } // Expect a comma for an upcoming value // or a closing bracket for the end of the array if (currentToken.getType() == CLOSE_BRACKET) { break; } else if (currentToken.getType() != COMMA) { throw new JsonException("Expected a value or " + CLOSE_BRACKET.getLabel() + " " + "on line: " + currentToken.getStartLine() + " " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } } return content; } /** * Parses an object from the lexer * * @param lexer * the lexer * @return a Map representing a JSON object */ private Map parseObject(JsonLexer lexer, Closure handler) { Map content = new HashMap(); JsonToken previousToken = null; JsonToken currentToken = null; for (; ; ) { currentToken = lexer.nextToken(); if (currentToken == null) { throw new JsonException("Expected a String key on line: " + lexer.getReader().getLine() + ", " + "column: " + lexer.getReader().getColumn() + ".\n" + "But got an unterminated object."); } // expect a string key, or already a closing curly brace if (currentToken.getType() == CLOSE_CURLY) { return content; } else if (currentToken.getType() != STRING) { throw new JsonException("Expected " + STRING.getLabel() + " key " + "on line: " + currentToken.getStartLine() + ", " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } String mapKey = (String) currentToken.getValue(); currentToken = lexer.nextToken(); if (currentToken == null) { throw new JsonException("Expected a " + COLON.getLabel() + " " + "on line: " + lexer.getReader().getLine() + ", " + "column: " + lexer.getReader().getColumn() + ".\n" + "But got an unterminated object."); } // expect a colon between the key and value pair if (currentToken.getType() != COLON) { throw new JsonException("Expected " + COLON.getLabel() + " " + "on line: " + currentToken.getStartLine() + ", " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } currentToken = lexer.nextToken(); if (currentToken == null) { throw new JsonException("Expected a value " + "on line: " + lexer.getReader().getLine() + ", " + "column: " + lexer.getReader().getColumn() + ".\n" + "But got an unterminated object."); } // value can be an object, an array, a number, string, boolean or // null values if (currentToken.getType() == OPEN_CURLY) { content.put(mapKey, parseObject(lexer, null)); } else if (currentToken.getType() == OPEN_BRACKET) { if (mapKey.equals(QueryResult.FIELD_RESULT)) { content.put(mapKey, parseArray(lexer, handler)); } else { content.put(mapKey, parseArray(lexer, null)); } } else if (currentToken.getType().ordinal() >= NULL.ordinal()) { content.put(mapKey, currentToken.getValue()); } else { throw new JsonException("Expected a value, an array, or an object " + "on line: " + currentToken.getStartLine() + ", " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } previousToken = currentToken; currentToken = lexer.nextToken(); // premature end of the object if (currentToken == null) { throw new JsonException("Expected " + CLOSE_CURLY.getLabel() + " or " + COMMA.getLabel() + " " + "on line: " + previousToken.getEndLine() + ", " + "column: " + previousToken.getEndColumn() + ".\n" + "But got an unterminated object."); } // Expect a comma for an upcoming key/value pair // or a closing curly brace for the end of the object if (currentToken.getType() == CLOSE_CURLY) { break; } else if (currentToken.getType() != COMMA) { throw new JsonException("Expected a value or " + CLOSE_CURLY.getLabel() + " " + "on line: " + currentToken.getStartLine() + ", " + "column: " + currentToken.getStartColumn() + ".\n" + "But got '" + currentToken.getText() + "' instead."); } } return content; } }