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&lt;String, Object&gt;}.
+ * <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"));
+    }
+}