blob: 68d6ba07b4ed51f9aa4122a097d73230ae680ee5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 freemarker.core;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.util.Enumeration;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateModel;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.StringUtil;
/**
* Static methods and command-line tool for printing the AST of a template.
*/
public class ASTPrinter {
private final Configuration cfg;
private int successfulCounter;
private int failedCounter;
static public void main(String[] args) throws IOException {
if (args.length == 0) {
usage();
System.exit(-1);
}
ASTPrinter astp = new ASTPrinter();
if (args[0].equalsIgnoreCase("-r")) {
astp.mainRecursive(args);
} else {
astp.mainSingleTemplate(args);
}
}
private ASTPrinter() {
cfg = new Configuration();
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_20);
}
private void mainSingleTemplate(String[] args) throws IOException, FileNotFoundException {
final String templateFileName;
final String templateContent;
if (args[0].startsWith("ftl:")) {
templateFileName = null;
templateContent = args[0];
} else {
templateFileName = args[0];
templateContent = null;
}
Template t = new Template(
templateFileName,
templateFileName == null ? new StringReader(templateContent) : new FileReader(templateFileName),
cfg);
p(getASTAsString(t));
}
private void mainRecursive(String[] args) throws IOException {
if (args.length != 4) {
p("Number of arguments must be 4, but was: " + args.length);
usage();
System.exit(-1);
}
final String srcDirPath = args[1].trim();
File srcDir = new File(srcDirPath);
if (!srcDir.isDirectory()) {
p("This should be an existing directory: " + srcDirPath);
System.exit(-1);
}
Pattern fnPattern;
try {
fnPattern = Pattern.compile(args[2]);
} catch (PatternSyntaxException e) {
p(StringUtil.jQuote(args[2]) + " is not a valid regular expression");
System.exit(-1);
return;
}
final String dstDirPath = args[3].trim();
File dstDir = new File(dstDirPath);
if (!dstDir.isDirectory()) {
p("This should be an existing directory: " + dstDirPath);
System.exit(-1);
}
long startTime = System.currentTimeMillis();
recurse(srcDir, fnPattern, dstDir);
long endTime = System.currentTimeMillis();
p("Templates successfully processed " + successfulCounter + ", failed " + failedCounter
+ ". Time taken: " + (endTime - startTime) / 1000.0 + " s");
}
private void recurse(File srcDir, Pattern fnPattern, File dstDir) throws IOException {
File[] files = srcDir.listFiles();
if (files == null) {
throw new IOException("Failed to kust directory: " + srcDir);
}
for (File file : files) {
if (file.isDirectory()) {
recurse(file, fnPattern, new File(dstDir, file.getName()));
} else {
if (fnPattern.matcher(file.getName()).matches()) {
File dstFile = new File(dstDir, file.getName());
String res;
try {
Template t = new Template(file.getPath().replace('\\', '/'), loadIntoString(file), cfg);
res = getASTAsString(t);
successfulCounter++;
} catch (ParseException e) {
res = "<<<FAILED>>>\n" + e.getMessage();
failedCounter++;
p("");
p("-------------------------failed-------------------------");
p("Error message was saved into: " + dstFile.getAbsolutePath());
p("");
p(e.getMessage());
}
save(res, dstFile);
}
}
}
}
private String loadIntoString(File file) throws IOException {
long ln = file.length();
if (ln < 0) {
throw new IOException("Failed to get the length of " + file);
}
byte[] buffer = new byte[(int) ln];
InputStream in = new FileInputStream(file);
try {
int offset = 0;
int bytesRead;
while (offset < buffer.length) {
bytesRead = in.read(buffer, offset, buffer.length - offset);
if (bytesRead == -1) {
throw new IOException("Unexpected end of file: " + file);
}
offset += bytesRead;
}
} finally {
in.close();
}
try {
return decode(buffer, Charset.forName("UTF-8"));
} catch (CharacterCodingException e) {
return decode(buffer, Charset.forName("ISO-8859-1"));
}
}
private String decode(byte[] buffer, Charset charset) throws CharacterCodingException {
return charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(buffer)).toString();
}
private void save(String astStr, File file) throws IOException {
File parentDir = file.getParentFile();
if (!parentDir.isDirectory() && !parentDir.mkdirs()) {
throw new IOException("Failed to create parent directory: " + parentDir);
}
Writer w = new BufferedWriter(new FileWriter(file));
try {
w.write(astStr);
} finally {
w.close();
}
}
private static void usage() {
p("Prints template Abstract Syntax Tree (AST) as plain text.");
p("Usage:");
p(" java freemarker.core.PrintAST <templateFile>");
p(" java freemarker.core.PrintAST ftl:<templateSource>");
p(" java freemarker.core.PrintAST -r <src-directory> <regexp> <dst-directory>");
}
private static final String INDENTATION = " ";
public static String getASTAsString(String ftl) throws IOException {
return getASTAsString(ftl, (Options) null);
}
public static String getASTAsString(String ftl, Options opts) throws IOException {
return getASTAsString(null, ftl, opts);
}
public static String getASTAsString(String templateName, String ftl) throws IOException {
return getASTAsString(templateName, ftl, null);
}
public static String getASTAsString(String templateName, String ftl, Options opts) throws IOException {
Configuration cfg = new Configuration();
Template t = new Template(templateName, ftl, cfg);
return getASTAsString(t, opts);
}
public static String getASTAsString(Template t) throws IOException {
return getASTAsString(t, null);
}
public static String getASTAsString(Template t, Options opts) throws IOException {
validateAST(t);
StringWriter out = new StringWriter();
printNode(t.getRootTreeNode(), "", null, opts != null ? opts : Options.DEFAULT_INSTANCE, out);
return out.toString();
}
public static void validateAST(Template t) throws InvalidASTException {
final TemplateElement node = t.getRootTreeNode();
if (node.getParentElement() != null) {
throw new InvalidASTException("Root node parent must be null."
+ "\nRoot node: " + node.dump(false)
+ "\nParent"
+ ": " + node.getParentElement().getClass() + ", " + node.getParentElement().dump(false));
}
validateAST(node);
}
private static void validateAST(TemplateElement te) {
int childCount = te.getChildCount();
for (int i = 0; i < childCount; i++) {
TemplateElement child = te.getChild(i);
TemplateElement parentElement = child.getParentElement();
// As MixedContent.accept does nothing but return its regulatedChildren, it's optimized out in the final
// AST tree. While it will be present as a child, the parent element also will have regularedChildren
// that contains the children of the MixedContent directly.
if (parentElement instanceof MixedContent && parentElement.getParentElement() != null) {
parentElement = parentElement.getParentElement();
}
if (parentElement != te) {
throw new InvalidASTException("Wrong parent node."
+ "\nNode: " + child.dump(false)
+ "\nExpected parent: " + te.dump(false)
+ "\nActual parent: " + parentElement.dump(false));
}
if (child.getIndex() != i) {
throw new InvalidASTException("Wrong node index."
+ "\nNode: " + child.dump(false)
+ "\nExpected index: " + i
+ "\nActual index: " + child.getIndex());
}
}
if (te instanceof MixedContent && te.getChildCount() < 2) {
throw new InvalidASTException("Mixed content with child count less than 2 should removed by optimizatoin, "
+ "but found one with " + te.getChildCount() + " child(ren).");
}
TemplateElement[] regulatedChildren = te.getChildBuffer();
if (regulatedChildren != null) {
if (childCount == 0) {
throw new InvalidASTException(
"regularChildren must be null when regularChild is 0."
+ "\nNode: " + te.dump(false));
}
for (int i = 0; i < te.getChildCount(); i++) {
if (regulatedChildren[i] == null) {
throw new InvalidASTException(
"regularChildren can't be null at index " + i
+ "\nNode: " + te.dump(false));
}
}
for (int i = te.getChildCount(); i < regulatedChildren.length; i++) {
if (regulatedChildren[i] != null) {
throw new InvalidASTException(
"regularChildren can't be non-null at index " + i
+ "\nNode: " + te.dump(false));
}
}
} else {
if (childCount != 0) {
throw new InvalidASTException(
"regularChildren mustn't be null when regularChild isn't 0."
+ "\nNode: " + te.dump(false));
}
}
}
private static void printNode(Object node, String ind, ParameterRole paramRole, Options opts, Writer out) throws IOException {
if (node instanceof TemplateObject) {
TemplateObject tObj = (TemplateObject) node;
printNodeLineStart(paramRole, ind, out);
out.write(tObj.getNodeTypeSymbol());
printNodeLineEnd(node, out, opts);
if (opts.getShowConstantValue() && node instanceof Expression) {
TemplateModel tm = ((Expression) node).constantValue;
if (tm != null) {
out.write(INDENTATION);
out.write(ind);
out.write("= const ");
out.write(ClassUtil.getFTLTypeDescription(tm));
out.write(' ');
out.write(tm.toString());
out.write('\n');
}
}
int paramCnt = tObj.getParameterCount();
for (int i = 0; i < paramCnt; i++) {
ParameterRole role = tObj.getParameterRole(i);
if (role == null) throw new NullPointerException("parameter role");
Object value = tObj.getParameterValue(i);
printNode(value, ind + INDENTATION, role, opts, out);
}
if (tObj instanceof TemplateElement) {
Enumeration enu = ((TemplateElement) tObj).children();
while (enu.hasMoreElements()) {
printNode(enu.nextElement(), INDENTATION + ind, null, opts, out);
}
}
} else {
printNodeLineStart(paramRole, ind, out);
out.write(StringUtil.jQuote(node));
printNodeLineEnd(node, out, opts);
}
}
protected static void printNodeLineEnd(Object node, Writer out, Options opts) throws IOException {
boolean commentStared = false;
if (opts.getShowJavaClass()) {
out.write(" // ");
commentStared = true;
out.write(ClassUtil.getShortClassNameOfObject(node, true));
}
if (opts.getShowLocation() && node instanceof TemplateObject) {
if (!commentStared) {
out.write(" // ");
commentStared = true;
} else {
out.write("; ");
}
TemplateObject tObj = (TemplateObject) node;
out.write("Location " + tObj.beginLine + ":" + tObj.beginColumn + "-" + tObj.endLine + ":" + tObj.endColumn);
}
out.write('\n');
}
private static void printNodeLineStart(ParameterRole paramRole, String ind, Writer out) throws IOException {
out.write(ind);
if (paramRole != null) {
out.write("- ");
out.write(paramRole.toString());
out.write(": ");
}
}
public static class Options {
private final static Options DEFAULT_INSTANCE = new Options();
private boolean showJavaClass = true;
private boolean showConstantValue = false;
private boolean showLocation = false;
public boolean getShowJavaClass() {
return showJavaClass;
}
public void setShowJavaClass(boolean showJavaClass) {
this.showJavaClass = showJavaClass;
}
public boolean getShowConstantValue() {
return showConstantValue;
}
public void setShowConstantValue(boolean showConstantValue) {
this.showConstantValue = showConstantValue;
}
public boolean getShowLocation() {
return showLocation;
}
public void setShowLocation(boolean showLocation) {
this.showLocation = showLocation;
}
}
private static void p(Object obj) {
System.out.println(obj);
}
public static class InvalidASTException extends RuntimeException {
public InvalidASTException(String message, Throwable cause) {
super(message, cause);
}
public InvalidASTException(String message) {
super(message);
}
}
}