/*
* Copyright 2016 Skynav, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.skynav.ttv.app;
import java.nio.CharBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import com.skynav.xml.helpers.XML;
public class RestartOptions {
private List<Section> sections;
private RestartOptions(List<Section> sections) {
this.sections = sections;
}
public boolean isEmpty() {
return sections.isEmpty();
}
public Collection<Section> getSections() {
return Collections.unmodifiableCollection(sections);
}
public static RestartOptions valueOf(String value) {
if ((value == null) || value.isEmpty())
throw new IllegalArgumentException();
else {
Parser p = new Parser();
return p.parse(CharBuffer.wrap(value.trim()));
}
}
public static class Section {
private String name;
private Map<String,Object> options;
Section(String name, Map<String,Object> options) {
assert name != null;
this.name = name;
this.options = Collections.unmodifiableMap(options);
}
public String getName() {
return name;
}
public Map<String,Object> getOptions() {
return options;
}
}
private static class Parser {
private State state;
private boolean skipWhitespace;
Parser() {
this.state = new State();
this.skipWhitespace = true;
}
RestartOptions parse(CharBuffer cb) {
if (cb.hasRemaining()) {
state.setInput(cb, skipWhitespace);
parseSections();
state.expect(Token.EOS);
return new RestartOptions(state.popSections());
} else
throw new ParserException("empty options");
}
private void parseSections() {
Token t = state.next();
while (t != Token.EOS) {
if (t.isIdent()) {
parseSection();
} else
break;
t = state.next();
}
}
private void parseSection() {
Token t0 = state.next();
if (t0 == null) {
state.error();
} else if (t0.isIdent()) {
state.openSection(t0);
state.consume();
state.expect(Token.OPEN);
parseOptions();
state.expect(Token.CLOSE);
state.closeSection();
} else {
state.error(Token.IDENT, t0);
}
}
private void parseOptions() {
Token t = state.next();
while (t != Token.EOS) {
if (t.isIdent()) {
parseOption();
} else
break;
t = state.next();
}
}
private void parseOption() {
Token t0 = state.next();
Token t1 = null;
if (t0 == null) {
state.error();
} else if (t0.isIdent()) {
state.consume();
Token s1 = state.next();
if (s1 == Token.COLON) {
state.consume();
t1 = state.next();
if (t1.isLiteral()) {
state.consume();
} else
state.error(t1);
} else if (s1 == Token.SEMICOLON) {
t1 = Token.UNDEFINED;
} else
state.error(s1);
Token s2 = state.next();
if (s2 == Token.SEMICOLON) {
state.consume();
state.addOption(t0, t1);
} else if (s2 == Token.CLOSE) {
state.addOption(t0, t1);
} else {
state.error(s2);
}
} else {
state.error(Token.IDENT, t0);
}
}
}
private static class State {
Tokenizer tokenizer; // input tokenizer
Deque<Token> tokens; // token list
State() {
this.tokens = new java.util.ArrayDeque<Token>();
}
Deque<Token> tokens() {
return tokens;
}
void setInput(CharBuffer cb, boolean skipWhitespace) {
this.tokenizer = new Tokenizer(cb, skipWhitespace);
tokens().clear();
}
Token next() {
assert tokenizer != null;
return tokenizer.next();
}
void consume() {
assert tokenizer != null;
tokenizer.consume();
}
void expect(Token t) {
assert tokenizer != null;
tokenizer.expect(t);
}
void error() {
error(null);
}
void error(Token actual) {
error(null, actual);
}
void error(Token expected, Token actual) {
tokenizer.error(expected, actual);
}
void openSection(Token t) {
pushToken(t);
}
void addOption(Token n, Token v) {
pushToken(n);
pushToken(v);
}
void closeSection() {
pushToken(Token.SENTINEL);
}
void pushToken(Token t) {
tokens().add(t);
}
List<Section> popSections() {
List<Section> sections = new java.util.ArrayList<Section>();
while (tokens().peek() != null) {
Token sectionName = tokens().remove();
assert sectionName.isIdent();
Map<String,Object> options = new java.util.HashMap<String,Object>();
Token t = tokens().peek();
while (t != null) {
if (t == Token.SENTINEL) {
tokens().remove();
break;
} else if (tokens().size() >= 3) {
Token optionName = tokens().remove();
assert optionName.isIdent();
Token optionValue = tokens().remove();
assert optionValue.isLiteral();
options.put(optionName.getValue(), optionValue.getLiteralValue());
} else
throw new IllegalStateException();
t = tokens.peek();
}
sections.add(new Section(sectionName.getValue(), options));
}
return sections;
}
}
public static class Token {
enum Type {
BOOLEAN ("#B"), // boolean literal
CLOSE ("}" ), // open group
COLON (":" ), // name value separator
EOS ("#E"), // end of stream literal
IDENT ("#I"), // symbol (identifier) literal
NUMERIC ("#N"), // numeric literal
OPEN ("{" ), // open group
SEMICOLON (";" ), // name value separator
SENTINEL ("##"), // sentinel
SPACE ("#W"), // whitespace literal
STRING ("#S"), // string literal
UNDEFINED ("#U"); // undefined literal
private String shorthand;
private Type(String shorthand) {
this.shorthand = shorthand;
}
String shorthand() {
return shorthand;
}
boolean isLiteral() {
return shorthand.charAt(0) == '#';
}
@Override
public String toString() {
return shorthand();
}
static Type valueOfShorthand(String shorthand) {
if ((shorthand == null) || shorthand.isEmpty())
throw new IllegalArgumentException();
for (Type v: values()) {
if (shorthand.equals(v.shorthand()))
return v;
}
throw new IllegalArgumentException();
}
}
static final Token CLOSE = new Token(Type.CLOSE);
static final Token COLON = new Token(Type.COLON);
static final Token EOS = new Token(Type.EOS);
static final Token IDENT = new Token(Type.IDENT);
static final Token OPEN = new Token(Type.OPEN);
static final Token SEMICOLON = new Token(Type.SEMICOLON);
static final Token SENTINEL = new Token(Type.SENTINEL);
static final Token UNDEFINED = new Token(Type.UNDEFINED);
private Type type;
private String value;
Token(Type type) {
this(type, null);
}
Token(String shorthand, String value) {
this(Type.valueOfShorthand(shorthand), value);
}
Token(Type type, String value) {
this.type = type;
this.value = value;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
}
public Object getLiteralValue() {
assert isLiteral();
if (type == Type.IDENT) {
return value;
} else if (type == Type.STRING) {
return value;
} else if (type == Type.NUMERIC) {
try {
return Double.valueOf(value);
} catch (NumberFormatException e) {
throw new IllegalStateException(e);
}
} else if (type == Type.BOOLEAN) {
return Boolean.valueOf(value);
} else if (type == Type.EOS) {
return null;
} else {
return null;
}
}
public int length() {
if (isLiteral()) {
assert value != null;
return value.length();
} else
return getType().shorthand().length();
}
public boolean isLiteral() {
return type.shorthand().charAt(0) == '#';
}
public boolean isIdent() {
return isLiteral() && type.shorthand().charAt(1) == 'I';
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(getType().toString());
if (isLiteral() && (getValue() != null)) {
sb.append('(');
sb.append(getValue());
sb.append(')');
}
return sb.toString();
}
}
private static class Tokenizer {
private CharBuffer cb;
private boolean skipWhitespace;
Tokenizer(CharBuffer cb, boolean skipWhitespace) {
this.cb = cb;
this.skipWhitespace = skipWhitespace;
}
Token next() {
return getToken(cb, false, false);
}
void consume() {
getToken(cb, true, skipWhitespace);
}
void expect(Token token) {
Token next = next();
if ((next != null) && next.equals(token))
consume();
else
error(token, next);
}
void error(Token expected, Token actual) {
throw new UnexpectedTokenException(cb, expected, actual);
}
private static Token getToken(CharBuffer cb, boolean consume, boolean skipWhitespace) {
Token t;
int p0 = cb.position();
int n = cb.remaining();
char c1 = (n > 0) ? cb.charAt(0) : 0;
// the following clauses are order dependent
if (c1 == 0) {
t = Token.EOS;
} else if (c1 == '{') {
t = Token.OPEN;
} else if (c1 == '}') {
t = Token.CLOSE;
} else if (c1 == ':') {
t = Token.COLON;
} else if (c1 == ';') {
t = Token.SEMICOLON;
} else if (isString(cb)) {
t = getString(cb);
} else if (isNumeric(cb)) {
t = getNumeric(cb);
} else if (isBoolean(cb)) {
t = getBoolean(cb);
} else if (isUndefined(cb)) {
t = getUndefined(cb);
} else if (isIdent(cb)) {
t = getIdent(cb);
} else
t = null;
if (consume && (t != null)) {
int p1 = p0;
if (!t.isLiteral())
p1 += t.length();
else
p1 = cb.position();
cb.position(p1);
if (skipWhitespace) {
if (isWhitespace(cb))
getWhitespace(cb);
}
} else
cb.position(p0);
return t;
}
private static boolean isWhitespace(CharBuffer cb) {
return getWhitespace(cb, false) != null;
}
private static Token getWhitespace(CharBuffer cb) {
return getWhitespace(cb, true);
}
private static Token getWhitespace(CharBuffer cb, boolean consume) {
int i = 0; // index from cb[position]
int n = cb.remaining(); // # indices remaining
StringBuffer sb = new StringBuffer();
while (i < n) {
char c = cb.charAt(i);
if (Character.isWhitespace(c)) {
sb.append(c);
++i;
} else
break;
}
if (sb.length() > 0) {
if (consume)
cb.position(cb.position() + i);
return new Token(Token.Type.SPACE, sb.toString());
} else
return null;
}
private static boolean isString(CharBuffer cb) {
return getString(cb, false) != null;
}
private static Token getString(CharBuffer cb) {
return getString(cb, true);
}
private static Token getString(CharBuffer cb, boolean consume) {
int i = 0; // index from cb[position]
int n = cb.remaining(); // # indices remaining
char d; // string delimiter
StringBuffer sb = new StringBuffer();
// get string delimiter
d = (i < n) ? cb.charAt(i) : 0;
if ((d == '\'') || (d == '\"')) {
++i;
// get delimited content, handling escapes
char c = 0;
while (i < n) {
c = (i < n) ? cb.charAt(i) : 0;
if (c == d) {
++i;
break;
} else if (c == '\\') {
++i;
if (i < n) {
c = (i < n) ? cb.charAt(i) : 0;
sb.append(c);
++i;
} else
break;
} else {
sb.append(c);
++i;
}
}
if (c != d)
sb.setLength(0);
}
if (sb.length() > 0) {
if (consume)
cb.position(cb.position() + i);
return new Token(Token.Type.STRING, sb.toString());
} else
return null;
}
private static boolean isNumeric(CharBuffer cb) {
return getNumeric(cb, false) != null;
}
private static Token getNumeric(CharBuffer cb) {
return getNumeric(cb, true);
}
private static Token getNumeric(CharBuffer cb, boolean consume) {
int i = 0; // index from cb[position]
int j; // helper index
int k; // helper index
int n = cb.remaining(); // # indices remaining
int m = 0; // # digits in mantissa
int e = 0; // # digits in exponent
char c;
StringBuffer sb = new StringBuffer();
// integral component
j = i;
c = (i < n) ? cb.charAt(i) : 0;
if (c == '0') {
sb.append(c);
++i;
} else if ((c >= '1') && (c <= '9')) {
sb.append(c);
++i;
while (i < n) {
c = cb.charAt(i);
if ((c >= '0') && (c <= '9')) {
sb.append(c);
++i;
} else
break;
}
}
m += j - i;
// fractional component
j = i;
c = (i < n) ? cb.charAt(i) : 0;
if (c == '.') {
sb.append(c);
++i;
while (i < n) {
c = cb.charAt(i);
if ((c >= '0') && (c <= '9')) {
sb.append(c);
++i;
} else
break;
}
}
m += j - i;
// ensure mantissa is non-empty
if (m == 0)
return null;
// exponent component
k = sb.length();
c = (i < n) ? cb.charAt(i) : 0;
if ((c == 'e') || (c == 'E')) {
sb.append('E');
++i;
c = (i < n) ? cb.charAt(i) : 0;
if ((c == '+') || (c == '-')) {
sb.append(c);
++i;
}
j = i;
while (i < n) {
c = cb.charAt(i);
if ((c >= '0') && (c <= '9')) {
sb.append(c);
++i;
} else
break;
}
e += j - i;
}
if (e == 0)
sb.setLength(k);
if (sb.length() > 0) {
if (consume)
cb.position(cb.position() + i);
return new Token(Token.Type.NUMERIC, sb.toString());
} else
return null;
}
private static boolean isBoolean(CharBuffer cb) {
return getBoolean(cb, false) != null;
}
private static Token getBoolean(CharBuffer cb) {
return getBoolean(cb, true);
}
private static Token getBoolean(CharBuffer cb, boolean consume) {
Token t = getIdent(cb, false);
if (t != null) {
String ident = t.getValue();
assert ident != null;
if (ident.equals("true") || ident.equals("false"))
t = new Token(Token.Type.BOOLEAN, ident);
else
t = null;
}
if (consume && (t != null))
cb.position(cb.position() + t.length());
return t;
}
private static boolean isUndefined(CharBuffer cb) {
return getUndefined(cb, false) != null;
}
private static Token getUndefined(CharBuffer cb) {
return getUndefined(cb, true);
}
private static Token getUndefined(CharBuffer cb, boolean consume) {
Token t = getIdent(cb, false);
if (t != null) {
String ident = t.getValue();
assert ident != null;
if (ident.equals("undefined"))
t = new Token(Token.Type.UNDEFINED, ident);
else
t = null;
}
if (consume && (t != null))
cb.position(cb.position() + t.length());
return t;
}
private static boolean isIdent(CharBuffer cb) {
return getIdent(cb, false) != null;
}
private static Token getIdent(CharBuffer cb) {
return getIdent(cb, true);
}
private static Token getIdent(CharBuffer cb, boolean consume) {
int i = 0; // index from cb[position]
int n = cb.remaining(); // # indices remaining
char c;
StringBuffer sb = new StringBuffer();
c = (i < n) ? cb.charAt(i) : 0;
if (XML.isNCNameCharStart(c)) {
sb.append(c);
++i;
while (i < n) {
c = (i < n) ? cb.charAt(i) : 0;
if (XML.isNCNameCharPart(c)) {
sb.append(c);
++i;
} else
break;
}
}
if (sb.length() > 0) {
if (consume)
cb.position(cb.position() + i);
return new Token(Token.Type.IDENT, sb.toString());
} else
return null;
}
}
public static class ParserException extends RuntimeException {
static final long serialVersionUID = 0;
public ParserException(String message) {
super(message);
}
}
public static class UnexpectedTokenException extends ParserException {
static final long serialVersionUID = 0;
UnexpectedTokenException(CharBuffer cb, Token expected, Token actual) {
this(cb, (expected != null) ? expected.toString() : null, (actual != null) ? actual.toString() : null);
}
UnexpectedTokenException(CharBuffer cb, String expected, String actual) {
super(makeMessage(cb, expected, actual));
}
private static String makeMessage(CharBuffer cb, String expected, String actual) {
StringBuffer sb = new StringBuffer();
if (expected != null)
sb.append("expected " + expected);
if (sb.length() > 0)
sb.append(", ");
if (actual != null)
sb.append("got " + actual);
if (sb.length() > 0)
sb.append(", ");
if (cb != null)
sb.append("remaining input \"" + cb + "\"");
return sb.toString();
}
}
}