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.marshall.html; 018 019import static org.apache.juneau.commons.utils.ClassUtils.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.marshall.html.AsideFloat.*; 023 024import java.util.*; 025 026import org.apache.juneau.commons.lang.*; 027import org.apache.juneau.commons.utils.*; 028 029/** 030 * A basic template for the HTML doc serializer. 031 * 032 * <p> 033 * This class can be subclassed to customize page rendering. 034 * 035 * <h5 class='section'>See Also:</h5><ul> 036 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlSupport">HTML</a> 037 * </ul> 038 */ 039@SuppressWarnings({ 040 "java:S110", // Body-var-resolving anonymous writer inherits the inherent CRTP writer chain (Writer->SerializerWriter->XmlWriter->HtmlWriter->BasicHtmlWriter); depth is not reducible without unwinding CRTP. 041 "java:S112", // Generic exception thrown; template methods throw Exception for subclass flexibility 042 "java:S1192", // HTML tag names (e.g. aside, section) repeated in template; constants would obscure markup 043 "resource" // Resource management handled externally 044}) 045public class BasicHtmlDocTemplate implements HtmlDocTemplate { 046 047 private static boolean exists(String s) { 048 return nn(s) && neq(s, "NONE"); 049 } 050 051 private static boolean isEmptyObject(Object o) { 052 if (o == null) 053 return true; 054 if (o instanceof Collection<?> o2) 055 return o2.isEmpty(); 056 if (o instanceof Map<?,?> o2) 057 return o2.isEmpty(); 058 if (isArray(o)) 059 return (java.lang.reflect.Array.getLength(o) == 0); 060 return o.toString().isEmpty(); 061 } 062 063 /** Lowercase marshall URI schemes recognized in bare <code>key: href</code> navlinks in {@link #nav}. */ 064 private static final List<String> MARSHALL_URI_NAVLINK_SCHEMES = List.of("request", "servlet", "context"); 065 066 @Override /* Overridden from HtmlDocTemplate */ 067 public void writeTo(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 068 w.sTag("html").nl(0); 069 w.sTag(1, "head").nl(1); 070 head(session, w, o); 071 w.eTag(1, "head").nl(1); 072 w.sTag(1, "body").nl(1); 073 body(session, w, o); 074 w.eTag(1, "body").nl(1); 075 w.eTag("html").nl(0); 076 } 077 078 /** 079 * Renders the contents of the <code><xt><body></xt>/<xt><article></xt></code> element. 080 * 081 * @param session The current serializer session. 082 * @param w The writer being written to. 083 * @param o The object being serialized. 084 * @throws Exception Any exception can be thrown. 085 */ 086 protected void article(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 087 // To allow for page formatting using CSS, we encapsulate the data inside two div tags: 088 // <div class='outerdata'><div class='data' id='data'>...</div></div> 089 w.oTag(4, "div").attr("class", "outerdata").append('>').nl(4); 090 w.oTag(5, "div").attr("class", "data").attr("id", "data").append('>').nl(5); 091 092 if (o == null) { 093 w.append(6, "<null/>").nl(6); 094 } else if (isEmptyObject(o)) { 095 String m = session.getNoResultsMessage(); 096 if (exists(m)) 097 w.append(6, session.resolveHtmlDoc(m)).nl(6); 098 } else { 099 session.indent = 6; 100 w.flush(); 101 if (session.isResolveBodyVars()) { 102 w = new BasicHtmlWriter(w) { 103 @Override 104 public BasicHtmlWriter text(Object value, boolean preserveWhitespace) { 105 return super.text(session.resolve(Shorts.s(value)), preserveWhitespace); 106 } 107 }; 108 } 109 session.parentSerialize(w, o); 110 } 111 112 w.ie(5).eTag("div").nl(5); 113 w.ie(4).eTag("div").nl(4); 114 } 115 116 /** 117 * Renders the contents of the <code><xt><body></xt>/<xt><aside></xt></code> element. 118 * 119 * @param session The current serializer session. 120 * @param w The writer being written to. 121 * @param o The object being serialized. 122 * @throws Exception Any exception can be thrown. 123 */ 124 protected void aside(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 125 var aside = session.getAside(); 126 for (var i = 0; i < aside.length; i++) 127 w.sIf(i > 0).appendln(4, session.resolveHtmlDoc(aside[i])); 128 } 129 130 /** 131 * Renders the contents of the <code><xt><body></xt></code> element. 132 * 133 * @param session The current serializer session. 134 * @param w The writer being written to. 135 * @param o The object being serialized. 136 * @throws Exception Any exception can be thrown. 137 */ 138 protected void body(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 139 140 AsideFloat asideFloat = session.getAsideFloat(); 141 boolean hasAside = hasAside(session); 142 143 if (hasHeader(session)) { 144 w.sTag(2, "header").nl(2); 145 header(session, w, o); 146 w.ie(2).eTag("header").nl(2); 147 } 148 149 if (hasNav(session)) { 150 w.sTag(2, "nav").nl(2); 151 nav(session, w, o); 152 w.ie(2).eTag("nav").nl(2); 153 } 154 155 if (hasAside && asideFloat.is(TOP)) { 156 w.sTag(2, "section").nl(2); 157 w.sTag(3, "aside").nl(3); 158 aside(session, w, o); 159 w.ie(3).eTag("aside").nl(3); 160 w.ie(2).eTag("section").nl(2); 161 } 162 163 w.sTag(2, "section").nl(2); 164 165 if (hasAside && asideFloat.is(LEFT)) { 166 w.sTag(3, "aside").nl(3); 167 aside(session, w, o); 168 w.ie(3).eTag("aside").nl(3); 169 } 170 171 w.sTag(3, "article").nl(3); 172 article(session, w, o); 173 w.ie(3).eTag("article").nl(3); 174 175 if (hasAside && asideFloat.isAny(RIGHT, DEFAULT)) { 176 w.sTag(3, "aside").nl(3); 177 aside(session, w, o); 178 w.ie(3).eTag("aside").nl(3); 179 } 180 181 w.ie(2).eTag("section").nl(2); 182 183 if (hasAside && asideFloat.is(BOTTOM)) { 184 w.sTag(2, "section").nl(2); 185 w.sTag(3, "aside").nl(3); 186 aside(session, w, o); 187 w.ie(3).eTag("aside").nl(3); 188 w.ie(2).eTag("section").nl(2); 189 } 190 191 if (hasFooter(session)) { 192 w.sTag(2, "footer").nl(2); 193 footer(session, w, o); 194 w.ie(2).eTag("footer").nl(2); 195 } 196 } 197 198 /** 199 * Renders the contents of the <code><xt><body></xt>/<xt><footer></xt></code> element. 200 * 201 * @param session The current serializer session. 202 * @param w The writer being written to. 203 * @param o The object being serialized. 204 * @throws Exception Any exception can be thrown. 205 */ 206 protected void footer(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 207 var footer = session.getFooter(); 208 for (var i = 0; i < footer.length; i++) 209 w.sIf(i > 0).appendln(3, session.resolveHtmlDoc(footer[i])); 210 } 211 212 /** 213 * Returns <jk>true</jk> if this page should render a <code><xt><body></xt>/<xt><aside></xt></code> 214 * element. 215 * 216 * @param session The current serializer session. 217 * @return A boolean flag. 218 */ 219 protected boolean hasAside(HtmlDocSerializerSession session) { 220 return session.getAside().length > 0; 221 } 222 223 /** 224 * Returns <jk>true</jk> if this page should render a <code><xt><body></xt>/<xt><footer></xt></code> 225 * element. 226 * 227 * @param session The current serializer session. 228 * @return A boolean flag. 229 */ 230 protected boolean hasFooter(HtmlDocSerializerSession session) { 231 return session.getFooter().length > 0; 232 } 233 234 /** 235 * Returns <jk>true</jk> if this page should render a <code><xt><body></xt>/<xt><header></xt></code> 236 * element. 237 * 238 * @param session The current serializer session. 239 * @return A boolean flag. 240 */ 241 protected boolean hasHeader(HtmlDocSerializerSession session) { 242 return session.getHeader().length > 0; 243 } 244 245 /** 246 * Returns <jk>true</jk> if this page should render a <code><xt><body></xt>/<xt><nav></xt></code> 247 * element. 248 * 249 * @param session The current serializer session. 250 * @return A boolean flag. 251 */ 252 protected boolean hasNav(HtmlDocSerializerSession session) { 253 return session.getNav().length > 0 || session.getNavLinks().length > 0; 254 } 255 256 /** 257 * Returns <jk>true</jk> if this page should render a <code><xt><head></xt>/<xt><script></xt></code> element. 258 * 259 * @param session The current serializer session. 260 * @return A boolean flag. 261 */ 262 protected boolean hasScript(HtmlDocSerializerSession session) { 263 return true; 264 } 265 266 /** 267 * Returns <jk>true</jk> if this page should render a <code><xt><head></xt>/<xt><style></xt></code> element. 268 * 269 * @param session The current serializer session. 270 * @return A boolean flag. 271 */ 272 protected boolean hasStyle(HtmlDocSerializerSession session) { 273 return true; 274 } 275 276 /** 277 * Renders the contents of the <code><xt><head></xt></code> element. 278 * 279 * @param session The current serializer session. 280 * @param w The writer being written to. 281 * @param o The object being serialized. 282 * @throws Exception Any exception can be thrown. 283 */ 284 protected void head(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 285 286 var head = session.getHead(); 287 for (var i = 0; i < head.length; i++) 288 w.sIf(i > 0).appendln(2, session.resolveHtmlDoc(head[i])); 289 290 var nonce = session.getNonce(); 291 if (hasStyle(session)) { 292 if (nonce != null) 293 w.oTag(2, "style").attr("nonce", nonce).cTag().nl(2); 294 else 295 w.sTag(2, "style").nl(2); 296 style(session, w, o); 297 w.ie(2).eTag("style").nl(2); 298 } 299 if (hasScript(session)) { 300 if (nonce != null) 301 w.oTag(2, "script").attr("nonce", nonce).cTag().nl(2); 302 else 303 w.sTag(2, "script").nl(2); 304 script(session, w, o); 305 w.ie(2).eTag("script").nl(2); 306 } 307 } 308 309 /** 310 * Renders the contents of the <code><xt><body></xt>/<xt><header></xt></code> element. 311 * 312 * @param session The current serializer session. 313 * @param w The writer being written to. 314 * @param o The object being serialized. 315 * @throws Exception Any exception can be thrown. 316 */ 317 protected void header(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 318 // Write the title of the page. 319 var header = session.getHeader(); 320 for (var i = 0; i < header.length; i++) 321 w.sIf(i > 0).appendln(3, session.resolveHtmlDoc(header[i])); 322 } 323 324 /** 325 * Renders the contents of the <code><xt><body></xt>/<xt><nav></xt></code> element. 326 * 327 * @param session The current serializer session. 328 * @param w The writer being written to. 329 * @param o The object being serialized. 330 * @throws Exception Any exception can be thrown. 331 */ 332 @SuppressWarnings({ 333 "java:S3776" // Cognitive complexity acceptable for navigation link rendering with conditional logic 334 }) 335 protected void nav(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 336 String[] links = session.getNavLinks(); 337 if (links.length > 0 && ! contains("NONE", links)) { 338 w.sTag(3, "ol").nl(3); 339 for (var l : links) { 340 w.sTag(4, "li"); 341 var original = l; 342 l = session.resolve(l); 343 if (l.matches("(?s)\\S+\\:.*")) { 344 var i = l.indexOf(':'); 345 var key = l.substring(0, i); 346 var val = l.substring(i + 1).trim(); 347 // Bare "request:/?q", "request:?q", "servlet:...", "context:..." were split so key was the scheme 348 // and val lost the scheme; resolveUri then saw /? or ? only and browsers turned "request:?q" into a 349 // bogus path segment. Rejoin to a full marshall URI when val is path/query/fragment only. 350 var scheme = MARSHALL_URI_NAVLINK_SCHEMES.stream() 351 .filter(sch -> eqic(sch, key)) 352 .findFirst() 353 .orElse(null); 354 if (scheme != null && ! val.isEmpty()) { 355 var c0 = val.charAt(0); 356 if (c0 == '/' || c0 == '?' || c0 == '#') 357 val = scheme + ':' + val; 358 } 359 if (val.startsWith("<")) { 360 var encoded = session.resolveHtmlDoc(original); 361 var i2 = encoded.indexOf(':'); 362 if (i2 != -1) 363 encoded = encoded.substring(i2 + 1).trim(); 364 w.nl(4).appendln(5, encoded); 365 } else 366 w.oTag("a").attr("href", session.resolveUri(val), true).cTag().text(key, true).eTag("a"); 367 w.eTag("li").nl(4); 368 } else { 369 w.nl(4).appendln(5, session.resolveHtmlDoc(original)); 370 w.eTag(4, "li").nl(4); 371 } 372 } 373 w.eTag(3, "ol").nl(3); 374 } 375 var nav = session.getNav(); 376 if (nav.length > 0) { 377 for (var i = 0; i < nav.length; i++) 378 w.sIf(i > 0).appendln(3, session.resolveHtmlDoc(nav[i])); 379 } 380 } 381 382 /** 383 * Renders the contents of the <code><xt><head></xt>/<xt><script></xt></code> element. 384 * 385 * @param session The current serializer session. 386 * @param w The writer being written to. 387 * @param o The object being serialized. 388 * @throws Exception Any exception can be thrown. 389 */ 390 protected void script(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 391 var addSpace = Flag.create(); 392 for (var s : session.getScript()) 393 w.sIf(addSpace.getAndSet()).append(3, session.resolveScriptStyle(s)).append('\n'); // Must always append a newline even if whitespace disabled! 394 session.forEachWidget(x -> w.sIf(addSpace.getAndSet()).append(3, session.resolveScriptStyle(x.getScript(session.getVarResolver()))).w('\n')); // Must always append a newline even if whitespace disabled! 395 } 396 397 /** 398 * Renders the contents of the <code><xt><head></xt>/<xt><style></xt></code> element. 399 * 400 * @param session The current serializer session. 401 * @param w The writer being written to. 402 * @param o The object being serialized. 403 * @throws Exception Any exception can be thrown. 404 */ 405 protected void style(HtmlDocSerializerSession session, HtmlWriter<?> w, Object o) throws Exception { 406 var addSpace = Flag.create(); 407 for (var s : session.getStylesheet()) 408 w.sIf(addSpace.getAndSet()).append(3, "@import ").q().append(session.resolveUri(session.resolve(s))).q().appendln(";"); 409 if (session.isNowrap()) 410 w.appendln(3, "div.data * {white-space:nowrap;} "); 411 for (var s : session.getStyle()) 412 w.sIf(addSpace.getAndSet()).appendln(3, session.resolveScriptStyle(s)); 413 session.forEachWidget(x -> w.sIf(addSpace.getAndSet()).appendln(3, session.resolveScriptStyle(x.getStyle(session.getVarResolver())))); 414 } 415}