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.views;
018
019import static org.apache.juneau.bean.html5.HtmlBuilder.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.escapeForScript;
022
023import java.util.*;
024
025import org.apache.juneau.bean.html5.*;
026import org.apache.juneau.marshall.marshaller.*;
027
028/**
029 * Builds the server-rendered delivery tree for a {@link RegionDef} &mdash; an <b>empty</b>
030 * {@code data-juneau-region} container plus <b>one sidecar per host</b> carrying every region descriptor that host
031 * declares.
032 *
033 * <h5 class='section'>The container carries identity; the sidecar carries configuration:</h5>
034 * <p>
035 * The container is deliberately empty and deliberately attribute-only &mdash; id, type, and the handshake stamp and
036 * nothing else. Configuration (the populator name, {@code dataUrl}, {@code params}, {@code renderer}, {@code lazy},
037 * {@code refreshMs}, the field catalog) rides the sidecar, for the same reason {@code PAGE_META} does: an attribute
038 * per descriptor key would put author strings in attribute position on every host, and a JSON envelope is the shape
039 * the client already parses. The attributes are exactly what the runtime scans for, mirroring the existing
040 * {@code data-juneau-page} / {@code data-juneau-view} / {@code data-juneau-card} markers.
041 *
042 * <p>
043 * <b>One sidecar per host, not per region.</b> One {@code <script>} per card in a twelve-card grid is twelve parses;
044 * the existing pattern is one sidecar per view / page / header / bar, and this follows it.
045 *
046 * <h5 class='section'>Two sidecar forms, and why the second exists:</h5>
047 * <p>
048 * {@link #sidecar(String, List)} stamps a document-unique HTML {@code id}, for an ordinary once-per-response host
049 * (a page, a card grid). {@link #detailSidecar(List)} ships {@code id}-less and is found by attribute instead,
050 * because a row-detail sidecar lives inside the {@code <template data-juneau-row-detail>} that is <b>cloned per
051 * expanded row</b> &mdash; a stamped {@code id} would collide across every open panel. This is exactly the split
052 * {@link org.apache.juneau.console.widgets.BarSlot} already makes, for the same
053 * reason.
054 *
055 * @since 10.0.0
056 */
057public class RegionTable {
058
059   /** Marker attribute carrying a region's own id: {@code data-juneau-region}. */
060   public static final String REGION_ATTR = "data-juneau-region";
061
062   /** Attribute carrying the region's type ({@link RegionDef#TYPE_ROW_DETAIL} and friends). */
063   public static final String REGION_TYPE_ATTR = "data-juneau-region-type";
064
065   /**
066    * Attribute carrying the region's {@linkplain RegionDef#populate populator name}:
067    * {@code data-juneau-region-populate}.
068    *
069    * <p>
070    * <b>This attribute is how the populator name reaches the runtime at all, so omitting it fails silently in the
071    * worst possible way.</b> {@code juneau-regions.js}'s {@code mintRegion} reads the name with
072    * {@code el.getAttribute("data-juneau-region-populate")}; a container without it yields {@code null}, which
073    * resolves to the <b>default</b> populator rather than to an error. So a page whose author registered a named
074    * populate would paint the framework default instead, with no console error, no error pane, and a rendered
075    * result that looks plausible &mdash; the author's populate simply never runs.
076    *
077    * <p>
078    * Deliberately an <b>attribute</b> rather than a sidecar member, unlike most of a region's configuration. The
079    * name is needed to decide <i>which function to call</i> at mint time, before any per-host sidecar has been
080    * matched to this container; identity-and-dispatch data rides the element, and everything else rides the sidecar.
081    */
082   public static final String REGION_POPULATE_ATTR = "data-juneau-region-populate";
083
084   /** Attribute carrying {@link RegionDef#CONTRACT_VERSION} on the region container (the per-host handshake stamp). */
085   public static final String REGION_CONTRACT_ATTR = "data-juneau-region-contract";
086
087   /**
088    * Attribute carrying the enclosing response's CSRF token on a region host that is <b>not</b> a {@code <table>}
089    * &mdash; deliberately {@link ViewTable#CSRF_ATTR}, the <b>same</b> attribute the view table already
090    * stamps, not a region-specific second name.
091    *
092    * <p>
093    * A region that performs a write has no token source of its own: the existing stamp lives on the view
094    * {@code <table>}, and a card-body or tab-body region is not beneath one. Rather than mint a second token or
095    * teach every populate to hunt for the table's, the host that emits a non-table region stamps the same
096    * per-response token it already holds.
097    *
098    * <p>
099    * Reusing the existing attribute name is what makes this cost <b>zero client change</b>: {@code ctx.write(...)}
100    * already resolves its token with {@code el.closest("[data-juneau-csrf]")}, so a region container carrying this
101    * attribute is found by the code that is already there. A region-specific name would have required teaching that
102    * resolver a second selector, and the two would then have been able to disagree.
103    *
104    * <p>
105    * A row-detail region <b>is</b> beneath the view table and therefore does <b>not</b> carry this attribute: it
106    * inherits the table's by {@code closest(...)}, and a second stamp on the same subtree is the hazard rather than
107    * the fix for it.
108    */
109   public static final String REGION_CSRF_ATTR = ViewTable.CSRF_ATTR;
110
111   /**
112    * Attribute carrying the region's own contract descriptor (its {@code params} included) as JSON, for a region
113    * placed outside a sidecar-bearing host.
114    *
115    * <p>
116    * Stamped by {@link #of(RegionDef, String)} only when the region declares {@code params}; the region runtime
117    * reads it as the placeholder's declared descriptor.
118    */
119   public static final String REGION_DECLARED_ATTR = "data-juneau-region-declared";
120
121   /** Attribute the {@code id}-less {@link #detailSidecar(List)} form is found by. */
122   public static final String REGION_META_ATTR = "data-juneau-region-meta";
123
124   /** HTML {@code id} prefix for the {@link #sidecar(String, List)} form. */
125   public static final String SIDECAR_ID_PREFIX = "juneau-region:";
126
127   /** The {@code class} on every region container. */
128   public static final String REGION_CLASS = "juneau-region";
129
130   private RegionTable() {}
131
132   /**
133    * Emits one empty region container.
134    *
135    * <p>
136    * In-table region HTML ({@link ViewTable}'s detail template).  Page slots: author HTML plus
137    * {@code JuneauViews.regions.mount({ id: populatorName })}.  Not {@code @Deprecated}.
138    * </p>
139    *
140    * @param region The region. Must not be <jk>null</jk>.
141    * @return An empty {@code <div class='juneau-region'>} carrying identity, type and the handshake stamp.
142    */
143   public static Div of(RegionDef region) {
144      return of(region, null);
145   }
146
147   /**
148    * Emits one empty region container, optionally carrying the non-table host's CSRF stamp.
149    *
150    * <p>
151    * In-table region HTML ({@link ViewTable}'s detail template).  Page slots: author HTML plus
152    * {@code JuneauViews.regions.mount({ id: populatorName })}.  Not {@code @Deprecated}.
153    * </p>
154    *
155    * @param region The region. Must not be <jk>null</jk>.
156    * @param csrfToken The enclosing response's token for a non-table host, or <jk>null</jk> to omit the stamp
157    *    &mdash; which is the correct choice for a row-detail region, since it sits beneath the view table's own
158    *    stamp and must not carry a second one.
159    * @return An empty {@code <div class='juneau-region'>}.
160    */
161   public static Div of(RegionDef region, String csrfToken) {
162      if (region == null)
163         throw iaex("region must not be null.");
164      if (region.id == null || region.id.isBlank())
165         throw iaex("RegionDef id must not be null or blank.");
166      var d = div()
167         .class_(REGION_CLASS)
168         .attr(REGION_ATTR, region.id)
169         .attr(REGION_CONTRACT_ATTR, RegionDef.CONTRACT_VERSION);
170      if (region.type != null)
171         d.attr(REGION_TYPE_ATTR, region.type);
172      // Only when the author named one: an ABSENT attribute means "use the default populator", which is exactly
173      // what mintRegion infers from a null read.  Stamping a blank would be a third state nobody handles.
174      if (region.populate != null && ! region.populate.isBlank())
175         d.attr(REGION_POPULATE_ATTR, region.populate);
176      // A region with no params needs nothing beyond the attributes above; one with params has nowhere else to carry
177      // them when it is placed on its own (a card body), so the contract descriptor travels on the container.
178      if (region.params != null && ! region.params.isEmpty())
179         d.attr(REGION_DECLARED_ATTR, Json.of(region.toContractMap()));
180      if (inb(csrfToken))
181         d.attr(REGION_CSRF_ATTR, csrfToken);
182      return d;
183   }
184
185   /**
186    * Emits the per-host sidecar with a document-unique HTML {@code id}.
187    *
188    * <p>
189    * No in-table keeper in this module's production emit path; leftover page-slot callers migrate to
190    * {@code JuneauViews.regions.mount}.  Not {@code @Deprecated}.
191    * </p>
192    *
193    * @param hostId The host's own id (a page id, a card-grid id); the sidecar's {@code id} is
194    *    {@link #SIDECAR_ID_PREFIX} + this. Must not be <jk>null</jk> or blank.
195    * @param regions Every region this host declares, in emit order. Must not be <jk>null</jk> or empty.
196    * @return The {@code <script type='application/json'>} sidecar.
197    */
198   public static Script sidecar(String hostId, List<RegionDef> regions) {
199      if (ib(hostId))
200         throw iaex("hostId must not be null or blank.");
201      var s = sidecar(regions);
202      s.id(SIDECAR_ID_PREFIX + hostId);
203      return s;
204   }
205
206   /**
207    * Emits the {@code id}-less, attribute-found sidecar for a host whose subtree is cloned per row.
208    *
209    * <p>
210    * See this class's Javadoc: a row-detail sidecar lives inside the cloned
211    * {@code <template data-juneau-row-detail>}, so a stamped {@code id} would collide across every open panel.
212    *
213    * <p>
214    * In-table region HTML ({@link ViewTable}'s detail template).  Not {@code @Deprecated}.
215    * </p>
216    *
217    * @param regions Every region this host declares, in emit order. Must not be <jk>null</jk> or empty.
218    * @return The {@code <script type='application/json'>} sidecar, carrying {@link #REGION_META_ATTR}.
219    */
220   public static Script detailSidecar(List<RegionDef> regions) {
221      var s = sidecar(regions);
222      s.attr(REGION_META_ATTR, "1");
223      return s;
224   }
225
226   private static Script sidecar(List<RegionDef> regions) {
227      if (ie(regions))
228         throw iaex("regions must not be null or empty.");
229      var json = escapeForScript(Json.of(buildMeta(regions)));
230      return script().type("application/json").text(rawText(json));
231   }
232
233   /**
234    * Builds the sidecar envelope: the contract version once, then one descriptor per region in emit order.
235    *
236    * @param regions The regions, in emit order. Must not be <jk>null</jk> or empty.
237    * @return An ordered {@link java.util.Map} ready for {@code Json.of(...)}.
238    */
239   public static java.util.Map<String,Object> buildMeta(List<RegionDef> regions) {
240      if (ie(regions))
241         throw iaex("regions must not be null or empty.");
242      var descriptors = new ArrayList<java.util.Map<String,Object>>();
243      for (var r : regions) {
244         if (r == null)
245            throw iaex("regions entry must not be null.");
246         descriptors.add(r.toContractMap());
247      }
248      var m = new LinkedHashMap<String,Object>();
249      m.put("contractVersion", RegionDef.CONTRACT_VERSION);
250      m.put("regions", descriptors);
251      return m;
252   }
253}