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>&lt;body&gt;</xt>/<xt>&lt;article&gt;</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>&lt;body&gt;</xt>/<xt>&lt;aside&gt;</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>&lt;body&gt;</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>&lt;body&gt;</xt>/<xt>&lt;footer&gt;</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>&lt;body&gt;</xt>/<xt>&lt;aside&gt;</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>&lt;body&gt;</xt>/<xt>&lt;footer&gt;</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>&lt;body&gt;</xt>/<xt>&lt;header&gt;</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>&lt;body&gt;</xt>/<xt>&lt;nav&gt;</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>&lt;head&gt;</xt>/<xt>&lt;script&gt;</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>&lt;head&gt;</xt>/<xt>&lt;style&gt;</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>&lt;head&gt;</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>&lt;body&gt;</xt>/<xt>&lt;header&gt;</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>&lt;body&gt;</xt>/<xt>&lt;nav&gt;</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>&lt;head&gt;</xt>/<xt>&lt;script&gt;</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>&lt;head&gt;</xt>/<xt>&lt;style&gt;</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}