package org.teiid.resource.adapter.google.dataprotocol; import java.io.IOException; import java.io.PushbackReader; import java.io.Reader; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.LinkedHashMap; import java.util.List; import org.teiid.core.util.StringUtil; import org.teiid.translator.google.api.SpreadsheetOperationException; /** * Parsing google json is a little non-standard. They assume a js binding, so array syntax, strings, and date are used. * This parser supports most of the customizations except for unquoted dictionary keys. * * Assumes all numbers are properly represented by Double. */ public class GoogleJSONParser { private static final class ReaderCharSequence implements CharSequence { Reader r; int i = -1; @Override public CharSequence subSequence(int start, int end) { throw new UnsupportedOperationException(); } @Override public int length() { return Integer.MAX_VALUE; } @Override public char charAt(int index) { if (index != ++i) { throw new IllegalStateException(); } int result; try { result = r.read(); } catch (IOException e) { throw new SpreadsheetOperationException(e); } if (result == -1) { throw new SpreadsheetOperationException("Read end of stream before the end of a string value"); } return (char)result; } } private Calendar cal; private int[] parts = new int[7]; private StringBuilder sb = new StringBuilder(); private ReaderCharSequence charSequence = new ReaderCharSequence(); public Object parseObject(Reader r, boolean wrapped) throws IOException { if (wrapped) { while (true) { int c = r.read(); if (c == -1) { return null; } if (c == '(') { break; } } } return parseObject(new PushbackReader(r), skipWhitespace(r)); } private Object parseObject(PushbackReader r, int c) throws IOException { switch (c) { case '{': LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>(); while (true) { c = skipWhitespace(r); switch (c) { case '}': return map; case ',': //this is lenient continue; } String s = parseString(r, c); c = skipWhitespace(r); if (c != ':') { throw new SpreadsheetOperationException("Expected : in object name value pair"); } c = skipWhitespace(r); Object o = parseObject(r, c); map.put(s, o); } case '[': List<Object> array = new ArrayList<Object>(); boolean seenComma = true; while (true) { c = skipWhitespace(r); switch (c) { case ',': //special handling for google arrays if (seenComma) { array.add(null); } seenComma = true; break; case ']': return array; default: seenComma = false; Object o = parseObject(r, c); array.add(o); } } case -1: return null; case '"': case '\'': return parseString(r, c); default: return parseLiteral(r, (char)c); } } private Object parseLiteral(PushbackReader r, char c) throws IOException { sb.setLength(0); do { sb.append(c); int i = r.read(); if (i == -1) { break; } c = (char)i; } while (!Character.isWhitespace(c) && c != ',' && c != ']' && c != '}'); //date handling if (areEquals(sb, "new")) { //$NON-NLS-1$ sb.setLength(0); int length = 0; Arrays.fill(parts, 0); for (int i = 0; ; i++) { if (i > 5) { //remove " Date(" if (c == ',' || c == ')') { if (length > 6) { throw new SpreadsheetOperationException("Too many date fields"); } parts[length++] = Integer.valueOf(sb.toString()); if (c == ')') { break; } sb.setLength(0); } else { sb.append(c); } } int chr = r.read(); if (chr == -1) { throw new SpreadsheetOperationException("Encountered end of stream in date value"); } c = (char)chr; } if (length > 3) { Calendar calendar = getCalendar(); calendar.set(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]); Timestamp ts = new Timestamp(calendar.getTimeInMillis()); ts.setNanos(parts[6]*1000000); //convert from millis to nanos return ts; } Calendar calendar = getCalendar(); calendar.set(parts[0], parts[1], parts[2]); return new java.sql.Date(cal.getTimeInMillis()); } if (!Character.isWhitespace(c)) { r.unread(c); //the terminating character is still needed by the caller //TODO could hold this state so that a pushback reader is not needed } if (areEquals(sb, "false")) { //$NON-NLS-1$ return Boolean.FALSE; } else if (areEquals(sb, "true")) { //$NON-NLS-1$ return Boolean.TRUE; } else if (areEquals(sb, "null")) { //$NON-NLS-1$ return null; } return Double.valueOf(sb.toString()); } private boolean areEquals(CharSequence cs, CharSequence cs1) { if (cs.length() != cs1.length()) { return false; } for (int i = 0; i < cs.length(); i++) { if (cs.charAt(i) != cs1.charAt(i)) { return false; } } return true; } Calendar getCalendar() { if (cal == null) { cal = Calendar.getInstance(); } cal.clear(); return cal; } public void setCalendar(Calendar cal) { this.cal = cal; } private String parseString(final Reader r, int quoteChar) { if (quoteChar != '"' && quoteChar != '\'') { throw new IllegalStateException(); } charSequence.i = -1; charSequence.r = r; sb.setLength(0); return StringUtil.unescape(charSequence, quoteChar, false, sb); } private int skipWhitespace(Reader r) throws IOException { while (true) { int c = r.read(); if (c == -1) { return c; } if (!Character.isWhitespace((char)c)) { return c; } } } }