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} — 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 — 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> — 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 — 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 * — 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 * — 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}