| /* |
| * 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.dm.runtime; |
| |
| import java.io.ByteArrayOutputStream; |
| 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; |
| |
| /** |
| * A very small JSON parser. |
| * Code adapted from the org.apache.felix.serializer.impl.json.JsonParser.java, |
| * from the Apache Felix Converter project. |
| * |
| * 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 JsonReader { |
| 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 JsonReader(CharSequence json) { |
| String str = json.toString(); |
| str = str.trim().replace('\n', ' '); |
| parsed = parseObject(str); |
| } |
| |
| public JsonReader(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); |
| 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<>(); |
| |
| 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().isEmpty()) |
| return Collections.emptyList(); |
| 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 = 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); |
| } |
| |
| 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(); |
| } |
| } |
| } |