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.html;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021import static org.apache.juneau.html.AsideFloat.*;
022
023import java.util.Collection;
024import java.util.Map;
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/HtmlBasics">HTML Basics</a>
037 * </ul>
038 */
039@SuppressWarnings("resource")
040public class BasicHtmlDocTemplate implements HtmlDocTemplate {
041
042   private static boolean exists(String s) {
043      return nn(s) && ! "NONE".equals(s);
044   }
045
046   private static boolean isEmptyObject(Object o) {
047      if (o == null)
048         return true;
049      if (o instanceof Collection<?> o2)
050         return o2.isEmpty();
051      if (o instanceof Map<?,?> o2)
052         return o2.isEmpty();
053      if (isArray(o))
054         return (java.lang.reflect.Array.getLength(o) == 0);
055      return o.toString().isEmpty();
056   }
057
058   @Override /* Overridden from HtmlDocTemplate */
059   public void writeTo(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
060      w.sTag("html").nl(0);
061      w.sTag(1, "head").nl(1);
062      head(session, w, o);
063      w.eTag(1, "head").nl(1);
064      w.sTag(1, "body").nl(1);
065      body(session, w, o);
066      w.eTag(1, "body").nl(1);
067      w.eTag("html").nl(0);
068   }
069
070   /**
071    * Renders the contents of the <code><xt>&lt;body&gt;</xt>/<xt>&lt;article&gt;</xt></code> element.
072    *
073    * @param session The current serializer session.
074    * @param w The writer being written to.
075    * @param o The object being serialized.
076    * @throws Exception Any exception can be thrown.
077    */
078   protected void article(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
079      // To allow for page formatting using CSS, we encapsulate the data inside two div tags:
080      // <div class='outerdata'><div class='data' id='data'>...</div></div>
081      w.oTag(4, "div").attr("class", "outerdata").append('>').nl(4);
082      w.oTag(5, "div").attr("class", "data").attr("id", "data").append('>').nl(5);
083
084      if (o == null) {
085         w.append(6, "<null/>").nl(6);
086      } else if (isEmptyObject(o)) {
087         String m = session.getNoResultsMessage();
088         if (exists(m))
089            w.append(6, session.resolve(m)).nl(6);
090      } else {
091         session.indent = 6;
092         w.flush();
093         if (session.isResolveBodyVars()) {
094            w = new HtmlWriter(w) {
095               @Override
096               public HtmlWriter text(Object value, boolean preserveWhitespace) {
097                  return super.text(session.resolve(Utils.s(value)), preserveWhitespace);
098               }
099            };
100         }
101         session.parentSerialize(w, o);
102      }
103
104      w.ie(5).eTag("div").nl(5);
105      w.ie(4).eTag("div").nl(4);
106   }
107
108   /**
109    * Renders the contents of the <code><xt>&lt;body&gt;</xt>/<xt>&lt;aside&gt;</xt></code> element.
110    *
111    * @param session The current serializer session.
112    * @param w The writer being written to.
113    * @param o The object being serialized.
114    * @throws Exception Any exception can be thrown.
115    */
116   protected void aside(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
117      var aside = session.getAside();
118      for (var i = 0; i < aside.length; i++)
119         w.sIf(i > 0).appendln(4, session.resolve(aside[i]));
120   }
121
122   /**
123    * Renders the contents of the <code><xt>&lt;body&gt;</xt></code> element.
124    *
125    * @param session The current serializer session.
126    * @param w The writer being written to.
127    * @param o The object being serialized.
128    * @throws Exception Any exception can be thrown.
129    */
130   protected void body(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
131
132      AsideFloat asideFloat = session.getAsideFloat();
133      boolean hasAside = hasAside(session);
134
135      if (hasHeader(session)) {
136         w.sTag(2, "header").nl(2);
137         header(session, w, o);
138         w.ie(2).eTag("header").nl(2);
139      }
140
141      if (hasNav(session)) {
142         w.sTag(2, "nav").nl(2);
143         nav(session, w, o);
144         w.ie(2).eTag("nav").nl(2);
145      }
146
147      if (hasAside && asideFloat.is(TOP)) {
148         w.sTag(2, "section").nl(2);
149         w.sTag(3, "aside").nl(3);
150         aside(session, w, o);
151         w.ie(3).eTag("aside").nl(3);
152         w.ie(2).eTag("section").nl(2);
153      }
154
155      w.sTag(2, "section").nl(2);
156
157      if (hasAside && asideFloat.is(LEFT)) {
158         w.sTag(3, "aside").nl(3);
159         aside(session, w, o);
160         w.ie(3).eTag("aside").nl(3);
161      }
162
163      w.sTag(3, "article").nl(3);
164      article(session, w, o);
165      w.ie(3).eTag("article").nl(3);
166
167      if (hasAside && asideFloat.isAny(RIGHT, DEFAULT)) {
168         w.sTag(3, "aside").nl(3);
169         aside(session, w, o);
170         w.ie(3).eTag("aside").nl(3);
171      }
172
173      w.ie(2).eTag("section").nl(2);
174
175      if (hasAside && asideFloat.is(BOTTOM)) {
176         w.sTag(2, "section").nl(2);
177         w.sTag(3, "aside").nl(3);
178         aside(session, w, o);
179         w.ie(3).eTag("aside").nl(3);
180         w.ie(2).eTag("section").nl(2);
181      }
182
183      if (hasFooter(session)) {
184         w.sTag(2, "footer").nl(2);
185         footer(session, w, o);
186         w.ie(2).eTag("footer").nl(2);
187      }
188   }
189
190   /**
191    * Renders the contents of the <code><xt>&lt;body&gt;</xt>/<xt>&lt;footer&gt;</xt></code> element.
192    *
193    * @param session The current serializer session.
194    * @param w The writer being written to.
195    * @param o The object being serialized.
196    * @throws Exception Any exception can be thrown.
197    */
198   protected void footer(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
199      var footer = session.getFooter();
200      for (var i = 0; i < footer.length; i++)
201         w.sIf(i > 0).appendln(3, session.resolve(footer[i]));
202   }
203
204   /**
205    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;body&gt;</xt>/<xt>&lt;aside&gt;</xt></code>
206    * element.
207    *
208    * @param session The current serializer session.
209    * @return A boolean flag.
210    */
211   protected boolean hasAside(HtmlDocSerializerSession session) {
212      return session.getAside().length > 0;
213   }
214
215   /**
216    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;body&gt;</xt>/<xt>&lt;footer&gt;</xt></code>
217    * element.
218    *
219    * @param session The current serializer session.
220    * @return A boolean flag.
221    */
222   protected boolean hasFooter(HtmlDocSerializerSession session) {
223      return session.getFooter().length > 0;
224   }
225
226   /**
227    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;body&gt;</xt>/<xt>&lt;header&gt;</xt></code>
228    * element.
229    *
230    * @param session The current serializer session.
231    * @return A boolean flag.
232    */
233   protected boolean hasHeader(HtmlDocSerializerSession session) {
234      return session.getHeader().length > 0;
235   }
236
237   /**
238    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;body&gt;</xt>/<xt>&lt;nav&gt;</xt></code>
239    * element.
240    *
241    * @param session The current serializer session.
242    * @return A boolean flag.
243    */
244   protected boolean hasNav(HtmlDocSerializerSession session) {
245      return session.getNav().length > 0 || session.getNavLinks().length > 0;
246   }
247
248   /**
249    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;head&gt;</xt>/<xt>&lt;script&gt;</xt></code> element.
250    *
251    * @param session The current serializer session.
252    * @return A boolean flag.
253    */
254   protected boolean hasScript(HtmlDocSerializerSession session) {
255      return true;
256   }
257
258   /**
259    * Returns <jk>true</jk> if this page should render a <code><xt>&lt;head&gt;</xt>/<xt>&lt;style&gt;</xt></code> element.
260    *
261    * @param session The current serializer session.
262    * @return A boolean flag.
263    */
264   protected boolean hasStyle(HtmlDocSerializerSession session) {
265      return true;
266   }
267
268   /**
269    * Renders the contents of the <code><xt>&lt;head&gt;</xt></code> element.
270    *
271    * @param session The current serializer session.
272    * @param w The writer being written to.
273    * @param o The object being serialized.
274    * @throws Exception Any exception can be thrown.
275    */
276   protected void head(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
277
278      var head = session.getHead();
279      for (var i = 0; i < head.length; i++)
280         w.sIf(i > 0).appendln(2, session.resolve(head[i]));
281
282      if (hasStyle(session)) {
283         w.sTag(2, "style").nl(2);
284         style(session, w, o);
285         w.ie(2).eTag("style").nl(2);
286      }
287      if (hasScript(session)) {
288         w.sTag(2, "script").nl(2);
289         script(session, w, o);
290         w.ie(2).eTag("script").nl(2);
291      }
292   }
293
294   /**
295    * Renders the contents of the <code><xt>&lt;body&gt;</xt>/<xt>&lt;header&gt;</xt></code> element.
296    *
297    * @param session The current serializer session.
298    * @param w The writer being written to.
299    * @param o The object being serialized.
300    * @throws Exception Any exception can be thrown.
301    */
302   protected void header(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
303      // Write the title of the page.
304      var header = session.getHeader();
305      for (var i = 0; i < header.length; i++)
306         w.sIf(i > 0).appendln(3, session.resolve(header[i]));
307   }
308
309   /**
310    * Renders the contents of the <code><xt>&lt;body&gt;</xt>/<xt>&lt;nav&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 nav(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
318      String[] links = session.getNavLinks();
319      if (links.length > 0 && ! contains("NONE", links)) {
320         w.sTag(3, "ol").nl(3);
321         for (var l : links) {
322            w.sTag(4, "li");
323            l = session.resolve(l);
324            if (l.matches("(?s)\\S+\\:.*")) {
325               var i = l.indexOf(':');
326               var key = l.substring(0, i);
327               var val = l.substring(i + 1).trim();
328               if (val.startsWith("<"))
329                  w.nl(4).appendln(5, val);
330               else
331                  w.oTag("a").attr("href", session.resolveUri(val), true).cTag().text(key, true).eTag("a");
332               w.eTag("li").nl(4);
333            } else {
334               w.nl(4).appendln(5, l);
335               w.eTag(4, "li").nl(4);
336            }
337         }
338         w.eTag(3, "ol").nl(3);
339      }
340      var nav = session.getNav();
341      if (nav.length > 0) {
342         for (var i = 0; i < nav.length; i++)
343            w.sIf(i > 0).appendln(3, session.resolve(nav[i]));
344      }
345   }
346
347   /**
348    * Renders the contents of the <code><xt>&lt;head&gt;</xt>/<xt>&lt;script&gt;</xt></code> element.
349    *
350    * @param session The current serializer session.
351    * @param w The writer being written to.
352    * @param o The object being serialized.
353    * @throws Exception Any exception can be thrown.
354    */
355   protected void script(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
356      var addSpace = Flag.create();
357      for (var s : session.getScript())
358         w.sIf(addSpace.getAndSet()).append(3, session.resolve(s)).append('\n'); // Must always append a newline even if whitespace disabled!
359      session.forEachWidget(x -> {
360         w.sIf(addSpace.getAndSet()).append(3, session.resolve(x.getScript(session.getVarResolver()))).w('\n'); // Must always append a newline even if whitespace disabled!
361      });
362   }
363
364   /**
365    * Renders the contents of the <code><xt>&lt;head&gt;</xt>/<xt>&lt;style&gt;</xt></code> element.
366    *
367    * @param session The current serializer session.
368    * @param w The writer being written to.
369    * @param o The object being serialized.
370    * @throws Exception Any exception can be thrown.
371    */
372   protected void style(HtmlDocSerializerSession session, HtmlWriter w, Object o) throws Exception {
373      var addSpace = Flag.create();
374      for (var s : session.getStylesheet())
375         w.sIf(addSpace.getAndSet()).append(3, "@import ").q().append(session.resolveUri(session.resolve(s))).q().appendln(";");
376      if (session.isNowrap())
377         w.appendln(3, "div.data * {white-space:nowrap;} ");
378      for (var s : session.getStyle())
379         w.sIf(addSpace.getAndSet()).appendln(3, session.resolve(s));
380      session.forEachWidget(x -> {
381         w.sIf(addSpace.getAndSet()).appendln(3, session.resolve(x.getStyle(session.getVarResolver())));
382      });
383   }
384}