/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.rest;
import java.io.IOException;
import java.io.Writer;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DurationFormatUtils;
import org.eclipse.skalli.commons.CharacterStack;
import org.eclipse.skalli.commons.FormatUtils;
import org.eclipse.skalli.services.rest.RestWriter;
import org.eclipse.skalli.services.rest.RestWriterBase;
import org.restlet.data.MediaType;
public class JSONRestWriter extends RestWriterBase implements RestWriter {
/** Option to enable @-prefixed attributes. */
public static final int PREFIXED_ATTRIBUTES = 0x10000;
/** Option to enable the rendering of namespace atributes. */
public static final int NAMESPACE_ATTRIBUTES = 0x20000;
/** Option to allow a key for the root object/array. */
public static final int NAMED_ROOT = 0x40000;
private static final MediaType MEDIA_TYPE = MediaType.APPLICATION_JSON;
private static final char STATE_INITIAL = '\u03B1';
private static final char STATE_FINAL = '\u03C9';
private static final char STATE_ARRAY = 'A';
private static final char STATE_OBJECT = 'O';
private static final char STATE_ITEM = 'I';
private static final char EXPECT_SEQUENCE = '\u03B1';
private static final char IN_SEQUENCE = ',';
private static final char EXPECT_END_SEQUENCE = '\u03C9';
private static final String VALUE_KEY = "value"; //$NON-NLS-1$
private static final String VALUES_KEY = "values"; //$NON-NLS-1$
private static final String MILLIS_KEY = "millis"; //$NON-NLS-1$
private static final String LINKS_KEY = "links"; //$NON-NLS-1$
private static final String LINK_KEY = "link"; //$NON-NLS-1$
private static final String HREF_KEY = "href"; //$NON-NLS-1$
private static final String REL_KEY = "rel"; //$NON-NLS-1$
private static final String XMLNS_PREFIX = "xmlns:"; //$NON-NLS-1$
// stack for state machine
private CharacterStack states;
// the current state
private char state;
// state that record whether we are in a sequence of elements,
// or expect the begin/end of a sequence;
// a sequence is a comma-separated list of elements
private char sequenceState;
// true, if the next element is the first in a sequence, i.e.
// we must not render a comma before the element
private boolean firstInSequence;
// the key to assign to the next element
private String nextKey;
public JSONRestWriter(Writer writer, String webLocator) {
this(writer, webLocator, 0);
}
/**
*
* @param writer
* @param options
*/
public JSONRestWriter(Writer writer, String webLocator, int options) {
super(writer, webLocator, options);
this.options = options;
states = new CharacterStack();
states.push(state = STATE_INITIAL);
sequenceState = EXPECT_SEQUENCE;
firstInSequence = true;
}
@Override
public MediaType getMediaType() {
return MEDIA_TYPE;
}
@Override
public void flush() throws IOException {
if (state != STATE_FINAL) {
throw new IllegalStateException("Final state not yet reached");
}
writer.flush();
}
@Override
public RestWriter key(String key) {
nextKey = StringUtils.isNotBlank(key)? key : null;
return this;
}
@Override
public RestWriter array() throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced array: Final state already reached");
}
if (sequenceState == EXPECT_END_SEQUENCE) {
throw new IllegalStateException("Unexpected array after value");
}
appendComma();
if (nextKey != null && (state == STATE_OBJECT || (state == STATE_INITIAL && isSet(NAMED_ROOT)))) {
appendKey(nextKey);
writer.append(':');
}
writer.append('[');
states.push(state = STATE_ARRAY);
sequenceState = IN_SEQUENCE;
firstInSequence = true;
nextKey = null;
return this;
}
@Override
public RestWriter array(String itemKey) throws IOException {
return array();
}
@Override
public RestWriter array(String key, String itemKey) throws IOException {
return key(key).array();
}
@Override
public RestWriter item() throws IOException {
if (sequenceState == EXPECT_END_SEQUENCE) {
throw new IllegalStateException("Unexpected item after value");
}
appendComma();
if (state != STATE_ITEM) {
while (state != STATE_ARRAY) {
end();
}
if (state == STATE_ARRAY) {
states.push(state = STATE_ITEM);
}
}
firstInSequence = true;
return this;
}
@Override
public RestWriter item(String itemKey) throws IOException {
return item();
}
@Override
public RestWriter object() throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpected object: Final state already reached");
}
if (sequenceState == EXPECT_END_SEQUENCE) {
throw new IllegalStateException("Unexpected object after value");
}
appendComma();
if (nextKey != null && (state == STATE_OBJECT || (state == STATE_INITIAL && isSet(NAMED_ROOT)))) {
appendKey(nextKey);
writer.append(':');
}
writer.append('{');
states.push(state = STATE_OBJECT);
sequenceState = IN_SEQUENCE;
nextKey = null;
firstInSequence = true;
return this;
}
@Override
public RestWriter object(String key) throws IOException {
return key(key).object();
}
@Override
public RestWriter end() throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Final state already reached");
}
if (state == STATE_INITIAL) {
throw new IllegalStateException("Still in initial state");
}
state = states.pop();
if (state == STATE_ARRAY) {
writer.append(']');
} else if (state == STATE_OBJECT) {
writer.append('}');
} else if (state == STATE_INITIAL) {
throw new IllegalStateException("Still in initial state");
}
state = states.peek();
if (state == STATE_INITIAL) {
state = STATE_FINAL;
}
sequenceState = IN_SEQUENCE;
firstInSequence = false;
return this;
}
@Override
public RestWriter links() throws IOException {
return links(LINKS_KEY);
}
@Override
public RestWriter links(String key) throws IOException {
return key(key).array();
}
@Override
public RestWriter link(String rel, String href) throws IOException {
return object(LINK_KEY)
.attribute(REL_KEY, rel)
.attribute(HREF_KEY, href).end();
}
@Override
public RestWriter value(String s) throws IOException {
return value(s, true);
}
@Override
public RestWriter value(long l) throws IOException {
return value(l, false);
}
@Override
public RestWriter value(double d) throws IOException {
return value(d, false);
}
@Override
public RestWriter value(Number n) throws IOException {
return value(n, false);
}
@Override
public RestWriter value(boolean b) throws IOException {
return value(b, false);
}
@Override
public RestWriter value(UUID uuid) throws IOException {
return value(uuid, true);
}
@Override
public RestWriter href(Object... pathSegments) throws IOException {
return value(hrefOf(pathSegments));
}
@Override
public RestWriter date(long millis) throws IOException {
return value(DateFormatUtils.formatUTC(millis, "yyyy-MM-dd")); //$NON-NLS-1$
}
@Override
public RestWriter datetime(long millis) throws IOException {
return value(FormatUtils.formatUTC(millis));
}
@Override
public RestWriter duration(long millis) throws IOException {
return value(DurationFormatUtils.formatDurationISO(millis));
}
@Override
public RestWriter pair(String key, String s) throws IOException {
return value(key, s, true);
}
@Override
public RestWriter pair(String key, long l) throws IOException {
return value(key, l, false);
}
@Override
public RestWriter pair(String key, double d) throws IOException {
return value(key, d, false);
}
@Override
public RestWriter pair(String key, Number n) throws IOException {
return value(key, n, false);
}
@Override
public RestWriter pair(String key, boolean b) throws IOException {
return value(key, b, false);
}
@Override
public RestWriter pair(String key, UUID uuid) throws IOException {
return value(key, uuid, true);
}
@Override
public RestWriter date(String key, long millis) throws IOException {
return object(key).pair(MILLIS_KEY, millis).value(DateFormatUtils.formatUTC(millis, "yyyy-MM-dd")); //$NON-NLS-1$
}
@Override
public RestWriter datetime(String key, long millis) throws IOException {
return object(key).pair(MILLIS_KEY, millis).value(FormatUtils.formatUTC(millis));
}
@Override
public RestWriter duration(String key, long millis) throws IOException {
return object(key).pair(MILLIS_KEY, millis).value(DurationFormatUtils.formatDurationISO(millis));
}
@Override
public RestWriter href(String key, Object... pathSegments) throws IOException {
return pair(key, hrefOf(pathSegments));
}
@Override
public RestWriter attribute(String name, String value) throws IOException {
return attribute(name, value, true);
}
@Override
public RestWriter attribute(String name, long l) throws IOException {
return attribute(name, l, false);
}
@Override
public RestWriter attribute(String name, double d) throws IOException {
return attribute(name, d, false);
}
@Override
public RestWriter attribute(String name, Number n) throws IOException {
return attribute(name, n, false);
}
@Override
public RestWriter attribute(String name, boolean b) throws IOException {
return attribute(name, b, false);
}
@Override
public RestWriter attribute(String name, UUID uuid) throws IOException {
return attribute(name, uuid, true);
}
@Override
public RestWriter namespace(String name, String value) throws IOException {
if (isSet(NAMESPACE_ATTRIBUTES)) {
attribute(StringUtils.isBlank(name)? XMLNS_PREFIX : name, value);
}
return this;
}
private void append(String s, boolean quoted) throws IOException {
if (quoted) {
writer.append('"');
escaped(s);
writer.append('"');
} else {
escaped(s);
}
}
private void appendComma() throws IOException {
if (!firstInSequence) {
writer.append(',');
}
}
private void appendKey(String key) throws IOException {
append(StringUtils.isBlank(key)? VALUE_KEY : key, true);
}
private RestWriter value(Object value, boolean quoted) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced value: Final state already reached");
}
appendComma();
if (state == STATE_ARRAY || state == STATE_ITEM) {
append(value.toString(), quoted);
} else if (state == STATE_OBJECT && sequenceState == IN_SEQUENCE) {
append(VALUE_KEY, true);
writer.append(':');
append(value.toString(), quoted);
sequenceState = EXPECT_END_SEQUENCE;
} else {
throw new IllegalStateException("Unexpected value");
}
nextKey = null;
firstInSequence = false;
return this;
}
private RestWriter value(String key, Object value, boolean quoted) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced named attribute: Final state already reached");
}
if (StringUtils.isBlank(key)) {
throw new IllegalStateException("Missing key");
}
if (state == STATE_OBJECT) {
String str = value != null? value.toString() : null;
if (StringUtils.isNotBlank(str) || isSet(ALL_MEMBERS)) {
appendComma();
appendKey(key);
writer.append(':');
if (str == null) {
append("null", false); //$NON-NLS-1$
} else {
append(str, quoted);
}
firstInSequence = false;
}
} else {
throw new IllegalStateException("Unexpected named attribute");
}
nextKey = null;
return this;
}
private RestWriter attribute(String name, Object value, boolean quoted) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced attribute: Final state already reached");
}
String key = isSet(PREFIXED_ATTRIBUTES)? "@" + name : name; //$NON-NLS-1$
if (sequenceState == IN_SEQUENCE) {
if (state == STATE_ARRAY || state == STATE_ITEM) {
object();
value(key, value, quoted);
end();
} else {
value(key, value, quoted);
}
} else {
throw new IllegalStateException("Unexpected attribute");
}
nextKey = null;
return this;
}
@SuppressWarnings("nls")
private RestWriter escaped(String s) throws IOException {
int len = s.length();
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
switch (c) {
case '"':
writer.append("\\\"");
break;
case '\\':
writer.append("\\\\");
break;
case '<':
writer.append('\u0047');
break;
case '\n':
writer.append("\\n");
break;
case '\r':
writer.append("\\r");
break;
case '\t':
writer.append("\\t");
break;
case '\b':
writer.append("\\b");
break;
case '\f':
writer.append("\\f");
break;
default:
if (c < 0x20 || c >= 0x80 && c <= 0xa0 || c >= '\u2000' && c < '\u2100') {
writer.append("\\u");
writer.append(StringUtils.leftPad(Integer.toHexString(c), 4, '0'));
} else {
writer.append(c);
}
}
}
return this;
}
}