/*
* JBoss, Home of Professional Open Source
* Copyright 2014, Red Hat, Inc., and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* 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.jboss.weld.probe;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.enterprise.inject.Vetoed;
import org.jboss.weld.util.Preconditions;
/**
* Simple JSON generator. A third-party library is not used intentionally - we don't need any other dependencies.
*
* @author Martin Kouba
*/
@Vetoed
final class Json {
private static final String NAME = "name";
private static final String OBJECT_START = "{";
private static final String OBJECT_END = "}";
private static final String ARRAY_START = "[";
private static final String ARRAY_END = "]";
private static final String NAME_VAL_SEPARATOR = ":";
private static final String ENTRY_SEPARATOR = ",";
private static final int CONTROL_CHAR_START = 0;
private static final int CONTROL_CHAR_END = 0x1f;
private static final Map<Character, String> REPLACEMENTS;
static {
REPLACEMENTS = new HashMap<>();
// control characters
for (int i = CONTROL_CHAR_START; i <= CONTROL_CHAR_END; i++) {
REPLACEMENTS.put((char) i, String.format("\\u%04x", i));
}
// quotation mark
REPLACEMENTS.put('"', "\\\"");
// reverse solidus
REPLACEMENTS.put('\\', "\\\\");
}
private static final char CHAR_QUOTATION_MARK = '"';
private Json() {
}
/**
* @return the new JSON array builder, empty builders are not ignored
*/
static JsonArrayBuilder arrayBuilder() {
return new JsonArrayBuilder(false);
}
/**
*
* @param ignoreEmptyBuilders
* @return the new JSON array builder
* @see JsonBuilder#ignoreEmptyBuilders
*/
static JsonArrayBuilder arrayBuilder(boolean ignoreEmptyBuilders) {
return new JsonArrayBuilder(ignoreEmptyBuilders);
}
/**
*
* @return the new JSON object builder, empty builders are not ignored
*/
static JsonObjectBuilder objectBuilder() {
return new JsonObjectBuilder(false);
}
/**
*
* @param ignoreEmptyBuilders
* @return the new JSON object builder
* @see JsonBuilder#ignoreEmptyBuilders
*/
static JsonObjectBuilder objectBuilder(boolean ignoreEmptyBuilders) {
return new JsonObjectBuilder(ignoreEmptyBuilders);
}
/**
*
* @author Martin Kouba
*
* @param <T> Builder type
*/
abstract static class JsonBuilder<T> {
protected boolean ignoreEmptyBuilders = false;
/**
*
* @param ignoreEmptyBuilders If set to true all empty builders added to this builder will be ignored during {@link #build()}
*/
JsonBuilder(boolean ignoreEmptyBuilders) {
this.ignoreEmptyBuilders = ignoreEmptyBuilders;
}
/**
*
* @return <code>true</code> if there are no elements/properties, <code>false</code> otherwise
*/
abstract boolean isEmpty();
/**
*
* @return a string representation
*/
abstract String build();
/**
*
* @param value
* @return <code>true</code> if the value is null or an empty builder and {@link #ignoreEmptyBuilders} is set to <code>true</code>, <code>false</code>
* otherwise
*/
protected boolean isIgnored(Object value) {
return value == null || (ignoreEmptyBuilders && value instanceof JsonBuilder && ((JsonBuilder<?>) value).isEmpty());
}
protected boolean isValuesEmpty(Collection<Object> values) {
if (values.isEmpty()) {
return true;
}
for (Object object : values) {
if (object instanceof JsonBuilder) {
if (!((JsonBuilder<?>) object).isEmpty()) {
return false;
}
} else {
return false;
}
}
return true;
}
protected abstract T self();
}
/**
* JSON array builder.
*
* @author Martin Kouba
*/
static class JsonArrayBuilder extends JsonBuilder<JsonArrayBuilder> {
private final List<Object> values;
private JsonArrayBuilder(boolean ignoreEmptyBuilders) {
super(ignoreEmptyBuilders);
this.values = new ArrayList<Object>();
}
JsonArrayBuilder add(JsonArrayBuilder value) {
addInternal(value);
return this;
}
JsonArrayBuilder add(JsonObjectBuilder value) {
addInternal(value);
return this;
}
JsonArrayBuilder add(String value) {
addInternal(value);
return this;
}
JsonArrayBuilder add(Boolean value) {
addInternal(value);
return this;
}
JsonArrayBuilder add(Integer value) {
addInternal(value);
return this;
}
JsonArrayBuilder add(Long value) {
addInternal(value);
return this;
}
private void addInternal(Object value) {
if (value != null) {
values.add(value);
}
}
boolean isEmpty() {
return isValuesEmpty(values);
}
String build() {
StringBuilder builder = new StringBuilder();
builder.append(ARRAY_START);
int idx = 0;
for (ListIterator<Object> iterator = values.listIterator(); iterator.hasNext();) {
Object value = iterator.next();
if (isIgnored(value)) {
continue;
}
if (++idx > 1) {
builder.append(ENTRY_SEPARATOR);
}
appendValue(builder, value);
}
builder.append(ARRAY_END);
return builder.toString();
}
@Override
protected JsonArrayBuilder self() {
return this;
}
}
/**
* JSON object builder.
*
* @author Martin Kouba
*/
static class JsonObjectBuilder extends JsonBuilder<JsonObjectBuilder> {
private final Map<String, Object> properties;
private JsonObjectBuilder(boolean ignoreEmptyBuilders) {
super(ignoreEmptyBuilders);
this.properties = new LinkedHashMap<String, Object>();
}
JsonObjectBuilder add(String name, String value) {
addInternal(name, value);
return this;
}
JsonObjectBuilder add(String name, JsonObjectBuilder value) {
addInternal(name, value);
return this;
}
JsonObjectBuilder add(String name, JsonArrayBuilder value) {
addInternal(name, value);
return this;
}
JsonObjectBuilder add(String name, Boolean value) {
addInternal(name, value);
return this;
}
JsonObjectBuilder add(String name, Integer value) {
addInternal(name, value);
return this;
}
JsonObjectBuilder add(String name, Long value) {
addInternal(name, value);
return this;
}
boolean has(String name) {
return properties.containsKey(name);
}
private void addInternal(String name, Object value) {
Preconditions.checkArgumentNotNull(name, NAME);
if (value != null) {
properties.put(name, value);
}
}
boolean isEmpty() {
if (properties.isEmpty()) {
return true;
}
return isValuesEmpty(properties.values());
}
String build() {
StringBuilder builder = new StringBuilder();
builder.append(OBJECT_START);
int idx = 0;
for (Iterator<Entry<String, Object>> iterator = properties.entrySet().iterator(); iterator.hasNext();) {
Entry<String, Object> entry = iterator.next();
if (isIgnored(entry.getValue())) {
continue;
}
if (++idx > 1) {
builder.append(ENTRY_SEPARATOR);
}
appendStringValue(builder, entry.getKey());
builder.append(NAME_VAL_SEPARATOR);
appendValue(builder, entry.getValue());
}
builder.append(OBJECT_END);
return builder.toString();
}
@Override
protected JsonObjectBuilder self() {
return this;
}
}
static void appendValue(StringBuilder builder, Object value) {
if (value instanceof JsonObjectBuilder) {
builder.append(((JsonObjectBuilder) value).build());
} else if (value instanceof JsonArrayBuilder) {
builder.append(((JsonArrayBuilder) value).build());
} else if (value instanceof String) {
appendStringValue(builder, value.toString());
} else if (value instanceof Boolean || value instanceof Integer || value instanceof Long) {
builder.append(value.toString());
} else {
throw new IllegalStateException("Unsupported value type: " + value);
}
}
static void appendStringValue(StringBuilder builder, String value) {
builder.append(CHAR_QUOTATION_MARK);
builder.append(escape(value));
builder.append(CHAR_QUOTATION_MARK);
}
/**
* Escape quotation mark, reverse solidus and control characters (U+0000 through U+001F).
*
* @param value
* @return escaped value
* @see <a href="http://www.ietf.org/rfc/rfc4627.txt">http://www.ietf.org/rfc/rfc4627.txt</a>
*/
static String escape(String value) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
String replacement = REPLACEMENTS.get(c);
if (replacement != null) {
builder.append(replacement);
} else {
builder.append(c);
}
}
return builder.toString();
}
}