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 2016/07/05 12:34:20 UTC

svn commit: r1751460 - in /felix/trunk/converter: pom.xml src/main/java/org/apache/felix/converter/impl/Util.java src/main/java/org/apache/felix/converter/impl/json/JsonParser.java src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java

Author: davidb
Date: Tue Jul  5 12:34:20 2016
New Revision: 1751460

URL: http://svn.apache.org/viewvc?rev=1751460&view=rev
Log:
Felix Converter Service - add the start of a simple JSON parser

Added:
    felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonParser.java
    felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java
Modified:
    felix/trunk/converter/pom.xml
    felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/Util.java

Modified: felix/trunk/converter/pom.xml
URL: http://svn.apache.org/viewvc/felix/trunk/converter/pom.xml?rev=1751460&r1=1751459&r2=1751460&view=diff
==============================================================================
--- felix/trunk/converter/pom.xml (original)
+++ felix/trunk/converter/pom.xml Tue Jul  5 12:34:20 2016
@@ -101,14 +101,23 @@
             <groupId>org.osgi</groupId>
             <artifactId>osgi.annotation</artifactId>
             <version>6.0.1</version>
+            <scope>provided</scope>
         </dependency>
         
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>osgi.core</artifactId>
             <version>6.0.0</version>
+            <scope>provided</scope>
         </dependency>
-
+        
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.17</version>
+            <scope>provided</scope>
+        </dependency>
+        
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>

Modified: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/Util.java
URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/Util.java?rev=1751460&r1=1751459&r2=1751460&view=diff
==============================================================================
--- felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/Util.java (original)
+++ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/Util.java Tue Jul  5 12:34:20 2016
@@ -16,6 +16,9 @@
  */
 package org.apache.felix.converter.impl;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.HashMap;
@@ -51,4 +54,29 @@ public class Util {
         else
             return cls;
     }
+
+    public static byte [] readStream(InputStream is) throws IOException {
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            byte[] bytes = new byte[8192];
+
+            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/converter/src/main/java/org/apache/felix/converter/impl/json/JsonParser.java
URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonParser.java?rev=1751460&view=auto
==============================================================================
--- felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonParser.java (added)
+++ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonParser.java Tue Jul  5 12:34:20 2016
@@ -0,0 +1,251 @@
+/*
+ * 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.converter.impl.json;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+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.converter.impl.Util;
+
+/**
+ * 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 are represented as a {@link Long} (TODO support floats).
+ * <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>
+ *
+ * @author David Bosschaert
+ */
+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(String json) {
+        json = json.trim().replace('\n', ' ');
+        parsed = parseObject(json);
+    }
+
+    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<>(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<>();
+            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);
+        default:
+            return Long.parseLong(jsonValue);
+        }
+    }
+
+    private static Map<String, Object> parseObject(String jsonObject) {
+        if (!(jsonObject.startsWith("{") && jsonObject.endsWith("}")))
+            throw new IllegalArgumentException("Malformatted JSON object: " + jsonObject);
+
+        jsonObject = jsonObject.substring(1, jsonObject.length() - 1);
+        Map<String, Object> values = new HashMap<>();
+        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) {
+        jsonKeyValueList = jsonKeyValueList + ","; // append comma to simplify parsing
+        List<String> elements = new ArrayList<>();
+
+        int i=0;
+        int start=0;
+        Stack<Scope> 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<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 = Util.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/converter/src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java?rev=1751460&view=auto
==============================================================================
--- felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java (added)
+++ felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JsonParserTest.java Tue Jul  5 12:34:20 2016
@@ -0,0 +1,52 @@
+/*
+ * 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.converter.impl.json;
+
+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 testJSON() {
+        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 testJSON2() {
+        String json = "{\"b\": {\"x\": 12, \"y\": 42, \"z\": {\"test test\": \"hello hello\"}}}";
+        JsonParser jp = new JsonParser(json);
+        Map<String, Object> m = jp.getParsed();
+        assertEquals(1, m.size());
+        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"));
+    }
+}