/*
* Copyright 2015 Lukas Krejci
*
* Licensed 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.revapi.configuration;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.jboss.dmr.ModelNode;
/**
* A utility class for JSON files. The JSON specification doesn't allow comments but the extension's
* configuration is likely to contain comments so that users can "annotate" what individual configuration items mean.
*
* @author Lukas Krejci
* @since 0.1
*/
public final class JSONUtil {
private enum State {
NORMAL, FIRST_SLASH, SINGLE_LINE, MULTI_LINE, STAR_IN_MULTI_LINE
}
private JSONUtil() {
}
/**
* @param json the JSON-encoded data
* @param charset the charset of the data
* @return an input stream that strips comments from json data provided as an input stream.
*/
public static InputStream stripComments(InputStream json, Charset charset) {
Reader rdr = stripComments(new InputStreamReader(json, Charset.forName("UTF-8")));
return new ReaderInputStream(rdr, charset);
}
/**
* @param json the JSON-encoded data
* @return a String with comments stripped from the provided json data.
*/
public static String stripComments(String json) {
try {
try (Reader rdr = stripComments(new StringReader(json))) {
StringBuilder bld = new StringBuilder();
char[] buf = new char[1024];
int cnt;
while ((cnt = rdr.read(buf)) != -1) {
bld.append(buf, 0, cnt);
}
return bld.toString();
}
} catch (IOException e) {
//doesn't happen with strings
return null;
}
}
/**
* @param json the JSON-encoded data
* @return a reader that strips comments from json data provided as a reader.
*/
public static Reader stripComments(final Reader json) {
return new Reader() {
State state = State.NORMAL;
int lastChar = -1;
@Override
public int read() throws IOException {
while (true) {
boolean cont = true;
int emit = lastChar;
switch (state) {
case NORMAL:
switch (lastChar) {
case '/':
state = State.FIRST_SLASH;
break;
default:
if (lastChar != -1) {
lastChar = -1;
cont = false;
}
}
break;
case FIRST_SLASH:
switch (lastChar) {
case '/':
state = State.SINGLE_LINE;
break;
case '*':
state = State.MULTI_LINE;
break;
default:
emit = '/';
state = State.NORMAL;
cont = false;
}
break;
case SINGLE_LINE:
switch (lastChar) {
case '\n':
state = State.NORMAL;
lastChar = -1;
cont = false;
break;
default: break;
}
break;
case MULTI_LINE:
switch (lastChar) {
case '*':
state = State.STAR_IN_MULTI_LINE;
break;
}
break;
case STAR_IN_MULTI_LINE:
switch (lastChar) {
case '/':
state = State.NORMAL;
break;
}
break;
}
if (cont) {
int ci = json.read();
if (ci == -1) {
lastChar = -1;
return emit;
}
lastChar = ci;
} else {
return emit;
}
}
}
@Override
public int read(@Nonnull char[] cbuf, int off, int len) throws IOException {
for (int i = 0; i < len; ++i) {
int c = read();
if (c == -1) {
return i == 0 ? -1 : i;
}
cbuf[off + i] = (char) c;
}
return len;
}
@Override
public void close() throws IOException {
json.close();
}
};
}
/**
* Converts the provided javascript object to JSON string.
*
* <p>If the object is a Map instance, it is stringified as key-value pairs, if it is a list, it is stringified as
* a list, otherwise the object is merely converted to string using the {@code toString()} method.
*
* @param object the object to stringify.
*
* @return the object as a JSON string
*/
public static String stringifyJavascriptObject(Object object) {
StringBuilder bld = new StringBuilder();
stringify(object, bld);
return bld.toString();
}
public static ModelNode toModelNode(Object object) {
ModelNode ret = new ModelNode();
if (object instanceof Map) {
ret.setEmptyObject();
for (Map.Entry<?, ?> e : ((Map<?, ?>) object).entrySet()) {
String key = e.getKey().toString();
try {
//noinspection ResultOfMethodCallIgnored
int idx = Integer.parseInt(key);
if (!ret.has(0)) {
boolean isMap = false;
try {
ret.keys();
} catch (IllegalArgumentException ignored) {
isMap = true;
}
if (isMap) {
throw new IllegalArgumentException("Mixed javascript list and object not supported.");
}
ret.setEmptyList();
}
ret.get(idx).set(toModelNode(e.getValue()));
} catch (NumberFormatException ignored) {
ret.get(e.getKey().toString()).set(toModelNode(e.getValue()));
}
}
} else if (object instanceof List) {
ret.setEmptyList();
for (Object o : (List<?>) object) {
ret.add(toModelNode(o));
}
} else if (object instanceof Integer) {
ret.set((Integer) object);
} else if (object instanceof Double) {
ret.set((Double) object);
} else if (object instanceof Boolean) {
ret.set((Boolean) object);
} else if (object instanceof Long) {
ret.set((Long) object);
} else if (object instanceof String) {
ret.set((String) object);
} else if (object instanceof BigDecimal) {
ret.set((BigDecimal) object);
} else if (object instanceof BigInteger) {
ret.set((BigInteger) object);
} else if (object != null) {
ret.set(object.toString());
}
return ret;
}
private static void stringify(Object object, StringBuilder bld) {
if (object instanceof Map) {
bld.append("{");
@SuppressWarnings("unchecked")
Iterator<Map.Entry> it = ((Map) object).entrySet().iterator();
if (it.hasNext()) {
Map.Entry<?, ?> e = it.next();
bld.append("\"").append(e.getKey()).append("\":");
stringify(e.getValue(), bld);
}
while (it.hasNext()) {
bld.append(",");
Map.Entry<?, ?> e = it.next();
bld.append("\"").append(e.getKey()).append("\":");
stringify(e.getValue(), bld);
}
bld.append("}");
} else if (object instanceof List) {
bld.append("[");
Iterator<?> it = ((List<?>) object).iterator();
if (it.hasNext()) {
stringify(it.next(), bld);
}
while (it.hasNext()) {
bld.append(",");
stringify(it.next(), bld);
}
bld.append("]");
} else if (object == null) {
bld.append("null");
} else {
bld.append(object.toString());
}
}
}