blob: 054be853c3821159e650907de7750594e855deac [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.sling.jcr.contentparser.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonReaderFactory;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.stream.JsonParsingException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.sling.jcr.contentparser.ContentHandler;
import org.apache.sling.jcr.contentparser.ContentParser;
import org.apache.sling.jcr.contentparser.JsonParserFeature;
import org.apache.sling.jcr.contentparser.ParseException;
import org.apache.sling.jcr.contentparser.ParserOptions;
/**
* Parses JSON files that contains content fragments.
* Instance of this class is thread-safe.
*/
public final class JsonContentParser implements ContentParser {
private final ParserHelper helper;
/*
* Implementation note: This parser uses JsonReader instead of the (more memory-efficient)
* JsonParser Stream API because otherwise it would not be possible to report parent resources
* including all properties properly before their children.
*/
private final JsonReaderFactory jsonReaderFactory;
private final boolean jsonQuoteTickets;
public JsonContentParser(ParserOptions options) {
this.helper = new ParserHelper(options);
Map<String,Object> jsonReaderFactoryConfig = new HashMap<>();
// allow comments in JSON files?
if (options.getJsonParserFeatures().contains(JsonParserFeature.COMMENTS)) {
jsonReaderFactoryConfig.put("org.apache.johnzon.supports-comments", true);
}
jsonQuoteTickets = options.getJsonParserFeatures().contains(JsonParserFeature.QUOTE_TICK);
jsonReaderFactory = Json.createReaderFactory(jsonReaderFactoryConfig);
}
@Override
public void parse(ContentHandler handler, InputStream is) throws IOException, ParseException {
parse(handler, toJsonObject(is), "/");
}
private JsonObject toJsonObject(InputStream is) {
if (jsonQuoteTickets) {
return toJsonObjectWithJsonTicks(is);
}
try (JsonReader reader = jsonReaderFactory.createReader(is)) {
return reader.readObject();
}
catch (JsonParsingException ex) {
throw new ParseException("Error parsing JSON content: " + ex.getMessage(), ex);
}
}
private JsonObject toJsonObjectWithJsonTicks(InputStream is) {
String jsonString;
try {
jsonString = IOUtils.toString(is, CharEncoding.UTF_8);
}
catch (IOException ex) {
throw new ParseException("Error getting JSON string.", ex);
}
// convert ticks to double quotes
jsonString = JsonTicksConverter.tickToDoubleQuote(jsonString);
try (JsonReader reader = jsonReaderFactory.createReader(new StringReader(jsonString))) {
return reader.readObject();
}
catch (JsonParsingException ex) {
throw new ParseException("Error parsing JSON content: " + ex.getMessage(), ex);
}
}
private void parse(ContentHandler handler, JsonObject object, String path) {
// parse JSON object
Map<String,Object> properties = new HashMap<>();
Map<String,JsonObject> children = new LinkedHashMap<>();
for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
String childName = entry.getKey();
Object value = convertValue(entry.getValue());
boolean isResource = (value instanceof JsonObject);
boolean ignore = false;
if (isResource) {
ignore = helper.ignoreResource(childName);
}
else {
childName = helper.cleanupPropertyName(childName);
ignore = helper.ignoreProperty(childName);
}
if (!ignore) {
if (isResource) {
children.put(childName, (JsonObject)value);
}
else {
properties.put(childName, value);
}
}
}
helper.ensureDefaultPrimaryType(properties);
// report current JSON object
handler.resource(path, properties);
// parse and report children
for (Map.Entry<String,JsonObject> entry : children.entrySet()) {
String childPath = helper.concatenatePath(path, entry.getKey());;
parse(handler, entry.getValue(), childPath);
}
}
private Object convertValue(JsonValue value) {
switch (value.getValueType()) {
case STRING:
String stringValue = ((JsonString)value).getString();
Calendar calendarValue = helper.tryParseCalendar(stringValue);
if (calendarValue != null) {
return calendarValue;
}
else {
return stringValue;
}
case NUMBER:
JsonNumber numberValue = (JsonNumber)value;
if (numberValue.isIntegral()) {
return numberValue.longValue();
}
else {
return numberValue.bigDecimalValue();
}
case TRUE:
return true;
case FALSE:
return false;
case NULL:
return null;
case ARRAY:
JsonArray arrayValue = (JsonArray)value;
Object[] values = new Object[arrayValue.size()];
for (int i=0; i<values.length; i++) {
values[i] = convertValue(arrayValue.get(i));
}
return helper.convertSingleTypeArray(values);
case OBJECT:
return (JsonObject)value;
default:
throw new ParseException("Unexpected JSON value type: " + value.getValueType());
}
}
}