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 * {{#&#64;context}}
054 * {{message}} {{! message here will only ever resolve against &#64;context and not the parent }}
055 * {{/&#64;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}