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.commons.utils.Shorts.*; 020 021import java.lang.reflect.*; 022import java.util.*; 023 024import org.apache.juneau.commons.Schema; 025import org.apache.juneau.commons.bean.*; 026import org.apache.juneau.commons.reflect.*; 027import org.apache.juneau.marshall.*; 028 029/** 030 * Generates a DataTables <a class="doclink" href="https://datatables.net/reference/option/columns">{@code columns}</a> 031 * array from the bean properties of a row type, so the browser-side column list doesn't have to be hand-duplicated. 032 * 033 * <p> 034 * Each generated column descriptor carries the four fields DataTables reads for a server-side-processing column: 035 * <ul class='spaced-list'> 036 * <li><c>data</c> — the bean property name (also the JSON key DataTables reads from each row). 037 * <li><c>title</c> — the human-readable header. Sourced from {@link Schema#title() @Schema(title)} on the 038 * property (field or getter) when present, otherwise a humanized form of the property name 039 * (e.g. {@code "releaseDate"} → {@code "Release Date"}). 040 * <li><c>orderable</c> / <c>searchable</c> — both default to <jk>true</jk> (DataTables' own defaults), emitted 041 * explicitly so the array is self-describing. 042 * </ul> 043 * 044 * <p> 045 * Columns are emitted in Juneau bean-property order (the same order the negotiated serializer uses for the row data), 046 * so a generated {@code columns} array lines up positionally with the {@code data} rows in a 047 * {@link DataTablesResults} envelope. 048 * 049 * <h5 class='section'>Example:</h5> 050 * <p class='bjava'> 051 * <jc>// Bean whose properties drive both the server response and the browser column list.</jc> 052 * <jk>public class</jk> Release { 053 * <jk>public</jk> String getName() {...} 054 * <ja>@Schema</ja>(title=<js>"Ship date"</js>) 055 * <jk>public</jk> Date getReleaseDate() {...} 056 * } 057 * 058 * <jc>// [{data:'name',title:'Name',orderable:true,searchable:true},</jc> 059 * <jc>// {data:'releaseDate',title:'Ship date',orderable:true,searchable:true}]</jc> 060 * List<Map<String,Object>> <jv>columns</jv> = DataTablesColumns.<jsm>of</jsm>(Release.<jk>class</jk>); 061 * </p> 062 * 063 * <p> 064 * The result is an ordinary {@code List<Map<String,Object>>}; serialize it with any Juneau serializer (or embed it in a 065 * page) to produce the JSON DataTables' {@code columns} option expects. 066 * 067 * <h5 class='section'>See Also:</h5><ul> 068 * <li class='jc'>{@link org.apache.juneau.console.datatables.adapter.DataTablesQuery} 069 * <li class='jc'>{@link DataTablesTable} 070 * <li class='link'><a class="doclink" href="https://datatables.net/reference/option/columns">DataTables columns option</a> 071 * </ul> 072 * 073 * @since 10.0.0 074 */ 075public class DataTablesColumns { 076 077 private DataTablesColumns() {} 078 079 /** 080 * Generates the DataTables {@code columns} array for the specified bean type using the default marshalling context. 081 * 082 * @param beanClass The row bean type. Must not be <jk>null</jk> and must be a bean. 083 * @return A new mutable list of column descriptors, in bean-property order. 084 * @throws IllegalArgumentException If the class is not a bean (no readable bean properties could be resolved). 085 */ 086 public static List<Map<String,Object>> of(Class<?> beanClass) { 087 return of(MarshallingContext.DEFAULT, beanClass); 088 } 089 090 /** 091 * Generates the DataTables {@code columns} array for the specified bean type using the specified marshalling context. 092 * 093 * @param ctx The marshalling context supplying the bean metadata (e.g. property naming/ordering). Must not be <jk>null</jk>. 094 * @param beanClass The row bean type. Must not be <jk>null</jk> and must be a bean. 095 * @return A new mutable list of column descriptors, in bean-property order. 096 * @throws IllegalArgumentException If the class is not a bean (no readable bean properties could be resolved). 097 */ 098 public static List<Map<String,Object>> of(MarshallingContext ctx, Class<?> beanClass) { 099 var bm = ctx.getBeanMeta(beanClass); 100 req(bm != null, "Class '%s' is not a bean.", beanClass.getName()); 101 102 var out = new ArrayList<Map<String,Object>>(); 103 for (var pm : bm.getProperties().values()) { 104 // Skip write-only (setter-only) properties - getProperties() surfaces them, but a column whose data key 105 // can't be read back from a row is useless to DataTables (and DataTablesTable would fail reading it). 106 if (! pm.canRead()) 107 continue; 108 Map<String,Object> col = m("data", pm.getName(), "title", title(pm), "orderable", Boolean.TRUE, "searchable", Boolean.TRUE); 109 out.add(col); 110 } 111 return out; 112 } 113 114 /** Resolves a column title: {@code @Schema(title)} on the property (field then getter), else the humanized name. */ 115 private static String title(BeanPropertyMeta pm) { 116 var fromField = schemaTitle(pm.getField()); 117 if (fromField != null) 118 return fromField; 119 var fromGetter = pm.getGetter() == null ? null : schemaTitleOf(pm.getGetter().inner()); 120 if (fromGetter != null) 121 return fromGetter; 122 return humanize(pm.getName()); 123 } 124 125 private static String schemaTitle(FieldInfo f) { 126 return f == null ? null : schemaTitleOf(f.inner()); 127 } 128 129 private static String schemaTitleOf(AnnotatedElement el) { 130 var s = el.getAnnotation(Schema.class); 131 if (s == null || s.title().isEmpty()) 132 return null; 133 return s.title(); 134 } 135 136 /** 137 * Converts a bean property name into a human-readable title by splitting camel-case boundaries and capitalizing the 138 * first letter (e.g. {@code "releaseDate"} → {@code "Release Date"}, {@code "name"} → {@code "Name"}). 139 */ 140 static String humanize(String name) { 141 var sb = new StringBuilder(name.length() + 4); 142 for (var i = 0; i < name.length(); i++) { 143 var ch = name.charAt(i); 144 if (i == 0) { 145 sb.append(Character.toUpperCase(ch)); 146 } else { 147 if (Character.isUpperCase(ch) && Character.isLowerCase(name.charAt(i - 1))) 148 sb.append(' '); 149 sb.append(ch); 150 } 151 } 152 return sb.toString(); 153 } 154}