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.datatables;
018
019import static org.apache.juneau.bean.html5.HtmlBuilder.*;
020
021import java.util.*;
022
023import org.apache.juneau.bean.html5.Table;
024import org.apache.juneau.commons.bean.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.html.*;
027
028/**
029 * Builds a DataTables-ready HTML {@link Table} DOM bean from a collection of rows — a server-rendered "plain
030 * table" that the browser upgrades in place with {@code $('#id').DataTable()}.
031 *
032 * <p>
033 * The generated table carries:
034 * <ul class='spaced-list'>
035 *    <li>a stable {@code id} attribute (the selector the browser initializer targets),
036 *    <li>a {@code data-juneau-datatable} marker attribute (the hook the shipped {@code juneau-datatables.js} glue
037 *       auto-initializes &mdash; see {@link DataTablesMixin}),
038 *    <li>a {@code <thead>} of column titles (from {@link DataTablesColumns}), and
039 *    <li>a {@code <tbody>} of one {@code <tr>} per row, with cells pulled positionally from the column {@code data} keys.
040 * </ul>
041 *
042 * <p>
043 * Because it returns a Juneau {@link Table} DOM bean (not a raw HTML string), the markup is produced by the normal
044 * {@code HtmlSerializer} with its usual escaping &mdash; there's no bespoke string concatenation to get wrong.  This
045 * helper is intentionally scoped to the {@code juneau-console-datatables} module so the marshall core and
046 * {@code HtmlDocSerializer} stay decoupled from any DataTables-specific rendering.
047 *
048 * <h5 class='section'>Example:</h5>
049 * <p class='bjava'>
050 *    <jc>// Server-render the rows; the browser calls $('#releases').DataTable() to upgrade it.</jc>
051 *    Table <jv>t</jv> = DataTablesTable.<jsm>of</jsm>(<js>"releases"</js>, <jv>releases</jv>, Release.<jk>class</jk>);
052 * </p>
053 *
054 * <h5 class='section'>See Also:</h5><ul>
055 *    <li class='jc'>{@link DataTablesColumns}
056 *    <li class='jc'>{@link DataTablesMixin}
057 *    <li class='link'><a class="doclink" href="https://datatables.net/examples/data_sources/dom">DataTables from a pre-rendered DOM table</a>
058 * </ul>
059 *
060 * @since 10.0.0
061 */
062public class DataTablesTable {
063
064   /** Marker attribute the shipped {@code juneau-datatables.js} glue looks for to auto-initialize a table. */
065   public static final String MARKER_ATTR = "data-juneau-datatable";
066
067   /**
068    * Marker attribute carrying a server endpoint URL that the shipped {@code juneau-datatables.js} glue auto-wires
069    * into {@code opts.ajax} via {@code JuneauDataTables.ajax(url)} (with {@code opts.serverSide} defaulted to
070    * <jk>true</jk>) when the parsed {@link #MARKER_ATTR} options have no {@code ajax} key.
071    */
072   public static final String AJAX_ATTR = "data-juneau-datatable-ajax";
073
074   private DataTablesTable() {}
075
076   /**
077    * Builds a DataTables-ready table, deriving the columns from the specified row bean type via
078    * {@link DataTablesColumns#of(Class)}.
079    *
080    * @param id The table's DOM {@code id} (the {@code $('#id')} selector).  Must not be <jk>null</jk>.
081    * @param rows The rows to render (beans or maps).  Must not be <jk>null</jk>; may be empty.
082    * @param rowType The row bean type used to derive the columns.  Must not be <jk>null</jk> and must be a bean.
083    * @return A new {@link Table} DOM bean.
084    */
085   public static Table of(String id, Collection<?> rows, Class<?> rowType) {
086      return of(MarshallingContext.DEFAULT, id, rows, rowType);
087   }
088
089   /**
090    * Builds a DataTables-ready table using the specified marshalling context, deriving the columns from the specified
091    * row bean type via {@link DataTablesColumns#of(MarshallingContext, Class)}, additively honoring each bean
092    * property's {@link Html @Html(render)} the same way the ordinary {@code HtmlSerializer}/{@code HtmlDocSerializer}
093    * path already does.
094    *
095    * <p>
096    * Using one context for both column derivation and cell reads keeps the {@code data} keys and the row values
097    * consistent when a resource applies custom bean settings.
098    *
099    * <p>
100    * <b>Behavior note (not purely additive):</b> a bean already carrying {@code @Html(render=...)} for the existing
101    * HtmlDoc/serializer path now ALSO renders through that same render when passed through this overload &mdash;
102    * this overload no longer delegates to the raw-value {@link #of(MarshallingContext, String, Collection, List)}
103    * columns-overload path the way it used to.
104    *
105    * <p>
106    * {@code @Html(render)} honoring applies to <b>bean rows only</b>. A {@code Map}-based row (bean {@code rowType}
107    * for columns, {@code Map} rows for values &mdash; supported today via {@link #value(MarshallingContext, Object,
108    * String) value()}'s {@code row instanceof Map} branch) has no {@link BeanPropertyMeta} to resolve a render from,
109    * so it keeps rendering the raw value, exactly as before.
110    *
111    * @param ctx The marshalling context supplying bean metadata (property naming/ordering) and cell reads.  Must not be <jk>null</jk>.
112    * @param id The table's DOM {@code id} (the {@code $('#id')} selector).  Must not be <jk>null</jk>.
113    * @param rows The rows to render (beans or maps).  Must not be <jk>null</jk>; may be empty.
114    * @param rowType The row bean type used to derive the columns.  Must not be <jk>null</jk> and must be a bean.
115    * @return A new {@link Table} DOM bean.
116    */
117   @SuppressWarnings({
118      "unchecked" // render.getContent(session, v) invokes HtmlRender's erased T via the raw type resolveRender() returns; mirrors HtmlSerializerSession's own private getRender()/getStyle() helpers.
119   })
120   public static Table of(MarshallingContext ctx, String id, Collection<?> rows, Class<?> rowType) {
121      var bm = ctx.getBeanMeta(rowType);
122      var columns = DataTablesColumns.of(ctx, rowType);
123
124      var headerCells = columns.stream().map(col -> th(String.valueOf(col.get("title")))).toArray();
125
126      var htmlSerializer = HtmlSerializer.create().marshallingContext(ctx).build();
127      var session = htmlSerializer.getSession();
128
129      var bodyRows = new ArrayList<>(rows.size());
130      for (var row : rows) {
131         var cells = new ArrayList<>(columns.size());
132         for (var col : columns) {
133            var key = String.valueOf(col.get("data"));
134            var v = value(ctx, row, key);
135            if (v == null) {
136               cells.add(td(""));
137            } else if (row instanceof Map<?,?>) {
138               // A Map row has no BeanPropertyMeta / @Html(render) to resolve -- keep the raw value.
139               cells.add(td(v));
140            } else {
141               var render = resolveRender(htmlSerializer, bm.getPropertyMeta(key), ctx, v);
142               cells.add(td(render == null ? v : render.getContent(session, v)));
143            }
144         }
145         bodyRows.add(tr(cells.toArray()));
146      }
147
148      return table(thead(tr(headerCells)), tbody(bodyRows.toArray()))
149         .id(id)
150         .attr(MARKER_ATTR, "");
151   }
152
153   /**
154    * Resolves the {@link HtmlRender} for a cell the same way {@code HtmlSerializerSession.getRender()} does
155    * privately: property-level {@code @Html(render)} first, falling back to the value's runtime class-level
156    * {@code @Html(render)} &mdash; reimplemented here via the public {@link HtmlSerializer} meta APIs
157    * ({@link HtmlSerializer#getHtmlBeanPropertyMeta}/{@link HtmlSerializer#getHtmlClassMeta}), bound to the table's
158    * own {@code ctx} so custom bean settings stay consistent between columns and cells.
159    */
160   @SuppressWarnings({
161      "rawtypes" // HtmlRender is used raw here, mirroring HtmlSerializerSession's own private getRender() helper.
162   })
163   private static HtmlRender resolveRender(HtmlSerializer htmlSerializer, BeanPropertyMeta pm, MarshallingContext ctx, Object value) {
164      if (pm != null) {
165         var render = htmlSerializer.getHtmlBeanPropertyMeta(pm).getRender();
166         if (render != null)
167            return render;
168      }
169      var cm = ctx.getClassMetaForObject(value);
170      return cm == null ? null : htmlSerializer.getHtmlClassMeta(cm).getRender();
171   }
172
173   /**
174    * Builds a DataTables-ready table from an explicit column list (e.g. one produced by {@link DataTablesColumns} and
175    * then adjusted).
176    *
177    * @param id The table's DOM {@code id} (the {@code $('#id')} selector).  Must not be <jk>null</jk>.
178    * @param rows The rows to render (beans or maps).  Must not be <jk>null</jk>; may be empty.
179    * @param columns The column descriptors (each carrying at least {@code data} and {@code title}).  Must not be
180    *    <jk>null</jk>.
181    * @return A new {@link Table} DOM bean.
182    */
183   public static Table of(String id, Collection<?> rows, List<Map<String,Object>> columns) {
184      return of(MarshallingContext.DEFAULT, id, rows, columns);
185   }
186
187   /**
188    * Builds a DataTables-ready table from an explicit column list using the specified marshalling context for
189    * bean-property cell reads.
190    *
191    * @param ctx The marshalling context used to read bean-property cell values.  Must not be <jk>null</jk>.
192    * @param id The table's DOM {@code id} (the {@code $('#id')} selector).  Must not be <jk>null</jk>.
193    * @param rows The rows to render (beans or maps).  Must not be <jk>null</jk>; may be empty.
194    * @param columns The column descriptors (each carrying at least {@code data} and {@code title}).  Must not be
195    *    <jk>null</jk>.
196    * @return A new {@link Table} DOM bean.
197    */
198   public static Table of(MarshallingContext ctx, String id, Collection<?> rows, List<Map<String,Object>> columns) {
199      var headerCells = columns.stream().map(col -> th(String.valueOf(col.get("title")))).toArray();
200
201      var bodyRows = new ArrayList<>(rows.size());
202      for (var row : rows) {
203         var cells = columns.stream().map(col -> {
204            var v = value(ctx, row, String.valueOf(col.get("data")));
205            return td(v == null ? "" : v);
206         }).toArray();
207         bodyRows.add(tr(cells));
208      }
209
210      return table(thead(tr(headerCells)), tbody(bodyRows.toArray()))
211         .id(id)
212         .attr(MARKER_ATTR, "");
213   }
214
215   /** Reads a column value from a row: a direct key lookup for a {@code Map}, a bean-property read otherwise. */
216   private static Object value(MarshallingContext ctx, Object row, String key) {
217      if (row instanceof Map<?,?> m)
218         return m.get(key);
219      return ctx.toBeanMap(row).get(key);
220   }
221}