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}