You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by da...@apache.org on 2017/02/10 01:43:23 UTC
svn commit: r1782421 - in /felix/trunk/utils/src:
main/java/org/apache/felix/utils/io/ main/java/org/apache/felix/utils/json/
test/java/org/apache/felix/utils/json/
Author: davidb
Date: Fri Feb 10 01:43:23 2017
New Revision: 1782421
URL: http://svn.apache.org/viewvc?rev=1782421&view=rev
Log:
FELIX-5508 Add simple JSON Parser to utils project.
Also included: InputStreams utility method to read an InputStream fully.
Added:
felix/trunk/utils/src/main/java/org/apache/felix/utils/io/
felix/trunk/utils/src/main/java/org/apache/felix/utils/io/InputStreams.java
felix/trunk/utils/src/main/java/org/apache/felix/utils/json/JSONParser.java
felix/trunk/utils/src/test/java/org/apache/felix/utils/json/
felix/trunk/utils/src/test/java/org/apache/felix/utils/json/JSONParserTest.java
Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/io/InputStreams.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/io/InputStreams.java?rev=1782421&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/io/InputStreams.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/io/InputStreams.java Fri Feb 10 01:43:23 2017
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.felix.utils.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class InputStreams {
+ private InputStreams() {} // prevent instantiation
+
+ /**
+ * Read an entire input stream into a byte array.
+ * @param is The input stream to read.
+ * @return The byte array with the contents of the input stream.
+ * @throws IOException if the underlying read operation on the input stream
+ * throws an error.
+ */
+ public static byte [] readStream(InputStream is) throws IOException {
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] bytes = new byte[65536];
+
+ int length = 0;
+ int offset = 0;
+
+ while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) {
+ offset += length;
+
+ if (offset == bytes.length) {
+ baos.write(bytes, 0, bytes.length);
+ offset = 0;
+ }
+ }
+ if (offset != 0) {
+ baos.write(bytes, 0, offset);
+ }
+ return baos.toByteArray();
+ } finally {
+ is.close();
+ }
+ }
+}
Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/json/JSONParser.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/json/JSONParser.java?rev=1782421&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/json/JSONParser.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/json/JSONParser.java Fri Feb 10 01:43:23 2017
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.felix.utils.json;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.felix.utils.io.InputStreams;
+
+/**
+ * A very small JSON parser.
+ *
+ * The JSON input is parsed into an object structure in the following way:
+ * <ul>
+ * <li>Object names are represented as a {@link String}.
+ * <li>String values are represented as a {@link String}.
+ * <li>Numeric values without a decimal separator are represented as a {@link Long}.
+ * <li>Numeric values with a decimal separator are represented as a {@link Double}.
+ * <li>Boolean values are represented as a {@link Boolean}.
+ * <li>Nested JSON objects are parsed into a {@link java.util.Map Map<String, Object>}.
+ * <li>JSON lists are parsed into a {@link java.util.List} which may contain any of the above values.
+ * </ul>
+ */
+public class JSONParser {
+ private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^\\s*[\"](.+?)[\"]\\s*[:]\\s*(.+)$");
+
+ private enum Scope { QUOTE, CURLY, BRACKET;
+ static Scope getScope(char c) {
+ switch (c) {
+ case '"':
+ return QUOTE;
+ case '[':
+ case ']':
+ return BRACKET;
+ case '{':
+ case '}':
+ return CURLY;
+ default:
+ return null;
+ }
+ }
+ }
+
+ static class Pair<K, V> {
+ final K key;
+ final V value;
+
+ Pair(K k, V v) {
+ key = k;
+ value = v;
+ }
+ }
+
+ private final Map<String, Object> parsed;
+
+ public JSONParser(CharSequence json) {
+ String str = json.toString();
+ str = str.trim().replace('\n', ' ');
+ parsed = parseObject(str);
+ }
+
+ public JSONParser(InputStream is) throws IOException {
+ this(readStreamAsString(is));
+ }
+
+ public Map<String, Object> getParsed() {
+ return parsed;
+ }
+
+ private static Pair<String, Object> parseKeyValue(String jsonKeyValue) {
+ Matcher matcher = KEY_VALUE_PATTERN.matcher(jsonKeyValue);
+ if (!matcher.matches() || matcher.groupCount() < 2) {
+ throw new IllegalArgumentException("Malformatted JSON key-value pair: " + jsonKeyValue);
+ }
+
+ return new Pair<String, Object>(matcher.group(1), parseValue(matcher.group(2)));
+ }
+
+ private static Object parseValue(String jsonValue) {
+ jsonValue = jsonValue.trim();
+
+ switch (jsonValue.charAt(0)) {
+ case '\"':
+ if (!jsonValue.endsWith("\""))
+ throw new IllegalArgumentException("Malformatted JSON string: " + jsonValue);
+
+ return jsonValue.substring(1, jsonValue.length() - 1);
+ case '[':
+ List<Object> entries = new ArrayList<Object>();
+ for (String v : parseListValuesRaw(jsonValue)) {
+ entries.add(parseValue(v));
+ }
+ return entries;
+ case '{':
+ return parseObject(jsonValue);
+ case 't':
+ case 'T':
+ case 'f':
+ case 'F':
+ return Boolean.parseBoolean(jsonValue);
+ case 'n':
+ case 'N':
+ return null;
+ default:
+ if (jsonValue.contains(".")) {
+ return Double.parseDouble(jsonValue);
+ }
+ return Long.parseLong(jsonValue);
+ }
+ }
+
+ private static Map<String, Object> parseObject(String jsonObject) {
+ if (!(jsonObject.startsWith("{") && jsonObject.endsWith("}")))
+ throw new IllegalArgumentException("Malformatted JSON object: " + jsonObject);
+
+ Map<String, Object> values = new HashMap<String, Object>();
+
+ jsonObject = jsonObject.substring(1, jsonObject.length() - 1).trim();
+ if (jsonObject.length() == 0)
+ return values;
+
+ for (String element : parseKeyValueListRaw(jsonObject)) {
+ Pair<String, Object> pair = parseKeyValue(element);
+ values.put(pair.key, pair.value);
+ }
+
+ return values;
+ }
+
+ private static List<String> parseKeyValueListRaw(String jsonKeyValueList) {
+ if (jsonKeyValueList.trim().length() == 0)
+ return Collections.emptyList();
+ jsonKeyValueList = jsonKeyValueList + ","; // append comma to simplify parsing
+ List<String> elements = new ArrayList<String>();
+
+ int i=0;
+ int start=0;
+ Stack<Scope> scopeStack = new Stack<Scope>();
+ while (i < jsonKeyValueList.length()) {
+ char curChar = jsonKeyValueList.charAt(i);
+ switch (curChar) {
+ case '"':
+ if (i > 0 && jsonKeyValueList.charAt(i-1) == '\\') {
+ // it's escaped, ignore for now
+ } else {
+ if (!scopeStack.empty() && scopeStack.peek() == Scope.QUOTE) {
+ scopeStack.pop();
+ } else {
+ scopeStack.push(Scope.QUOTE);
+ }
+ }
+ break;
+ case '[':
+ case '{':
+ if ((scopeStack.empty() ? null : scopeStack.peek()) == Scope.QUOTE) {
+ // inside quotes, ignore
+ } else {
+ scopeStack.push(Scope.getScope(curChar));
+ }
+ break;
+ case ']':
+ case '}':
+ Scope curScope = scopeStack.empty() ? null : scopeStack.peek();
+ if (curScope == Scope.QUOTE) {
+ // inside quotes, ignore
+ } else {
+ Scope newScope = Scope.getScope(curChar);
+ if (curScope == newScope) {
+ scopeStack.pop();
+ } else {
+ throw new IllegalArgumentException("Unbalanced closing " +
+ curChar + " in: " + jsonKeyValueList);
+ }
+ }
+ break;
+ case ',':
+ if (scopeStack.empty()) {
+ elements.add(jsonKeyValueList.substring(start, i));
+ start = i+1;
+ }
+ break;
+ }
+
+ i++;
+ }
+ return elements;
+ }
+
+ private static List<String> parseListValuesRaw(String jsonList) {
+ if (!(jsonList.startsWith("[") && jsonList.endsWith("]")))
+ throw new IllegalArgumentException("Malformatted JSON list: " + jsonList);
+
+ jsonList = jsonList.substring(1, jsonList.length() - 1);
+ return parseKeyValueListRaw(jsonList);
+ }
+
+ private static String readStreamAsString(InputStream is) throws IOException {
+ byte [] bytes = InputStreams.readStream(is);
+ if (bytes.length < 5)
+ // need at least 5 bytes to establish the encoding
+ throw new IllegalArgumentException("Malformatted JSON");
+
+ int offset = 0;
+ if ((bytes[0] == -1 && bytes[1] == -2)
+ || (bytes[0] == -2 && bytes[1] == -1)) {
+ // Skip UTF16/UTF32 Byte Order Mark (BOM)
+ offset = 2;
+ }
+
+ /* Infer the encoding as described in section 3 of http://www.ietf.org/rfc/rfc4627.txt
+ * which reads:
+ * Encoding
+ *
+ * JSON text SHALL be encoded in Unicode. The default encoding is
+ * UTF-8.
+ *
+ * Since the first two characters of a JSON text will always be ASCII
+ * characters [RFC0020], it is possible to determine whether an octet
+ * stream is UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or LE) by looking
+ * at the pattern of nulls in the first four octets.
+ *
+ * 00 00 00 xx UTF-32BE
+ * 00 xx 00 xx UTF-16BE
+ * xx 00 00 00 UTF-32LE
+ * xx 00 xx 00 UTF-16LE
+ * xx xx xx xx UTF-8
+ */
+ String encoding;
+ if (bytes[offset + 2] == 0) {
+ if (bytes[offset + 1] != 0) {
+ encoding = "UTF-16";
+ } else {
+ encoding = "UTF-32";
+ }
+ } else if (bytes[offset + 1] == 0) {
+ encoding = "UTF-16";
+ } else {
+ encoding = "UTF-8";
+ }
+ return new String(bytes, encoding);
+ }
+}
\ No newline at end of file
Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/json/JSONParserTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/json/JSONParserTest.java?rev=1782421&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/json/JSONParserTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/json/JSONParserTest.java Fri Feb 10 01:43:23 2017
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.felix.utils.json;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JSONParserTest {
+ @Test
+ public void testJsonSimple() {
+ String json = "{\"hi\": \"ho\", \"ha\": true}";
+ JSONParser jp = new JSONParser(json);
+ Map<String, Object> m = jp.getParsed();
+ assertEquals(2, m.size());
+ assertEquals("ho", m.get("hi"));
+ assertTrue((Boolean) m.get("ha"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testJsonComplex() {
+ String json = "{\"a\": [1,2,3,4,5], \"b\": {\"x\": 12, \"y\": 42, \"z\": {\"test test\": \"hello hello\"}}, \"ddd\": 12.34}";
+ JSONParser jp = new JSONParser(json);
+ Map<String, Object> m = jp.getParsed();
+ assertEquals(3, m.size());
+ assertEquals(Arrays.asList(1L, 2L, 3L, 4L, 5L), m.get("a"));
+ Map<String, Object> mb = (Map<String, Object>) m.get("b");
+ assertEquals(3, mb.size());
+ assertEquals(12L, mb.get("x"));
+ assertEquals(42L, mb.get("y"));
+ Map<String, Object> mz = (Map<String, Object>) mb.get("z");
+ assertEquals(1, mz.size());
+ assertEquals("hello hello", mz.get("test test"));
+ assertEquals(12.34d, ((Double) m.get("ddd")).doubleValue(), 0.0001d);
+ }
+
+ @Test
+ public void testJsonArray() {
+ String json = "{\"abc\": [\"x\", \"y\", \"z\"]}";
+ JSONParser jp = new JSONParser(json);
+ Map<String, Object> m = jp.getParsed();
+ assertEquals(1, m.size());
+ assertEquals(Arrays.asList("x", "y", "z"), m.get("abc"));
+ }
+
+ @Test
+ public void testEmptyJsonArray() {
+ String json = "{\"abc\": {\"def\": []}}";
+ JSONParser jp = new JSONParser(json);
+ Map<String, Object> m = jp.getParsed();
+ assertEquals(1, m.size());
+ Map<String, Object> result = new HashMap<String, Object>();
+ result.put("def", Collections.emptyList());
+ assertEquals(result, m.get("abc"));
+ }
+}