blob: 4b5863bb0d9eb886a2ed08bc13db07e74934baca [file] [log] [blame]
/*
* 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.jackrabbit.oak.plugins.document.rdb;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.commons.json.JsopReader;
import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.plugins.document.Revision;
import org.apache.jackrabbit.oak.plugins.document.StableRevisionComparator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Utilities that provide JSON support on top of the existing
* {@link JsopTokenizer} support in oak-commons.
* <p>
* The result of parsing uses the simplest possible Java representation of the
* JSON values (see Section 3 of RFC 7159), thus
* <ul>
* <li>{@code null}, {@link Boolean#TRUE}, {@link Boolean#FALSE}, {@link Number}
* , or {@link String}, or
* <li>a {@link List} of representations, or
* <li>a {@link Map}, mapping member names to representations.
* </ul>
* <p>
* The boolean parameter of the constructor ({link
* {@link #RDBJSONSupport(boolean)}) allows changing the default for the maps to
* use sorted maps using {@link Revision}s as keys, as used internally be the
* {@link DocumentNodeStore}.
*/
public class RDBJSONSupport {
private final boolean useRevisionMaps;
/**
* @param useRevisionMaps
* whether to use revision maps instead of regular
* {@link Map}s.
*/
public RDBJSONSupport(boolean useRevisionMaps) {
this.useRevisionMaps = useRevisionMaps;
}
/**
* Parses the supplied JSON.
*/
@Nullable
public Object parse(@NotNull String json) {
return parse(new JsopTokenizer(json));
}
/**
* Parses the supplied JSON.
*/
@Nullable
public Object parse(@NotNull JsopTokenizer json) {
switch (json.read()) {
case JsopReader.NULL:
return null;
case JsopReader.TRUE:
return Boolean.TRUE;
case JsopReader.FALSE:
return Boolean.FALSE;
case JsopReader.NUMBER:
String t = json.getToken();
try {
return Long.parseLong(t);
}
catch (NumberFormatException ex) {
return Double.parseDouble(t);
}
case JsopReader.STRING:
return json.getToken();
case '{':
if (useRevisionMaps) {
Map<Revision, Object> map = new TreeMap<Revision, Object>(StableRevisionComparator.REVERSE);
while (true) {
if (json.matches('}')) {
break;
}
String k = json.readString();
if (k == null) {
throw new IllegalArgumentException("unexpected null revision");
}
json.read(':');
map.put(Revision.fromString(k), parse(json));
json.matches(',');
}
return map;
} else {
Map<String, Object> map = new HashMap<String, Object>();
while (true) {
if (json.matches('}')) {
break;
}
String k = json.readString();
if (k == null) {
throw new IllegalArgumentException("unexpected null key");
}
json.read(':');
map.put(k, parse(json));
json.matches(',');
}
return map;
}
case '[':
List<Object> list = new ArrayList<Object>();
while (true) {
if (json.matches(']')) {
break;
}
list.add(parse(json));
json.matches(',');
}
return list;
default:
throw new IllegalArgumentException(json.readRawValue());
}
}
public static void appendJsonMember(StringBuilder sb, String key, Object value) {
appendJsonString(sb, key);
sb.append(":");
appendJsonValue(sb, value);
}
public static void appendJsonString(StringBuilder sb, String s) {
sb.append('"');
JsopBuilder.escape(s, sb);
sb.append('"');
}
public static void appendJsonMap(StringBuilder sb, Map<Object, Object> map) {
sb.append("{");
boolean needComma = false;
for (Map.Entry<Object, Object> e : map.entrySet()) {
if (needComma) {
sb.append(",");
}
appendJsonMember(sb, e.getKey().toString(), e.getValue());
needComma = true;
}
sb.append("}");
}
public static void appendJsonValue(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
} else if (value instanceof Number) {
sb.append(value.toString());
} else if (value instanceof Boolean) {
sb.append(value.toString());
} else if (value instanceof String) {
appendJsonString(sb, (String) value);
} else if (value instanceof Map) {
appendJsonMap(sb, (Map<Object, Object>) value);
} else {
throw new IllegalArgumentException("unexpected type: " + value.getClass());
}
}
}