All Downloads are FREE. Search and download functionalities are using the official Maven repository.

auto.parse.processor.Template Maven / Gradle / Ivy

/*
 * Copyright (C) 2012 Google, 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 auto.parse.processor;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Template processor for a small templating language.
 * The idea is that a certain numver of variables are defined beforehand, and you can then write:
 * 
    *
  • $[var] to get the toString() value of that variable *
  • $[var?text] to get the text only if that variable is "true", using a JavaScript-ish * notion of truth where null and 0 and empty strings are false. (That's horrible in * JavaScript but expedient here.) *
  • $[var!text] to get the text only if that variable is "false". *
  • $[var?[text1][text2]] to get text1 if the variable is "true" and text2 if it is "false" *
  • $[var![text1][text2]] to get text1 if the variable is "false" and text2 if it is "true" *
  • $[iterablevar:loopvar|sep|text] to get the text repeated for each element of iterablevar, * which must be an Iterable, with the sep string appearing between each repetition, and * loopvar defined to be the element for substitutions within the text. For example if * the variable "strings" is ["foo", "bar", "baz"] then "$[strings:string|, |'$[string]']" * will become "'foo', 'bar', 'baz'". *
*

Everywhere you can write "var" you can also write "var.foo" to use the result of calling * the public foo() method on that variable, and also "var.foo.bar" etc. * * @author Éamonn McManus */ class Template { private final String template; private final Node rootNode; private Template(String template) { this.template = stripComments(template); rootNode = parse(this.template, 0, this.template.length()); } static Template compile(String template) { return new Template(template); } String rewrite(Map vars) { StringBuilder sb = new StringBuilder(); rootNode.appendTo(sb, this, vars); return sb.toString(); } private static Node parse(String template, int start, int stop) { List nodes = new ArrayList(); int index = start; while (index < stop) { int dollar = indexOf(template, "$[", index, stop); int endLiteral = (dollar < 0) ? stop : dollar; if (endLiteral > index) { nodes.add(new LiteralNode(index, template.substring(index, endLiteral))); index = endLiteral; } if (dollar >= 0) { int closeSquare = matchingCloseSquare(template, dollar); nodes.add(parseDollar(template, dollar, closeSquare)); index = closeSquare + 1; } } assert index == stop; return new CompoundNode(start, nodes); } private static Node parseDollar(String template, int dollar, int closeSquare) { assert template.startsWith("$[", dollar); assert template.charAt(closeSquare) == ']'; int afterOpen = dollar + "$[".length(); int i = scanVarRef(template, afterOpen, closeSquare); String varRef = template.substring(afterOpen, i); Node node; char c = template.charAt(i); switch (c) { case ']': node = new VarNode(i, varRef); break; case '?': case '!': node = parseConditional(template, dollar, varRef, c, i + 1, closeSquare); break; case ':': node = parseIteration(template, dollar, varRef, i + 1, closeSquare); break; default: throw new IllegalArgumentException( "Unexpected character after variable name at " + excerpt(template, dollar)); } return node; } // $[var?trueText] $[var?[trueText][falseText] // $[var!falseText] $[var![falseText][trueText] private static ConditionalNode parseConditional( String template, int dollar, String varRef, char queryOrBang, int afterQueryOrBang, int stop) { assert template.charAt(stop) == ']'; Node firstNode; Node secondNode; if (template.charAt(afterQueryOrBang) == '[') { int endFirstPart = matchingCloseSquare(template, afterQueryOrBang); if (template.charAt(endFirstPart + 1) != '[' || matchingCloseSquare(template, endFirstPart + 1) != stop - 1) { throw new IllegalArgumentException( "Could not scan [firstPart][secondPart] at " + excerpt(template, afterQueryOrBang + 1)); } firstNode = parse(template, afterQueryOrBang + 1, endFirstPart); secondNode = parse(template, endFirstPart + 2, stop - 1); } else { firstNode = parse(template, afterQueryOrBang, stop); secondNode = new LiteralNode(stop, ""); } Node[] nodes; // [0] is false case, [1] is true case. if (queryOrBang == '?') { nodes = new Node[] {secondNode, firstNode}; } else { assert queryOrBang == '!'; nodes = new Node[] {firstNode, secondNode}; } return new ConditionalNode(dollar, varRef, nodes); } // $[iterablevar:loopvar|sep|text] private static IterationNode parseIteration( String template, int dollar, String varRef, int afterColon, int stop) { int firstBar = indexOf(template, "|", afterColon, stop); int secondBar = indexOf(template, "|", firstBar + 1, stop); // If firstBar is -1, firstBar + 1 is 0, and secondBar will be before the starting point // (including the -1 case). if (secondBar < afterColon) { throw new IllegalArgumentException( "Expected $[listVar:iterVar|sep|...] at " + excerpt(template, dollar)); } String iterationVarName = template.substring(afterColon, firstBar); String separator = template.substring(firstBar + 1, secondBar); Node iterated = parse(template, secondBar + 1, stop); return new IterationNode(dollar, varRef, iterationVarName, separator, iterated); } private static int indexOf(String container, String pattern, int start, int stop) { for (int i = start; i < stop; i++) { if (container.startsWith(pattern, i)) { return i; } } return -1; } private static final Pattern varRefPattern = Pattern.compile("\\p{javaJavaIdentifierPart}+(\\.\\p{javaJavaIdentifierPart}+)*"); private static int scanVarRef(String text, int start, int stop) { Matcher matcher = varRefPattern.matcher(text.substring(start, stop)); if (matcher.lookingAt()) { return start + matcher.end(); } else { throw new IllegalArgumentException("Expected id after $[ at " + excerpt(text, start)); } } // Return the index of the first ] that matches the first [ at or after i. private static int matchingCloseSquare(String s, int i) { int squares = 0; for (int j = i; j < s.length(); j++) { char c = s.charAt(j); if (c == '[') { squares++; } else if (c == ']') { squares--; if (squares == 0) { return j; } } } throw new IllegalArgumentException("No closing ] to match text starting " + excerpt(s, i)); } // Remove comments that begin with #, which are comments to help the reader of the template itself // rather than the reader of the generated code. We do not currently have a quote mechanism that // would allow you to get a literal # into the output text. // If a line consists of nothing other than a # comment, we delete it completely rather than // leaving a blank line in the output. private static final Pattern entireLineCommentPattern = Pattern.compile("^\\s*#.*$\n", Pattern.MULTILINE); private static final Pattern midLineCommentPattern = Pattern.compile("#.*$", Pattern.MULTILINE); private static String stripComments(String template) { template = entireLineCommentPattern.matcher(template).replaceAll(""); return midLineCommentPattern.matcher(template).replaceAll(""); } private static final int EXCERPT_LENGTH = 40; private static String excerpt(String s, int startI) { if (s.length() - startI <= EXCERPT_LENGTH) { return s.substring(startI); } else { return s.substring(startI, startI + EXCERPT_LENGTH) + "..."; } } // A node in the syntax tree. private abstract static class Node { final int templateIndex; Node(int templateIndex) { this.templateIndex = templateIndex; } abstract void appendTo(StringBuilder sb, Template template, Map vars); } // A node representing literal text. private static class LiteralNode extends Node { private final String text; LiteralNode(int templateIndex, String text) { super(templateIndex); this.text = text; } @Override void appendTo(StringBuilder sb, Template template, Map vars) { sb.append(text); } } // A node that is the composition of a sequence of other nodes. private static class CompoundNode extends Node { private final List nodes; CompoundNode(int templateIndex, List nodes) { super(templateIndex); this.nodes = nodes; } @Override void appendTo(StringBuilder sb, Template template, Map vars) { for (Node node : nodes) { node.appendTo(sb, template, vars); } } } // Parent class for nodes that reference variables, i.e. start with $[var private abstract static class VarRefNode extends Node { private final String varRef; VarRefNode(int templateIndex, String varRef) { super(templateIndex); this.varRef = varRef; } // Get the value of the variable, including resolving compound.names Object getVar(Map vars, Template template) { // We already checked that varRef is sane using varRefPattern, which protects us against // String.split's funkier behaviours. String[] parts = varRef.split("\\."); Object value = vars.get(parts[0]); if (value == null) { throw new IllegalArgumentException("Reference to undefined var $[" + parts[0] + "] at " + excerpt(template.template, templateIndex)); } for (int i = 1; i < parts.length; i++) { String part = parts[i]; Method method = findPublicMethod(value.getClass(), part); if (method == null) { throw new IllegalArgumentException("No method \"" + part + "\" in " + value.getClass()); } try { value = method.invoke(value); } catch (Exception e) { throw new IllegalArgumentException( "Failed to invoke " + value.getClass().getName() + "." + part + "() on " + value, e); } } return value; } // This method works around the irritating problem that a Method referencing a public method // in a non-public class cannot be invoked (without setAccessible), even if a Method referencing // the same method in a public superclass or interface could be. For example, a Method // referencing Collections.SingletonList.iterator() can't be invoked even though a Method // referencing List.iterator() could on the same object. private static Method findPublicMethod(Class c, String methodName) { try { Method method = c.getMethod(methodName); if (Modifier.isPublic(c.getModifiers()) || c.getName().startsWith("auto.parse")) { // Hack to allow us not to make AutoParseProcessor.Property a public class. return method; } if (c.getSuperclass() != null) { method = findPublicMethod(c.getSuperclass(), methodName); } if (method == null) { for (Class intf : c.getInterfaces()) { method = findPublicMethod(intf, methodName); if (method != null) { break; } } } return method; } catch (NoSuchMethodException e) { return null; } } } // $[var] private static class VarNode extends VarRefNode { VarNode(int templateIndex, String varRef) { super(templateIndex, varRef); } @Override void appendTo(StringBuilder sb, Template template, Map vars) { Object value = getVar(vars, template); sb.append(value); } } // $[var?trueText] $[var?[trueText][falseText] // $[var!falseText] $[var![falseText][trueText] private static class ConditionalNode extends VarRefNode { private final Node[] nodes; // [0] is false, [1] is true ConditionalNode(int templateIndex, String varRef, Node[] nodes) { super(templateIndex, varRef); assert nodes.length == 2; this.nodes = nodes; } @Override void appendTo(StringBuilder sb, Template template, Map vars) { Object value = getVar(vars, template); boolean truth = truth(value, template); Node node = truth ? nodes[1] : nodes[0]; node.appendTo(sb, template, vars); } private boolean truth(Object x, Template template) { if (x == null) { return false; } else if (x instanceof Boolean) { return (Boolean) x; } else if (x instanceof Number) { return ((Number) x).doubleValue() != 0; } else if (x instanceof CharSequence) { return ((CharSequence) x).length() != 0; } else if (x instanceof Iterable) { return ((Iterable) x).iterator().hasNext(); } else { throw new IllegalArgumentException("Don't know how to evaluate the truth of " + x + " at " + excerpt(template.template, templateIndex)); } } } // $[iterablevar:iterationVar|separator|iteratedText] private static class IterationNode extends VarRefNode { private final String iterationVarName; private final String separator; private final Node iteratedNode; IterationNode(int templateIndex, String varRef, String iterationVarName, String separator, Node iteratedNode) { super(templateIndex, varRef); this.iterationVarName = iterationVarName; this.separator = separator; this.iteratedNode = iteratedNode; } @Override void appendTo(StringBuilder sb, Template template, Map vars) { if (vars.containsKey(iterationVarName)) { throw new IllegalArgumentException("Iteration variable name " + iterationVarName + " is already defined at " + excerpt(template.template, templateIndex)); } Map newVars = new HashMap(vars); Object iterableValue = getVar(vars, template); if (!(iterableValue instanceof Iterable)) { throw new IllegalArgumentException("Value (" + iterableValue + ") is not Iterable at " + excerpt(template.template, templateIndex)); } Iterable iterable = (Iterable) iterableValue; String sep = ""; for (Object value : iterable) { newVars.put(iterationVarName, value); sb.append(sep); iteratedNode.appendTo(sb, template, newVars); sep = separator; } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy