001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.template;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.function.*;
023
024import org.apache.juneau.commons.svl.*;
025
026/**
027 * Renders SVL templates written by authors who are less trusted than the application.
028 *
029 * <p>
030 * A template resolver is a locked-down {@link VarResolver}: it registers no system vars, permits only the functions in
031 * {@link SafeFunctions}, and reads data only from the map passed to each render.  The built-in functions give lists, tables
032 * and per-row projections; see the package documentation for the grammar and the encoding rule.
033 *
034 * <p>
035 * Inside a projection over a list of maps, <c>size</c> and <c>empty</c> resolve to the row map's own size and emptiness
036 * even when the row has no such key, so they do not fall back to a scope entry of the same name.
037 *
038 * <h2 class='section'>Example:</h2>
039 * <p class='bjava'>
040 *    <jk>var</jk> <jv>resolver</jv> = TemplateResolver.<jsm>create</jsm>().destination(Destinations.<jsf>HTML</jsf>).build();
041 *    <jk>var</jk> <jv>template</jv> = <jv>resolver</jv>.parse(<js>"Failed: ${n}\n#{ul(failed.![id + ' - ' + msg])}"</js>);
042 *    <jk>var</jk> <jv>html</jv> = <jv>resolver</jv>.render(<jv>template</jv>, Map.<jsm>of</jsm>(<js>"n"</js>, 2, <js>"failed"</js>, <jv>rows</jv>));
043 * </p>
044 *
045 * <h2 class='section'>Notes:</h2><ul>
046 *    <li class='note'>Inside a projection (<c>.![...]</c>, <c>.?[...]</c>) row fields are bare identifiers.  A
047 *       <c>${id}</c> there reads the render scope, not the row.
048 *    <li class='note'>Functions added with {@link Builder#functions(VarFunction...)} are subject to the allowlist; add them
049 *       with {@link Builder#allowFunctions(String...)}.
050 *    <li class='note'>Instances are immutable and thread-safe.  {@link ParsedTemplate} instances are too.
051 * </ul>
052 */
053public final class TemplateResolver {
054
055   /**
056    * A resolver with the default configuration: lenient, no destination.
057    *
058    * <p>
059    * It does <b>no encoding</b> and renders structures with {@link Structure#toString()}.  Resolved values reach the
060    * output unescaped, so it is not for HTML, Markdown, JSON or URLs; build a resolver with a {@link Destinations}
061    * preset (or an explicit encoder) for those.
062    */
063   public static final TemplateResolver DEFAULT = create().build();
064
065   /**
066    * Creates a builder.
067    *
068    * @return A new builder.
069    */
070   public static Builder create() { return new Builder(); }
071
072   /** Builder for {@link TemplateResolver}. */
073   public static final class Builder {
074      private final List<Var> vars = new ArrayList<>();
075      private final List<VarFunction> functions = new ArrayList<>();
076      private final List<String> extra = new ArrayList<>();
077      private final List<String> only = new ArrayList<>();
078      private boolean replaced;
079      private boolean strict;
080      private int maxCollectionSize = VarResolver.DEFAULT_MAX_COLLECTION_SIZE;
081      private StructureRenderer renderer;
082      private Destination destination;
083
084      private Builder() {}
085
086      /**
087       * Registers additional vars.  None are registered by default except the internal scope var.
088       *
089       * @param values The vars.  Must not be <jk>null</jk>.  A var named <c>P</c> replaces the scope var, which rebinds
090       *    the <c>${name}</c> shortcut.
091       * @return This object.
092       */
093      public Builder vars(Var...values) {
094         for (var v : values)
095            vars.add(Objects.requireNonNull(v, "var must not be null"));
096         return this;
097      }
098
099      /**
100       * Registers additional functions.
101       *
102       * @param values The functions.  Must not be <jk>null</jk>.  They are also subject to the allowlist.
103       * @return This object.
104       */
105      public Builder functions(VarFunction...values) {
106         for (var f : values)
107            functions.add(Objects.requireNonNull(f, "function must not be null"));
108         return this;
109      }
110
111      /**
112       * Adds function-name globs to the default allowlist ({@link SafeFunctions#patterns()}).
113       *
114       * <p>
115       * Repeated calls are additive.  A bare <c>*</c> is rejected because it would switch default-deny off; use
116       * {@link #allowOnlyFunctions(String...)} to replace the defaults.
117       *
118       * @param patterns The patterns.  Must not be <jk>null</jk>, and must not be <c>*</c>.
119       * @return This object.
120       * @throws IllegalArgumentException If a pattern is <c>*</c>.
121       */
122      public Builder allowFunctions(String...patterns) {
123         addPatterns(extra, patterns);
124         return this;
125      }
126
127      /**
128       * Replaces the default function allowlist ({@link SafeFunctions#patterns()}) with exactly these globs.
129       *
130       * <p>
131       * Include <c>$op.*</c> to keep the operators.  Patterns from {@link #allowFunctions(String...)} are still added.
132       * A bare <c>*</c> is rejected.
133       *
134       * @param patterns The patterns.  Must not be <jk>null</jk>, and must not be <c>*</c>.
135       * @return This object.
136       * @throws IllegalArgumentException If a pattern is <c>*</c>.
137       */
138      public Builder allowOnlyFunctions(String...patterns) {
139         replaced = true;
140         addPatterns(only, patterns);
141         return this;
142      }
143
144      private static void addPatterns(List<String> to, String[] patterns) {
145         for (var p : patterns) {
146            Objects.requireNonNull(p, "pattern must not be null");
147            if (p.trim().equals("*"))
148               throw iaex("allowFunctions pattern '*' would disable default-deny");
149            to.add(p);
150         }
151      }
152
153      /**
154       * Enables strict mode: an unknown or denied function, or an expression error, throws instead of being emitted
155       * as written.
156       *
157       * @param value Whether to enable strict mode.
158       * @return This object.
159       */
160      public Builder strict(boolean value) {
161         strict = value;
162         return this;
163      }
164
165      /**
166       * Sets the maximum number of elements one expression may iterate, and the maximum per container when formatting.
167       *
168       * @param value The limit.  Must be positive.
169       * @return This object.
170       */
171      public Builder maxCollectionSize(int value) {
172         if (value <= 0)
173            throw iaex("maxCollectionSize must be positive: %s", value);
174         maxCollectionSize = value;
175         return this;
176      }
177
178      /**
179       * Sets how lists, blocks and tables are rendered.  Overrides the renderer of {@link #destination(Destination)}.
180       *
181       * <p>
182       * Without a renderer {@link Structure#toString()} is used, and its items are not encoded.
183       *
184       * @param value The renderer.  <jk>null</jk> clears it.
185       * @return This object.
186       */
187      public Builder structureRenderer(StructureRenderer value) {
188         renderer = value;
189         return this;
190      }
191
192      /**
193       * Sets the default value encoder and structure renderer from a destination preset.
194       *
195       * @param value The destination.  <jk>null</jk> clears it.
196       * @return This object.
197       */
198      public Builder destination(Destination value) {
199         destination = value;
200         return this;
201      }
202
203      /**
204       * Builds the resolver.
205       *
206       * @return A new resolver.
207       */
208      public TemplateResolver build() {
209         var b = VarResolver.create().defaultFunctions().strict(strict).maxCollectionSize(maxCollectionSize);
210         b.vars(new ScopeVar());
211         b.vars(vars.toArray(new Var[0]));
212         b.functions(functions.toArray(new VarFunction[0]));
213         var patterns = new ArrayList<>(replaced ? only : SafeFunctions.patterns());
214         patterns.addAll(extra);
215         b.allowFunctions(patterns.toArray(new String[0]));
216         var r = renderer;
217         if (r == null && destination != null)
218            r = destination.renderer();
219         var e = destination == null ? null : destination.encoder();
220         return new TemplateResolver(b.build(), r, e, new JsonValueFormatter(maxCollectionSize, strict));
221      }
222   }
223
224   private final VarResolver resolver;
225   private final UnaryOperator<String> encoder;
226   private final Function<Object,String> formatter;
227
228   private TemplateResolver(VarResolver resolver, StructureRenderer renderer, UnaryOperator<String> encoder, Function<Object,String> baseFormatter) {
229      this.resolver = resolver;
230      this.encoder = encoder;
231      this.formatter = renderer == null ? null : new StructureFormatter(renderer, baseFormatter);
232   }
233
234   /** Sends structures to the renderer and everything else to the base formatter. */
235   private record StructureFormatter(StructureRenderer renderer, Function<Object,String> base) implements Function<Object,String> {
236      @Override public String apply(Object o) {
237         return o instanceof Structure s ? renderer.render(s, base) : base.apply(o);
238      }
239   }
240
241   /**
242    * Parses a template.
243    *
244    * @param template The template text.  May be <jk>null</jk>.
245    * @return The parsed template.  Never <jk>null</jk>.
246    * @throws ExprException In strict mode, if the template is malformed or uses an unknown or denied function.
247    *    In lenient mode nothing is thrown.
248    */
249   public ParsedTemplate parse(String template) {
250      return new ParsedTemplate(resolver.compile(template), this);
251   }
252
253   /**
254    * Renders a template using the default value encoder of the configured destination (none if there is no destination).
255    *
256    * @param template The template.  Must not be <jk>null</jk>, and must have been parsed by this resolver.
257    * @param scope The names the template can read.  May be <jk>null</jk> (no names).
258    * @return The rendered text.
259    * @throws IllegalArgumentException If the template was parsed by a different resolver.
260    */
261   public String render(ParsedTemplate template, Map<String,?> scope) {
262      return render(template, scope, null);
263   }
264
265   /**
266    * Renders a template.
267    *
268    * @param template The template.  Must not be <jk>null</jk>, and must have been parsed by this resolver.
269    * @param scope The names the template can read.  May be <jk>null</jk> (no names).
270    * @param valueEncoder The value encoder.  Overrides the destination's encoder; <jk>null</jk> uses the destination's.
271    * @return The rendered text.
272    */
273   public String render(ParsedTemplate template, Map<String,?> scope, UnaryOperator<String> valueEncoder) {
274      Objects.requireNonNull(template, "template must not be null");
275      if (template.owner() != this)
276         throw iaex("template was parsed by a different TemplateResolver");
277      Function<String,Object> lookup = n -> n == null ? null : scope.get(n);
278      if (scope == null)
279         lookup = n -> null;
280      var session = resolver.createSession().scope(lookup);
281      if (formatter != null)
282         session.valueFormatter(formatter);
283      return template.template().resolve(session, valueEncoder != null ? valueEncoder : encoder);
284   }
285
286   /**
287    * Checks a template against a catalog without rendering it.  Never throws.
288    *
289    * <p>
290    * Reports, in source order: an identifier not in the catalog, a row field not in its list's catalog, a row field read
291    * through a name that is not a declared list, an unknown or denied function (unless the catalog allows them), an
292    * unknown var, and a marker that does not compile.  The key of a <c>${name}</c> or <c>${name:default}</c> reference is
293    * checked like an identifier, so <c>${secret}</c> is a problem when <c>secret</c> is not declared; a key that is
294    * itself computed (<c>${${x}}</c>) is read as written and is reported as unknown.  The default text after <c>:</c> is
295    * not checked.
296    *
297    * @param template The template text.  May be <jk>null</jk>.
298    * @param catalog The catalog.  Must not be <jk>null</jk>.
299    * @return The problems.  Empty if the template is clean.
300    */
301   public List<TemplateProblem> check(String template, TemplateCatalog catalog) {
302      Objects.requireNonNull(catalog, "catalog must not be null");
303      return check(compileOrProblems(template), catalog);
304   }
305
306   /** A compiled template, or the one problem that stopped it compiling. */
307   private record Compiled(VarTemplate template, List<TemplateProblem> failure) {}
308
309   private Compiled compileOrProblems(String template) {
310      try {
311         return new Compiled(resolver.compile(template), null);
312      } catch (ExprException e) {
313         var at = Math.max(0, e.getPos());
314         return new Compiled(null, List.of(new TemplateProblem(at, at + 1, e.getBaseMessage())));
315      }
316   }
317
318   private List<TemplateProblem> check(Compiled c, TemplateCatalog catalog) {
319      if (c.failure() != null)
320         return c.failure();
321      var t = c.template();
322      var out = new ArrayList<TemplateProblem>();
323      for (var r : t.references())
324         switch (r.kind()) {
325            case SYNTAX -> out.add(new TemplateProblem(r.start(), r.end(), r.name()));
326            case VAR -> {
327               if (! r.bound())
328                  out.add(new TemplateProblem(r.start(), r.end(), "unknown var '" + r.name() + "'"));
329            }
330            case FUNCTION -> {
331               if (! r.bound() && ! catalog.allowUnknownFunctions())
332                  out.add(new TemplateProblem(r.start(), r.end(), "unknown or denied function '" + r.name() + "'"));
333            }
334            case IDENT -> checkIdent(r, catalog, out);
335         }
336      out.sort(Comparator.comparingInt(TemplateProblem::start));
337      return out;
338   }
339
340   private static void checkIdent(TemplateReference r, TemplateCatalog catalog, List<TemplateProblem> out) {
341      var name = r.name();
342      var list = r.listName();
343      if (list == null) {
344         if (! catalog.hasName(name))
345            out.add(new TemplateProblem(r.start(), r.end(), "unknown name '" + name + "'"));
346      } else if (catalog.isList(list)) {
347         var rows = catalog.rowFields(list);
348         if (rows != null && ! rows.has(name) && ! catalog.hasName(name))
349            out.add(new TemplateProblem(r.start(), r.end(), "unknown field '" + name + "' in list '" + list + "'"));
350      } else if (catalog.hasName(list)) {
351         out.add(new TemplateProblem(r.start(), r.end(), "'" + list + "' is not a declared list"));
352      }
353      // An unknown list name is reported where it is read, so it is not repeated for each row field.
354   }
355
356   /**
357    * Like {@link #check(String, TemplateCatalog)} but throws.
358    *
359    * @param template The template text.  May be <jk>null</jk>.
360    * @param catalog The catalog.  Must not be <jk>null</jk>.
361    * @return The parsed template, if it is clean.
362    * @throws TemplateValidationException If there are problems.
363    */
364   public ParsedTemplate validate(String template, TemplateCatalog catalog) {
365      Objects.requireNonNull(catalog, "catalog must not be null");
366      var c = compileOrProblems(template);
367      var problems = check(c, catalog);
368      if (! problems.isEmpty())
369         throw new TemplateValidationException(problems);
370      return new ParsedTemplate(c.template(), this);
371   }
372}