Java tutorial
/* * The MIT License (MIT) * Copyright (c) 2013 Lassiter Consulting Group, LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.lassitercg.faces.components.sheet; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.faces.component.UIComponent; import javax.faces.component.behavior.ClientBehavior; import javax.faces.component.behavior.ClientBehaviorContext; import javax.faces.component.behavior.ClientBehaviorHolder; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.model.SelectItem; import javax.faces.render.FacesRenderer; import javax.faces.render.Renderer; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.primefaces.json.JSONArray; import org.primefaces.json.JSONException; import org.primefaces.json.JSONObject; import org.primefaces.util.WidgetBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.lassitercg.faces.components.util.VarBuilder; /** * The Sheet renderer. * <p> * @author <a href="mailto:mlassiter@lassitercg.com">Mark Lassiter</a> * @version $Id: $ */ @FacesRenderer(componentFamily = Sheet.FAMILY, rendererType = Sheet.RENDERERTYPE) public class SheetRenderer extends Renderer { /** * Logger for this class */ private static final Logger LOG = LoggerFactory.getLogger(SheetRenderer.class); /** * Encodes the Sheet component */ @Override public void encodeBegin(FacesContext context, UIComponent component) throws IOException { final Sheet sheet = (Sheet) component; // update column mappings on render sheet.updateColumnMappings(); // sort data sheet.sortAndFilter(); ResponseWriter responseWriter = context.getResponseWriter(); // encode markup encodeMarkup(context, sheet, responseWriter); // encode javascript encodeJavascript(context, sheet, responseWriter); } /** * Encodes the HTML markup for the sheet. * <p> * @param context * @param sheet * @throws IOException */ protected void encodeMarkup(FacesContext context, Sheet sheet, ResponseWriter responseWriter) throws IOException { /* * <div id="..." name="..." class="" style=""> */ String styleClass = sheet.getStyleClass(); String clientId = sheet.getClientId(context); Integer width = sheet.getWidth(); Integer height = sheet.getHeight(); // outer div to wrapper table responseWriter.startElement("div", null); responseWriter.writeAttribute("id", clientId, "id"); responseWriter.writeAttribute("name", clientId, "clientId"); // note: can't use ui-datatable here because it will mess with // handsontable cell rendering String divclass = "ui-handsontable ui-widget"; if (styleClass != null) divclass = divclass + " " + styleClass; if (!sheet.isValid()) divclass = divclass + " ui-state-error"; responseWriter.writeAttribute("class", divclass, "styleClass"); if (width != null) responseWriter.writeAttribute("style", "width: " + width + "px;", null); encodeHeader(context, responseWriter, sheet); // handsontable div responseWriter.startElement("div", null); responseWriter.writeAttribute("id", clientId + "_tbl", "id"); responseWriter.writeAttribute("name", clientId + "_tbl", "clientId"); responseWriter.writeAttribute("class", "handsontable-inner", "styleClass"); String style = ""; if (width != null) style = style + "width: " + width + "px;"; if (height != null) style = style + "height: " + height + "px;"; if (style.length() > 0) responseWriter.writeAttribute("style", style, null); encodeHiddenInputs(responseWriter, sheet, clientId); encodeFilterValues(context, responseWriter, sheet, clientId); responseWriter.endElement("div"); encodeFooter(context, responseWriter, sheet); responseWriter.endElement("div"); } /** * Encodes an optional attribute to the widget builder specified. * <p> * @param wb * the WidgetBuilder to append to * @param attrName * the attribute name * @param value * the value * @throws IOException */ protected void encodeOptionalAttr(WidgetBuilder wb, String attrName, String value) throws IOException { if (value != null) wb.attr(attrName, value); } /** * Encodes an optional native attribute (unquoted). * <p> * @param wb * the WidgetBuilder to append to * @param attrName * the attribute name * @param value * the value * @throws IOException */ protected void encodeOptionalNativeAttr(WidgetBuilder wb, String attrName, Object value) throws IOException { if (value != null) wb.nativeAttr(attrName, value.toString()); } /** * Encodes the Javascript for the sheet. * <p> * @param context * @param sheet * @throws IOException */ protected void encodeJavascript(FacesContext context, Sheet sheet, ResponseWriter responseWriter) throws IOException { String widgetVar = sheet.resolveWidgetVar(); String clientId = sheet.getClientId(context); WidgetBuilder wb = new WidgetBuilder(context); wb.initWithDomReady("Sheet", widgetVar, clientId); // errors encodeBadData(context, sheet, wb); // data encodeData(context, sheet, wb); // the delta var that will be used to track changes client side // stringified and placed in hidden input for submission wb.nativeAttr("delta", "{}"); // filters encodeFilterVar(context, sheet, wb); // sortable encodeSortVar(context, sheet, wb); // behaviors encodeBehaviors(context, sheet, wb); encodeOptionalNativeAttr(wb, "fixedColumnsLeft", sheet.getFixedCols()); encodeOptionalNativeAttr(wb, "fixedRowsTop", sheet.getFixedRows()); encodeOptionalNativeAttr(wb, "width", sheet.getWidth()); encodeOptionalNativeAttr(wb, "height", sheet.getHeight()); String emptyMessage = sheet.getEmptyMessage(); if (StringUtils.isEmpty(emptyMessage)) { emptyMessage = "No Records Found"; } encodeOptionalAttr(wb, "emptyMessage", emptyMessage); encodeOptionalAttr(wb, "stretchH", sheet.getStretchH()); encodeOptionalAttr(wb, "currentRowClassName", sheet.getCurrentRowClass()); encodeOptionalAttr(wb, "currentColClassName", sheet.getCurrentColClass()); wb.nativeAttr("rowHeaders", sheet.isShowRowHeaders().toString()); encodeColHeaders(context, sheet, wb); encodeColOptions(context, sheet, wb); wb.finish(); } /** * Encodes the necessary JS to render bad data * @throws IOException */ protected void encodeBadData(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { wb.attr("errors", sheet.getBadDataValue()); } /** * Encode the column headers * <p> * @param context * @param writer * @param sheet * @throws IOException */ protected void encodeColHeaders(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { VarBuilder vb = new VarBuilder(null, false); for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; vb.appendArrayValue(column.getHeaderText(), true); } wb.nativeAttr("colHeaders", vb.closeVar().toString()); } /** * Encode the column options * <p> * @param context * @param writer * @param sheet * @throws IOException */ protected void encodeColOptions(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { VarBuilder vb = new VarBuilder(null, false); for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; VarBuilder options = new VarBuilder(null, true); options.appendProperty("type", column.getColType(), true); Integer width = column.getColWidth(); if (width != null) options.appendProperty("width", width.toString(), false); if (column.isReadonly()) options.appendProperty("readOnly", "true", false); options.appendProperty("headerStyleClass", column.getHeaderStyleClass(), true); vb.appendArrayValue(options.closeVar().toString(), false); } wb.nativeAttr("columns", vb.closeVar().toString()); } /** * Encode the row data. Builds row data, style data and read only object. * <p> * @param context * @param sheet * @param jsDataVar * @throws IOException */ protected void encodeData(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { VarBuilder vbData = new VarBuilder(null, false); VarBuilder vbStyle = new VarBuilder(null, true); VarBuilder vbRowStyle = new VarBuilder(null, false); VarBuilder vbReadOnly = new VarBuilder(null, true); VarBuilder vbComment = new VarBuilder(null, false); List<Object> values = sheet.getSortedValues(); int row = 0; for (Object value : values) { sheet.setRowIndex(context, row); encodeRow(context, vbData, vbRowStyle, vbStyle, vbReadOnly, vbComment, sheet, value, row); row++; } sheet.setRowIndex(context, -1); wb.nativeAttr("data", vbData.closeVar().toString()); wb.nativeAttr("styles", vbStyle.closeVar().toString()); wb.nativeAttr("rowStyles", vbRowStyle.closeVar().toString()); wb.nativeAttr("readOnly", vbReadOnly.closeVar().toString()); wb.nativeAttr("comments", vbComment.closeVar().toString()); } /** * Encode a single row. * <p> * @param context * @param writer * @param sheet * @param clientId * @throws IOException */ protected void encodeRow(FacesContext context, VarBuilder vbData, VarBuilder vbRowStyle, VarBuilder vbStyle, VarBuilder vbReadOnly, VarBuilder vbComment, Sheet sheet, Object data, int rowIndex) throws IOException { // encode rowStyle (if any) String rowStyleClass = sheet.getRowStyleClass(); if (rowStyleClass == null) vbRowStyle.appendArrayValue("null", false); else vbRowStyle.appendArrayValue(rowStyleClass, true); // data is array of array of data VarBuilder vbRow = new VarBuilder(null, false); int renderCol = 0; for (int col = 0; col < sheet.getColumns().size(); col++) { final Column column = sheet.getColumns().get(col); if (!column.isRendered()) continue; // render data value String value = sheet.getRenderValueForCell(context, sheet.getRowKeyValue(context), col); vbRow.appendArrayValue(value, true); // custom style String styleClass = column.getStyleClass(); if (styleClass != null) { vbStyle.appendRowColProperty(rowIndex, renderCol, styleClass, true); } String comment = sheet.getCommentForCell(context, sheet.getRowKeyValue(context), col); if (StringUtils.isNotEmpty(comment)) { StringBuilder rowComments = new StringBuilder(); comment = StringEscapeUtils.escapeJava(comment); comment = comment.replace("\\\\n", "\\n"); rowComments.append("{row: " + rowIndex + ", col: " + renderCol + ", comment: \"" + comment + "\"}"); vbComment.appendArrayValue(rowComments.toString(), false); } // read only per cell boolean readOnly = column.isReadonly() || column.isReadonlyCell(); if (readOnly) vbReadOnly.appendRowColProperty(rowIndex, renderCol, "true", true); renderCol++; } // close row and append to vbData vbData.appendArrayValue(vbRow.closeVar().toString(), false); } /** * Encode hidden input fields * @param responseWriter * @param sheet * @param clientId * @throws IOException */ private void encodeHiddenInputs(ResponseWriter responseWriter, final Sheet sheet, String clientId) throws IOException { responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_input", "id"); responseWriter.writeAttribute("name", clientId + "_input", "name"); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("value", "", null); responseWriter.endElement("input"); responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_focus", "id"); responseWriter.writeAttribute("name", clientId + "_focus", "name"); responseWriter.writeAttribute("type", "hidden", null); if (sheet.getFocusId() == null) responseWriter.writeAttribute("value", "", null); else responseWriter.writeAttribute("value", sheet.getFocusId(), null); responseWriter.endElement("input"); responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_selection", "id"); responseWriter.writeAttribute("name", clientId + "_selection", "name"); responseWriter.writeAttribute("type", "hidden", null); if (sheet.getSelection() == null) responseWriter.writeAttribute("value", "", null); else responseWriter.writeAttribute("value", sheet.getSelection(), null); responseWriter.endElement("input"); // sort col and order if specified and supported int sortCol = sheet.getSortColRenderIndex(); responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_sortby", "id"); responseWriter.writeAttribute("name", clientId + "_sortby", "name"); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("value", sortCol, null); responseWriter.endElement("input"); responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_sortorder", "id"); responseWriter.writeAttribute("name", clientId + "_sortorder", "name"); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("value", sheet.getSortOrder().toLowerCase(), null); responseWriter.endElement("input"); responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_comment", "id"); responseWriter.writeAttribute("name", clientId + "_comment", "name"); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("value", "", null); responseWriter.endElement("input"); } /** * Encode client behaviors to widget config * <P> * @param context * @param sheet * @param wb * @throws IOException */ private void encodeBehaviors(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { // note we write out the onchange event here so we have the selected // cell too Map<String, List<ClientBehavior>> behaviors = sheet.getClientBehaviors(); wb.append(",behaviors:{"); String clientId = sheet.getClientId(); // sort event (manual since callBack prepends leading comma) wb.append("sort").append(":").append("function(s, event)").append("{").append("PrimeFaces.ab({source: '") .append(clientId).append("',event: 'sort', process: '").append(clientId).append("', update: '") .append(clientId).append("'}, arguments[1]);}"); // filter wb.callback("filter", "function(s, event)", "PrimeFaces.ab({source: '" + clientId + "', event: 'filter', process: '" + clientId + "', update: '" + clientId + "'}, arguments[1]);"); if (behaviors.containsKey("change")) { ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, sheet, "change", sheet.getClientId(context), null); wb.callback("change", "function(source, event)", behaviors.get("change").get(0).getScript(behaviorContext)); } if (behaviors.containsKey("cellSelect")) { ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, sheet, "cellSelect", sheet.getClientId(context), null); wb.callback("cellSelect", "function(source, event)", behaviors.get("cellSelect").get(0).getScript(behaviorContext)); } wb.append("}"); } /** * Encode the sheet footer * @param context * @param responseWriter * @param sheet * @throws IOException */ private void encodeFooter(FacesContext context, ResponseWriter responseWriter, final Sheet sheet) throws IOException { // footer UIComponent footer = sheet.getFacet("footer"); if (footer != null) { responseWriter.startElement("div", null); responseWriter.writeAttribute("class", "ui-datatable-footer ui-widget-header ui-corner-bottom", null); footer.encodeAll(context); responseWriter.endElement("div"); } } /** * Encode the Sheet header * @param context * @param responseWriter * @param sheet * @throws IOException */ private void encodeHeader(FacesContext context, ResponseWriter responseWriter, final Sheet sheet) throws IOException { // header UIComponent header = sheet.getFacet("header"); if (header != null) { responseWriter.startElement("div", null); responseWriter.writeAttribute("class", "ui-datatable-header ui-widget-header ui-corner-top", null); header.encodeAll(context); responseWriter.endElement("div"); } } /** * Encodes the filter values. * <p> * @param context * @param responseWriter * @param sheet * @throws IOException */ protected void encodeFilterValues(FacesContext context, ResponseWriter responseWriter, Sheet sheet, String clientId) throws IOException { int renderIdx = 0; for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; if (column.getValueExpression("filterBy") != null) { responseWriter.startElement("input", null); responseWriter.writeAttribute("id", clientId + "_filter_" + renderIdx, "id"); responseWriter.writeAttribute("name", clientId + "_filter_" + renderIdx, "name"); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("value", column.getFilterValue(), null); responseWriter.endElement("input"); } renderIdx++; } } /** * Encodes a javascript filter var that informs the col header event of the * column's filtering options. The var is an array in the form: * <p> * ["false","true",["option 1", "option 2"]] * <p> * False indicates no filtering for the column. * <p> * True indicates simple input text filter. * <p> * Array of values indicates a drop down filter with the listed options. * <p> * <p> * @param context * @param sheet * @param jsFilterVar * @throws IOException */ protected void encodeFilterVar(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { VarBuilder vb = new VarBuilder(null, false); for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; if (column.getValueExpression("filterBy") == null) { vb.appendArrayValue("false", true); continue; } Collection<SelectItem> options = column.getFilterOptions(); if (options == null) vb.appendArrayValue("true", true); else { VarBuilder vbOptions = new VarBuilder(null, false); for (SelectItem item : options) { vbOptions.appendArrayValue( "{ label: \"" + item.getLabel() + "\", value: \"" + item.getValue() + "\"}", false); } vb.appendArrayValue(vbOptions.closeVar().toString(), false); } } wb.nativeAttr("filters", vb.closeVar().toString()); } /** * Encodes a javascript sort var that informs the col header event of the * column's sorting options. The var is an array of boolean indicating * whether or not the column is sortable. * <p> * @param context * @param sheet * @param jsFilterVar * @throws IOException */ protected void encodeSortVar(FacesContext context, Sheet sheet, WidgetBuilder wb) throws IOException { VarBuilder vb = new VarBuilder(null, false); for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; if (column.getValueExpression("sortBy") == null) vb.appendArrayValue("false", false); else vb.appendArrayValue("true", false); } wb.nativeAttr("sortable", vb.closeVar().toString()); } /** * Overrides decode and to parse the request parameters for the two hidden * input fields: * <ul> * <li>clientid_input: any new changes provided by the user * <li>clientid_selection: the user's cell selections * <ul> * These are JSON values and are parsed into our submitted values data on * the Sheet component. * <p> */ @Override public void decode(FacesContext context, UIComponent component) { final Sheet sheet = (Sheet) component; // clear updates from previous decode sheet.getUpdates().clear(); sheet.setRowIndex(context, -1); // get parameters // we'll need the request parameters Map<String, String> params = context.getExternalContext().getRequestParameterMap(); String clientId = sheet.getClientId(context); // get our input fields String jsonUpdates = params.get(clientId + "_input"); String jsonSelection = params.get(clientId + "_selection"); String jsonComments = params.get(clientId + "_comment"); // decode into submitted values on the Sheet decodeSubmittedValues(context, sheet, jsonUpdates); // decode the selected range so we can puke it back decodeSelection(context, sheet, jsonSelection); decodeComments(context, sheet, jsonComments); // decode client behaviors decodeBehaviors(context, sheet); // decode filters decodeFilters(context, sheet, params, clientId); String sortBy = params.get(clientId + "_sortby"); String sortOrder = params.get(clientId + "_sortorder"); if (sortBy != null) { int col = Integer.valueOf(sortBy); if (col >= 0) { col = sheet.getMappedColumn(col); sheet.setSortByValueExpression(sheet.getColumns().get(col).getValueExpression("sortBy")); } } if (sortOrder != null) sheet.setSortOrder(sortOrder); String focus = params.get(clientId + "_focus"); sheet.setFocusId(focus); } /** * Decodes the filter values * @param context * @param sheet */ protected void decodeFilters(FacesContext context, Sheet sheet, Map<String, String> params, String clientId) { int renderIdx = 0; for (Column column : sheet.getColumns()) { if (!column.isRendered()) continue; if (column.getValueExpression("filterBy") != null) { String value = params.get(clientId + "_filter_" + renderIdx); column.setFilterValue(value); } renderIdx++; } } /** * Decodes client behaviors (ajax events). * <p> * @param context * the FacesContext * @param component * the Component being decodes */ protected void decodeBehaviors(FacesContext context, UIComponent component) { // get current behaviors Map<String, List<ClientBehavior>> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors(); // if empty, done if (behaviors.isEmpty()) return; // get the parameter map and the behaviorEvent fired Map<String, String> params = context.getExternalContext().getRequestParameterMap(); String behaviorEvent = params.get("javax.faces.behavior.event"); // if no event, done if (behaviorEvent == null) return; // get behaviors for the event List<ClientBehavior> behaviorsForEvent = behaviors.get(behaviorEvent); if (behaviorsForEvent == null || behaviorsForEvent.isEmpty()) return; // decode event if we are the source String behaviorSource = params.get("javax.faces.source"); String clientId = component.getClientId(); if (behaviorSource != null && clientId.equals(behaviorSource)) { for (ClientBehavior behavior : behaviorsForEvent) { behavior.decode(context, component); } } } /** * Decodes the user Selection JSON data * <p> * @param context * @param sheet * @param jsonSelection */ private void decodeSelection(FacesContext context, Sheet sheet, String jsonSelection) { if (StringUtils.isEmpty(jsonSelection)) return; try { // data comes in: [ [row, col, oldValue, newValue] ... ] JSONArray array = new JSONArray(jsonSelection); sheet.setSelectedRow(array.getInt(0)); sheet.setSelectedColumn(sheet.getMappedColumn(array.getInt(1))); sheet.setSelectedLastRow(array.getInt(2)); sheet.setSelectedLastColumn(array.getInt(3)); sheet.setSelection(jsonSelection); } catch (JSONException e) { LOG.error("Failed parsing Ajax JSON message for cell selection: {}", e.getMessage(), e); } } /** * Converts the JSON data received from the in the request params into our * sumitted values map. The map is cleared first. * <p> * @param jsonData * the submitted JSON data */ private void decodeSubmittedValues(FacesContext context, Sheet sheet, String jsonData) { if (StringUtils.isEmpty(jsonData)) return; try { // data comes in as a JSON Object with named properties for the row // and columns updated // this is so that multiple updates to the same cell overwrite // previous deltas prior to submission // we don't care about the property names, just the values, which // we'll process in turn JSONObject obj = new JSONObject(jsonData); @SuppressWarnings("unchecked") Iterator<String> keys = obj.keys(); while (keys.hasNext()) { final String key = keys.next(); // data comes in: [row, col, oldValue, newValue] JSONArray update = obj.getJSONArray(key); final int row = update.getInt(0); final int col = sheet.getMappedColumn(update.getInt(1)); final String newValue = update.getString(3); sheet.setSubmittedValue(context, row, col, newValue); } } catch (JSONException e) { LOG.error("Failed parsing Ajax JSON message for cell change event: {}", e.getMessage(), e); } } private void decodeComments(FacesContext context, Sheet sheet, String jsonData) { if (StringUtils.isEmpty(jsonData)) return; try { JSONObject obj = new JSONObject(jsonData); @SuppressWarnings("unchecked") Iterator<String> keys = obj.keys(); while (keys.hasNext()) { final String key = keys.next(); JSONArray update = obj.getJSONArray(key); final int row = update.getInt(0); final int col = sheet.getMappedColumn(update.getInt(1)); String comment = null; if (update.length() > 3) { comment = buildComment(update.getJSONArray(3)); } sheet.setComment(context, row, col, comment); } } catch (JSONException e) { LOG.error("Failed parsing Ajax JSON message for cell change event: {}", e.getMessage(), e); } } private String buildComment(JSONArray commentLines) throws JSONException { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < commentLines.length(); i++) { if (i != 0) { stringBuilder.append("\\n"); } stringBuilder.append(commentLines.getString(i)); } return stringBuilder.toString(); } /** * We render the columns (the children). */ @Override public boolean getRendersChildren() { return true; } }