/*******************************************************************************
* 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.Stack;
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 XMLRestWriter extends RestWriterBase implements RestWriter {
private static final MediaType MEDIA_TYPE = MediaType.TEXT_XML;
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_OPENING_TAG = '\u03B1';
private static final char OPENING_TAG = '<';
private static final char EXPECT_TEXT_NODE = '#';
private static final char EXPECT_CLOSING_TAG = '\u03C9';
private static final String MILLIS_KEY = "millis"; //$NON-NLS-1$
private static final String ITEM = "item"; //$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 VALUE_KEY = "value"; //$NON-NLS-1$
private static final String VALUE_TAG = '<' + VALUE_KEY + '>';
private static final String VALUE_END_TAG = "</" + VALUE_KEY + '>'; //$NON-NLS-1$
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; //$NON-NLS-1$
private static final String XMLNS_PREFIX = "xmlns:"; //$NON-NLS-1$
// stack for state machine
private CharacterStack states;
// stack for names of nested tags
private Stack<String> tags;
// the current state
private char state;
// state that record where we are currently within a
// tag, i.e. whether we are in the opening tag,
// expect the text node or the begin of a new tag
private char tagState;
// the name of the tag we are currently in
private String tag;
// the name to assign to the next tag
private String nextKey;
// the name to assign to the next tag within an array
private String nextItem;
public XMLRestWriter(Writer writer, String webLocator) {
this(writer, webLocator, 0);
}
public XMLRestWriter(Writer writer, String webLocator, int options) {
super(writer, webLocator, options);
states = new CharacterStack();
tags = new Stack<String>();
states.push(state = STATE_INITIAL);
tagState = EXPECT_OPENING_TAG;
}
@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 {
return array(null, null);
}
@Override
public RestWriter array(String itemKey) throws IOException {
return array(null, itemKey);
}
@Override
public RestWriter array(String key, String itemKey) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced array: Final state already reached");
}
if (tagState == EXPECT_CLOSING_TAG) {
throw new IllegalStateException("Unexpected array after value");
}
if (state == STATE_INITIAL) {
writer.append(XML_HEADER);
}
closeOpeningTag();
String tag = key;
if (StringUtils.isBlank(tag)) {
tag = nextKey;
if (StringUtils.isBlank(tag)) {
if (state == STATE_ITEM || state == STATE_ARRAY) {
tag = nextItem;
}
}
}
nextItem = itemKey;
if (StringUtils.isBlank(nextItem)) {
nextItem = ITEM;
}
if (StringUtils.isNotBlank(tag)) {
writer.append('<').append(tag);
tagState = OPENING_TAG;
}
states.push(state = STATE_ARRAY);
tags.push(tag);
nextKey = null;
return this;
}
@Override
public RestWriter item() throws IOException {
if (state == STATE_INITIAL) {
throw new IllegalStateException("Still in initial state");
}
if (tagState == EXPECT_CLOSING_TAG) {
throw new IllegalStateException("Unexpected item after value");
}
closeOpeningTag();
if (state != STATE_ITEM) {
while (state != STATE_ARRAY) {
end();
}
if (state == STATE_ARRAY) {
states.push(state = STATE_ITEM);
tags.push(nextItem);
}
}
return this;
}
@Override
public RestWriter item(String itemKey) throws IOException {
nextItem = StringUtils.isNotBlank(itemKey)? itemKey : null;
return item();
}
@Override
public RestWriter object() throws IOException {
return object(null);
}
@Override
public RestWriter object(String key) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced object: Final state already reached");
}
if (tagState == EXPECT_CLOSING_TAG) {
throw new IllegalStateException("Unexpected object after value");
}
if (state == STATE_INITIAL) {
writer.append(XML_HEADER);
}
closeOpeningTag();
String tag = key;
if (StringUtils.isBlank(tag)) {
tag = nextKey;
if (StringUtils.isBlank(tag)) {
if (state == STATE_ITEM || state == STATE_ARRAY) {
tag = nextItem;
}
}
}
if (StringUtils.isNotBlank(tag)) {
writer.append('<').append(tag);
tagState = OPENING_TAG;
}
states.push(state = STATE_OBJECT);
tags.push(tag);
nextKey = null;
return this;
}
@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();
tag = tags.pop();
if (state == STATE_INITIAL) {
throw new IllegalStateException("Still in initial state");
}
if (state == STATE_ARRAY || state == STATE_OBJECT) {
closeTag();
}
state = states.peek();
if (state == STATE_INITIAL) {
state = STATE_FINAL;
}
if (state == STATE_ARRAY || state == STATE_ITEM) {
nextItem = tag;
}
return this;
}
@Override
public RestWriter links() throws IOException {
return array();
}
@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 value) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced value: Final state already reached");
}
boolean isItem = (state == STATE_ITEM || state == STATE_ARRAY)
&& StringUtils.isNotBlank(nextItem);
if (isItem) {
closeOpeningTag();
writer.append('<').append(nextItem).append('>');
escaped(value);
writer.append('<').append('/').append(nextItem).append('>');
tagState = EXPECT_OPENING_TAG;
} else if (state == STATE_OBJECT && tagState != EXPECT_CLOSING_TAG) {
closeOpeningTag();
if (tagState == EXPECT_OPENING_TAG) {
writer.append(VALUE_TAG);
escaped(value);
writer.append(VALUE_END_TAG);
} else {
escaped(value);
}
tagState = EXPECT_CLOSING_TAG;
} else {
throw new IllegalStateException("Unexpected value");
}
nextKey = null;
return this;
}
@Override
public RestWriter value(long l) throws IOException {
return value(Long.toString(l));
}
@Override
public RestWriter value(double d) throws IOException {
return value(Double.toString(d));
}
@Override
public RestWriter value(Number n) throws IOException {
return value(n.toString());
}
@Override
public RestWriter value(boolean b) throws IOException {
return value(Boolean.toString(b));
}
@Override
public RestWriter value(UUID uuid) throws IOException {
return value(uuid.toString());
}
@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 href(Object... pathSegments) throws IOException {
return value(hrefOf(pathSegments));
}
@Override
public RestWriter pair(String key, String value) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced attribute: Final state already reached");
}
if (StringUtils.isBlank(key)) {
throw new IllegalStateException("Missing tag name");
}
if (state == STATE_OBJECT) {
closeOpeningTag();
if (StringUtils.isNotBlank(value) || isSet(ALL_MEMBERS)) {
writer.append('<').append(key);
if (value != null) {
writer.append('>');
escaped(value);
writer.append("</").append(key).append('>'); //$NON-NLS-1$
} else {
writer.append("/>"); //$NON-NLS-1$
}
}
} else {
throw new IllegalStateException("Unexpected attribute without tag");
}
nextKey = null;
return this;
}
@Override
public RestWriter pair(String key, long l) throws IOException {
return pair(key, Long.toString(l));
}
@Override
public RestWriter pair(String key, double d) throws IOException {
return pair(key, Double.toString(d));
}
@Override
public RestWriter pair(String key, Number n) throws IOException {
return pair(key, n.toString());
}
@Override
public RestWriter pair(String key, boolean b) throws IOException {
return pair(key, Boolean.toString(b));
}
@Override
public RestWriter pair(String key, UUID uuid) throws IOException {
return pair(key, uuid != null ? uuid.toString(): null);
}
@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 key, String value) throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Unexpeced attribute: Final state already reached");
}
if (StringUtils.isBlank(key)) {
throw new IllegalStateException("Missing attribute name");
}
if (tagState == OPENING_TAG && value != null) {
writer.append(' ').append(key).append('=');
writer.append('"');
if (value != null) {
escaped(value);
}
writer.append('"');
} else if (tagState == EXPECT_OPENING_TAG) {
if (state == STATE_OBJECT) {
pair(key, value);
} else if (state == STATE_ARRAY || state == STATE_ITEM) {
array().attribute(key, value).end();
}
} else {
throw new IllegalStateException("Unexpected attribute");
}
nextKey = null;
return this;
}
@Override
public RestWriter attribute(String key, long l) throws IOException {
return attribute(key, Long.toString(l));
}
@Override
public RestWriter attribute(String key, double d) throws IOException {
return attribute(key, Double.toString(d));
}
@Override
public RestWriter attribute(String key, Number n) throws IOException {
return attribute(key, n.toString());
}
@Override
public RestWriter attribute(String key, boolean b) throws IOException {
return attribute(key, Boolean.toString(b));
}
@Override
public RestWriter attribute(String key, UUID uuid) throws IOException {
return attribute(key, uuid != null? uuid.toString() : null);
}
@Override
public RestWriter namespace(String key, String value) throws IOException {
attribute(StringUtils.isBlank(key)? XMLNS_PREFIX : key, value);
return this;
}
private void closeOpeningTag() throws IOException {
if (tagState == OPENING_TAG) {
writer.append('>');
tagState = EXPECT_TEXT_NODE;
}
}
private void closeTag() throws IOException {
if (state == STATE_FINAL) {
throw new IllegalStateException("Final state already reached");
}
if (state == STATE_INITIAL) {
throw new IllegalStateException("Initial state");
}
if (tagState == OPENING_TAG) {
writer.append("/>"); //$NON-NLS-1$
tagState = EXPECT_OPENING_TAG;
} else if (tag != null) {
writer.append("</").append(tag).append('>'); //$NON-NLS-1$
tagState = EXPECT_OPENING_TAG;
}
}
@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(">");
break;
case '"':
writer.append(""");
break;
case '\'':
writer.append("'");
break;
default:
writer.append(c);
}
}
return this;
}
}