001package io.jstach.jstachio.context; 002 003import java.lang.reflect.Array; 004import java.util.Arrays; 005import java.util.Collections; 006import java.util.Iterator; 007import java.util.Map; 008import java.util.Optional; 009import java.util.function.Function; 010import java.util.stream.Stream; 011import java.util.stream.StreamSupport; 012 013import org.eclipse.jdt.annotation.NonNull; 014import org.eclipse.jdt.annotation.Nullable; 015 016import io.jstach.jstache.JStacheType; 017import io.jstach.jstachio.Appender; 018import io.jstach.jstachio.Formatter; 019import io.jstach.jstachio.Formatter.Formattable; 020import io.jstach.jstachio.Output; 021import io.jstach.jstachio.context.Internal.ContextNodeFactory; 022import io.jstach.jstachio.context.Internal.EmptyContextNode; 023import io.jstach.jstachio.context.Internal.FunctionContextNode; 024 025/** 026 * This interface serves three puproses: 027 * 028 * <ol> 029 * <li>A way to represent the current context stack (see {@link #parent()}) 030 * <li>Allow you to simulate JSON/Javscript object node like trees without being coupled 031 * to a particularly JSON lib. 032 * <li>Represent per request context data in a web framework like CSRF tokens. 033 * </ol> 034 * The interface simply wraps {@link Map} and {@link Iterable} (and arrays) lazily through 035 * composition but generally cannot wrap other context nodes. If an object is wrapped that 036 * is not a Map or Iterable it becomes a leaf node similar to JSON. 037 * <p> 038 * It is not recommended you use this interface as it avoids much of the type checking 039 * safty of this library, decreases performance as well as increase coupling however it 040 * does provide a slightly better bridge to legacy {@code Map<String,?>} models over using 041 * the maps directly. 042 * <p> 043 * Context Node while similar to a Map does not follow the same rules of resolution where 044 * Map resolves bindings always last. It will resolve first and thus it is easy to 045 * accidentally get stuck in the Context Node context. To prevent this it is highly 046 * recommended you do not open a context node with a section block and prefer dotted 047 * notation to access it. 048 * 049 * <h2>Example:</h2> 050 * 051 * <pre><code class="language-hbs"> 052 * {{message}} 053 * {{#@context}} 054 * {{message}} {{! message here will only ever resolve against @context and not the parent }} 055 * {{/@context}} 056 * </code> </pre> 057 * 058 * @apiNote The parents do not know anything about their children as it is the child that 059 * has reference to the parent. This interface unlike most of JStachio API is very 060 * <code>null</code> heavy because JSON and Javascript allow <code>null</code>. 061 * @author agentgt 062 * @see ContextJStachio 063 */ 064public sealed interface ContextNode extends Formattable, Iterable<@Nullable ContextNode> { 065 066 /** 067 * The default binding name in mustache for the context parameter. The context comes 068 * from the context parameter from either 069 * {@link ContextJStachio#execute(Object, ContextNode, Output)} or 070 * {@link ContextJStachio#write(Object, ContextNode, io.jstach.jstachio.Output.EncodedOutput)}. 071 * <p> 072 * <strong>This variable is not bound if the generated template is 073 * {@link JStacheType#STACHE}</strong> 074 */ 075 public static final String CONTEXT_BINDING_NAME = "@context"; 076 077 /** 078 * Creates a root context node with the given function to look up children. 079 * @param function used to find children with a given name 080 * @return root context node powered by a function 081 * @apiNote Unlike many other methods in this class this is not nullable. 082 */ 083 public static ContextNode of(Function<String, ?> function) { 084 if (isNull(function)) { 085 throw new NullPointerException("function is required"); 086 } 087 return new FunctionContextNode(function); 088 } 089 090 private static boolean isNull(Object o) { 091 return o == null; 092 } 093 094 /** 095 * An empty context node that is safe to use identify comparison. 096 * @return empty singleton context node 097 */ 098 public static ContextNode empty() { 099 return EmptyContextNode.EMPTY; 100 } 101 102 /** 103 * Resolves the context node from an object. 104 * @param o object that maybe a context or have a context. 105 * @return {@link #empty()} if not found. 106 */ 107 public static ContextNode resolve(Object o) { 108 if (o instanceof ContextSupplier cs) { 109 return cs.context(); 110 } 111 if (o instanceof ContextNode n) { 112 return n; 113 } 114 return ContextNode.empty(); 115 } 116 117 /** 118 * Resolves the context node trying first and then second. 119 * @param first first object to try 120 * @param second second object to try 121 * @return {@link #empty()} if not found. 122 */ 123 public static ContextNode resolve(Object first, Object second) { 124 var f = resolve(first); 125 if (f == ContextNode.empty()) { 126 return resolve(second); 127 } 128 return f; 129 } 130 131 /** 132 * Creates the root node from an Object. 133 * @param o the object to be wrapped. Maybe <code>null</code>. 134 * @return {@link ContextNode#empty()} if the root object is null otherwise a new root 135 * node. 136 * @apiNote this method is legacy and mainly used for testing. Prefer 137 * {@link #of(Function)}. Prior to 1.3.0 the method may return null but now it will 138 * always return nonnull. 139 */ 140 public static ContextNode ofRoot(@Nullable Object o) { 141 if (o == null) { 142 return ContextNode.empty(); 143 } 144 return ContextNodeFactory.INSTANCE.create(null, o); 145 } 146 147 /** 148 * Gets a field from a ContextNode. This is direct access (end of a dotted path) and 149 * does not check the parents. The default implementation will check if the wrapping 150 * object is a {@link Map} and use it to return a child context node. 151 * 152 * Just like {@link Map} <code>null</code> will be returned if no field is found. 153 * @param field the name of the field 154 * @return a new child node. Maybe <code>null</code>. 155 */ 156 public @Nullable ContextNode get(String field); 157 158 /** 159 * Will search up the tree for a field starting at this nodes children first. 160 * @param field context name (e.g. section name) 161 * @return <code>null</code> if not found otherwise creates a new node from the map or 162 * object containing the field. 163 */ 164 public @Nullable ContextNode find(String field); 165 166 /** 167 * The object being wrapped. 168 * @return the Map, Iterable or object that was wrapped. Never <code>null</code>. 169 */ 170 public Object object(); 171 172 /** 173 * Convenience method for calling <code>toString</code> on the wrapped object. 174 * @return a toString on the wrapped object. 175 */ 176 default String renderString() { 177 return String.valueOf(object()); 178 } 179 180 /** 181 * The parent node. 182 * @return the parent node or <code>null</code> if this is the root. 183 */ 184 default @Nullable ContextNode parent() { 185 return null; 186 } 187 188 /** 189 * If the node is a Map or a non iterable/array a singleton iterator will be returned. 190 * Otherwise if it is an iterable/array new child context nodes will be created 191 * lazily. 192 * @return lazy iterator of context nodes. 193 * @apiNote Notice that return iterator may return <code>null</code> elements as JSON 194 * lists may contain <code>null</code> elements. 195 */ 196 @SuppressWarnings("exports") 197 @Override 198 public Iterator<@Nullable ContextNode> iterator(); 199 200 /** 201 * Determines if the node is falsey. If falsey (return of true) inverted section 202 * blocks will be executed. The default checks if {@link #iterator()} has any next 203 * elements and if it does not it is falsey. 204 * @return true if falsey. 205 */ 206 @SuppressWarnings("AmbiguousMethodReference") 207 default boolean isFalsey() { 208 return !iterator().hasNext(); 209 } 210 211 /** 212 * Determines if an object is falsey based on mustache spec semantics where: 213 * <code>null</code>, empty iterables, empty arrays and boolean <code>false</code> are 214 * falsey however <strong>empty Map is not falsey</strong>. {@link Optional} is falsey 215 * if it is empty. 216 * @param context a context object. ContextNode are allowed as input as well as 217 * <code>null</code>. 218 * @return true if the object is falsey. 219 */ 220 @SuppressWarnings("AmbiguousMethodReference") 221 static boolean isFalsey(@Nullable Object context) { 222 if ((context == null) || Boolean.FALSE.equals(context)) { 223 return true; 224 } 225 if (context instanceof Optional<?> o) { 226 return o.isEmpty(); 227 } 228 if (context instanceof Iterable<?> it) { 229 return !it.iterator().hasNext(); 230 } 231 if (context.getClass().isArray() && Array.getLength(context) == 0) { 232 return true; 233 } 234 if (context instanceof ContextNode n) { 235 return isFalsey(n); 236 } 237 return false; 238 } 239 240 /** 241 * Determines if the node is falsey based on mustache spec semantics where: 242 * <code>null</code>, empty iterables, empty arrays and boolean <code>false</code> are 243 * falsey however <strong>empty Map is not falsey</strong> but 244 * {@link ContextNode#empty()} is always falsey. 245 * @param context a context node. <code>null</code>. 246 * @return true if the node is falsey. 247 */ 248 @SuppressWarnings("AmbiguousMethodReference") 249 static boolean isFalsey(@Nullable ContextNode context) { 250 if (context == null) { 251 return true; 252 } 253 return context.isFalsey(); 254 } 255 256} 257 258@SuppressWarnings("exports") 259sealed interface Internal extends ContextNode { 260 261 /** 262 * Creates a named child node off of this node where the return child nodes parent 263 * will be this node. 264 * @param name the context name. 265 * @param o the object to be wrapped. 266 * @return <code>null</code> if the child object is null otherwise a new child node. 267 * @throws IllegalArgumentException if the input object is a {@link ContextNode} 268 */ 269 default @Nullable ContextNode ofChild(String name, @Nullable Object o) throws IllegalArgumentException { 270 return ContextNodeFactory.INSTANCE.ofChild(this, name, o); 271 } 272 273 /** 274 * Creates an indexed child node off of this node where the return child nodes parent 275 * will be this node. 276 * @param index a numeric index 277 * @param o the object to be wrapped. Maybe <code>null</code>. 278 * @return <code>null</code> if the child object is null otherwise a new child node. 279 * @throws IllegalArgumentException if the input object is a {@link ContextNode} 280 * @apiNote there is no checking to see if the same index is reused as the parent 281 * knows nothing of the child. 282 */ 283 default @Nullable ContextNode ofChild(int index, @Nullable Object o) { 284 return ContextNodeFactory.INSTANCE.ofChild(this, index, o); 285 } 286 287 @Override 288 default @Nullable ContextNode find(String field) { 289 /* 290 * In theory we could make a special RenderingContext for ContextNode to go up the 291 * stack (generated code) but it would probably look similar to the following. 292 */ 293 ContextNode child = get(field); 294 if (child != null) { 295 return child; 296 } 297 var parent = parent(); 298 if (parent != null && parent != this) { 299 child = parent.find(field); 300 if (child != null) { 301 child = ofChild(field, child.object()); 302 } 303 } 304 return child; 305 } 306 307 enum ContextNodeFactory { 308 309 INSTANCE; 310 311 @Nullable 312 ContextNode ofChild(ContextNode parent, String name, @Nullable Object o) { 313 if (o == null) { 314 return null; 315 } 316 return create(parent, o); 317 } 318 319 @Nullable 320 ContextNode ofChild(ContextNode parent, int index, @Nullable Object o) { 321 if (o == null) { 322 return null; 323 } 324 return create(parent, o); 325 326 } 327 328 ContextNode create(@Nullable ContextNode parent, Object o) { 329 if (o instanceof ContextNode) { 330 throw new IllegalArgumentException("Cannot wrap ContextNode around another ContextNode"); 331 } 332 if (o instanceof Iterable<?> it) { 333 return new IterableContextNode(it, parent); 334 } 335 if (o instanceof Map<?, ?> m) { 336 return new MapContextNode(m, parent); 337 } 338 if (o instanceof Optional<?> opt) { 339 return new OptionalContextNode(opt, parent); 340 } 341 return new ValueContextNode(o, parent); 342 343 } 344 345 public Iterator<@Nullable ContextNode> iteratorOf(ContextNode parent, Iterable<?> it) { 346 int[] j = { -1 }; 347 return StreamSupport.stream(it.spliterator(), false) // 348 .<@Nullable ContextNode>map(i -> this.ofChild(parent, (j[0] += 1), i)) // 349 .iterator(); 350 } 351 352 public Iterator<@Nullable ContextNode> iteratorOf(ContextNode parent, @Nullable Object o) { 353 354 if (o == null || Boolean.FALSE.equals(o)) { 355 return Collections.emptyIterator(); 356 } 357 else if (o instanceof Iterable<?>) { 358 return iteratorOf(parent, o); 359 } 360 else if (o instanceof Optional<?> opt) { 361 return opt.stream() // 362 .<@Nullable ContextNode>map(i -> this.ofChild(parent, 0, i)) // 363 .iterator(); 364 } 365 else if (o.getClass().isArray()) { 366 367 Stream<? extends @Nullable Object> s = arrayToStream(o); 368 int[] j = { -1 }; 369 return s.<@Nullable ContextNode>map(i -> this.ofChild(parent, (j[0] += 1), i)).iterator(); 370 } 371 372 return Collections.<@Nullable ContextNode>singletonList(parent).iterator(); 373 374 } 375 376 private static Stream<? extends @Nullable Object> arrayToStream(Object o) { 377 378 if (o instanceof int[] a) { 379 return Arrays.stream(a).boxed(); 380 } 381 else if (o instanceof long[] a) { 382 return Arrays.stream(a).boxed(); 383 } 384 else if (o instanceof double[] a) { 385 return Arrays.stream(a).boxed(); 386 } 387 else if (o instanceof Object[] a) { 388 return Arrays.asList(a).stream(); 389 } 390 391 /* 392 * There is probably an easier way to do this 393 */ 394 final Stream.Builder<@Nullable Object> b = Stream.builder(); 395 396 if (o instanceof boolean[] a) { 397 for (var _a : a) { 398 b.add(_a); 399 } 400 } 401 else if (o instanceof char[] a) { 402 for (var _a : a) { 403 b.add(_a); 404 } 405 } 406 else if (o instanceof byte[] a) { 407 for (var _a : a) { 408 b.add(_a); 409 } 410 } 411 else if (o instanceof float[] a) { 412 for (var _a : a) { 413 b.add(_a); 414 } 415 } 416 else if (o instanceof short[] a) { 417 for (var _a : a) { 418 b.add(_a); 419 } 420 } 421 else { 422 throw new IllegalArgumentException("array type not supported: " + o.getClass()); 423 } 424 return b.build(); 425 } 426 427 } 428 429 sealed interface ObjectContextNode extends Internal permits ObjectContext, FunctionContextNode, MapContextNode { 430 431 /** 432 * Get value like map. 433 * @param key not null 434 * @return value mapped to key. 435 */ 436 public @Nullable Object getValue(String key); 437 438 @Override 439 default @Nullable ContextNode get(String field) { 440 return ofChild(field, getValue(field)); 441 } 442 443 @Override 444 default Iterator<@Nullable ContextNode> iterator() { 445 return Collections.<@Nullable ContextNode>singleton(this).iterator(); 446 } 447 448 @Override 449 default boolean isFalsey() { 450 return false; 451 } 452 453 @Override 454 default <A extends Output<E>, E extends Exception> void format(Formatter formatter, Appender downstream, 455 String path, A a) throws E { 456 throw new UnsupportedOperationException("ContextNode cannot be formatted. object: " + object()); 457 } 458 459 } 460 461 sealed interface ListNode extends Internal { 462 463 @Override 464 default @Nullable ContextNode get(String field) { 465 return null; 466 } 467 468 @Override 469 public Iterator<@Nullable ContextNode> iterator(); 470 471 @Override 472 default <A extends Output<E>, E extends Exception> void format(Formatter formatter, Appender downstream, 473 String path, A a) throws E { 474 throw new UnsupportedOperationException( 475 "Possible bug. Iterable node cannot be formatted. object: " + object()); 476 } 477 478 } 479 480 record IterableContextNode(Iterable<?> object, @Nullable ContextNode parent) implements ListNode { 481 482 @Override 483 public Iterator<@Nullable ContextNode> iterator() { 484 return ContextNodeFactory.INSTANCE.iteratorOf(this, object); 485 } 486 487 } 488 489 sealed interface ValueNode extends Internal { 490 491 @Override 492 default @Nullable ContextNode get(String field) { 493 return null; 494 } 495 496 } 497 498 record OptionalContextNode(Optional<?> object, @Nullable ContextNode parent) implements ValueNode { 499 @Override 500 public boolean isFalsey() { 501 return object.isEmpty(); 502 } 503 504 @Override 505 public @NonNull Iterator<@Nullable ContextNode> iterator() { 506 return object.stream().<@Nullable ContextNode>map(i -> this.ofChild(0, i)).iterator(); 507 } 508 509 @Override 510 public <A extends Output<E>, E extends Exception> void format(Formatter formatter, Appender downstream, 511 String path, A a) throws E { 512 throw new UnsupportedOperationException("Optional<?> node cannot be formatted. object: " + object()); 513 } 514 515 } 516 517 record ValueContextNode(Object object, @Nullable ContextNode parent) implements ValueNode { 518 @Override 519 public String toString() { 520 return renderString(); 521 } 522 523 @Override 524 public Iterator<@Nullable ContextNode> iterator() { 525 return ContextNodeFactory.INSTANCE.iteratorOf(this, object); 526 } 527 528 @Override 529 public boolean isFalsey() { 530 return ContextNode.isFalsey(object); 531 } 532 533 @Override 534 public <A extends Output<E>, E extends Exception> void format(Formatter formatter, Appender downstream, 535 String path, A a) throws E { 536 var o = object(); 537 formatter.format(downstream, a, path, o.getClass(), o); 538 } 539 540 } 541 542 record FunctionContextNode(Function<String, ?> object) implements ObjectContextNode { 543 @Override 544 public String toString() { 545 return renderString(); 546 } 547 548 @Override 549 public @Nullable Object getValue(String key) { 550 return object.apply(key); 551 } 552 } 553 554 record MapContextNode(Map<?, ?> object, @Nullable ContextNode parent) implements ObjectContextNode { 555 @Override 556 public String toString() { 557 return renderString(); 558 } 559 560 @Override 561 public @Nullable Object getValue(String key) { 562 return object.get(key); 563 } 564 } 565 566 enum EmptyContextNode implements Internal { 567 568 EMPTY; 569 570 @Override 571 public Object object() { 572 return Map.of(); 573 } 574 575 @Override 576 public @Nullable ContextNode get(String field) { 577 return null; 578 } 579 580 @Override 581 public @Nullable ContextNode find(String field) { 582 return null; 583 } 584 585 @Override 586 public Iterator<@Nullable ContextNode> iterator() { 587 return Collections.emptyIterator(); 588 } 589 590 @Override 591 public <A extends Output<E>, E extends Exception> void format(Formatter formatter, Appender downstream, 592 String path, A a) throws E { 593 formatter.format(downstream, a, path, this.toString()); 594 } 595 596 } 597 598}