package org.cacert.gigi.output.template;
-import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Reader;
import java.net.URISyntaxException;
import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.cacert.gigi.DevelLauncher;
-import org.cacert.gigi.Language;
-import org.cacert.gigi.output.Outputable;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.util.DayDate;
+import org.cacert.gigi.util.EditDistance;
+import org.cacert.gigi.util.HTMLEncoder;
+/**
+ * Represents a loaded template file.
+ */
public class Template implements Outputable {
- static class TemplateBlock implements Outputable {
- String[] contents;
- Outputable[] vars;
-
- public TemplateBlock(String[] contents, Outputable[] vars) {
- this.contents = contents;
- this.vars = vars;
- }
-
- @Override
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- for (int i = 0; i < contents.length; i++) {
- out.print(contents[i]);
- if (i < this.vars.length) {
- this.vars[i].output(out, l, vars);
- }
- }
- }
-
- }
-
- TemplateBlock data;
-
- long lastLoaded;
- File source;
-
- private static final Pattern IF_PATTERN = Pattern.compile(" ?if\\(\\$([^)]+)\\) ?\\{ ?");
-
- public Template(URL u) {
- try {
- Reader r = new InputStreamReader(u.openStream(), "UTF-8");
- try {
- if (u.getProtocol().equals("file") && DevelLauncher.DEVEL) {
- source = new File(u.toURI());
- lastLoaded = source.lastModified() + 1000;
- }
- } catch (URISyntaxException e) {
- e.printStackTrace();
- }
- data = parse(r);
- r.close();
- } catch (IOException e) {
- throw new Error(e);
- }
- }
-
- public Template(Reader r) {
- try {
- data = parse(r);
- r.close();
- } catch (IOException e) {
- throw new Error(e);
- }
- }
-
- private TemplateBlock parse(Reader r) throws IOException {
- LinkedList<String> splitted = new LinkedList<String>();
- LinkedList<Outputable> commands = new LinkedList<Outputable>();
- StringBuffer buf = new StringBuffer();
- int ch = r.read();
- outer: while (true) {
- while (!endsWith(buf, "<?")) {
- if (ch == -1) {
- break outer;
- }
- buf.append((char) ch);
- ch = r.read();
- }
- buf.delete(buf.length() - 2, buf.length());
- splitted.add(buf.toString());
- buf.delete(0, buf.length());
- while (!endsWith(buf, "?>")) {
- buf.append((char) ch);
- ch = r.read();
- if (ch == -1) {
- throw new EOFException();
- }
- }
- buf.delete(buf.length() - 2, buf.length());
- String com = buf.toString().replace("\n", "");
- buf.delete(0, buf.length());
- Matcher m = IF_PATTERN.matcher(com);
- if (m.matches()) {
- final String variable = m.group(1);
- final TemplateBlock body = parse(r);
- commands.add(new Outputable() {
-
- @Override
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- Object o = vars.get(variable);
- if (o instanceof Boolean && o == Boolean.TRUE) {
- body.output(out, l, vars);
- }
- }
- });
- continue;
- }
- if (com.matches(" ?\\} ?")) {
- break;
- }
- commands.add(parseCommand(com));
- }
- splitted.add(buf.toString());
- String[] contents = splitted.toArray(new String[splitted.size()]);
- Outputable[] vars = commands.toArray(new Outputable[commands.size()]);
- return new TemplateBlock(contents, vars);
- }
-
- private boolean endsWith(StringBuffer buf, String string) {
- return buf.length() >= string.length()
- && buf.substring(buf.length() - string.length(), buf.length()).equals(string);
- }
-
- private Outputable parseCommand(String s2) {
- if (s2.startsWith("=_")) {
- final String raw = s2.substring(2);
- return new Outputable() {
-
- @Override
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- out.print(l.getTranslation(raw));
- }
- };
- } else if (s2.startsWith("=$")) {
- final String raw = s2.substring(2);
- return new Outputable() {
-
- @Override
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- outputVar(out, l, vars, raw);
- }
- };
- } else if (s2.startsWith("=s,")) {
- String command = s2.substring(3);
- final LinkedList<String> store = new LinkedList<String>();
- while (command.startsWith("$")) {
- int idx = command.indexOf(",");
- store.add(command.substring(0, idx));
- command = command.substring(idx + 1);
- }
- final String text = command;
- return new Outputable() {
-
- @Override
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- String[] parts = l.getTranslation(text).split("%s");
- String[] myvars = store.toArray(new String[store.size()]);
- out.print(parts[0]);
- for (int j = 1; j < parts.length; j++) {
- outputVar(out, l, vars, myvars[j - 1].substring(1));
- out.print(parts[j]);
- }
- }
- };
- } else {
- System.out.println("Unknown processing instruction: " + s2);
- }
- return null;
- }
-
- public void output(PrintWriter out, Language l, Map<String, Object> vars) {
- if (source != null && DevelLauncher.DEVEL) {
- if (lastLoaded < source.lastModified()) {
- try {
- System.out.println("Reloading template.... " + source);
- InputStreamReader r = new InputStreamReader(new FileInputStream(source), "UTF-8");
- parse(r);
- r.close();
- lastLoaded = source.lastModified() + 1000;
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- data.output(out, l, vars);
- }
-
- private void outputVar(PrintWriter out, Language l, Map<String, Object> vars, String varname) {
- Object s = vars.get(varname);
-
- if (s == null) {
- System.out.println("Empty variable: " + varname);
- }
- if (s instanceof Outputable) {
- ((Outputable) s).output(out, l, vars);
- } else {
- out.print(s);
- }
- }
+
+ protected static class ParseResult {
+
+ TemplateBlock block;
+
+ String endType;
+
+ public ParseResult(TemplateBlock block, String endType) {
+ this.block = block;
+ this.endType = endType;
+ }
+
+ public String getEndType() {
+ return endType;
+ }
+
+ public TemplateBlock getBlock(String reqType) {
+ if (endType == null && reqType == null) {
+ return block;
+ }
+ if (endType == null || reqType == null) {
+ throw new Error("Invalid block type: " + endType);
+ }
+ if (endType.equals(reqType)) {
+ return block;
+ }
+ throw new Error("Invalid block type: " + endType);
+ }
+ }
+
+ private TemplateBlock data;
+
+ private long lastLoaded;
+
+ private File source;
+
+ private static final Pattern CONTROL_PATTERN = Pattern.compile(" ?([a-zA-Z]+)\\(\\$([^)]+)\\) ?\\{ ?");
+
+ private static final Pattern ELSE_PATTERN = Pattern.compile(" ?\\} ?else ?\\{ ?");
+
+ private static final String[] POSSIBLE_CONTROL_PATTERNS = new String[] {
+ "if", "else", "foreach"
+ };
+
+ private static final String UNKOWN_CONTROL_STRUCTURE_MSG = "Unknown control structure \"%s\", did you mean \"%s\"?";
+
+ /**
+ * Creates a new template by parsing the contents from the given URL. This
+ * constructor will fail on syntax error. When the URL points to a file,
+ * {@link File#lastModified()} is monitored for changes of the template.
+ *
+ * @param u
+ * the URL to load the template from. UTF-8 is chosen as charset.
+ */
+ public Template(URL u) {
+ try (Reader r = new InputStreamReader(u.openStream(), "UTF-8")) {
+ try {
+ if (u.getProtocol().equals("file")) {
+ source = new File(u.toURI());
+ lastLoaded = source.lastModified() + 1000;
+ }
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ data = parse(r).getBlock(null);
+ } catch (IOException e) {
+ throw new Error(e);
+ }
+ }
+
+ /**
+ * Creates a new template by parsing the contents from the given reader.
+ * This constructor will fail on syntax error.
+ *
+ * @param r
+ * the Reader containing the data.
+ */
+ public Template(Reader r) {
+ try {
+ data = parse(r).getBlock(null);
+ r.close();
+ } catch (IOException e) {
+ throw new Error(e);
+ }
+ }
+
+ protected ParseResult parse(Reader r) throws IOException {
+ return parseContent(r);
+ }
+
+ protected ParseResult parseContent(Reader r) throws IOException {
+ ParseContext context = new ParseContext(r);
+ ParseResult result = parseContent(context);
+ if (context.parseException.isEmpty()) {
+ return result;
+ }
+ while (context.curChar != -1) {
+ parseContent(context);
+ }
+ throw context.parseException;
+ }
+
+ protected ParseResult parseContent(ParseContext context) throws IOException {
+ LinkedList<String> splitted = new LinkedList<String>();
+ LinkedList<Translatable> commands = new LinkedList<Translatable>();
+ StringBuffer buf = new StringBuffer();
+ String blockType = null;
+ ParseContext tContext = null;
+ outer:
+ while (true) {
+ if (tContext != null) {
+ context.merge(tContext);
+ }
+ while ( !endsWith(buf, "<?")) {
+ int ch = context.read();
+ if (ch == -1) {
+ break outer;
+ }
+ buf.append((char) ch);
+ if (endsWith(buf, "\\\n")) {
+ buf.delete(buf.length() - 2, buf.length());
+ }
+ }
+ tContext = context.copy();
+ buf.delete(buf.length() - 2, buf.length());
+ splitted.add(buf.toString());
+ buf.delete(0, buf.length());
+ while ( !endsWith(buf, "?>")) {
+ int ch = context.read();
+ if (ch == -1) {
+ context.addError("Expected \"?>\"");
+ return null;
+ }
+ buf.append((char) ch);
+ }
+ buf.delete(buf.length() - 2, buf.length());
+ String com = buf.toString().replace("\n", "");
+ buf.delete(0, buf.length());
+ Matcher m = CONTROL_PATTERN.matcher(com);
+ if (m.matches()) {
+ String type = m.group(1);
+ String variable = m.group(2);
+ ParseContext bodyContext = tContext.copy();
+ ParseResult body = parseContent(bodyContext);
+ if (type.equals("if")) {
+ if ("else".equals(body.getEndType())) {
+ ParseContext bodyContext2 = bodyContext.copy();
+ commands.add(new IfStatement(variable, body.getBlock("else"), parseContent(bodyContext).getBlock("}")));
+ bodyContext.merge(bodyContext2);
+ } else {
+ commands.add(new IfStatement(variable, body.getBlock("}")));
+ }
+ } else if (type.equals("foreach")) {
+ commands.add(new ForeachStatement(variable, body.getBlock("}")));
+ } else {
+ String bestMatching = EditDistance.getBestMatchingStringByEditDistance(type, POSSIBLE_CONTROL_PATTERNS);
+ tContext.addError(String.format(UNKOWN_CONTROL_STRUCTURE_MSG, type, bestMatching));
+ }
+ tContext.merge(bodyContext);
+ continue;
+ } else if ((m = ELSE_PATTERN.matcher(com)).matches()) {
+ blockType = "else";
+ break;
+ } else if (com.matches(" ?\\} ?")) {
+ blockType = "}";
+ break;
+ } else {
+ commands.add(parseCommand(com, tContext));
+ }
+ }
+ if (tContext != null) {
+ context.merge(tContext);
+ }
+ splitted.add(buf.toString());
+ ParseResult result = new ParseResult(new TemplateBlock(splitted.toArray(new String[splitted.size()]), commands.toArray(new Translatable[commands.size()])), blockType);
+ return result;
+ }
+
+ private boolean endsWith(StringBuffer buf, String string) {
+ return buf.length() >= string.length() && buf.substring(buf.length() - string.length(), buf.length()).equals(string);
+ }
+
+ private Translatable parseCommand(String s2, ParseContext context) {
+ if (s2.startsWith("=_")) {
+ final String raw = s2.substring(2);
+ if ( !s2.contains("$") && !s2.contains("!'")) {
+ return new TranslateCommand(raw);
+ } else {
+ return new SprintfCommand(raw);
+ }
+ } else if (s2.startsWith("=$")) {
+ final String raw = s2.substring(2);
+ return new OutputVariableCommand(raw);
+ } else {
+ context.addError("Unknown processing instruction \"" + s2 + "\"," + " did you mean \"" + EditDistance.getBestMatchingStringByEditDistance(s2, new String[] {
+ "=_", "=$"
+ }) + "\"?");
+ return null;
+ }
+ }
+
+ @Override
+ public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+ tryReload();
+ data.output(out, l, vars);
+ }
+
+ protected void tryReload() {
+ if (source != null && lastLoaded < source.lastModified()) {
+ try {
+ System.out.println("Reloading template.... " + source);
+ InputStreamReader r = new InputStreamReader(new FileInputStream(source), "UTF-8");
+ data = parse(r).getBlock(null);
+ r.close();
+ lastLoaded = source.lastModified() + 1000;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ protected static void outputVar(PrintWriter out, Language l, Map<String, Object> vars, String varname, boolean unescaped) {
+ if (vars.containsKey(Outputable.OUT_KEY_PLAIN)) {
+ unescaped = true;
+ }
+ Object s = vars.get(varname);
+
+ if (s == null) {
+ System.err.println("Empty variable: " + varname);
+ }
+ if (s instanceof Outputable) {
+ ((Outputable) s).output(out, l, vars);
+ } else if (s instanceof DayDate) {
+ out.print(DateSelector.getDateFormat().format(((DayDate) s).toDate()));
+ } else if (s instanceof Boolean) {
+ out.print(((Boolean) s) ? l.getTranslation("yes") : l.getTranslation("no"));
+ } else if (s instanceof Date) {
+ SimpleDateFormat sdfUI = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ out.print("<time datetime=\"" + sdf.format(s) + "\">");
+ out.print(sdfUI.format(s));
+ out.print(" UTC</time>");
+ } else {
+ out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString())));
+ }
+ }
+
+ public void addTranslations(Collection<String> s) {
+ data.addTranslations(s);
+ }
+
+ private class ParseContext {
+
+ public static final int CONTEXT_LENGTH = 20;
+
+ private Reader reader;
+
+ public final TemplateParseException parseException = new TemplateParseException(source);
+
+ int line = 1;
+
+ int column = 0;
+
+ private int curChar = -1;
+
+ private int[] charContext = new int[CONTEXT_LENGTH];
+
+ protected int contextPosition = 0;
+
+ public ParseContext(Reader reader) {
+ this.reader = reader;
+ }
+
+ public void addError(String message) {
+ addError(line, column, message);
+ }
+
+ public void addError(int line, int column, String message) {
+ StringBuffer charContextBuffer = new StringBuffer();
+ int j = contextPosition;
+ for (int i = 0; i < CONTEXT_LENGTH; i++) {
+ if (charContext[j] != 0) {
+ if (charContext[j] == '\n') {
+ charContextBuffer.append("\\n");
+ } else {
+ charContextBuffer.appendCodePoint(charContext[j]);
+ }
+ }
+ j = (j + 1) % CONTEXT_LENGTH;
+ }
+ parseException.addError(line, column, message, charContextBuffer.toString());
+ }
+
+ public void merge(ParseContext other) {
+ line = other.line;
+ column = other.column;
+ parseException.append(other.parseException);
+ }
+
+ public void append(ParseContext other) {
+ parseException.append(other.parseException);
+ }
+
+ public int read() throws IOException {
+ int ch;
+ while ((ch = reader.read()) == '\r') {
+ }
+ curChar = ch;
+ if (ch == '\n') {
+ line++;
+ column = 0;
+ } else {
+ column++;
+ }
+ if (ch != -1) {
+ charContext[contextPosition] = ch;
+ contextPosition = (contextPosition + 1) % CONTEXT_LENGTH;
+ }
+ return ch;
+ }
+
+ public ParseContext copy() {
+ ParseContext newParseContext = new ParseContext(reader);
+ newParseContext.line = line;
+ newParseContext.column = column;
+ newParseContext.charContext = Arrays.copyOf(charContext, charContext.length);
+ newParseContext.contextPosition = contextPosition;
+ return newParseContext;
+ }
+ }
}