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