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> &mdash; the bean property name (also the JSON key DataTables reads from each row).
037 *    <li><c>title</c> &mdash; 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"} &rarr; {@code "Release Date"}).
040 *    <li><c>orderable</c> / <c>searchable</c> &mdash; 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&lt;Map&lt;String,Object&gt;&gt; <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"} &rarr; {@code "Release Date"}, {@code "name"} &rarr; {@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}