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