1 package club.wpia.gigi.output.template;
4 import java.io.FileInputStream;
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.io.PrintWriter;
9 import java.net.URISyntaxException;
11 import java.text.SimpleDateFormat;
12 import java.util.Arrays;
13 import java.util.Collection;
14 import java.util.Date;
15 import java.util.LinkedList;
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
20 import club.wpia.gigi.localisation.Language;
21 import club.wpia.gigi.output.DateSelector;
22 import club.wpia.gigi.util.DayDate;
23 import club.wpia.gigi.util.EditDistance;
24 import club.wpia.gigi.util.HTMLEncoder;
27 * Represents a loaded template file.
29 public class Template implements Outputable {
31 protected static class ParseResult {
37 public ParseResult(TemplateBlock block, String endType) {
39 this.endType = endType;
42 public String getEndType() {
46 public TemplateBlock getBlock(String reqType) {
47 if (endType == null && reqType == null) {
50 if (endType == null || reqType == null) {
51 throw new Error("Invalid block type: " + endType);
53 if (endType.equals(reqType)) {
56 throw new Error("Invalid block type: " + endType);
60 private TemplateBlock data;
62 private long lastLoaded;
66 private static final Pattern CONTROL_PATTERN = Pattern.compile(" ?([a-zA-Z]+)\\(\\$([^)]+)\\) ?\\{ ?");
68 private static final Pattern ELSE_PATTERN = Pattern.compile(" ?\\} ?else ?\\{ ?");
70 private static final String[] POSSIBLE_CONTROL_PATTERNS = new String[] {
71 "if", "else", "foreach"
74 private static final String UNKOWN_CONTROL_STRUCTURE_MSG = "Unknown control structure \"%s\", did you mean \"%s\"?";
77 * Creates a new template by parsing the contents from the given URL. This
78 * constructor will fail on syntax error. When the URL points to a file,
79 * {@link File#lastModified()} is monitored for changes of the template.
82 * the URL to load the template from. UTF-8 is chosen as charset.
84 public Template(URL u) {
85 try (Reader r = new InputStreamReader(u.openStream(), "UTF-8")) {
87 if (u.getProtocol().equals("file")) {
88 source = new File(u.toURI());
89 lastLoaded = source.lastModified() + 1000;
91 } catch (URISyntaxException e) {
94 data = parse(r).getBlock(null);
95 } catch (IOException e) {
101 * Creates a new template by parsing the contents from the given reader.
102 * This constructor will fail on syntax error.
105 * the Reader containing the data.
107 public Template(Reader r) {
109 data = parse(r).getBlock(null);
111 } catch (IOException e) {
116 protected ParseResult parse(Reader r) throws IOException {
117 return parseContent(r);
120 protected ParseResult parseContent(Reader r) throws IOException {
121 ParseContext context = new ParseContext(r);
122 ParseResult result = parseContent(context);
123 if (context.parseException.isEmpty()) {
126 while (context.curChar != -1) {
127 parseContent(context);
129 throw context.parseException;
132 protected ParseResult parseContent(ParseContext context) throws IOException {
133 LinkedList<String> splitted = new LinkedList<String>();
134 LinkedList<Translatable> commands = new LinkedList<Translatable>();
135 StringBuffer buf = new StringBuffer();
136 String blockType = null;
137 ParseContext tContext = null;
140 if (tContext != null) {
141 context.merge(tContext);
143 while ( !endsWith(buf, "<?")) {
144 int ch = context.read();
148 buf.append((char) ch);
149 if (endsWith(buf, "\\\n")) {
150 buf.delete(buf.length() - 2, buf.length());
153 tContext = context.copy();
154 buf.delete(buf.length() - 2, buf.length());
155 splitted.add(buf.toString());
156 buf.delete(0, buf.length());
157 while ( !endsWith(buf, "?>")) {
158 int ch = context.read();
160 context.addError("Expected \"?>\"");
163 buf.append((char) ch);
165 buf.delete(buf.length() - 2, buf.length());
166 String com = buf.toString().replace("\n", "");
167 buf.delete(0, buf.length());
168 Matcher m = CONTROL_PATTERN.matcher(com);
170 String type = m.group(1);
171 String variable = m.group(2);
172 ParseContext bodyContext = tContext.copy();
173 ParseResult body = parseContent(bodyContext);
174 if (type.equals("if")) {
175 if ("else".equals(body.getEndType())) {
176 ParseContext bodyContext2 = bodyContext.copy();
177 commands.add(new IfStatement(variable, body.getBlock("else"), parseContent(bodyContext).getBlock("}")));
178 bodyContext.merge(bodyContext2);
180 commands.add(new IfStatement(variable, body.getBlock("}")));
182 } else if (type.equals("foreach")) {
183 commands.add(new ForeachStatement(variable, body.getBlock("}")));
185 String bestMatching = EditDistance.getBestMatchingStringByEditDistance(type, POSSIBLE_CONTROL_PATTERNS);
186 tContext.addError(String.format(UNKOWN_CONTROL_STRUCTURE_MSG, type, bestMatching));
188 tContext.merge(bodyContext);
190 } else if ((m = ELSE_PATTERN.matcher(com)).matches()) {
193 } else if (com.matches(" ?\\} ?")) {
197 commands.add(parseCommand(com, tContext));
200 if (tContext != null) {
201 context.merge(tContext);
203 splitted.add(buf.toString());
204 ParseResult result = new ParseResult(new TemplateBlock(splitted.toArray(new String[splitted.size()]), commands.toArray(new Translatable[commands.size()])), blockType);
208 private boolean endsWith(StringBuffer buf, String string) {
209 return buf.length() >= string.length() && buf.substring(buf.length() - string.length(), buf.length()).equals(string);
212 private Translatable parseCommand(String s2, ParseContext context) {
213 if (s2.startsWith("=_")) {
214 final String raw = s2.substring(2);
215 if ( !s2.contains("$") && !s2.contains("!'")) {
216 return new TranslateCommand(raw);
218 return new SprintfCommand(raw);
220 } else if (s2.startsWith("=$")) {
221 final String raw = s2.substring(2);
222 return new OutputVariableCommand(raw);
224 context.addError("Unknown processing instruction \"" + s2 + "\"," + " did you mean \"" + EditDistance.getBestMatchingStringByEditDistance(s2, new String[] {
232 public void output(PrintWriter out, Language l, Map<String, Object> vars) {
234 data.output(out, l, vars);
237 protected void tryReload() {
238 if (source != null && lastLoaded < source.lastModified()) {
239 System.out.println("Reloading template.... " + source);
240 try (InputStreamReader r = new InputStreamReader(new FileInputStream(source), "UTF-8")) {
241 data = parse(r).getBlock(null);
243 lastLoaded = source.lastModified() + 1000;
244 } catch (IOException e) {
250 protected static void outputVar(PrintWriter out, Language l, Map<String, Object> vars, String varname, boolean unescaped) {
251 if (vars.containsKey(Outputable.OUT_KEY_PLAIN)) {
254 Object s = vars.get(varname);
257 System.err.println("Empty variable: " + varname);
259 if (s instanceof Outputable) {
260 ((Outputable) s).output(out, l, vars);
261 } else if (s instanceof DayDate) {
262 out.print(DateSelector.getDateFormat().format(((DayDate) s).toDate()));
263 } else if (s instanceof Boolean) {
264 out.print(((Boolean) s) ? l.getTranslation("yes") : l.getTranslation("no"));
265 } else if (s instanceof Date) {
266 SimpleDateFormat sdfUI = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
267 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
268 out.print("<time datetime=\"" + sdf.format(s) + "\">");
269 out.print(sdfUI.format(s));
270 out.print(" UTC</time>");
272 out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString())));
276 public void addTranslations(Collection<String> s) {
277 data.addTranslations(s);
280 private class ParseContext {
282 public static final int CONTEXT_LENGTH = 20;
284 private Reader reader;
286 public final TemplateParseException parseException = new TemplateParseException(source);
292 private int curChar = -1;
294 private int[] charContext = new int[CONTEXT_LENGTH];
296 protected int contextPosition = 0;
298 public ParseContext(Reader reader) {
299 this.reader = reader;
302 public void addError(String message) {
303 addError(line, column, message);
306 public void addError(int line, int column, String message) {
307 StringBuffer charContextBuffer = new StringBuffer();
308 int j = contextPosition;
309 for (int i = 0; i < CONTEXT_LENGTH; i++) {
310 if (charContext[j] != 0) {
311 if (charContext[j] == '\n') {
312 charContextBuffer.append("\\n");
314 charContextBuffer.appendCodePoint(charContext[j]);
317 j = (j + 1) % CONTEXT_LENGTH;
319 parseException.addError(line, column, message, charContextBuffer.toString());
322 public void merge(ParseContext other) {
324 column = other.column;
328 public void append(ParseContext other) {
329 parseException.append(other.parseException);
332 public int read() throws IOException {
334 while ((ch = reader.read()) == '\r') {
344 charContext[contextPosition] = ch;
345 contextPosition = (contextPosition + 1) % CONTEXT_LENGTH;
350 public ParseContext copy() {
351 ParseContext newParseContext = new ParseContext(reader);
352 newParseContext.line = line;
353 newParseContext.column = column;
354 newParseContext.charContext = Arrays.copyOf(charContext, charContext.length);
355 newParseContext.contextPosition = contextPosition;
356 return newParseContext;