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.io.*;
022import java.util.*;
023
024import freemarker.core.*;
025import freemarker.template.*;
026
027/**
028 * The {@code <@navigation>} console FreeMarker directive: opens the nav tree on the {@link PageCapture}.
029 *
030 * <p>
031 * Nested {@code <@node>}s add {@code nav[]} entries of any depth; the shell renders the nav and sets
032 * {@code aria-current}. An omitted {@code layout=} leaves {@code navLayout} out of the contract (the shell defaults
033 * to horizontal). {@code format=}, any unknown attribute, and nesting inside another {@code <@navigation>} or outside
034 * {@code <@console>} are all rejected (fail closed).
035 *
036 * @since 10.0.0
037 */
038public final class NavigationDirectiveModel implements TemplateDirectiveModel {
039
040   /** The shared-variable name this directive registers under. */
041   public static final String NAME = "navigation";
042
043   static final Set<String> ATTRS = Set.of("layout");
044
045   NavigationDirectiveModel() {}
046
047   @Override
048   @SuppressWarnings({
049      "unchecked" // FreeMarker's raw params Map is String-keyed by contract.
050   })
051   public void execute(Environment env, @SuppressWarnings("rawtypes") Map params, TemplateModel[] loopVars,
052         TemplateDirectiveBody body) throws TemplateException, IOException {
053      Map<String, TemplateModel> p = params;
054      if (p.containsKey("format"))
055         throw FtlAttrLists.reject("<@navigation> has no format= attribute.");
056      FtlAttrLists.rejectUnknown(p, NAME, ATTRS);
057
058      var cap = PageCapture.get(env);
059      if (n(cap) || ! cap.consoleOpen)
060         throw FtlAttrLists.reject(f("<@%s> must be nested inside <@console>.", NAME));
061      if (cap.navOpen)
062         throw FtlAttrLists.reject("<@navigation> cannot be nested inside another <@navigation>.");
063
064      var layout = FtlAttrLists.scalar(p, "layout");
065      if (! layout.isEmpty()) {
066         if (! (eqa(layout, "horizontal", "vertical")))
067            throw FtlAttrLists.reject(f(
068               "<@navigation> layout= must be horizontal or vertical; got '%s'.", layout));
069         cap.navLayout(layout);
070      }
071
072      cap.navOpen = true;
073      cap.navCursor.push(cap.navRoot());
074      try (var sink = new StringWriter()) {
075         if (nn(body))
076            body.render(sink);
077      } finally {
078         cap.navCursor.pop();
079         cap.navOpen = false;
080      }
081   }
082}