View Javadoc
1   /*
2    * Copyright (C) 2002-2003,2017-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
3    */
4   package de.powerstat.phplib.templateengine;
5   
6   
7   import java.io.BufferedReader;
8   import java.io.File;
9   import java.io.FileNotFoundException;
10  import java.io.IOException;
11  import java.io.InputStream;
12  import java.io.InputStreamReader;
13  import java.nio.charset.StandardCharsets;
14  import java.util.Collections;
15  import java.util.List;
16  import java.util.Objects;
17  import java.util.regex.Pattern;
18  
19  import de.powerstat.phplib.templateengine.intern.BlockManager;
20  import de.powerstat.phplib.templateengine.intern.FileManager;
21  import de.powerstat.phplib.templateengine.intern.VariableManager;
22  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
23  
24  
25  /**
26   * PHPLib compatible template engine.
27   *
28   * Unconditionally thread safe.
29   * Not serializable, because serialization is dangerous, use Protocol Buffers or JSON instead!
30   *
31   * @author Kai Hofmann
32   * @see <a href="https://sourceforge.net/projects/phplib/">https://sourceforge.net/projects/phplib/</a>
33   * @see <a href="https://pear.php.net/package/HTML_Template_PHPLIB">https://pear.php.net/package/HTML_Template_PHPLIB</a>
34   */
35  public final class TemplateEngine
36   {
37    /* *
38     * Logger.
39     */
40    // private static final Logger LOGGER = LogManager.getLogger(TemplateEngine.class);
41  
42    /**
43     * Template name constant.
44     */
45    private static final String TEMPLATE = "template"; //$NON-NLS-1$
46  
47    /**
48     * Template is empty message.
49     */
50    private static final String TEMPLATE_IS_EMPTY = "template is empty"; //$NON-NLS-1$
51  
52    /**
53     * Maximum varname size.
54     */
55    private static final int MAX_VARNAME_SIZE = 64;
56  
57    /**
58     * Varname name constant.
59     */
60    private static final String VARNAME = "varname"; //$NON-NLS-1$
61  
62    /**
63     * Varname is empty error message constant.
64     */
65    private static final String VARNAME_IS_EMPTY = "varname is empty"; //$NON-NLS-1$
66  
67    /**
68     * Varname is to long error message constant.
69     */
70    private static final String VARNAME_IS_TO_LONG = "varname is to long"; //$NON-NLS-1$
71  
72    /**
73     * Varname does not match name pattern error message constant.
74     */
75    private static final String VARNAME_DOES_NOT_MATCH_NAME_PATTERN = "varname does not match name pattern"; //$NON-NLS-1$
76  
77    /**
78     * Varname regexp pattern.
79     */
80    private static final Pattern VARNAME_REGEXP = Pattern.compile("^\\w{1,64}$", Pattern.UNICODE_CHARACTER_CLASS); //$NON-NLS-1$
81  
82    /**
83     * Block matcher regexp.
84     */
85    private static final Pattern BLOCK_MATCHER_REGEXP = Pattern.compile("\\{([^ \\t\\r\\n}]+)\\}"); //$NON-NLS-1$
86  
87    /**
88     * Maximum template size.
89     */
90    private static final int MAX_TEMPLATE_SIZE = 1048576;
91  
92    /**
93     * Handling of undefined template variables.
94     *
95     * "remove"/0  =&gt; remove undefined variables
96     * "keep"/1    =&gt; keep undefined variables
97     * "comment"/2 =&gt; replace undefined variables with comments
98     */
99    private final HandleUndefined unknowns;
100 
101   /**
102    * Variable manager.
103    */
104   private final VariableManager variableManager;
105 
106   /**
107    * File manager.
108    */
109   private final FileManager fileManager;
110 
111   /**
112    * Block manager.
113    */
114   private final BlockManager blockManager;
115 
116 
117   /**
118    * Copy constructor.
119    *
120    * @param engine Template engine
121    * @throws NullPointerException If engine is null
122    */
123   public TemplateEngine(final TemplateEngine engine)
124    {
125     Objects.requireNonNull(engine, "engine"); //$NON-NLS-1$
126     this.unknowns = engine.unknowns;
127     this.variableManager = new VariableManager(engine.variableManager);
128     this.fileManager = new FileManager(this.variableManager, engine.fileManager);
129     this.blockManager = new BlockManager(this.variableManager, engine.blockManager);
130    }
131 
132 
133   /**
134    * Constructor.
135    *
136    * @param unknowns Handling of unknown template variables
137    * @see HandleUndefined
138    */
139   public TemplateEngine(final HandleUndefined unknowns)
140    {
141     this.unknowns = unknowns;
142     this.variableManager = new VariableManager();
143     this.fileManager = new FileManager(this.variableManager);
144     this.blockManager = new BlockManager(this.variableManager);
145    }
146 
147 
148   /**
149    * Default constructor - unknown variables will be handled as "remove", root is current directory.
150    */
151   public TemplateEngine()
152    {
153     this(HandleUndefined.REMOVE);
154    }
155 
156 
157   /**
158    * Copy factory.
159    *
160    * @param engine TemplateEngine to copy
161    * @return A new TemplateEngine instance that is a copy of engine.
162    * @throws NullPointerException If engine is null
163    */
164   public static TemplateEngine newInstance(final TemplateEngine engine)
165    {
166     return new TemplateEngine(engine);
167    }
168 
169 
170   /**
171    * Get new instance from a UTF-8 encoded text file.
172    *
173    * @param file Text file (UTF-8 encoded) to load as template
174    * @return A new TemplateEngine instance where the template variable name is 'template'
175    * @throws FileNotFoundException When the given file does not exist
176    * @throws IOException When the given file is to large
177    * @throws IllegalArgumentException When the given file is null
178    * @throws NullPointerException If file is null
179    */
180   @SuppressWarnings("java:S1162")
181   public static TemplateEngine newInstance(final File file) throws IOException
182    {
183     Objects.requireNonNull(file, "file"); //$NON-NLS-1$
184     if (!file.isFile())
185      {
186       if (!file.exists())
187        {
188         throw new FileNotFoundException(file.getAbsolutePath());
189        }
190       // Load all files (*.tmpl) from directory?
191       throw new AssertionError(file.getAbsolutePath() + " is a directory and not a file!"); //$NON-NLS-1$
192      }
193     final long fileLen = file.length();
194     if (fileLen > TemplateEngine.MAX_TEMPLATE_SIZE)
195      {
196       throw new IOException("file to large: " + fileLen); //$NON-NLS-1$
197      }
198     final var templ = new TemplateEngine();
199     /*
200     String filename = file.getName();
201     final int extPos = filename.lastIndexOf('.');
202     if (extPos > -1)
203      {
204       filename = filename.substring(0, extPos);
205      }
206     filename = filename.toLowerCase(Locale.getDefault());
207     templ.setFile(filename, file);
208     */
209     templ.setFile(TemplateEngine.TEMPLATE, file);
210     return templ;
211    }
212 
213 
214   /**
215    * Get new instance from a stream.
216    *
217    * @param stream UTF-8 stream to read the template from
218    * @return A new TemplateEngine instance where the template variable name is 'template'.
219    * @throws IOException If an I/O error occurs
220    * @throws IllegalArgumentException When the given stream is null
221    * @throws IllegalStateException If the stream is empty
222    * @throws NullPointerException If stream is null
223    */
224   @SuppressFBWarnings("EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS")
225   public static TemplateEngine newInstance(final InputStream stream) throws IOException
226    {
227     Objects.requireNonNull(stream, "stream"); //$NON-NLS-1$
228     final var fileBuffer = new StringBuilder();
229     try (var reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)))
230      {
231       String line = reader.readLine();
232       while (line != null)
233        {
234         fileBuffer.append(line);
235         fileBuffer.append('\n');
236         line = reader.readLine();
237        }
238      }
239     if (fileBuffer.length() == 0)
240      {
241       throw new IllegalStateException("Empty stream"); //$NON-NLS-1$
242      }
243     final var templ = new TemplateEngine();
244     templ.setVar(TemplateEngine.TEMPLATE, fileBuffer.toString());
245     return templ;
246    }
247 
248 
249   /**
250    * Get new instance from a string.
251    *
252    * @param template Template string
253    * @return A new TemplateEngine instance where the template variable name is 'template'.
254    * @throws IllegalArgumentException When the given string is null or empty
255    * @throws NullPointerException If template is null
256    */
257   public static TemplateEngine newInstance(final String template)
258    {
259     Objects.requireNonNull(template, TemplateEngine.TEMPLATE);
260     if (template.isEmpty())
261      {
262       throw new IllegalArgumentException(TemplateEngine.TEMPLATE_IS_EMPTY);
263      }
264     if (template.length() > TemplateEngine.MAX_TEMPLATE_SIZE)
265      {
266       throw new IllegalArgumentException("template to large"); //$NON-NLS-1$
267      }
268     // if (!template.matches("^.+$"))
269     final var templ = new TemplateEngine();
270     templ.setVar(TemplateEngine.TEMPLATE, template);
271     return templ;
272    }
273 
274 
275   /**
276    * Set template file for variable.
277    *
278    * @param newVarname Variable that should hold the template
279    * @param newFile Template file UTF-8 encoded
280    * @return true when successful (file exists) otherwise false
281    * @throws NullPointerException If newVarname or newFile is null
282    * @throws IllegalArgumentException If newVarname is empty
283    */
284   @SuppressWarnings({"PMD.LinguisticNaming", "java:S3457"})
285   public boolean setFile(final String newVarname, final File newFile)
286    {
287     Objects.requireNonNull(newVarname, "newVarname"); //$NON-NLS-1$
288     Objects.requireNonNull(newFile, "newFile"); //$NON-NLS-1$
289     if (newVarname.isEmpty())
290      {
291       throw new IllegalArgumentException("newVarname is empty"); //$NON-NLS-1$
292      }
293     if (newVarname.length() > TemplateEngine.MAX_VARNAME_SIZE)
294      {
295       throw new IllegalArgumentException("newVarname is to long"); //$NON-NLS-1$
296      }
297     if (!TemplateEngine.VARNAME_REGEXP.matcher(newVarname).matches())
298      {
299       throw new IllegalArgumentException("newVarname does not match name pattern"); //$NON-NLS-1$
300      }
301     return this.fileManager.addFile(newVarname, newFile);
302    }
303 
304 
305   /**
306    * Get template variable value.
307    *
308    * @param varname Template variable name
309    * @return Template variables value
310    * @throws NullPointerException If varname is null
311    * @throws IllegalArgumentException If varname is empty
312    */
313   public String getVar(final String varname)
314    {
315     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
316     if (varname.isEmpty())
317      {
318       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_EMPTY);
319      }
320     if (varname.length() > TemplateEngine.MAX_VARNAME_SIZE)
321      {
322       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_TO_LONG);
323      }
324     if (!TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
325      {
326       throw new IllegalArgumentException(TemplateEngine.VARNAME_DOES_NOT_MATCH_NAME_PATTERN);
327      }
328     return this.variableManager.getVar(varname);
329    }
330 
331 
332   /**
333    * Set template variables value.
334    *
335    * @param varname Template variable name
336    * @param value Template variable value, could  be null
337    * @throws NullPointerException If varname is null
338    * @throws IllegalArgumentException If varname is empty
339    */
340   public void setVar(final String varname, final String value)
341    {
342     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
343     if (varname.isEmpty())
344      {
345       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_EMPTY);
346      }
347     if (varname.length() > TemplateEngine.MAX_VARNAME_SIZE)
348      {
349       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_TO_LONG);
350      }
351     if ((value != null) && (value.length() > TemplateEngine.MAX_TEMPLATE_SIZE))
352      {
353       throw new IllegalArgumentException("value is to large"); //$NON-NLS-1$
354      }
355     if (!TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
356      {
357       throw new IllegalArgumentException(TemplateEngine.VARNAME_DOES_NOT_MATCH_NAME_PATTERN);
358      }
359     this.variableManager.setVar(varname, value);
360    }
361 
362 
363   /**
364    * Set template variable as empty.
365    *
366    * @param varname Template variable name
367    * @throws NullPointerException If varname is null
368    * @throws IllegalArgumentException If varname is empty
369    */
370   public void setVar(final String varname)
371    {
372     setVar(varname, "");
373    }
374 
375 
376   /**
377    * Unset template variable.
378    *
379    * @param varname Template variable name
380    * @throws NullPointerException If varname is null
381    * @throws IllegalArgumentException If varname is empty
382    */
383   public void unsetVar(final String varname)
384    {
385     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
386     if (varname.isEmpty())
387      {
388       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_EMPTY);
389      }
390     if (varname.length() > TemplateEngine.MAX_VARNAME_SIZE)
391      {
392       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_TO_LONG);
393      }
394     if (!TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
395      {
396       throw new IllegalArgumentException(TemplateEngine.VARNAME_DOES_NOT_MATCH_NAME_PATTERN);
397      }
398     this.variableManager.unsetVar(varname);
399    }
400 
401 
402   /**
403    * Set template block (cut it from parent template and replace it with a variable).
404    *
405    * Used for repeatable blocks
406    *
407    * @param parent Name of parent template variable
408    * @param varname Name of template block
409    * @param name Name of variable in which the block will be placed - if empty will be the same as varname
410    * @return true on sucess otherwise false
411    * @throws IOException IO exception
412    * @throws IllegalStateException When no block with varname is found.
413    * @throws NullPointerException If parent or varname is null
414    * @throws IllegalArgumentException If parent or varname is empty
415    */
416   @SuppressWarnings("PMD.LinguisticNaming")
417   public boolean setBlock(final String parent, final String varname, final String name) throws IOException
418    {
419     Objects.requireNonNull(parent, "parent"); //$NON-NLS-1$
420     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
421     Objects.requireNonNull(name, "name"); //$NON-NLS-1$
422     if (parent.isEmpty() || varname.isEmpty())
423      {
424       throw new IllegalArgumentException("parent or varname is empty"); //$NON-NLS-1$
425      }
426     if ((parent.length() > TemplateEngine.MAX_VARNAME_SIZE) || (varname.length() > TemplateEngine.MAX_VARNAME_SIZE) || (name.length() > TemplateEngine.MAX_VARNAME_SIZE))
427      {
428       throw new IllegalArgumentException("parent, varname or name is to long"); //$NON-NLS-1$
429      }
430     if (!TemplateEngine.VARNAME_REGEXP.matcher(parent).matches() || !TemplateEngine.VARNAME_REGEXP.matcher(varname).matches() || (!name.isEmpty() && (!TemplateEngine.VARNAME_REGEXP.matcher(name).matches())))
431      {
432       throw new IllegalArgumentException("parent, varname or name does not match name pattern"); //$NON-NLS-1$
433      }
434     if (!this.fileManager.loadFile(parent))
435      {
436       return false;
437      }
438     return this.blockManager.setBlock(parent, varname, name);
439    }
440 
441 
442   /**
443    * Set template block (cut it from parent template and replace it with a variable).
444    *
445    * Used for on/off blocks
446    *
447    * @param parent Name of parent template variable
448    * @param varname Name of template block
449    * @return true on sucess otherwise false
450    * @throws IOException IO exception
451    * @throws IllegalStateException When no block with varname is found.
452    * @throws NullPointerException If parent or varname is null
453    * @throws IllegalArgumentException If parent or varname is empty
454    */
455   @SuppressWarnings("PMD.LinguisticNaming")
456   public boolean setBlock(final String parent, final String varname) throws IOException
457    {
458     return setBlock(parent, varname, "");
459    }
460 
461 
462   /**
463    * Substitute variable with its content.
464    *
465    * @param varname Variable name
466    * @return Replaced variable content or empty string
467    * @throws IOException File not found or IO exception
468    * @throws NullPointerException If varname is null
469    * @throws IllegalArgumentException If varname is empty
470    */
471   public String subst(final String varname) throws IOException
472    {
473     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
474     if (varname.isEmpty())
475      {
476       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_EMPTY);
477      }
478     if (varname.length() > TemplateEngine.MAX_VARNAME_SIZE)
479      {
480       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_TO_LONG);
481      }
482     if (!TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
483      {
484       throw new IllegalArgumentException(TemplateEngine.VARNAME_DOES_NOT_MATCH_NAME_PATTERN);
485      }
486     if (!this.fileManager.loadFile(varname))
487      {
488       return ""; //$NON-NLS-1$
489      }
490     return this.variableManager.subst(varname);
491    }
492 
493 
494   /**
495    * Parse a variable and replace all variables within it by their content.
496    *
497    * @param target Target for parsing operation
498    * @param varname Parse the content of this variable
499    * @param append true for appending blocks to target, otherwise false for replacing targets content
500    * @return Variables content after parsing
501    * @throws IOException File not found or IO exception
502    * @throws NullPointerException If target or varname is null
503    * @throws IllegalArgumentException If target or varname is empty
504    */
505   @SuppressWarnings("java:S2301")
506   public String parse(final String target, final String varname, final boolean append) throws IOException
507    {
508     Objects.requireNonNull(target, "target"); //$NON-NLS-1$
509     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
510     if (target.isEmpty() || varname.isEmpty())
511      {
512       throw new IllegalArgumentException("target or varname is empty"); //$NON-NLS-1$
513      }
514     if ((target.length() > TemplateEngine.MAX_VARNAME_SIZE) || (varname.length() > TemplateEngine.MAX_VARNAME_SIZE))
515      {
516       throw new IllegalArgumentException("target or varname is to long"); //$NON-NLS-1$
517      }
518     if (!TemplateEngine.VARNAME_REGEXP.matcher(target).matches() || !TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
519      {
520       throw new IllegalArgumentException("target or varname does not match name pattern"); //$NON-NLS-1$
521      }
522     if (!this.fileManager.loadFile(varname))
523      {
524       return ""; //$NON-NLS-1$
525      }
526     return this.variableManager.parse(target, varname, append);
527    }
528 
529 
530   /**
531    * Parse a variable and replace all variables within it by their content.
532    *
533    * Don't append
534    *
535    * @param target Target for parsing operation
536    * @param varname Parse the content of this variable
537    * @return Variables content after parsing
538    * @throws IOException File not found or IO exception
539    * @throws NullPointerException If target or varname is null
540    * @throws IllegalArgumentException If target or varname is empty
541    */
542   public String parse(final String target, final String varname) throws IOException
543    {
544     return parse(target, varname, false);
545    }
546 
547 
548   /**
549    * Get list of all template variables.
550    *
551    * @return Array with names of template variables
552    */
553   public List<String> getVars()
554    {
555     return this.variableManager.getVars();
556    }
557 
558 
559   /**
560    * Get list with all undefined template variables.
561    *
562    * @param varname Variable to parse for undefined variables
563    * @return List with undefined template variables names
564    * @throws IOException  File not found or IO exception
565    * @throws NullPointerException If varname is null
566    * @throws IllegalArgumentException If varname is empty
567    */
568   public List<String> getUndefined(final String varname) throws IOException
569    {
570     Objects.requireNonNull(varname, TemplateEngine.VARNAME);
571     if (varname.isEmpty())
572      {
573       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_EMPTY);
574      }
575     if (varname.length() > TemplateEngine.MAX_VARNAME_SIZE)
576      {
577       throw new IllegalArgumentException(TemplateEngine.VARNAME_IS_TO_LONG);
578      }
579     if (!TemplateEngine.VARNAME_REGEXP.matcher(varname).matches())
580      {
581       throw new IllegalArgumentException(TemplateEngine.VARNAME_DOES_NOT_MATCH_NAME_PATTERN);
582      }
583     if (!this.fileManager.loadFile(varname))
584      {
585       return Collections.emptyList();
586      }
587     return this.variableManager.getUndefined(varname);
588    }
589 
590 
591   /**
592    * Handle undefined template variables after parsing has happened.
593    *
594    * @param template Template to parse for unknown variables
595    * @return Modified template as specified by the "unknowns" setting
596    * @throws NullPointerException If varname is null
597    * @throws IllegalArgumentException If varname is empty
598    */
599   public String finish(final String template)
600    {
601     Objects.requireNonNull(template, TemplateEngine.TEMPLATE);
602     if (template.isEmpty())
603      {
604       throw new IllegalArgumentException(TemplateEngine.TEMPLATE_IS_EMPTY);
605      }
606     if (template.length() > TemplateEngine.MAX_TEMPLATE_SIZE)
607      {
608       throw new IllegalArgumentException("template is to large"); //$NON-NLS-1$
609      }
610     // if (!template.matches("^.+$"))
611     String result = template;
612     final var matcher = TemplateEngine.BLOCK_MATCHER_REGEXP.matcher(result);
613     switch (this.unknowns)
614      {
615       case KEEP:
616         break;
617       case REMOVE:
618         result = matcher.replaceAll(""); //$NON-NLS-1$
619         break;
620       case COMMENT:
621         result = matcher.replaceAll("<!-- Template variable '$1' undefined -->"); //$NON-NLS-1$
622         break;
623       default: // For the case that enum HandleUndefined will be extended!
624         throw new AssertionError(this.unknowns);
625      }
626     return result;
627    }
628 
629 
630   /**
631    * Shortcut for finish(getVar(varname)).
632    *
633    * @param varname Name of template variable
634    * @return Value of template variable
635    * @throws NullPointerException If varname is null
636    * @throws IllegalArgumentException If varname is empty
637    */
638   public String get(final String varname)
639    {
640     return finish(getVar(varname));
641    }
642 
643 
644   /**
645    * Returns the string representation of this TemplatEngine.
646    *
647    * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
648    *
649    * "TemplateEngine[unknowns=REMOVE, vManager=[name, ...]]"
650    *
651    * @return String representation of this TemplatEngine.
652    * @see java.lang.Object#toString()
653    */
654   @Override
655   public String toString()
656    {
657     return new StringBuilder().append("TemplateEngine[unknowns=").append(this.unknowns).append(", vManager=").append(this.variableManager).append(", fManager=").append(this.fileManager).append(", bManager=").append(this.blockManager).append(']').toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
658    }
659 
660 
661   /**
662    * Calculate hash code.
663    *
664    * @return Hash
665    * @see java.lang.Object#hashCode()
666    */
667   @Override
668   public int hashCode()
669    {
670     return Objects.hash(this.unknowns, this.variableManager, this.fileManager, this.blockManager);
671    }
672 
673 
674   /**
675    * Is equal with another object.
676    *
677    * @param obj Object
678    * @return true when equal, false otherwise
679    * @see java.lang.Object#equals(java.lang.Object)
680    */
681   @Override
682   public boolean equals(final Object obj)
683    {
684     if (this == obj)
685      {
686       return true;
687      }
688     if (!(obj instanceof TemplateEngine))
689      {
690       return false;
691      }
692     final TemplateEngine other = (TemplateEngine)obj;
693     return (this.unknowns == other.unknowns) && this.variableManager.equals(other.variableManager) && this.fileManager.equals(other.fileManager) && this.blockManager.equals(other.blockManager);
694    }
695 
696  }