Java tutorial
/* * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you 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 json.tests.reader; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import java.io.IOException; import java.io.Reader; /** * A custom JSON reader, which reads only a set of elements, from a JSON stream. */ public class JSONStreamReader extends JsonReader { // Name of the current element, if its a 'NAME' token private String currentElementName = null; // Value of the current element, if its a 'STRING'/'NUMBER'/'BOOLEAN' token private Object currentElementValue; // A stack of elements, to maintain the hierarchy of the current element. // Using a custom stack implementation for performance gain private ElementStack elementsStack = new ElementStack(10); // Elements to be retained when reading private String[] requiredElements; // Last token read by the reader private JsonToken lastToken; // Current token that this Reader points to private JsonToken currentToken = null; // Flag indicating the next object which follows after a BeginAObject/BeginArray token // is an anonymous object or not. private boolean isNextObjectAnonymous = true; // Temp name to assign for anonymous objects, when putting on to the elements stack private static final String ANONYMOUS = "ANONYMOUS"; // Element separator to define the full path to any element in the json, including its hierarchy private static final String SEPARATOR = "/"; private boolean hasNext = true; /** * Creates a new JSON reader that will read a JSON stream from a {@link java.io.Reader}. * @param in Reader * @param requiredElements Array of elements to be retained */ public JSONStreamReader(Reader in, String[] requiredElements) { super(in); this.requiredElements = requiredElements; } /** * {@inheritDoc} */ @Override public void beginArray() throws IOException { if (this.currentToken == JsonToken.BEGIN_ARRAY || super.peek() == JsonToken.BEGIN_ARRAY) { goToNextToken(false, true); } else { throw new IllegalStateException("Expected a BEGIN_ARRAY token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public void endArray() throws IOException { super.endArray(); this.elementsStack.pop(); this.isNextObjectAnonymous = true; goToNextToken(false, false); } /** * {@inheritDoc} */ @Override public void beginObject() throws IOException { if (this.currentToken == JsonToken.BEGIN_OBJECT || super.peek() == JsonToken.BEGIN_OBJECT) { goToNextToken(false, true); } else { throw new IllegalStateException("Expected a BEGIN_OBJECT token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public void endObject() throws IOException { super.endObject(); this.elementsStack.pop(); this.isNextObjectAnonymous = true; goToNextToken(false, false); } /** * {@inheritDoc} */ @Override public boolean hasNext() throws IOException { return this.hasNext; } /** * {@inheritDoc} */ @Override public JsonToken peek() throws IOException { if (this.currentToken == null) { return super.peek(); } else { return this.currentToken; } } /* * When any next****() method is called, current tokenName/tokenValue will be returned, and * the pointer will move on to the next required token. Current token is returned instead of * the next token, because this reader is always one token ahead of the super class. Because * we always have to consume the stream and read the token to check if its a required token, * so that peek() would always return only the next 'required' token, but not the next available * token. */ /** * {@inheritDoc} */ @Override public String nextName() throws IOException { if (this.currentToken == JsonToken.NAME) { String tempElementName = this.currentElementName; goToNextToken(false, false); return tempElementName; } else { throw new IllegalStateException("Expected a NAME token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public String nextString() throws IOException { if (this.currentToken == JsonToken.STRING || this.currentToken == JsonToken.NUMBER) { String tempElementValue; /* * {@link com.google.gson.JsonParser} reads all primitive values as String, * and then convert it to the corresponding type. Following block is to support * that requirement. */ if (this.currentElementValue instanceof Integer) { tempElementValue = Integer.toString((int) this.currentElementValue); } else if (this.currentElementValue instanceof Long) { tempElementValue = Long.toString((long) this.currentElementValue); } else if (this.currentElementValue instanceof Double) { tempElementValue = Double.toString((double) this.currentElementValue); } else if (this.currentElementValue instanceof Boolean) { tempElementValue = Boolean.toString((boolean) this.currentElementValue); } else { tempElementValue = (String) this.currentElementValue; } goToNextToken(false, false); return tempElementValue; } else { throw new IllegalStateException("Expected a STRING token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public boolean nextBoolean() throws IOException { if (this.currentToken == JsonToken.BOOLEAN) { boolean tempElementValue = (boolean) this.currentElementValue; goToNextToken(false, false); return tempElementValue; } else { throw new IllegalStateException("Expected a BOOLEAN token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public void nextNull() throws IOException { if (this.currentToken == JsonToken.NULL) { goToNextToken(false, false); } else { throw new IllegalStateException("Expected a NULL token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public double nextDouble() throws IOException { if (this.currentToken == JsonToken.NUMBER) { double tempElementValue = Double.parseDouble((String) this.currentElementValue); goToNextToken(false, false); return tempElementValue; } else { throw new IllegalStateException("Expected a NUMBER token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public long nextLong() throws IOException { if (this.currentToken == JsonToken.NUMBER) { long tempElementValue = Long.parseLong((String) this.currentElementValue); goToNextToken(false, false); return tempElementValue; } else { throw new IllegalStateException("Expected a NUMBER token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public int nextInt() throws IOException { if (this.currentToken == JsonToken.NUMBER) { int tempElementValue = Integer.parseInt((String) this.currentElementValue); goToNextToken(false, false); return tempElementValue; } else { throw new IllegalStateException("Expected a NUMBER token but found " + this.currentToken); } } /** * {@inheritDoc} */ @Override public void close() throws IOException { super.close(); } /** * {@inheritDoc} */ @Override public void skipValue() throws IOException { goToNextToken(false, false); } /** * {@inheritDoc} */ @Override public String toString() { return super.toString(); } /** * {@inheritDoc} */ @Override public String getPath() { return super.getPath(); } /** * Move the pointer to the next required element of the json stream. * * @param skip Flag indicating whether the current object is a required element * @param isStartElement Flag indicating whether to traverse inside the object or not. Value for this is * 'true' when this method is called by {@link #beginObject()} or {@link #beginArray()} * methods. * @throws IOException */ private void goToNextToken(boolean skip, boolean isStartElement) throws IOException { while (super.hasNext()) { this.hasNext = true; this.lastToken = this.currentToken; this.currentToken = super.peek(); switch (this.currentToken) { case BEGIN_OBJECT: if (skip) { super.skipValue(); popNonAnonymousObject(); break; } handleObject(skip, isStartElement); return; case BEGIN_ARRAY: if (skip) { super.skipValue(); popNonAnonymousObject(); break; } handleArray(skip, isStartElement); return; case NAME: this.currentElementName = super.nextName(); skip = !isRequired(this.currentElementName); // Whenever a element name is read, add it to the stack. this.elementsStack.push(new JsonElement(this.currentElementName, null)); this.isNextObjectAnonymous = false; if (!skip) { return; } break; case NUMBER: popPrimitive(); if (!skip) { /* * Number can be int/long/double. hence reading it as string, and then parsing it * to the corresponding type, during #next****() method. */ this.currentElementValue = super.nextString(); return; } super.skipValue(); break; case STRING: popPrimitive(); if (!skip) { this.currentElementValue = super.nextString(); return; } super.skipValue(); break; case BOOLEAN: popPrimitive(); if (!skip) { this.currentElementValue = super.nextBoolean(); return; } super.skipValue(); break; case END_DOCUMENT: this.hasNext = false; return; default: super.skipValue(); } isStartElement = false; } // when exiting the current element, set the current token type to END_OBJECT/END_ARRAY if (!this.elementsStack.isEmpty()) { ElementType startedElementType = this.elementsStack.peek().getType(); if (startedElementType == ElementType.OBJECT) { this.currentToken = JsonToken.END_OBJECT; } else if (startedElementType == ElementType.ARRAY) { this.currentToken = JsonToken.END_ARRAY; } } this.hasNext = false; } /** * If this is called by a beginObject/beginArray method, then traverse through the Object, * and go to the next required token. * Else, current token is the next required token, hence stop and return. * * @param skip Flag indicating whether the current object is a required element * @param isStartElement Flag indicating whether to traverse inside the object or not * @throws IOException */ private void handleObject(boolean skip, boolean isStartElement) throws IOException { if (isStartElement) { // if the object is anonymous, then add it to the stack if (this.isNextObjectAnonymous) { this.elementsStack.push(new JsonElement(ANONYMOUS, ElementType.OBJECT)); } else { // if the object is a named-object, then that element is already added to the stack during 'NAME' block. // Hence pop it from stack and re-add it with the element-type. this.elementsStack.pop(); this.elementsStack.push(new JsonElement(this.currentElementName, ElementType.OBJECT)); } super.beginObject(); this.isNextObjectAnonymous = true; goToNextToken(skip, false); } } /** If this is called by a beginObject/beginArray method, then traverse through the Object, * and go to the next required token. * Else, current token is the next required token, hence stop and return. * * @param skip Flag indicating whether the current object is a required element * @param isStartElement Flag indicating whether to traverse inside the object or not * @throws IOException */ private void handleArray(boolean skip, boolean isStartElement) throws IOException { if (isStartElement) { // if the array is anonymous, then add it to the stack if (this.isNextObjectAnonymous) { this.elementsStack.push(new JsonElement(ANONYMOUS, ElementType.ARRAY)); } else { // if the array is a named-array, then that element is already added to the stack during 'NAME' block. // Hence pop it from stack and re-add it with the element-type. this.elementsStack.pop(); this.elementsStack.push(new JsonElement(this.currentElementName, ElementType.ARRAY)); } super.beginArray(); this.isNextObjectAnonymous = true; goToNextToken(skip, false); } } /** * If the current object is not an anonymous object, pop the last element from stack, * which is added by the NAME block. An anonymous object is an object without a name. */ private void popNonAnonymousObject() { // no need to empty-check if (!this.isNextObjectAnonymous) { this.elementsStack.pop(); } } /** * Remove a primitive value from the current element stack. */ private void popPrimitive() { // no need to empty-check if (this.lastToken == JsonToken.NAME) { this.elementsStack.pop(); } } /** * Check whether this element is required. Return true if: * <ul> * <li>The current element is a parent of, any of the required elements.</li> * <li>One of the required elements, if a parent of this element.</li> * </ul> * * @param element Current element * @return Flag indicating whether this element is required or not */ private boolean isRequired(String element) { // If the required elements list is not set, consider all elements as required. if (this.requiredElements == null) { return true; } /* * Using string-concat over string builder here, as it is better in performance for * concatenating fewer items. Here the number of items to concat is equal to the depth of the * json element in the element hierarchy, which is a low value (less than 10) for most cases. */ String elementPath = SEPARATOR; for (JsonElement jsonElement : this.elementsStack.toArray()) { if (jsonElement != null && !jsonElement.getName().equals(ANONYMOUS)) { elementPath = elementPath + SEPARATOR + jsonElement.getName(); } } elementPath = elementPath + SEPARATOR + element; // check whether this element is in the required-list for (String reqiredElement : this.requiredElements) { if (reqiredElement.startsWith(elementPath) || elementPath.startsWith(reqiredElement)) { return true; } } return false; } }