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.console.freemarker;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import freemarker.template.*;
024
025/**
026 * One node in the console navigation tree. Nodes nest to any depth; {@code <@node>} directives append
027 * to the node returned by {@link PageCapture#navRoot()}.
028 *
029 * <h5 class='section'>Example:</h5>
030 * <p class='bjava'>
031 *    NavNode <jv>root</jv> = PageCapture.<jsm>of</jsm>(<jv>env</jv>).navRoot();
032 *    NavNode <jv>home</jv> = <jv>root</jv>.add(<js>"home"</js>, <js>"Home"</js>, <js>"/home"</js>);
033 *    <jv>home</jv>.add(<js>"about"</js>, <js>"About"</js>, <js>"/home/about"</js>);
034 *    <jv>root</jv>.find(List.<jsm>of</jsm>(<js>"home"</js>, <js>"about"</js>)).map(NavNode::label);  <jc>// Optional[About]</jc>
035 * </p>
036 *
037 * @since 10.0.0
038 */
039public final class NavNode {
040
041   private final NavNode parent;
042   private final String id;
043   private final String label;
044   private final String href;
045   private final List<NavNode> children = new ArrayList<>();
046
047   private NavNode(NavNode parent, String id, String label, String href) {
048      this.parent = parent;
049      this.id = id;
050      this.label = label;
051      this.href = href;
052   }
053
054   static NavNode root() {
055      return new NavNode(null, null, null, null);
056   }
057
058   /**
059    * Appends a child node.
060    *
061    * @param nodeId The node id, unique among its siblings.
062    * @param label The visible label.
063    * @param hrefOrNull The link target, or <jk>null</jk> for a parent-only node.
064    * @return The new child.
065    * @throws TemplateModelException If a sibling already has this id (E-3).
066    */
067   public NavNode add(String nodeId, String label, String hrefOrNull) throws TemplateModelException {
068      for (var c : children)
069         if (c.id.equals(nodeId))
070            throw FtlAttrLists.reject(f("<@node id='%s'> duplicates a sibling id under '%s'.", nodeId,
071               n(this.id) ? "(root)" : String.join("/", idPath())));
072      var n = new NavNode(this, nodeId, label, hrefOrNull);
073      children.add(n);
074      return n;
075   }
076
077   /**
078    * Walks a root-first id path from this node.
079    *
080    * @param idPath The ids, root first.
081    * @return The node at the end of the path, or empty.
082    */
083   public Optional<NavNode> find(List<String> idPath) {
084      var n = this;
085      for (var step : idPath) {
086         NavNode next = null;
087         for (var c : n.children)
088            if (c.id.equals(step))
089               next = c;
090         if (n(next))
091            return Optional.empty();
092         n = next;
093      }
094      return Optional.of(n);
095   }
096
097   /** @return The id, or <jk>null</jk> for the synthetic root. */
098   public String id() { return id; }
099
100   /** @return The label. */
101   public String label() { return label; }
102
103   /** @return The href, or <jk>null</jk>. */
104   public String href() { return href; }
105
106   /** @return The children, unmodifiable. */
107   public List<NavNode> children() { return u(children); }
108
109   List<String> idPath() {
110      var out = new LinkedList<String>();
111      for (var n = this; nn(n) && nn(n.id); n = n.parent)
112         out.addFirst(n.id);
113      return out;
114   }
115
116   List<String> paths() {
117      var out = new ArrayList<String>();
118      for (var c : children) {
119         out.add(String.join("/", c.idPath()));
120         out.addAll(c.paths());
121      }
122      return out;
123   }
124
125   List<Map<String,Object>> toList() {
126      var out = new ArrayList<Map<String,Object>>();
127      for (var c : children) {
128         var m = new LinkedHashMap<String,Object>();
129         m.put("id", c.id);
130         m.put("label", c.label);
131         if (nn(c.href))
132            m.put("href", c.href);
133         if (! c.children.isEmpty())
134            m.put("children", c.toList());
135         out.add(m);
136      }
137      return out;
138   }
139}