/** * Copyright 2014-2017 Riccardo Massera (TheCoder4.Eu) and Stephan Rauh (http://www.beyondjava.net). * * This file is part of BootsFaces. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.bootsfaces.component.dataTable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.el.ValueExpression; import javax.faces.FacesException; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.FacesRenderer; import net.bootsfaces.C; import net.bootsfaces.component.ajax.AJAXRenderer; import net.bootsfaces.component.dataTableColumn.DataTableColumn; import net.bootsfaces.render.CoreRenderer; import net.bootsfaces.render.Responsive; import net.bootsfaces.render.Tooltip; import net.bootsfaces.utils.BsfUtils; /** This class generates the HTML code of <b:dataTable />. */ @FacesRenderer(componentFamily = "net.bootsfaces.component", rendererType = "net.bootsfaces.component.dataTable.DataTable") public class DataTableRenderer extends CoreRenderer { @Override public void decode(FacesContext context, UIComponent component) { DataTable dataTable = (DataTable) component; if (dataTable.isDisabled()) { return; } decodeBehaviors(context, dataTable); // f:ajax String clientId = dataTable.getClientId(context); new AJAXRenderer().decode(context, component, clientId); } private static final Pattern NUMERIC_PATTERN = Pattern.compile("[0-9]+"); /** * This methods generates the HTML code of the current b:dataTable. * <code>encodeBegin</code> generates the start of the component. After the, * the JSF framework calls <code>encodeChildren()</code> to generate the * HTML code between the beginning and the end of the component. For * instance, in the case of a panel component the content of the panel is * generated by <code>encodeChildren()</code>. After that, * <code>encodeEnd()</code> is called to generate the rest of the HTML code. * * @param context * the FacesContext. * @param component * the current b:dataTable. * @throws IOException * thrown if something goes wrong when writing the HTML code. */ @Override public void encodeBegin(FacesContext context, UIComponent component) throws IOException { if (!component.isRendered()) { return; } DataTable dataTable = (DataTable) component; ResponseWriter rw = context.getResponseWriter(); String clientId = dataTable.getClientId(); boolean idHasBeenRendered=false; String responsiveStyle = Responsive.getResponsiveStyleClass(dataTable, false); if (null != responsiveStyle && responsiveStyle.trim().length() > 0) { rw.startElement("div", dataTable); rw.writeAttribute("class", responsiveStyle.trim(), null); rw.writeAttribute("id", clientId, "id"); idHasBeenRendered=true; } if (dataTable.isContentDisabled()) { if (beginDisabledFieldset(dataTable, rw)) { rw.writeAttribute("id", clientId, "id"); idHasBeenRendered=true; } } rw.startElement("table", dataTable); if (!idHasBeenRendered) { rw.writeAttribute("id", clientId, "id"); } else rw.writeAttribute("id", clientId + "Inner", "id"); // Table selection needs a valid AJAX id on the table tag String styleClass = "table "; if (dataTable.isBorder()) { styleClass += "table-bordered "; } if (dataTable.isStriped()) { styleClass += "table-striped "; } if (dataTable.isRowHighlight()) styleClass += "table-hover "; if (dataTable.getStyleClass() != null) styleClass += dataTable.getStyleClass(); styleClass += " " + clientId.replace(":", "") + "Table"; rw.writeAttribute("class", styleClass, "class"); Tooltip.generateTooltip(context, dataTable, rw); rw.writeAttribute("cellspacing", "0", "cellspacing"); rw.writeAttribute("style", dataTable.getStyle(), "style"); AJAXRenderer.generateBootsFacesAJAXAndJavaScript(context, dataTable, rw, false); generateHeader(context, dataTable, rw); generateBody(context, dataTable, rw); generateFooter(context, dataTable, rw); new AJAXRenderer().generateBootsFacesAJAXAndJavaScriptForJQuery(context, component, rw, "."+clientId.replace(":", "") + "Table", null); } private void generateFooter(FacesContext context, DataTable dataTable, ResponseWriter rw) throws IOException { boolean hasFooter = false; boolean hasSearchbar = false; if (dataTable.isMultiColumnSearch()) { String position = dataTable.getMultiColumnSearchPosition(); if ("both".equalsIgnoreCase(position) || "bottom".equalsIgnoreCase(position)) { hasSearchbar = true; } } for (UIComponent column : dataTable.getChildren()) { if (!column.isRendered()) { continue; } hasFooter |= column.getFacet("footer") != null; } if (hasFooter || hasSearchbar) { rw.startElement("tfoot", dataTable); if (hasSearchbar) { generateMultiColumnSearchRow(context, dataTable, rw); } if (hasFooter) { for (UIComponent column : dataTable.getChildren()) { if (!column.isRendered()) { continue; } rw.startElement("th", dataTable); Object footerStyle = column.getAttributes().get("footerStyle"); if (footerStyle != null) { rw.writeAttribute("style", footerStyle, null); } Object footerStyleClass = column.getAttributes().get("footerStyleClass"); if (footerStyleClass != null) { rw.writeAttribute("class", footerStyleClass, null); } if (column.getFacet("footer") != null) { UIComponent facet = column.getFacet("footer"); facet.encodeAll(context); } rw.endElement("th"); } } rw.endElement("tfoot"); } } private void generateMultiColumnSearchRow(FacesContext context, DataTable dataTable, ResponseWriter rw) throws IOException { rw.startElement("tr", dataTable); List<UIComponent> columns = dataTable.getChildren(); for (UIComponent column : columns) { if (!column.isRendered()) { continue; } rw.startElement("td", dataTable); Object footerStyle = column.getAttributes().get("footerStyle"); if (footerStyle != null) { rw.writeAttribute("style", footerStyle, null); } Object searchable = column.getAttributes().get("searchable"); if (searchable==null || ((searchable instanceof Boolean) && ((Boolean)searchable).equals(Boolean.TRUE)) || ((searchable instanceof String) && ((String)searchable).equalsIgnoreCase("true"))) { Object footerStyleClass = column.getAttributes().get("footerStyleClass"); if (footerStyleClass != null) { rw.writeAttribute("class", "bf-multisearch " + footerStyleClass, null); } else { rw.writeAttribute("class", "bf-multisearch", null); } if (column.getFacet("header") != null) { UIComponent facet = column.getFacet("header"); facet.encodeAll(context); } else if (column.getAttributes().get("label") != null) { rw.writeText(column.getAttributes().get("label"), null); } } rw.endElement("td"); } rw.endElement("tr"); } private void generateBody(FacesContext context, DataTable dataTable, ResponseWriter rw) throws IOException { rw.startElement("tbody", dataTable); int rows = dataTable.getRowCount(); dataTable.setRowIndex(-1); for (int row = 0; row < rows; row++) { dataTable.setRowIndex(row); if (dataTable.isRowAvailable()) { rw.startElement("tr", dataTable); List<UIComponent> columns = dataTable.getChildren(); for (UIComponent column : columns) { if (!column.isRendered()) { continue; } rw.startElement("td", dataTable); Object contentStyle = column.getAttributes().get("contentStyle"); Object style = column.getAttributes().get("style"); if (contentStyle != null && style==null) { rw.writeAttribute("style", contentStyle, null); } else if (contentStyle == null && style!=null) { rw.writeAttribute("style", style, null); } else if (contentStyle != null && style!=null) { rw.writeAttribute("style", style + ";" + contentStyle, null); } Object contentStyleClass = column.getAttributes().get("contentStyleClass"); Object styleClass = column.getAttributes().get("styleClass"); if (contentStyleClass != null && styleClass==null) { rw.writeAttribute("class", contentStyleClass, null); } else if (contentStyleClass == null && styleClass!=null) { rw.writeAttribute("class", styleClass, null); } else if (contentStyleClass != null && styleClass!=null) { rw.writeAttribute("class", styleClass + " " + contentStyleClass, null); } Object dataOrder = column.getAttributes().get("dataOrder"); if (dataOrder != null) rw.writeAttribute("data-order", dataOrder, null); Object dataSearch = column.getAttributes().get("dataSearch"); if (dataSearch != null) rw.writeAttribute("data-search", dataSearch, null); Object value = column.getAttributes().get("value"); if (value != null) { rw.writeText(value, null); } renderChildrenOfColumn(column, context); rw.endElement("td"); } rw.endElement("tr"); } } rw.endElement("tbody"); dataTable.setRowIndex(-1); } private void renderChildrenOfColumn(UIComponent column, FacesContext context) throws IOException { resetClientIdCacheRecursively(column); column.encodeChildren(context); } private void resetClientIdCacheRecursively(UIComponent c) { String id=c.getId(); if (null != id) { c.setId(id); // this strange operation clears the cache of the clientId } Iterator<UIComponent> children = c.getFacetsAndChildren(); if (children != null) { while (children.hasNext()) { UIComponent kid = children.next(); resetClientIdCacheRecursively(kid); } } } private void generateHeader(FacesContext context, DataTable dataTable, ResponseWriter rw) throws IOException { rw.startElement("thead", dataTable); // Putting input fields into the header doesn't work yet if (dataTable.isMultiColumnSearch()) { String position = dataTable.getMultiColumnSearchPosition(); if ("both".equalsIgnoreCase(position) || "top".equalsIgnoreCase(position)) { generateMultiColumnSearchRow(context, dataTable, rw); } } rw.startElement("tr", dataTable); int index = 0; List<UIComponent> columns = dataTable.getChildren(); for (UIComponent column : columns) { if (!column.isRendered()) { continue; } rw.startElement("th", dataTable); Object headerStyle = column.getAttributes().get("headerStyle"); Object style = column.getAttributes().get("style"); if (headerStyle != null && style == null) { rw.writeAttribute("style", headerStyle, null); } else if (headerStyle == null && style != null) { rw.writeAttribute("style", style, null); } else if (headerStyle != null && style != null) { rw.writeAttribute("style", style + ";" + headerStyle, null); } Object headerStyleClass = column.getAttributes().get("headerStyleClass"); Object styleClass = column.getAttributes().get("styleClass"); if (headerStyleClass != null && styleClass==null) { rw.writeAttribute("class", headerStyleClass, null); } else if (headerStyleClass == null && styleClass!=null) { rw.writeAttribute("class", styleClass, null); } else if (headerStyleClass != null && styleClass!=null) { rw.writeAttribute("class", styleClass + " " + headerStyleClass, null); } if (column.getFacet("header") != null) { UIComponent facet = column.getFacet("header"); facet.encodeAll(context); } else if (column.getAttributes().get("label") != null) { String labelStyleClass = (String)column.getAttributes().get("labelStyleClass"); String labelStyle = (String)column.getAttributes().get("labelStyle"); if (null != labelStyle || null != labelStyleClass) { rw.startElement("span", null); writeAttribute(rw, "style", labelStyle); writeAttribute(rw, "class", labelStyleClass); } rw.writeText(column.getAttributes().get("label"), null); if (null != labelStyle || null != labelStyleClass) { rw.endElement("span"); } } else { boolean labelHasBeenRendered = false; for (UIComponent c : column.getChildren()) { if (c.getAttributes().get("label") != null) { String labelStyleClass = (String)c.getAttributes().get("labelStyleClass"); String labelStyle = (String)c.getAttributes().get("labelStyle"); if (null != labelStyle || null != labelStyleClass) { rw.startElement("span", null); writeAttribute(rw, "style", labelStyle); writeAttribute(rw, "class", labelStyleClass); } rw.writeText(c.getAttributes().get("label"), null); if (null != labelStyle || null != labelStyleClass) { rw.endElement("span"); } labelHasBeenRendered = true; break; } } if (!labelHasBeenRendered) { ValueExpression ve = column.getValueExpression("value"); if (null != ve) { String exp = ve.getExpressionString(); int pos = exp.lastIndexOf('.'); if (pos > 0) { exp = exp.substring(pos + 1); } exp = exp.substring(0, 1).toUpperCase() + exp.substring(1); String labelStyleClass = (String)column.getAttributes().get("labelStyleClass"); String labelStyle = (String)column.getAttributes().get("labelStyle"); if (null != labelStyle || null != labelStyleClass) { rw.startElement("span", null); writeAttribute(rw, "style", labelStyle); writeAttribute(rw, "class", labelStyleClass); } rw.writeText(exp.substring(0, exp.length() - 1), null); if (null != labelStyle || null != labelStyleClass) { rw.endElement("span"); } labelHasBeenRendered = true; } } if (!labelHasBeenRendered) { rw.writeText("Column #" + index, null); } } String order = null; if (column.getFacet("order") != null) { UIComponent facet = column.getFacet("order"); order = facet.toString(); } else if (column.getAttributes().get("order") != null) { order = (String) column.getAttributes().get("order"); } if (null != order) { order = order.trim(); if ((!"asc".equals(order)) && (!"desc".equals(order))) { throw new FacesException("Invalid column order. Legal values are 'asc' and 'desc'."); } Map<Integer, String> columnSortOrder; if (dataTable.getColumnSortOrderMap() == null) { dataTable.initColumnSortOrderMap(); } columnSortOrder = dataTable.getColumnSortOrderMap(); columnSortOrder.put(index, order); } if (column.getAttributes().get("orderBy") != null) { String orderBy = (String) column.getAttributes().get("orderBy"); if (dataTable.getColumnInfo() == null) { List<String> infos = new ArrayList<String>(dataTable.getChildren().size()); for (int k = 0; k < dataTable.getChildren().size(); k++) { infos.add(null); } dataTable.setColumnInfo(infos); } List<String> infos = dataTable.getColumnInfo(); String s = infos.get(index); if (s == null) { infos.set(index, "'orderDataType': '" + orderBy+"'"); } else { infos.set(index, s + ",'orderDataType': '" + orderBy+"'"); } } if (column.getAttributes().get("dataType") != null) { String type = (String) column.getAttributes().get("dataType"); if (dataTable.getColumnInfo() == null) { List<String> infos = new ArrayList<String>(dataTable.getChildren().size()); for (int k = 0; k < dataTable.getChildren().size(); k++) { infos.add(null); } dataTable.setColumnInfo(infos); } List<String> infos = dataTable.getColumnInfo(); String s = infos.get(index); if (s == null) { infos.set(index, "'type': '" + type + "'"); } else infos.set(index, s + ",'type': '" + type + "'"); } if (column.getAttributes().get("orderable") != null) { String orderable = column.getAttributes().get("orderable").toString(); if ("false".equalsIgnoreCase(orderable)) { if (dataTable.getColumnInfo() == null) { List<String> infos = new ArrayList<String>(dataTable.getChildren().size()); for (int k = 0; k < dataTable.getChildren().size(); k++) { infos.add(null); } dataTable.setColumnInfo(infos); } List<String> infos = dataTable.getColumnInfo(); String s = infos.get(index); if (s == null) { infos.set(index, "'orderable': false"); } else infos.set(index, s + ",'orderable': false"); } } if (column.getAttributes().get("customOptions") != null) { String customOptions = column.getAttributes().get("customOptions").toString(); if (customOptions !=null && customOptions.length()>0) { if (dataTable.getColumnInfo() == null) { List<String> infos = new ArrayList<String>(dataTable.getChildren().size()); for (int k = 0; k < dataTable.getChildren().size(); k++) { infos.add(null); } dataTable.setColumnInfo(infos); } List<String> infos = dataTable.getColumnInfo(); String s = infos.get(index); if (s == null) { infos.set(index, customOptions); } else infos.set(index, s + "," +customOptions); } } rw.endElement("th"); index++; } rw.endElement("tr"); rw.endElement("thead"); } /** * This methods generates the HTML code of the current b:dataTable. * <code>encodeBegin</code> generates the start of the component. After the, * the JSF framework calls <code>encodeChildren()</code> to generate the * HTML code between the beginning and the end of the component. For * instance, in the case of a panel component the content of the panel is * generated by <code>encodeChildren()</code>. After that, * <code>encodeEnd()</code> is called to generate the rest of the HTML code. * * @param context * the FacesContext. * @param component * the current b:dataTable. * @throws IOException * thrown if something goes wrong when writing the HTML code. */ @Override public void encodeEnd(FacesContext context, UIComponent component) throws IOException { if (!component.isRendered()) { return; } DataTable dataTable = (DataTable) component; Map<Integer, String> columnSortOrder = dataTable.getColumnSortOrderMap(); int pageLength = dataTable.getPageLength(); String orderString = "[]"; if (columnSortOrder != null) { StringBuilder sb = new StringBuilder(); int i = 0; for (Map.Entry<Integer, String> entry : columnSortOrder.entrySet()) { String separator = (i > 0) ? "," : ""; sb.append(separator).append("[").append(entry.getKey()).append(",").append("'").append(entry.getValue()) .append("'").append("]"); i++; } orderString = sb.toString(); } ResponseWriter rw = context.getResponseWriter(); String clientIdRaw = dataTable.getClientId(); String clientId = clientIdRaw.replace(":", ""); String widgetVar = dataTable.getWidgetVar(); if (null == widgetVar) { widgetVar = BsfUtils.snakeCaseToCamelCase(clientIdRaw.replace(":", "-") + "Widget"); } String lang = determineLanguage(context, dataTable); rw.endElement("table"); endDisabledFieldset(dataTable, rw); String responsiveStyle = Responsive.getResponsiveStyleClass(dataTable, false); if (null != responsiveStyle && responsiveStyle.trim().length() > 0) { rw.endElement("div"); } Tooltip.activateTooltips(context, dataTable); rw.startElement("script", component); // # Start enclosure rw.writeText("$(document).ready(function() {", null); // # Enclosure-scoped variable initialization String options = ""; options = addOptions("fixedHeader: " + dataTable.isFixedHeader(), options); options = addOptions( "responsive: " + dataTable.isResponsive(), options); options = addOptions( "paging: " + dataTable.isPaginated(), options); if (!dataTable.isInfo()) { options = addOptions( "info: false", options); } options = addOptions( "pageLength: " + pageLength, options); options = addOptions( "lengthMenu: " + getPageLengthMenu(dataTable), options); options = addOptions( "searching: " + dataTable.isSearching() , options); options = addOptions( "order: " + orderString, options); options = addOptions( "stateSave: " + dataTable.isSaveState(), options); if (dataTable.isSelect() && "single".equalsIgnoreCase(dataTable.getSelectionMode())) { options = addOptions( "select: 'single'", options); } else { options = addOptions( "select: " + dataTable.isSelect(), options); } options = addOptions( generateScrollOptions(dataTable), options); options = addOptions( (BsfUtils.isStringValued(lang) ? " language: { url: '" + lang + "' } " : null), options); options = addOptions( generateColumnInfos(dataTable.getColumnInfo()), options); options = addOptions( dataTable.getCustomOptions(), options); rw.writeText(widgetVar + " = $('." + clientId + "Table" + "');" + // # Get instance of wrapper, and replace it with the unwrapped table. "var wrapper = $('#" + clientIdRaw.replace(":", "\\\\:") + "_wrapper');" + "wrapper.replaceWith(" + widgetVar + ");" + "var table = " + widgetVar + ".DataTable({" + options + "});", null); if (dataTable.isMultiColumnSearch()) { // # Footer stuff: // https://datatables.net/examples/api/multi_filter.html // # Convert footer column text to input textfields String filter="<div class=\"form-group has-feedback\">"; filter+="<input class=\"form-control input-sm datatable-filter-field\" type=\"text\" placeholder=\"' + title + '\" />"; filter+="<i class=\"fa fa-search form-control-feedback\"></i>"; filter+="</div>"; rw.writeText(widgetVar + ".find('.bf-multisearch').each(function(){" + "var title=$(this).text();" + "$(this).html('" + filter + "');" + "});", null); // # Add event listeners for each multisearch input rw.writeText("var inputs=$("+ widgetVar + ".find('.bf-multisearch input'));", null); rw.writeText("table.columns().every( function(col) {" + "var that=this;" + "inputs[col].value=table.columns(col).search()[0];" + "$(inputs[col]).on('keyup change', function(){if(that.search()!==this.value){" + "that.search(this.value).draw('page');}});", null); rw.writeText("});", null); int col=0; for (UIComponent column : dataTable.getChildren()) { if (!column.isRendered()) { continue; } String searchValue = null; if ((column instanceof DataTableColumn)) { searchValue = ((DataTableColumn) column).getSearchValue(); } else { Object sv = column.getAttributes().get("searchValue"); if (sv != null && (!"".equals(sv))) { searchValue = sv.toString(); } } if (null != searchValue) { rw.writeText("inputs[" + col + "].value='" + searchValue + "';", null); rw.writeText("table.columns(" + col+").search('"+searchValue + "').draw('page');", null); } col++; } } // # End enclosure rw.writeText("} );", null); rw.endElement("script"); } private String addOptions(String newOption, String options) { if (newOption!=null && newOption.length()>0) { if (options.length()>0) options += ","; options += newOption; } return options; } private String generateScrollOptions(DataTable dataTable) { String scrollY = dataTable.getScrollSize(); boolean scrollX = dataTable.isScrollX(); if (null == scrollY && (!scrollX)) { return ""; } String result = ""; if (null != scrollY) { if (!NUMERIC_PATTERN.matcher(scrollY).matches()) { // you can pass the scrollY either as a numeric value (in which // case it is the height in px) // or as a String containing the unit. If it's a String, it has // to be surround be ticks. scrollY = "'" + scrollY + "'"; } result += " scrollY: " + scrollY + ","; } if (scrollX) { result += "scrollX: true,"; } return result + "scrollCollapse: " + dataTable.isScrollCollapse(); } private String generateColumnInfos(List<String> columnInfo) { if (columnInfo == null) { return ""; } String result = "columns: ["; for (String col : columnInfo) { if (null == col) { result += "null,"; } else { result += "{" + col + "},"; if (col.contains("dom-text")) { if (!col.contains("type")) { throw new FacesException( "You have to specify the data type of the column if you want to sort it using order-by."); } } } } result = result.substring(0, result.length() - 1); // remove the // trailing comma result += "]"; return result; } private String getPageLengthMenu(DataTable dataTable) { String menu = dataTable.getPageLengthMenu(); if (menu != null) { menu = menu.trim(); if (!menu.startsWith("[")) { menu = "[" + menu; } if (!menu.endsWith("]")) { menu = menu + "]"; } } return menu; } /** * Determine if the user specify a lang Otherwise return null to avoid * language settings. * * @param fc * @param dataTable * @return */ private String determineLanguage(FacesContext fc, DataTable dataTable) { final Set<String> availableLanguages = new HashSet<String>( Arrays.asList(new String[] { "de", "en", "es", "fr", "hu", "it", "pl", "ru" })); if (BsfUtils.isStringValued(dataTable.getCustomLangUrl())) { return dataTable.getCustomLangUrl(); } else if (BsfUtils.isStringValued(dataTable.getLang())) { String lang = dataTable.getLang(); if (availableLanguages.contains(lang)) return determineLanguageUrl(fc, lang); } else { String lang = fc.getViewRoot().getLocale().getLanguage(); if (availableLanguages.contains(lang)) { return determineLanguageUrl(fc, lang); } } return null; } /** * Determine the locale to set-up to dataTable component. The locale is * determined in this order: - if customLangUrl is specified, it is the * value set up - otherwise, the system check if locale is explicit * specified - otherwise it takes from the ViewRoot * * @param fc * @param dataTable * @return */ private String determineLanguageUrl(FacesContext fc, String lang) { // Build resource url return fc.getApplication().getResourceHandler() .createResource("jq/ui/i18n/dt/datatable-" + lang + ".json", C.BSF_LIBRARY).getRequestPath(); } @Override public void encodeChildren(FacesContext context, UIComponent component) throws IOException { // Children are already rendered in encodeBegin() } }