Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id C0372200C25 for ; Fri, 10 Feb 2017 02:43:26 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id BEA7A160B64; Fri, 10 Feb 2017 01:43:26 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id BC008160B50 for ; Fri, 10 Feb 2017 02:43:25 +0100 (CET) Received: (qmail 31997 invoked by uid 500); 10 Feb 2017 01:43:24 -0000 Mailing-List: contact commits-help@felix.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@felix.apache.org Delivered-To: mailing list commits@felix.apache.org Received: (qmail 31988 invoked by uid 99); 10 Feb 2017 01:43:24 -0000 Received: from Unknown (HELO svn01-us-west.apache.org) (209.188.14.144) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 10 Feb 2017 01:43:24 +0000 Received: from svn01-us-west.apache.org (localhost [127.0.0.1]) by svn01-us-west.apache.org (ASF Mail Server at svn01-us-west.apache.org) with ESMTP id 341EA3A0F9A for ; Fri, 10 Feb 2017 01:43:24 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: 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/ Date: Fri, 10 Feb 2017 01:43:23 -0000 To: commits@felix.apache.org From: davidb@apache.org X-Mailer: svnmailer-1.0.9 Message-Id: <20170210014324.341EA3A0F9A@svn01-us-west.apache.org> archived-at: Fri, 10 Feb 2017 01:43:26 -0000 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: + *
    + *
  • Object names are represented as a {@link String}. + *
  • String values are represented as a {@link String}. + *
  • Numeric values without a decimal separator are represented as a {@link Long}. + *
  • Numeric values with a decimal separator are represented as a {@link Double}. + *
  • Boolean values are represented as a {@link Boolean}. + *
  • Nested JSON objects are parsed into a {@link java.util.Map Map<String, Object>}. + *
  • JSON lists are parsed into a {@link java.util.List} which may contain any of the above values. + *
+ */ +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 { + final K key; + final V value; + + Pair(K k, V v) { + key = k; + value = v; + } + } + + private final Map 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 getParsed() { + return parsed; + } + + private static Pair 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(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 entries = new ArrayList(); + 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 parseObject(String jsonObject) { + if (!(jsonObject.startsWith("{") && jsonObject.endsWith("}"))) + throw new IllegalArgumentException("Malformatted JSON object: " + jsonObject); + + Map values = new HashMap(); + + jsonObject = jsonObject.substring(1, jsonObject.length() - 1).trim(); + if (jsonObject.length() == 0) + return values; + + for (String element : parseKeyValueListRaw(jsonObject)) { + Pair pair = parseKeyValue(element); + values.put(pair.key, pair.value); + } + + return values; + } + + private static List parseKeyValueListRaw(String jsonKeyValueList) { + if (jsonKeyValueList.trim().length() == 0) + return Collections.emptyList(); + jsonKeyValueList = jsonKeyValueList + ","; // append comma to simplify parsing + List elements = new ArrayList(); + + int i=0; + int start=0; + Stack scopeStack = new Stack(); + 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 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 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 m = jp.getParsed(); + assertEquals(3, m.size()); + assertEquals(Arrays.asList(1L, 2L, 3L, 4L, 5L), m.get("a")); + Map mb = (Map) m.get("b"); + assertEquals(3, mb.size()); + assertEquals(12L, mb.get("x")); + assertEquals(42L, mb.get("y")); + Map mz = (Map) 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 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 m = jp.getParsed(); + assertEquals(1, m.size()); + Map result = new HashMap(); + result.put("def", Collections.emptyList()); + assertEquals(result, m.get("abc")); + } +}