Skip to content

Instantly share code, notes, and snippets.

@agnes1
Last active February 23, 2018 12:17
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save agnes1/08b157dba6a3f37b2ab6 to your computer and use it in GitHub Desktop.
Save agnes1/08b157dba6a3f37b2ab6 to your computer and use it in GitHub Desktop.
TreeML Parser
Thre TreeML parser can read trees of name-values structured either by C-style curly brackets or by tab indentation.
name : value
name2 : value2
[\t]child1 : cv1
[\t]child2 : cv2
name3 : listItem1, listItem2
name4 : true
name5 : "Use quotes for strings"
name6 : tokensDontNeedQuotes
name6 : "keys can repeat"
TreeML has a schema language defined in TreeML or course.
career:
id:beggar
careerName: "Beggar"
careerDescription: "The beggar makes a living by scrounging from kind-hearted (or soft) strangers."
minimumLevel: 0
status: -7
reputation: -7
alignment: 0
objective:
name:"Go Begging"
description:"Persuade perfect strangers to give you their spare coins."
type:receive
item:silver_coin
number:20
experience:20
repeat:-1
objective:
name:"Wander the Byways"
description:"Wander the cold pathways of Vangard visiting distant settlements, where the residents are not yet sick of you pestering them."
type:visit
entity:settlement
number:3
experience:20
repeat:-1
objective:
name:"Find a True Companion"
description:"Befriend a dog to keep you company as you wander friendless through the world."
type:acquire
entity:dog
number:1
experience:20
objective:
name:"Scavenge the Hedgerows"
description:"Collect the wild fruits of the fields and feed yourself."
type:acquire
item:apple
number:10
experience:10
repeat:-1
possession:
id:cloak
level:-1
number:1
possession:
id:cap
level:-1
number:1
possession:
id:wooden_cudgel
level:-1
number:1
possession:
id:boots
level:-1
number:1
exit_career:
id:hermit
level:3
exit_career:
id:laborer
level:2
exit_career:
id:peasant
level:2
exit_career:
id:footpad
level:2
skill:
id:brawling
level:2
skill:
id:subdue
level:2
skill:
id:skulk
level:2
skill:
id:hardened
level:2
skill:
id:beg
level:5
skill:
id:run
level:2
skill:
id:constitution
level:3
skill:
id:willpower
level:2
{
career: {
id:beggar
careerName: "Beggar"
alignment: 0
objective: {
name:"Go Begging"
description:"Persuade perfect strangers to give you their spare coins."
repeat:-1
}
objective: {
name:"Wander the Byways"
experience: 20
repeat:-1
}
}
}
package org.treeml;
import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;
/**
* Evaluates that a document (the referrer) makes references existing values in another document
* or group of documents (the source).
* Created by Ags on 6/25/2016.
*/
public class Dependency {
private final static Logger logger = Logger.getLogger(Dependency.class.getSimpleName());
private ParserIf parser;
@SuppressWarnings("unused")
public Dependency() {
parser = new Parser();
}
@SuppressWarnings("unused")
public Dependency(ParserIf parserIf) {
parser = parserIf;
}
public static void main(String[] args) throws IOException {
final String items = "/encyclopedia/items.treeml";
final String itemsAnimalParts = "/encyclopedia/items-animal-parts.treeml";
DocumentGroup dg = new DocumentGroup();
dg.documents.add(items);
dg.documents.add(itemsAnimalParts);
dg.path = Arrays.asList("item", "id", "nodeValue");
Dependency dp = new Dependency();
final Map<Integer, String> brokenReferences = dp.checkReferences(
"/encyclopedia/creatures.treeml",
Arrays.asList("creature", "parts", "token", "nodeName"),
dg
);
for (Map.Entry<Integer, String> entry : brokenReferences.entrySet()) {
logger.warning(entry.getKey() + " : " + entry.getValue());
}
logger.warning("Found " + brokenReferences.size() + " errors.");
}
public Map<Integer, String> checkReferences(String referrer, List<String> referrerPath, DocumentGroup source) throws IOException {
Comparer comparer = new Comparer();
comparer.result = new TreeMap<>();
comparer.values = source.eval(true, parser);
walkTree(false, comparer, parser.parse(referrer), referrerPath);
return comparer.result;
}
public interface TreeWalker {
void walk(boolean unique, List<Node> found, String finalStep);
}
public static class Collector implements TreeWalker {
Set<Object> result = new HashSet<>();
@Override
public void walk(boolean unique, List<Node> found, String finalStep) {
collectValues(unique, result, found, finalStep);
}
}
public static class Comparer implements TreeWalker {
Map<Integer,String> result = new HashMap<>();
Set<Object> values = new HashSet<>();
@Override
public void walk(boolean unique, List<Node> found, String finalStep) {
compareValues(result, values, found, finalStep);
}
}
private static void walkTree(boolean unique, TreeWalker walker, Node doc, List<String> path) {
List<Node> found = new ArrayList<>();
found.add(doc);
for (int i = 0; i < path.size() - 1; i++) {
String s = path.get(i);
List<Node> temp = new ArrayList<>();
for (Node foundLevel : found) {
for (Node nextLevel : foundLevel.children) {
final SchemaNode sn = new SchemaNode(null);
sn.name = s;
if (Schema.nameMatch(nextLevel, sn)) {
temp.add(nextLevel);
}
}
}
found = temp;
}
String finalStep = path.get(path.size() - 1);
walker.walk(unique, found, finalStep);
}
public static void collectValues(boolean unique, Set<Object> result, List<Node> found, String finalStep) {
if ("nodeName".equals(finalStep)) {
for (Node node : found) {
if (unique && result.contains(node.name)) {
throw new RuntimeException("L0001: Node name not unique: " + node.name + " at line " + node.line);
}
result.add(node.name);
}
} else if ("nodeValue".equals(finalStep)) {
for (Node node : found) {
if (unique && result.contains(node.value)) {
throw new RuntimeException("L0002: Node value not unique: " + node.value + " at line " + node.line);
}
result.add(node.value);
}
} else {
throw new RuntimeException("Final step in path must be nodeName or nodeValue.");
}
}
public static void compareValues(Map<Integer, String> result, Set<Object> values, List<Node> found, String finalStep) {
if ("nodeName".equals(finalStep)) {
found.stream().filter(node -> !values.contains(node.name)).forEach(node -> result.put(node.line, "L0003: Node name not in source: " + node.name));
} else if ("nodeValue".equals(finalStep)) {
found.stream().filter(node -> !values.contains(node.value)).forEach(node -> result.put(node.line, "L0004: Node value not in source: " + node.value));
} else {
throw new RuntimeException("Final step in path must be nodeName or nodeValue.");
}
}
public static class DocumentGroup {
public List<String> documents = new ArrayList<>();
public List<String> path = new ArrayList<>();
public Set<Object> eval(boolean unique, ParserIf parser) {
Collector colly = new Collector();
for (String document : documents) {
try {
final Node doc = parser.parse(document);
walkTree(unique, colly, doc, path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return colly.result;
}
}
}
package org.treeml;
import java.util.ArrayList;
import java.util.List;
/**
* A treeml file is parsed into nodes.
* Each node is on a separate line, and each line has a node
* with the exception of comment lines.
* Created by Ags on 6/26/2016.
*/
public class Node {
public String name;
public Object value;
public List<Node> children = new ArrayList<>();
public int line;
public Node(String name, Object value) {
this.name = name;
this.value = value;
}
public String toString() {
StringBuilder sb = new StringBuilder();
toStringHelper(sb, this, "");
return sb.toString();
}
private void toStringHelper(StringBuilder sb, Node node, String indent) {
sb.append(indent).append(node.name).append("---").append(node.value).append("\r\n");
for (Node child : node.children) {
toStringHelper(sb, child, indent + " ");
}
}
public List<Node> getNodes(String name) {
List<Node> result = new ArrayList<>();
for (int i = 0; i < children.size(); i++) {
if (name.equals(children.get(i).name)) {
result.add(children.get(i));
}
}
return result;
}
public <T> T getValueAt(String name, T defaultValue) {
T t = this.getValueAt(name);
return t == null ? defaultValue : t;
}
public <T> T getValueAt(String name) {
String[] steps = name.split("\\.");
Node node = this;
for (String step : steps) {
List<Node> nodes = node.getNodes(step);
if (nodes.isEmpty()) return null;
node = nodes.get(0);
}
//noinspection unchecked
return (T) node.value;
}
public Node getNode(String nameForNode) {
for (int i = 0; i < children.size(); i++) {
if (nameForNode.equals(children.get(i).name)) {
return children.get(i);
}
}
return null;
}
public String toTreeML() {
StringBuilder sb = new StringBuilder();
toTreeMLImpl(sb, 0);
return sb.toString();
}
public void toTreeMLImpl(StringBuilder sb, int indent) {
for (int i = 0; i < indent; i++) {
sb.append('\t');
}
sb.append(this.name).append(" : ").append(val(this.value)).append('\n');
for (Node child : this.children) {
child.toTreeMLImpl(sb, indent + 1);
}
}
private String val(Object v) {
if (v instanceof String) {
String s = (String) v;
s = s.replace("\r", "\\r").replace("\n", "\\n").replace("\"", "\"\"");
if (s.contains(" ") || !s.equals(v)) {
return '"' + s + '"';
} else {
return (String) v;
}
} else if (v instanceof Double) {
Double d = (Double) v;
if (Math.abs(d) < 0.001 || Math.abs(d) > 999999) {
return (Parser.DECIMAL_FORMAT.format(v));
}
} else if (v instanceof Long) {
Long lo = (Long) v;
if (Math.abs(lo) > 999999) {
return (Parser.LONG_FORMAT.format(lo));
}
} else if (v instanceof List<?>) {
boolean b = true;
StringBuilder sb2 = new StringBuilder();
for (Object o : (List<?>) v) {
if (b) {
b = false;
} else {
sb2.append(", ");
}
sb2.append(val(o));
}
return sb2.toString();
}
return String.valueOf(v);
}
}
package org.treeml;
import java.io.*;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.logging.Logger;
/**
* Parses a tab-indented or curly-indented file into a tree of Nodes.
* @author agnes.clarke
*/
@SuppressWarnings("WeakerAccess")
public class Parser implements ParserIf {
public static final String NADA = "nada", NULL = "null", TRUE = "true", FALSE = "false";
private int indentOfLastLine = 0;
public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public static final DecimalFormat LONG_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
static {
DECIMAL_FORMAT.setMaximumFractionDigits(18);
DECIMAL_FORMAT.setMinimumFractionDigits(1);
LONG_FORMAT.setMaximumFractionDigits(0);
}
public static void main(String[] args) throws IOException {
System.out.println("Usage: pathToTreeMLFile optionalPathToTreeMLSchema");
Reader fileReader = new FileReader(args[0]);
Node node = new Parser().parse(fileReader);
System.out.println(node);
}
@Override
public Node parse(String inputClassPath, String inputSchemaClassPath) {
try {
return parse(
new InputStreamReader(this.getClass().getResourceAsStream(inputClassPath)),
new InputStreamReader(this.getClass().getResourceAsStream(inputSchemaClassPath))
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Node parse(String inputClassPath, Schema schema) {
return parse(
new InputStreamReader(this.getClass().getResourceAsStream(inputClassPath)),
schema
);
}
private Node parse(InputStreamReader inputStreamReader, Schema schema) {
try {
return doParse(inputStreamReader, schema);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Node parse(File inputFile, File inputSchemaFile) {
try {
return parse(
new FileReader(inputFile),
new FileReader(inputSchemaFile)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Node parse(Reader input, Reader inputSchema) throws IOException {
Schema schema = parseSchema(inputSchema);
return doParse(input, schema);
}
@Override
public Schema parseSchema(File inputSchemaFile) {
try {
return parseSchema(
new FileReader(inputSchemaFile)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Schema parseSchema(String inputSchemaClassPath) {
try {
return parseSchema(
new InputStreamReader(this.getClass().getResourceAsStream(inputSchemaClassPath))
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Schema parseSchema(Reader inputSchema) throws IOException {
final InputStream schemaSchemaStream = Schema.class.getResourceAsStream("/org/treeml/schema-schema.treeml");
Reader ssr = new InputStreamReader(schemaSchemaStream);
Node schemaSchemaDocument = parse(ssr);
Schema schema = new Schema(schemaSchemaDocument);
final Node node = doParse(inputSchema, schema);
return new Schema(node);
}
@Override
public Node parse(String inputClassPath) throws IOException {
return doParse(
new InputStreamReader(this.getClass().getResourceAsStream(inputClassPath))
, Schema.PASS);
}
@Override
public Node parse(File inputFile) throws IOException {
return doParse(
new FileReader(inputFile)
, Schema.PASS);
}
@Override
public Node parse(Reader input) throws IOException {
return doParse(input, Schema.PASS);
}
private Node doParse(Reader input, Schema schema) throws IOException {
int lineNumber = 1;
List<Node> nodeStack = new ArrayList<>();
RootNode root = new RootNode();
nodeStack.add(root);
BufferedReader reader = new BufferedReader(input);
String line = reader.readLine();
while (line != null) {
doLine(schema, root, line, nodeStack, lineNumber++, -1);
line = reader.readLine();
}
validate(root, schema);
return root;
}
private void validate(RootNode document, Schema schema) {
if (schema.equals(Schema.PASS)) {
return;
}
List<SchemaNode> schemaNodes = schema.start.children;
List<Node> docNodes = document.children;
final TreeMap<Integer, String> validationResults = validate(docNodes, schemaNodes);
if (validationResults.size() > 0) {
validationResults.values().forEach(System.out::println);
throw new RuntimeException("Validation failed with " + validationResults.size() + " errors.");
}
}
private TreeMap<Integer, String> validate(List<Node> docNodes, List<SchemaNode> schemaNodes) {
TreeMap<Integer, String> errors = new TreeMap<>();
SchemaNode schemaNode = schemaNodes.get(0);
int i = 0;
boolean secondOrMore = false;
while (i < docNodes.size()) {
Node docNode = docNodes.get(i);
if (Schema.nameMatch(docNode, schemaNode)) {
if (docNode.children.size() > 0) {
errors.putAll(validate(docNode.children, schemaNode.children));
} else if (schemaNode.hasMandatoryChildren()) {
errors.put(docNode.line, "Validation error V003: " + docNode.name + " requires children.");
}
i++;
if (schemaNode.single) {
schemaNode = schemaNode.next;
secondOrMore = false;
} else {
secondOrMore = true;
}
} else {
if (!schemaNode.optional && !secondOrMore) {
errors.put(docNode.line, "Validation error V001: " + docNode.name + " not expected at line " + docNode.line + "; expected = " + schemaNode.name);
return errors;
} else {
schemaNode = schemaNode.next;
if (schemaNode == null) {
errors.put(docNode.line, "Validation error V002: " + docNode.name + " not expected at line " + docNode.line);
return errors;
}
}
}
}
return errors;
}
private final List<String> valueList = new ArrayList<>();
private int curlyStackPointer = 0;
private int curlyStackPointerIncrement = 0;
private void doLine(Schema schema, RootNode root, String line, List<Node> nodeStack, int lineNumber, int skip) {
line = preconditions(root, line, lineNumber, skip);
if (line == null) return;
Node newNode = new Node(null, null);
newNode.line = lineNumber;
int stackSize = nodeStack.size();
int stackPointer = 0; // node to append to
boolean startOfLine = true;
boolean insideValue = false;
boolean insideString = false;
boolean valueSeparatedByWhitespace = false;
StringBuilder nameOrValue = new StringBuilder();
for (int index = 0; index < line.length(); index++) {
char c = line.charAt(index);
if (c == '\t' && startOfLine) {
stackPointer++;
} else if (startOfLine && (c == '{' || c == '}')) {
curlyStackPointerIncrement = c == '{' ? 1 : -1;
doLine(schema, root, line, nodeStack, lineNumber, index);
return;
} else if (c == ' ' && startOfLine && curlyStackPointer == 0) {
Logger.getGlobal().warning("Mixed tabs and space at start of line: [" + line + ']');
} else {
if (startOfLine) {
if (c == '/' && nextCharEquals(line, index, '/')) {
// disregard line - it is a comment
return;
}
startOfLine = false;
// drop nodes higher than current stackPointer
int actualStackPointer = curlyStackPointer > 0 ? curlyStackPointer - 1 : stackPointer;
for (int i = stackSize - 1; i > actualStackPointer; i--) {
nodeStack.remove(i);
}
if (curlyStackPointer == 0 && actualStackPointer > indentOfLastLine + 1) {
throw new RuntimeException("Line " + lineNumber + ": illegal indentation");
} else {
indentOfLastLine = actualStackPointer;
}
}
if (!insideString && c == '/' && nextCharEquals(line, index, '/')) {
break;
}
if (!insideString && (c == ':' || c == ',')) {
if (newNode.name != null) {
// add values to list
valueList.add(nameOrValue.toString());
} else {
newNode.name = nameOrValue.toString();
}
insideValue = false;
nameOrValue = new StringBuilder();
valueSeparatedByWhitespace = false;
} else if (!insideString && (c == ' ' || c == '\t')) {
//noinspection ConstantConditions
if (insideValue && !insideString) {
valueSeparatedByWhitespace = true;
}
} else if (!insideString && (c == '{' || c == '}')) {
endOfLine(schema, root, nodeStack, lineNumber, newNode, stackPointer, nameOrValue, index, line);
curlyStackPointerIncrement = c == '{' ? 1 : -1;
return;
} else {
if (c == '"') {
if (!insideString && insideValue) {
throw new RuntimeException("Line " + lineNumber + ", char " + index
+ ": Illegal quote");
}
insideString = !insideString;
insideValue = true;
} else {
insideValue = true;
if (valueSeparatedByWhitespace) {
throw new RuntimeException("Line " + lineNumber + ", char " + index
+ ": Illegal whitespace");
}
if (insideString && (c == '\\' && nextCharEquals(line, index, 'n'))) {
nameOrValue.append('\n');
index++;
} else if (insideString && (c == '\\' && nextCharEquals(line, index, 'r'))) {
nameOrValue.append('\r');
index++;
} else if (insideString && (c == '\\' && nextCharEquals(line, index, '"'))) {
nameOrValue.append('"');
index++;
} else if (insideString && (c == '\\' && nextCharEquals(line, index, '\\'))) {
nameOrValue.append('\\');
index++;
} else {
nameOrValue.append(c);
}
}
}
}
}
endOfLine(schema, root, nodeStack, lineNumber, newNode, stackPointer, nameOrValue, -1, line);
}
private String preconditions(RootNode root, String line, int lineNumber, int skip) {
if (skip > 0) {
line = line.substring(skip);
}
if (lineNumber == 1 && line.startsWith("#")) {
root.value = line.substring(1).trim();
return null;
}
if (lineNumber == 2 && line.startsWith("#")) {
root.requires = line.substring(1).trim();
return null;
}
String trim = line.trim();
curlyStackPointer += curlyStackPointerIncrement;
curlyStackPointerIncrement = 0;
if (trim.equals("{") || trim.equals("}")) {
curlyStackPointerIncrement = trim.equals("{") ? 1 : -1;
return null;
}
if (trim.isEmpty()) {
return null;
}
valueList.clear();
return line;
}
private void endOfLine(Schema schema, RootNode root, List<Node> nodeStack, int lineNumber, Node newNode,
int stackPointer, StringBuilder nameOrValue, int skip, String line) {
Object value;
if (valueList.isEmpty()) {
value = nameOrValue.toString();
} else {
if ( ! nameOrValue.toString().isEmpty()) {
valueList.add(nameOrValue.toString());
}
value = new ArrayList<>(valueList);
}
newNode.value = value;
typifyNode(newNode, nodeStack);
try {
schema.validateNode(nodeStack, newNode, schema);
schema.refineType(newNode);
} catch (Exception e) {
throw new RuntimeException(e.getMessage() + " at line " + lineNumber, e);
}
int actualStackPointer = curlyStackPointer > 0 ? curlyStackPointer - 1 : stackPointer;
nodeStack.get(actualStackPointer).children.add(newNode);
nodeStack.add(newNode);
if (skip > 0) {
doLine(schema, root, line, nodeStack, lineNumber, skip);
}
}
// Get the types of values right, nada the nada elements
private void typifyNode(Node newNode, List<Node> nodeStack) {
Object val = newNode.value;
if (NADA.equals(val)) {
dropNewNode(newNode, nodeStack);
} else if ("".equals(val)) {
newNode.value = null;
} else if (!(val instanceof List<?>)) {
newNode.value = typifyValue(null, val);
} else {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) val;
if (list.removeAll(Collections.singletonList(NADA)) && list.isEmpty()) {
dropNewNode(newNode, nodeStack);
} else {
List<Object> newList = new ArrayList<>();
list.forEach(untypified -> typifyValue(newList, untypified));
newNode.value = newList;
}
}
}
private Object typifyValue(List<Object> valueList, Object untypified) {
if (NULL.equals(untypified)) {
return setValue(null, valueList);
} else if (TRUE.equals(untypified)) {
return setValue(true, valueList);
} else if (FALSE.equals(untypified)) {
return setValue(false, valueList);
} else if (untypified.toString().matches("[+-]?[0-9]+")) {
return setValue(Long.parseLong(untypified.toString()), valueList);
} else if (untypified.toString().matches("[+-]?[0-9]+(\\.[0-9]+)")) {
return setValue(Double.parseDouble(untypified.toString()), valueList);
}
return setValue(untypified.toString(), valueList);
}
private Object setValue(Object value, List<Object> valueList) {
if (valueList != null) {
valueList.add(value);
}
return value;
}
private void dropNewNode(Node newNode, List<Node> nodeStack) {
nodeStack.remove(newNode);
nodeStack.get(nodeStack.size() - 1).children.remove(newNode);
}
private boolean nextCharEquals(String line, int index, char... cs) {
boolean result = true;
int max = line.length();
for (int i = 0; i < cs.length; i++) {
char c = cs[i];
//noinspection StatementWithEmptyBody
if (index + 1 + i < max && line.charAt(index + 1 + i) == c) {
// match, do nothing
} else {
result = false;
}
}
return result;
}
@SuppressWarnings("WeakerAccess")
public static class RootNode extends Node {
public String requires;
public RootNode() {
super("root", null);
}
public String toString() {
return requires == null ? super.toString() : requires + "\r\n" + super.toString();
}
public String toTreeML() {
StringBuilder sb = new StringBuilder();
int indent = 0;
for (Node child : children) {
child.toTreeMLImpl(sb, indent);
}
return sb.toString();
}
}
}
package org.treeml;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
/**
* Abstracts wot the parser does.
* Created by Ags on 6/26/2016.
*/
public interface ParserIf {
Node parse(String inputClassPath, String inputSchemaClassPath);
Node parse(String inputClassPath, Schema schema);
Node parse(File inputFile, File inputSchemaFile);
Node parse(Reader input, Reader inputSchema) throws IOException;
Schema parseSchema(File inputSchemaFile);
Schema parseSchema(String inputSchemaClassPath);
Schema parseSchema(Reader inputSchema) throws IOException;
Node parse(String inputClassPath) throws IOException;
Node parse(File inputFile) throws IOException;
Node parse(Reader input) throws IOException;
}
package org.treeml;
import java.util.*;
/**
* Basic TreeML schema language.
* Created by Ags on 6/25/2016.
*/
@SuppressWarnings("Convert2streamapi")
public class Schema {
public Map<Node, SchemaNode> validated = new HashMap<>();
static final Schema PASS = new Schema(new Parser.RootNode()) {
@Override
public void validateNode(List<Node> nodeStack, Node parent, Schema schema) {
}
};
SchemaNode start = new SchemaNode(this);
public Schema(Node schemaDocument) {
//there is only one child of root in a schema
//allow zero for PASS
if (schemaDocument.children.size() > 0) {
start.next = makeSchemaNode(schemaDocument.children.get(0));
start.children.add(start.next);
}
}
/**
* Verify that the node matches the definition, and apply the schema type to the value.
*/
public void validateNode(List<Node> nodeStack, Node node, Schema schema) {
Node parent = nodeStack.get(nodeStack.size() - 1);
SchemaNode ancestrySn = validated.get(parent);
if (ancestrySn != null) {
boolean matched = false;
for (int i = nodeStack.size() - 1; i >= 1; i--) {
final Node ancestor = nodeStack.get(i);
if (nameMatch(ancestor, ancestrySn)) {
matched = true;
break;
}
}
if (!matched) {
throw new RuntimeException("Ancestor does not match. Expected: " + nodeStack + " got " + ancestrySn.name);
}
}
SchemaNode matchedSn = null;
if (ancestrySn == null) {
matchedSn = schema.start.next;
} else {
boolean matched = false;
for (SchemaNode childSn : ancestrySn.children) {
if (nameMatch(node, childSn)) {
matched = true;
matchedSn = childSn;
break;
}
}
if (!matched) {
List<String> ls = new ArrayList<>();
for (SchemaNode n : ancestrySn.children) {
ls.add(n.name);
}
throw new RuntimeException("Node does not match. Expected: " + ls + ", got: " + node.name);
}
}
validated.put(node, matchedSn);
previouslyValidated = node;
}
Node previouslyValidated = null;
void refineType(Node node) {
SchemaNode cursor = validated.get(node);
if (cursor == null) {
return;
}
if (cursor.list) {
if (!(node.value instanceof List)) {
String type = "string";
try {
Object refined;
if (cursor.integer) {
type = "integer";
refined = Collections.singletonList((Long) node.value);
} else if (cursor.bool) {
type = "boolean";
refined = Collections.singletonList((Boolean) node.value);
} else if (cursor.decimal) {
type = "decimal";
refined = Collections.singletonList((Double) node.value);
} else {
type = "stringlike";
refined = Collections.singletonList((String) node.value);
}
node.value = refined;
} catch (Exception e) {
throw new RuntimeException("Type mismatch: node " + node.name + " in typed " + type + " but has value of " + node.value.getClass().getSimpleName());
}
}
} else {
if (cursor.integer && !(node.value instanceof Long)) {
throw new RuntimeException("Type mismatch: node " + node.name + " is typed integer but has value of " + node.value.getClass().getSimpleName());
} else if (cursor.bool && !(node.value instanceof Boolean)) {
throw new RuntimeException("Type mismatch: node " + node.name + " is typed boolean but has value of " + node.value.getClass().getSimpleName());
} else if (cursor.decimal && !(node.value instanceof Double)) {
throw new RuntimeException("Type mismatch: node " + node.name + " is typed double but has value of " + node.value.getClass().getSimpleName());
} else if (node.value instanceof String && !(cursor.string || cursor.token || cursor.tokenid || cursor.tokenidref)) {
throw new RuntimeException("Type mismatch: node " + node.name + " is typed stringlike but has value of " + node.value.getClass().getSimpleName());
}
}
}
static boolean nameMatch(Node node, SchemaNode sn) {
String nodeName = node.name;
String snName = sn.name;
return nodeName.equals(snName) || "token".equals(snName);
}
SchemaNode makeSchemaNode(Node node) {
SchemaNode result = new SchemaNode(this);
result.name = node.name;
@SuppressWarnings("unchecked")
List<String> values = (List<String>) node.value;
result.single = values.contains("single");
result.optional = values.contains("optional");
result.token = values.contains("token");
result.string = values.contains("string");
result.tokenid = values.contains("tokenid");
result.tokenidref = values.contains("tokenidref");
result.integer = values.contains("integer");
result.decimal = values.contains("decimal");
result.bool = values.contains("boolean");
result.empty = values.contains("empty");
result.list = values.contains("list");
result.set = values.contains("set");
if (values.contains("enum")) {
result.hasEnum = true;
boolean copy = false;
for (String value : values) {
if (copy) {
result.enumVals.add(value);
}
copy = copy || "enum".equals(value);
}
}
SchemaNode prev = null;
for (Node child : node.children) {
final SchemaNode n = makeSchemaNode(child);
n.parent = result;
n.previous = prev;
if (prev != null) {
prev.next = n;
}
result.children.add(n);
prev = n;
}
return result;
}
public String toString() {
StringBuilder sb = new StringBuilder();
int depth = 0;
pl(this.start.next, sb, depth);
return sb.toString();
}
private void pl(SchemaNode n, StringBuilder sb, int depth) {
for (int i = 0; i < depth; i++) {
sb.append("\t");
}
sb.append(n.name).append(" - ").append(parent(n)).append(" - ").append(next(n)).append('\n');
for (SchemaNode child : n.children) {
pl(child, sb, depth + 1);
}
}
String parent(SchemaNode n) {
return n.parent == null ? null : n.parent.name;
}
String next(SchemaNode n) {
return n.next == null ? null : n.next.name;
}
}
package org.treeml;
import java.util.ArrayList;
import java.util.List;
/**
* What goes in a schema node:
* single|repeats, optional?, string|token|tokenid|tokenidref|integer|decimal|boolean|empty, (positive|negative|id)?, (list|set)?, enum
* Created by Ags on 6/25/2016.
*/
class SchemaNode {
Schema schema;
String name;
SchemaNode next;
SchemaNode previous;
SchemaNode parent;
List<SchemaNode> children = new ArrayList<>();
boolean single;
boolean optional;
boolean string;
boolean token;
boolean tokenid;
boolean tokenidref;
boolean integer;
boolean decimal;
boolean bool;
boolean empty;
boolean list;
boolean set;
boolean hasEnum;
List<String> enumVals = new ArrayList<>();
public SchemaNode(Schema schema) {
this.schema = schema;
}
public boolean hasMandatoryChildren() {
for (SchemaNode child : children) {
if (!child.optional) {
return true;
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment