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 org.apache.juneau.rest.server.view.freemarker.*;
025
026import freemarker.core.*;
027import freemarker.template.*;
028
029/**
030 * The {@code <@page>} shared FreeMarker directive: capture the nested body into {@link PageCapture}, resolve the
031 * {@code toolkit=} packs, then include the consumer chrome template. A child {@code .ftlh} file <i>is</i> the page
032 * call &mdash; it needs no leading include.
033 *
034 * <p>
035 * {@code <@page>} is the outermost console directive; it must not be nested or repeated. The chrome template it
036 * includes is expected to render exactly one {@code <@console>}, which in turn renders the page contract.
037 *
038 * @since 10.0.0
039 */
040public final class PageDirectiveModel implements TemplateDirectiveModel {
041
042   /** The shared-variable name this directive registers under. */
043   public static final String NAME = "page";
044
045   static final Set<String> ATTRS = Set.of("tab", "init", "css", "toolkit");
046
047   private final String chromeTemplate;
048   private final ToolkitPackRegistry packs;
049
050   PageDirectiveModel(String chromeTemplate, ToolkitPackRegistry packs) {
051      this.chromeTemplate = chromeTemplate;
052      this.packs = packs;
053   }
054
055   @Override
056   @SuppressWarnings({
057      "unchecked" // FreeMarker passes the directive params as a raw Map; it is assigned to Map<String,TemplateModel>, the type FreeMarker documents for them
058   })
059   public void execute(Environment env, @SuppressWarnings("rawtypes") Map params, TemplateModel[] loopVars,
060         TemplateDirectiveBody body) throws TemplateException, IOException {
061      Map<String, TemplateModel> p = params;
062      if (p.containsKey("format"))
063         throw FtlAttrLists.reject("<@page> has no format= attribute.");
064      FtlAttrLists.rejectUnknown(p, NAME, ATTRS);
065      if (n(body))
066         throw FtlAttrLists.reject("<@page> requires a nested body.");
067
068      var cap = PageCapture.of(env);
069      if (cap.pageOpen || cap.consoleOpen || cap.consoleDone)
070         throw FtlAttrLists.reject("<@page> must be the outermost console directive; it cannot be nested or repeated.");
071
072      var toolkit = FtlAttrLists.list(p, NAME, "toolkit");
073      try {
074         packs.requireKnown(toolkit, "<@page toolkit=>");  // Fail fast on an unknown pack, before the body renders.
075      } catch (IllegalArgumentException e) {
076         throw FtlAttrLists.reject(e.getMessage());
077      }
078      cap.mergeTab(FtlAttrLists.scalar(p, "tab"));
079      cap.mergeToolkit(toolkit);
080      cap.mergeCss(FtlAttrLists.list(p, NAME, "css"));
081      cap.mergeInit(FtlAttrLists.list(p, NAME, "init"));
082
083      cap.pageOpen = true;
084      try {
085         body.render(cap.pageBuffer());
086      } finally {
087         cap.pageOpen = false;
088      }
089      cap.flushSegment();
090
091      resolveAssets(env, cap, packs);
092
093      env.include(env.getConfiguration().getTemplate(chromeTemplate));
094
095      if (! cap.consoleDone)
096         throw FtlAttrLists.reject(f(
097            "<@page> chrome template '%s' rendered no <@console>; the legacy chrome was removed in 10.0.0.", chromeTemplate));
098   }
099
100   /**
101    * Resolves the page's toolkit and card-required packs into the capture's script and link tags.
102    *
103    * <p>
104    * Called after the page body (so the card requirements are known) against the merged toolkit list, so a pack named
105    * only by a {@link PageSpec} still has its assets emitted.  Shared with {@code <@console>}, which runs it for a
106    * page that never reached {@code <@page>} (a built page rendered straight through the chrome).
107    *
108    * <p>
109    * On the built-page path the template name used in diagnostics is the chrome template, not the page template.
110    */
111   static void resolveAssets(Environment env, PageCapture cap, ToolkitPackRegistry packs) throws TemplateException {
112      var roots = new java.util.ArrayList<>(cap.toolkits());
113      roots.addAll(cap.requiredPacks());
114      try {
115         var pageUrls = new java.util.ArrayList<String>(cap.initScripts());
116         pageUrls.addAll(cap.cssHrefs());
117         cap.toolkitAssets(packs.resolve(roots, FreemarkerRenderScope.request(), pageUrls, env.getMainTemplate().getName()));
118      } catch (IllegalArgumentException | IllegalStateException e) {
119         throw FtlAttrLists.reject(e.getMessage());
120      }
121   }
122}