/*
* $URL$
* $Author$
* $Date$
* $Revision$
* Copyright 2004-2005 Revolution Systems Inc.
*
* 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 com.revolsys.record.io.format.saif.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import com.revolsys.io.FileUtil;
public class CsnIterator {
public static final int ATTRIBUTE_NAME = 7;
public static final int ATTRIBUTE_PATH = 13;
public static final int ATTRIBUTE_TYPE = 8;
public static final int CLASS_NAME = 4;
public static final int COLLECTION_ATTRIBUTE = 9;
public static final int COLLECTION_ATTRIBUTE_TYPE = 10;
public static final int COMPONENT_NAME = 5;
public static final int END_DEFINITION = 3;
public static final int END_DOCUMENT = 1;
public static final int EXCLUDE_TYPE = 15;
public static final int FLOAT_VALUE = 18;
public static final int FORCE_TYPE = 14;
private static final Object IN_ATTRIBUTE = "attribute";
private static final Object IN_DEFAULT = "default";
private static final Object IN_DEFINITION = "definition";
private static final Object IN_DOCUMENT = "document";
private static final Object IN_RESTRICTION_VALUES = "restrictionValues";
private static final String IN_RESTRICTIONS = "restricted";
public static final int INTEGER_VALUE = 19;
public static final int OPTIONAL_ATTRIBUTE = 6;
private static final Set<String> RESERVED_WORDS = new HashSet<>(Arrays.asList(new String[] {
"subclass", "values", "comments", "attributes", "subclassing", "classAttributes", "defaults",
"constraints", "restricted", "classAttributeValues", "classAttributeDefaults"// "primitiveType",
}));
public static final int START_DEFINITION = 2;
public static final int START_DOCUMENT = 0;
public static final int STRING_ATTRIBUTE = 11;
public static final int STRING_ATTRIBUTE_LENGTH = 12;
public static final int STRING_LENGTH = 17;
public static final int TAG_NAME = 20;
public static final int UNKNOWN = -1;
public static final int VALUE = 16;
private StringBuilder buffer = new StringBuilder();
private int columnNumber;
private int currentColumnNumber;
private int currentLineNumber;
private int eventType = START_DOCUMENT;
private String fileName;
private String line;
private int lineNumber = 0;
private int nextEventType = START_DOCUMENT;
private Object nextToken;
private final BufferedReader reader;
private final Stack<Object> scopeStack = new Stack<>();
private Object value;
public CsnIterator(final File file) throws IOException {
this(FileUtil.getFileName(file), new FileReader(file));
}
public CsnIterator(final String fileName, final InputStream in) throws IOException {
this(fileName, FileUtil.newUtf8Reader(in));
}
public CsnIterator(final String fileName, final Reader reader) throws IOException {
this.reader = new BufferedReader(reader);
this.scopeStack.push(IN_DOCUMENT);
processNext();
}
public void close() throws IOException {
this.reader.close();
}
private String findClassName(final StringBuilder buffer) throws IOException {
final String className = findUpperName(buffer);
final StringBuilder newBuffer = getStrippedBuffer();
// If the class name is fullowed by '::' get and return the schema name
if (newBuffer.charAt(0) == ':' && newBuffer.charAt(1) == ':') {
removeExtraToken(0, 2);
final String schemaName = findUpperName(getStrippedBuffer());
return className + "::" + schemaName;
} else {
return className;
}
}
private String findEnumTag(final StringBuilder buffer) {
int endIndex = 1;
boolean validChar = true;
final int len = buffer.length();
while (validChar && endIndex < len) {
final char c = buffer.charAt(endIndex);
if (isCharacter(c) || c == '-') {
endIndex++;
} else {
validChar = false;
}
}
final String name = buffer.substring(0, endIndex);
removeToken(0, endIndex);
return name;
}
private Integer findInteger() {
int endIndex = 0;
boolean validChar = true;
final int len = this.buffer.length();
while (validChar && endIndex < len) {
final char c = this.buffer.charAt(endIndex);
if (isDigit(c)) {
endIndex++;
} else {
validChar = false;
}
}
final String number = this.buffer.substring(0, endIndex);
if (number.length() > 0) {
removeToken(0, endIndex);
return Integer.valueOf(number);
} else {
return null;
}
}
private String findLowerName(final StringBuilder buffer) {
if (isLowerCase(buffer.charAt(0))) {
return findName(buffer);
} else {
return null;
}
}
private String findName(final StringBuilder buffer) {
int endIndex = 0;
boolean validChar = true;
final int len = buffer.length();
while (validChar && endIndex < len) {
final char c = buffer.charAt(endIndex);
if (isCharacter(c)) {
endIndex++;
} else {
validChar = false;
}
}
final String name = buffer.substring(0, endIndex);
removeToken(0, endIndex);
return name;
}
private int findStartDefinition(final StringBuilder buffer) throws IOException {
if (buffer.charAt(0) != '<') {
return UNKNOWN;
} else {
removeToken(0, 1);
this.scopeStack.push(IN_DEFINITION);
return START_DEFINITION;
}
}
private String findUpperName(final StringBuilder buffer) {
if (isUpperCase(buffer.charAt(0))) {
return findName(buffer);
} else {
return null;
}
}
public boolean getBooleanValue() {
return ((Boolean)this.value).booleanValue();
}
private StringBuilder getBuffer() throws IOException {
if (this.buffer == null || this.buffer.length() == 0) {
this.line = this.reader.readLine();
this.lineNumber++;
while (this.line != null && (this.line.startsWith("//") || this.line.length() == 0)) {
this.line = this.reader.readLine();
this.lineNumber++;
}
if (this.line != null) {
this.buffer.append(this.line);
} else {
this.buffer = null;
}
}
return this.buffer;
}
public int getEventType() {
return this.eventType;
}
public float getFloatValue() {
return ((Float)this.value).floatValue();
}
public int getIntegerValue() {
return ((Integer)this.value).intValue();
}
public int getNextEventType() {
return this.nextEventType;
}
public String getPathValue() {
final String name = getStringValue();
return PathCache.getName(name);
}
public String getStringValue() {
return (String)this.value;
}
private StringBuilder getStrippedBuffer() throws IOException {
StringBuilder buffer = stripWhitespace(getBuffer());
while (buffer != null && buffer.length() == 0) {
buffer = stripWhitespace(getBuffer());
}
return buffer;
}
public Object getValue() {
return this.value;
}
private boolean isCharacter(final char c) {
return isLowerCase(c) || isUpperCase(c) || isDigit(c) || c == '_';
}
private boolean isDigit(final char c) {
return c >= '0' && c <= '9';
}
private boolean isLowerCase(final char c) {
return c >= 'a' && c <= 'z';
}
private boolean isReservedWord(final String name) throws IOException {
this.buffer = getStrippedBuffer();
if (RESERVED_WORDS.contains(name) && this.buffer.charAt(0) == ':') {
removeExtraToken(0, 1);
setNextToken(name);
this.scopeStack.pop();
this.scopeStack.push(name);
return true;
} else {
return false;
}
}
private boolean isUpperCase(final char c) {
return c >= 'A' && c <= 'Z';
}
public int next() throws IOException {
this.eventType = this.nextEventType;
this.value = this.nextToken;
processNext();
return this.eventType;
}
private void processAttribute(final StringBuilder buffer) throws IOException {
StringBuilder localBuffer = buffer;
if (this.nextEventType == OPTIONAL_ATTRIBUTE) {
final String fieldName = findLowerName(localBuffer);
if (fieldName != null) {
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == ']') {
removeToken(0, 1);
this.nextEventType = ATTRIBUTE_NAME;
setNextToken(fieldName);
// scopeStack.pop();
} else {
throw new IllegalStateException("Expecting end of optional attribute name ']'");
}
} else {
throw new IllegalStateException("Expecting an attribute name");
}
} else if (this.nextEventType == COLLECTION_ATTRIBUTE) {
final String className = findClassName(localBuffer);
if (className != null) {
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == ')') {
this.nextEventType = CLASS_NAME;
setNextToken(className);
removeExtraToken(0, 1);
this.scopeStack.pop();
} else {
throw new IllegalStateException("Expecting a ')");
}
} else {
throw new IllegalStateException("Expecting a class name");
}
} else if (this.nextEventType == STRING_ATTRIBUTE) {
final Integer length = findInteger();
if (length != null) {
localBuffer = getStrippedBuffer();
if (localBuffer.charAt(0) == ')') {
this.nextEventType = STRING_LENGTH;
setNextToken(length);
removeToken(0, 1);
this.scopeStack.pop();
} else {
throw new IllegalStateException("Expecting a ')");
}
} else {
throw new IllegalStateException("Expecting a length for the string");
}
} else {
final String className = findClassName(localBuffer);
if (className != null) {
if (className.equals("Set") || className.equals("List") || className.equals("Relation")) {
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == '(') {
this.nextEventType = COLLECTION_ATTRIBUTE;
removeExtraToken(0, 1);
} else {
throw new IllegalStateException("Expecting a '(");
}
} else if (className.equals("String")) {
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == '(') {
this.nextEventType = STRING_ATTRIBUTE;
removeExtraToken(0, 1);
} else {
this.nextEventType = STRING_ATTRIBUTE;
this.scopeStack.pop();
}
} else {
this.nextEventType = ATTRIBUTE_TYPE;
this.scopeStack.pop();
}
setNextToken(className);
} else {
throw new IllegalStateException("Expecting a class name");
}
}
}
private int processAttributePath() throws IOException {
StringBuilder buffer = getStrippedBuffer();
final StringBuilder attributePath = new StringBuilder();
String fieldName = findLowerName(buffer);
if (fieldName == null) {
return UNKNOWN;
} else if (isReservedWord(fieldName)) {
return COMPONENT_NAME;
} else {
attributePath.append(fieldName);
buffer = getStrippedBuffer();
while (buffer.charAt(0) != ':') {
if (buffer.charAt(0) == '{' && buffer.charAt(1) == '}') {
removeExtraToken(0, 2);
attributePath.append("{}");
}
buffer = getStrippedBuffer();
if (buffer.charAt(0) == '*') {
removeExtraToken(0, 1);
buffer = getStrippedBuffer();
fieldName = findName(buffer);
if (fieldName != null) {
attributePath.append('*').append(fieldName);
} else {
throw new IllegalArgumentException("Expecting an attribute name");
}
}
buffer = getStrippedBuffer();
if (buffer.charAt(0) == '.') {
removeExtraToken(0, 1);
buffer = getStrippedBuffer();
fieldName = findLowerName(buffer);
if (fieldName != null) {
attributePath.append('.').append(fieldName);
} else {
throw new IllegalArgumentException("Expecting an attribute name");
}
}
}
removeExtraToken(0, 1);
setNextToken(attributePath.toString());
return ATTRIBUTE_PATH;
}
}
public void processAttributes() throws IOException {
final StringBuilder buffer = getStrippedBuffer();
if (buffer.charAt(0) == '>') {
removeToken(0, 1);
this.scopeStack.pop();
this.scopeStack.pop();
this.nextEventType = END_DEFINITION;
} else if (buffer.charAt(0) == '[') {
this.nextEventType = OPTIONAL_ATTRIBUTE;
setNextToken(Boolean.TRUE);
removeToken(0, 1);
this.scopeStack.push(IN_ATTRIBUTE);
} else {
this.nextEventType = processFieldName(buffer);
if (this.nextEventType == UNKNOWN) {
throw new IllegalStateException("Expecting an attribute name or component name");
}
}
}
public void processClassAttributes() throws IOException {
final StringBuilder buffer = getStrippedBuffer();
if (buffer.charAt(0) == '>') {
removeToken(0, 1);
this.scopeStack.pop();
this.scopeStack.pop();
this.nextEventType = END_DEFINITION;
} else if (buffer.charAt(0) == '[') {
this.nextEventType = OPTIONAL_ATTRIBUTE;
setNextToken(Boolean.TRUE);
removeToken(0, 1);
this.scopeStack.push(IN_ATTRIBUTE);
} else {
this.nextEventType = processFieldName(buffer);
if (this.nextEventType == UNKNOWN) {
throw new IllegalStateException("Expecting an attribute name or component name");
}
}
}
public void processComments() throws IOException {
if (processValue() == VALUE) {
this.scopeStack.pop();
} else {
throw new IllegalStateException("Expecting comment string");
}
}
public void processComponent(final String componentName) throws IOException {
final String methodName = "process" + Character.toUpperCase(componentName.charAt(0))
+ componentName.substring(1);
try {
final Method method = getClass().getMethod(methodName, new Class[0]);
method.invoke(this, new Object[0]);
} catch (final SecurityException e) {
throw new RuntimeException("Unable to access method '" + methodName + "': " + e.getMessage(),
e);
} catch (final NoSuchMethodException e) {
throw new RuntimeException(
"No process method available for component '" + componentName + "': " + e.getMessage(), e);
} catch (final IllegalAccessException e) {
throw new RuntimeException("Unable to access method '" + methodName + "': " + e.getMessage(),
e);
} catch (final InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException)cause;
} else if (cause instanceof Error) {
throw (Error)cause;
} else if (cause instanceof IOException) {
throw (IOException)cause;
} else {
throw new RuntimeException(cause.getMessage(), cause);
}
}
}
public void processDefaults() throws IOException {
final StringBuilder buffer = getStrippedBuffer();
if (buffer.charAt(0) == '>') {
removeToken(0, 1);
this.scopeStack.pop();
this.scopeStack.pop();
this.nextEventType = END_DEFINITION;
} else {
this.nextEventType = processAttributePath();
if (this.nextEventType == ATTRIBUTE_PATH) {
this.scopeStack.push(IN_DEFAULT);
}
}
}
private void processDefinitions(final StringBuilder buffer) throws IOException {
final char c = buffer.charAt(0);
// End of Definition
if (c == '>') {
this.nextEventType = END_DEFINITION;
removeToken(0, 1);
this.scopeStack.pop();
} else if (isUpperCase(c)) {
// Parent name definition
final String className = findClassName(buffer);
this.nextEventType = CLASS_NAME;
setNextToken(className);
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == ',') {
removeExtraToken(0, 1);
}
} else if (isLowerCase(c)) {
final String componentName = findLowerName(buffer);
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == ':') {
removeExtraToken(0, 1);
this.nextEventType = COMPONENT_NAME;
setNextToken(componentName);
this.scopeStack.push(componentName);
} else {
throw new IllegalStateException("Expecting component definition ending in a ':'");
}
} else {
throw new IllegalStateException(
"Expecting end of definition, class name or definition component");
}
}
private int processDigitString() {
int endIndex = 0;
boolean validChar = true;
boolean hasPeriod = false;
final int len = this.buffer.length();
while (validChar && endIndex < len) {
final char c = this.buffer.charAt(endIndex);
if (c == '-' && endIndex == 0) {
endIndex++;
} else if (isDigit(c)) {
endIndex++;
} else if (c == '.') {
if (hasPeriod) {
this.nextEventType = UNKNOWN;
return this.nextEventType;
} else {
hasPeriod = true;
endIndex++;
}
} else {
validChar = false;
}
}
final String number = this.buffer.substring(0, endIndex);
if (number.length() > 0) {
removeToken(0, endIndex);
setNextToken(new BigDecimal(number));
this.nextEventType = VALUE;
}
return this.nextEventType;
}
private void processDocument(final StringBuilder buffer) throws IOException {
this.nextEventType = findStartDefinition(buffer);
if (this.nextEventType == UNKNOWN) {
throw new IllegalStateException(
this.lineNumber + ":" + "Expecting start of an object definition");
}
}
private int processFieldName(final StringBuilder buffer) throws IOException {
final String fieldName = findLowerName(buffer);
if (fieldName == null) {
return UNKNOWN;
} else if (isReservedWord(fieldName)) {
return COMPONENT_NAME;
} else {
setNextToken(fieldName);
this.scopeStack.push(IN_ATTRIBUTE);
return ATTRIBUTE_NAME;
}
}
private void processNext() throws IOException {
final StringBuilder buffer = getStrippedBuffer();
setNextToken(null);
if (buffer == null) {
this.nextEventType = END_DOCUMENT;
} else {
final Object scope = this.scopeStack.peek();
if (scope == IN_DOCUMENT) {
processDocument(buffer);
} else if (RESERVED_WORDS.contains(scope)) {
processComponent((String)scope);
} else if (scope == IN_DEFINITION) {
processDefinitions(buffer);
} else if (scope == IN_ATTRIBUTE) {
processAttribute(buffer);
} else if (scope == IN_DEFAULT) {
processValue();
this.scopeStack.pop();
} else if (scope == IN_RESTRICTIONS) {
processRestricted();
} else if (scope == IN_RESTRICTION_VALUES) {
processRestrictionValues();
}
}
}
private int processRange() {
int endIndex = 0;
boolean validChar = true;
int periodIndex = -1;
boolean hasSecondPeriod = false;
final int len = this.buffer.length();
while (validChar && endIndex < len) {
final char c = this.buffer.charAt(endIndex);
if (c == '-' && (endIndex == 0 || periodIndex == endIndex - 1)) {
endIndex++;
} else if (isDigit(c)) {
endIndex++;
} else if (c == '.') {
if (hasSecondPeriod) {
throw new IllegalStateException("A .. has already been defined for the range");
} else if (periodIndex == -1) {
periodIndex = endIndex;
endIndex++;
} else if (periodIndex == endIndex - 1) {
endIndex++;
hasSecondPeriod = true;
periodIndex = endIndex;
} else {
throw new IllegalStateException("A range must have two '..'");
}
} else {
validChar = false;
}
}
if (!hasSecondPeriod) {
throw new IllegalStateException("A range must have two '..'");
} else if (periodIndex == endIndex) {
throw new IllegalStateException("A range must be in the format '99..99'");
}
final String range = this.buffer.substring(0, endIndex);
if (range.length() > 0) {
removeToken(0, endIndex);
setNextToken(range);
this.nextEventType = VALUE;
}
return this.nextEventType;
}
public void processRestricted() throws IOException {
final StringBuilder newBuffer = getStrippedBuffer();
if (newBuffer.charAt(0) == '>') {
removeToken(0, 1);
this.nextEventType = END_DEFINITION;
this.scopeStack.pop();
this.scopeStack.pop();
} else {
this.nextEventType = processAttributePath();
if (this.nextEventType == COMPONENT_NAME) {
processComponent(getStringValue());
} else if (this.nextEventType == ATTRIBUTE_PATH) {
this.scopeStack.push(IN_RESTRICTION_VALUES);
}
}
}
private void processRestrictionValues() throws IOException {
StringBuilder buffer = getStrippedBuffer();
final char c = buffer.charAt(0);
if (c == '^') {
this.nextEventType = FORCE_TYPE;
removeToken(0, 1);
setNextToken(Boolean.TRUE);
} else if (c == '~') {
this.nextEventType = EXCLUDE_TYPE;
removeToken(0, 1);
setNextToken(Boolean.TRUE);
} else {
if (isUpperCase(c)) {
setNextToken(findClassName(buffer));
this.nextEventType = CLASS_NAME;
} else {
if (processValue() == UNKNOWN) {
processRange();
}
}
buffer = getStrippedBuffer();
if (buffer.charAt(0) == '|') {
removeExtraToken(0, 1);
} else {
this.scopeStack.pop();
}
}
}
public void processSubclass() throws IOException {
final String className = findClassName(getStrippedBuffer());
if (className != null) {
this.nextEventType = CLASS_NAME;
setNextToken(className);
this.scopeStack.pop();
} else {
throw new IllegalStateException("Expecting class name");
}
}
private int processValue() throws IOException {
StringBuilder buffer = getStrippedBuffer();
char c = buffer.charAt(0);
if (c == '"') {
removeToken(0, 1);
final StringBuilder text = new StringBuilder();
int endIndex = 0;
c = buffer.charAt(endIndex);
int len = buffer.length();
while (c != '"') {
endIndex++;
while (endIndex == len) {
text.append(buffer.substring(0, endIndex));
removeExtraToken(0, endIndex);
text.append('\n');
endIndex = 0;
buffer = getBuffer();
if (buffer != null) {
len = buffer.length();
} else {
throw new IllegalStateException("Unnexpected end of file");
}
}
c = buffer.charAt(endIndex);
}
text.append(buffer.substring(0, endIndex));
removeToken(0, endIndex + 1);
setNextToken(text.toString());
this.nextEventType = VALUE;
} else if (isLowerCase(c) || c == '$') {
final String enumTag = findEnumTag(buffer);
if (enumTag.equals("true")) {
setNextToken(Boolean.TRUE);
} else if (enumTag.equals("false")) {
setNextToken(Boolean.FALSE);
} else {
setNextToken(enumTag);
}
this.nextEventType = VALUE;
} else if (isUpperCase(c)) {
final String enumTag = findEnumTag(buffer);
if (enumTag.equals("true")) {
setNextToken(Boolean.TRUE);
} else if (enumTag.equals("false")) {
setNextToken(Boolean.FALSE);
} else {
setNextToken(enumTag);
}
this.nextEventType = VALUE;
} else if (c == '-' || c == '+' || isDigit(c)) {
processDigitString();
} else {
this.nextEventType = UNKNOWN;
}
return this.nextEventType;
}
public void processValues() throws IOException {
final String tagName = findLowerName(getStrippedBuffer());
if (tagName == null) {
if (getStrippedBuffer().charAt(0) == '>') {
this.nextEventType = END_DEFINITION;
removeToken(0, 1);
this.scopeStack.pop();
this.scopeStack.pop();
} else {
throw new IllegalStateException(
"Expecting a tag value, component name or end of definition");
}
} else if (tagName.equals("comments") && getStrippedBuffer().charAt(0) == ':') {
removeToken(0, 1);
this.scopeStack.pop();
this.nextEventType = COMPONENT_NAME;
this.scopeStack.push(tagName);
setNextToken(tagName);
} else {
this.nextEventType = TAG_NAME;
setNextToken(tagName);
}
}
private void removeExtraToken(final int start, final int end) {
this.currentColumnNumber += end;
this.buffer.delete(start, end);
}
private void removeToken(final int start, final int end) {
this.lineNumber = this.currentLineNumber;
this.columnNumber = this.currentColumnNumber;
this.currentColumnNumber += end;
this.buffer.delete(start, end);
}
private void setNextToken(final Object token) {
this.nextToken = token;
}
private StringBuilder stripWhitespace(final StringBuilder buffer) {
if (buffer == null) {
return null;
}
final int len = buffer.length();
int endIndex = 0;
while (endIndex < len && Character.isWhitespace(buffer.charAt(endIndex))) {
endIndex++;
}
removeToken(0, endIndex);
return buffer;
}
@Override
public String toString() {
return this.fileName + "[" + this.lineNumber + "," + this.columnNumber + "]";
}
}