/* * Copyright (C) 2014 Civilian Framework. * * Licensed under the Civilian License (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.civilian-framework.org/license.txt * * 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 org.civilian.template.mixin; import java.util.ArrayList; import org.civilian.template.ComponentBuilder; import org.civilian.template.HtmlUtil; import org.civilian.template.TemplateWriter; import org.civilian.util.Check; import org.civilian.util.Scanner; /** * TableMixin can be used to conveniently build complex HTML table output. * It is intended to be used for irregular tables with lot of colspan and rowspan attributes on * single cells. * These are the steps to use it: * <ol> * <li>You define the column layout of the table, by calling {@link #columns(int)}, * {@link #columns(String)} or {@link #columns(Model)}. * <li>You start the table output by calling {@link #startTable()} or {@link #startTable(String...)}. * <li>Then you call the {@link #startCell()} or one of its variants, to start the next table cell, * print the cell content and then call {@link #endCell()} to end the cell. * The TableMixin will keep track of what has been printed before, and automatically inserts * tr-elements. * <li>You can set rowspan and colspan of the next cell. Again TableMixin does the book-keeping for you. * <li>Custom attributes for row and cell elements can be set as default or for the next printed row or cell. * <li>You end printing the table by calling {@link #endTable()} * </ol> * When you define the column model of the table with a {{@link #columns(String) definition string}, * you can set width of columns, gap width, and column attributes.<br> * Example: The definition <code>[50]5[30%, align=left][class="help red"]</code> will define these * columns: * <ol> * <li>a column with width 50px, * <li>a gap column with width 5px * <li>a left aligned column with width 30% * <li>a column with class attribute set to "help red" * </ol> * See {@link #columns(String)} for a complete definition. */ public class TableMixin implements ComponentBuilder { /** * Creates a new TableMixin. */ public TableMixin(TemplateWriter out) { this.out = Check.notNull(out, "out"); } //--------------------------------- // column definition //--------------------------------- /** * Defines the column model which the TableMixin should use. * @return this */ public TableMixin columns(Model model) { if (tableStarted_) throw new IllegalStateException("can't set columns during table outout"); Check.notNull(model, "model"); columns_ = model.columns_; restRowSpans_ = new int[columns_.length]; rowIndex_ = -1; colIndex_ = -1; colspan_ = 1; rowspan_ = 1; cellStarted_ = false; nextCellAttrs_ = null; nextRowAttrs_ = null; return this; } /** * Defines to use a column layout with the given number of columns. * @param count the number of columns, >= 1. * @return this */ public TableMixin columns(int count) { return columns(new Model(count)); } /** * Specifies to use a column layout defined by the string. * The grammar of the definition is as follows: * <pre> * definition := column-list * column-list := column ( gap? column )* * column := '[' column-def ']' * col-def := ( col-def-item ( ',' col-def-item )* )? * col-def-item := col-width | col-attr * col-width = integer '%'? * col-attr = col-attr-name '=' col-attr-value * col-attr-name := [.^=\s]]+ * col-attr-value := simplestring | quotedstring * simplestring := [.^\s] * quotedstring := "'" [.^']+ "'" * gap := integer? * integer := [0-9]+ * </pre> */ public TableMixin columns(String definition) { return columns(new Model(definition)); } /** * Returns the number of columns. */ public int columns() { return columns_.length; } //--------------------------------- // table level methods //--------------------------------- /** * Prints the table start tag. * @return this */ public TableMixin startTable() { return startTable((String[])null); } /** * Prints the table start tag. * @param attrs a list of attribute name-value-pairs to be printed in the start tag, or null. */ public TableMixin startTable(String... attrs) { if (columns_.length == 0) throw new IllegalArgumentException("no columns defined"); tableStarted_ = true; printStartTag("table", attrs, null); return this; } /** * Ends the table. */ public TableMixin endTable() { if (tableStarted_) { tableStarted_ = false; endRow(); out.println("</table>"); } return this; } //--------------------------------- // col groups //--------------------------------- private void printColGroups() { for (int i=0; i<columns_.length; i++) { if (columns_[i].wantsColDefinition()) { out.println("<colgroup>"); out.increaseTab(); for (int j=0; j<columns_.length; j++) columns_[j].writeColDefintion(out); out.decreaseTab(); out.println("</colgroup>"); break; } } } //--------------------------------- // row level methods //--------------------------------- /** * Specifies the default attributes which should be printed on every * opening tr-tag. * @param attrs a list of attribute name-value-pairs, or null. * @return this */ public TableMixin defaultRowAttrs(String... attrs) { defaultRowAttrs_ = attrs; return this; } /** * Specifies the attributes which should be printed on the next * opening tr-tag. These will override the default row attributes. * @param attrs a list of attribute name-value-pairs, or null. * @return this */ public TableMixin rowAttrs(String... attrs) { nextRowAttrs_ = attrs; return this; } private void startRow() { // if we are before the first row, we print the colgroups (if we have one) // we cannot do it directly within startTable() since a table caption may be used if (rowIndex_ < 0) printColGroups(); printStartTag("tr", nextRowAttrs_, defaultRowAttrs_); nextRowAttrs_ = null; out.increaseTab(); rowIndex_++; } /** * Ends the current row. * @param printEmptyCells if true then empty td-cells are printed * for every missing column. * @return this */ public TableMixin endRow(boolean printEmptyCells) { if (colIndex_ >= 0) { if (printEmptyCells) { for (int i=colIndex_ + 1; i<columns_.length; i++) out.println("<td></td>"); } colIndex_ = -1; out.decreaseTab(); out.println("</tr>"); } return this; } /** * Ends the current row. * @return this */ public TableMixin endRow() { return endRow(false); } /** * Returns the current row index. */ public int rowIndex() { return rowIndex_; } /** * Sets the current row index. This lets you correct * the row index if you print a row for yourself. * @return this */ public TableMixin setRowIndex(int index) { rowIndex_ = index; return this; } /** * Increases the current row index by the given amount. * This lets you correct the row index if you print a row for yourself. * @return this */ public TableMixin increaseRowIndex(int amount) { rowIndex_ += amount; return this; } //--------------------------------- // cell level methods //--------------------------------- /** * Specifies that the next cell will span this number of columns. * @param colspan the number of columns to span. Must be >= 1. If it * is greater than the number of available cells, it is * is silently truncated. * @see #startCell(int) * @return this */ public TableMixin colspan(int colspan) { Check.greaterEquals(colspan, 1, "colspan"); colspan_ = colspan; return this; } /** * Specifies that the next cell will span this number of rows. * @param rowspan the number of rows to span. Must be >= 1. * @return this */ public TableMixin rowspan(int rowspan) { Check.greaterEquals(rowspan, 1, "rowspan"); rowspan_ = rowspan; return this; } /** * Specifies colspan and rowspan for the next cell. * @see #colspan(int) * @see #rowspan(int) * @return this */ public TableMixin span(int colspan, int rowspan) { colspan(colspan); return rowspan(rowspan); } /** * Specifies the attributes which should be printed on the next * opening td-tag. These will override the default column attributes. * @param attrs a list of attribute name-value-pairs, or null. * @return this */ public TableMixin attrs(String... attrs) { nextCellAttrs_ = attrs; return this; } /** * Starts a cell which spans the given number of columns. * @return this */ public TableMixin startCell(int colspan) { colspan(colspan); return startCell(); } /** * Starts a cell which spans the given number of columns. * @param attrs a list of attribute name-value-pairs, or null. * @return this */ public TableMixin startCell(String... attrs) { attrs(attrs); return startCell(); } /** * Starts a cell which spans the given number of columns. * @param attrs a list of attribute name-value-pairs, or null. * @return this */ public TableMixin startCell(int colspan, String... attrs) { colspan(colspan); return startCell(attrs); } /** * Starts a new cell. Automatically ends any currently started cell, * ends a row, if it was the last cell in the row, and starts a new * row if needed, and inserts gap cells for columns which are defined * as gap. * @return this */ public TableMixin startCell() { if (cellStarted_) endCell(); while(true) { if (colIndex_ < 0) startRow(); if (colIndex_ == columns_.length - 1) endRow(); if (restRowSpans_[++colIndex_] > 0) { restRowSpans_[colIndex_]--; continue; } Column column = columns_[colIndex_]; if (column.isGap()) { column.startCell(out, null, 1, 1); endCellImpl(); } else { startCell(column); break; } } return this; } private void startCell(Column column) { cellStarted_ = true; // calc colspan int colspan; if (colspan_ > 1) { colspan = calcEffectiveColspan(); colIndex_ += colspan - 1; colspan_ = 1; } else colspan = 1; // calc rowspan int rowspan = rowspan_; if (rowspan_ > 1) { int index = colIndex_; int cols = colspan; int rest = rowspan_ - 1; while ((cols > 0) && (index < columns_.length)) { restRowSpans_[index] += rest; if (!columns_[index++].isGap()) cols--; } rowspan_ = 1; } column.startCell(out, nextCellAttrs_, colspan, rowspan); nextCellAttrs_ = null; } private int calcEffectiveColspan() { int effective = 0; int toSpan = colspan_; int index = colIndex_; while ((toSpan > 0) && (index < columns_.length)) { effective++; if (!columns_[index++].isGap()) toSpan--; } return effective; } /** * Ends the currently started table cell. * The call has no effect if no table cell is started. * @return this */ public TableMixin endCell() { if (cellStarted_) { cellStarted_ = false; endCellImpl(); } return this; } private void endCellImpl() { out.println("</td>"); if (colIndex_ == columns_.length - 1) endRow(); } /** * Starts a new cell, prints the content and then ends the cell. * @return this */ public TableMixin cell(Object content) { startCell(); out.print(content); return endCell(); } /** * Returns the column index of the current cell. */ public int colIndex() { return colIndex_; } /** * Sets the current row index. This lets you correct * the row index if you print a row for yourself. * @return this */ public TableMixin setColIndex(int index) { colIndex_ = index; return this; } /** * Increases the current row index by the given amount. * This lets you correct the row index if you print a row for yourself. * @return this */ public TableMixin increaseColIndex(int amount) { rowIndex_ += amount; return this; } //--------------------------------- // ComponentBuilder //--------------------------------- /** * Implements ComponentBuilder. Calls startCell() */ @Override public void startComponent(boolean multiLine) { startCell(); if (multiLine) out.println(); } /** * Implements ComponentBuilder. Calls endCell() */ @Override public void endComponent(boolean multiLine) { if (multiLine) out.printlnIfNotEmpty(); endCell(); } //--------------------------------- // helper //--------------------------------- private void printStartTag(String tag, String[] attrs1, String[] attrs2) { out.print('<'); out.print(tag); String[] attrs = attrs1 != null ? attrs1 : attrs2; if (attrs != null) HtmlUtil.attrs(out, attrs); out.println('>'); } //--------------------------------- // Model //--------------------------------- /** * Model contains a list of columns, to be used by a TableMixin. */ public static class Model { /** * Creates a Model with the given number of columns. */ public Model(int count) { Check.greaterEquals(count, 1, "count"); columns_ = new Column[count]; for (int i=0; i<count; i++) columns_[i] = new Column(); } /** * Creates a column array from a definition string. */ public Model(String definition) { ArrayList<Column> columns = new ArrayList<>(); Scanner scanner = new Scanner(definition); while(scanner.hasMore()) { Column column; if (scanner.next("[")) column = parseColumn(scanner); else column = parseGap(scanner); if (column != null) columns.add(column); } if (columns.size() == 0) throw new IllegalArgumentException("model '" + definition + "' does not define any columns"); columns_ = columns.toArray(new Column[columns.size()]); } /** * Parse column definition */ private Column parseColumn(Scanner scanner) { Column column = new Column(); ArrayList<String> attrs = null; if (!scanner.next("]")) { while (true) { if (scanner.currentIsDigit()) { int width = scanner.consumeInt(); if (scanner.next("%")) column.setPercentWidth(width); else column.setWidth(width); } else { String name = scanner.consumeToken("]="); if (name == null) scanner.exception("invalid column definition"); scanner.expect("="); String value = null; if (scanner.current() == '\'') value = scanner.consumeQuotedString(); else value = scanner.consumeToken("],"); if (attrs == null) attrs = new ArrayList<>(); attrs.add(name); attrs.add(value); } if (scanner.next("]")) break; scanner.expect(","); } } if (attrs != null) column.setAttrs(attrs.toArray(new String[attrs.size()])); return column; } private Column parseGap(Scanner scanner) { if (scanner.currentIsDigit()) { int width = scanner.consumeInt(); if (width > 0) { Column gap = new Column(); gap.setGap(); gap.setWidth(width); return gap; } } return null; } /** * Returns the number of columns. */ public int getColumnCount() { return columns_.length; } /** * Returns the i-th column. */ public Column getColumn(int i) { return columns_[i]; } /** * Sets column attributes on every column. * @see Column#setAttrs(String...) */ public void setColumnAttrs(String... attrs) { for (int i=0; i<columns_.length; i++) columns_[i].setAttrs(attrs); } private Column[] columns_ = EMPTY_COLUMNS; } //--------------------------------- // Column //--------------------------------- /** * Column describes a table column. */ public static class Column { /** * Is the column a gap (of a certain width, with no content) or * is it a content column. Gap columns are used to build * a vertical spacer between two content columns. */ public boolean isGap() { return isGap_; } /** * Makes the column a gap column with a width of 20 pixels */ public Column setGap() { return setGap(20); } /** * Makes the column a gap columns. * @param width the width of the gap */ public Column setGap(int width) { isGap_ = true; setWidth(width); return this; } /** * Sets the column width. * @param width a string which can be used as value * of a html width attribute. */ public Column setWidth(String width) { width_ = width; return this; } /** * Sets the column width as percentage value. * @param percent a positive value */ public Column setPercentWidth(int percent) { return setWidth(Check.greaterEquals(percent, 1, "percent") + "%"); } /** * Sets the column width in pixels. * @param pixels a positive value */ public Column setWidth(int pixels) { return setWidth(Check.greaterEquals(pixels, 1, "pixels") + "px"); } /** * Returns the column width. */ public String getWidth() { return width_; } /** * Sets the column attributes. * If not null, then these attributes are included when * a opening td-tag is printed. */ public Column setAttrs(String... attrs) { attrs_ = attrs; return this; } /** * Returns the column attributes. */ public String[] getAttrs() { return attrs_; } boolean wantsColDefinition() { return width_ != null; } void writeColDefintion(TemplateWriter out) { out.print("<col"); if (width_ != null) HtmlUtil.attr(out, "width", width_); out.println(">"); } void startCell(TemplateWriter out, String[] cellAttrs, int colspan, int rowspan) { out.print("<td"); if (cellAttrs != null) HtmlUtil.attrs(out, cellAttrs); else { if (attrs_ != null) HtmlUtil.attrs(out, attrs_); } if (rowspan > 1) HtmlUtil.attr(out, "rowspan", rowspan); if (colspan > 1) HtmlUtil.attr(out, "colspan", colspan); out.print('>'); } private boolean isGap_; private String width_; private String[] attrs_; } private TemplateWriter out; private boolean tableStarted_; private String[] defaultRowAttrs_; private String[] nextRowAttrs_; private String[] nextCellAttrs_; private int rowIndex_; private int colIndex_; private int colspan_; private int rowspan_; private boolean cellStarted_; private int restRowSpans_[] = EMPTY_ROWSPANS; private Column[] columns_ = EMPTY_COLUMNS; private static final Column[] EMPTY_COLUMNS = new Column[0]; private static final int[] EMPTY_ROWSPANS = new int[0]; }