Last active
February 23, 2018 12:17
-
-
Save agnes1/08b157dba6a3f37b2ab6 to your computer and use it in GitHub Desktop.
TreeML Parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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