/* * Copyright 2000-2016 Vaadin Ltd. * * 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 com.vaadin.ui.components.grid; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.shared.ui.grid.SectionState.CellState; import com.vaadin.shared.ui.grid.SectionState.RowState; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.Column; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.DesignException; import com.vaadin.ui.declarative.DesignFormatter; /** * Represents the header or footer section of a Grid. * * @author Vaadin Ltd. * * @param <ROW> * the type of the rows in the section * * @since 8.0 */ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> implements Serializable { /** * Abstract base class for Grid header and footer rows. * * @param <CELL> * the type of the cells in the row */ public abstract static class StaticRow<CELL extends StaticCell> implements Serializable { private final RowState rowState = new RowState(); private final StaticSection<?> section; private final Map<String, CELL> cells = new LinkedHashMap<>(); /** * Creates a new row belonging to the given section. * * @param section * the section of the row */ protected StaticRow(StaticSection<?> section) { this.section = section; } /** * Creates and returns a new instance of the cell type. * * @return the created cell */ protected abstract CELL createCell(); /** * Returns the declarative tag name used for the cells in this row. * * @return the cell tag name */ protected abstract String getCellTagName(); /** * Adds a cell to this section, corresponding to the given user-defined * column id. * * @param columnId * the id of the column for which to add a cell */ protected void addCell(String columnId) { Column<?, ?> column = section.getGrid().getColumn(columnId); Objects.requireNonNull(column, "No column matching given identifier"); addCell(column); } /** * Adds a cell to this section for given column. * * @param column * the column for which to add a cell */ protected void addCell(Column<?, ?> column) { if (!section.getGrid().getColumns().contains(column)) { throw new IllegalArgumentException( "Given column does not exist in this Grid"); } internalAddCell(section.getInternalIdForColumn(column)); } /** * Adds a cell to this section, corresponding to the given internal * column id. * * @param internalId * the internal id of the column for which to add a cell */ protected void internalAddCell(String internalId) { CELL cell = createCell(); cell.setColumnId(internalId); cells.put(internalId, cell); rowState.cells.put(internalId, cell.getCellState()); } /** * Removes the cell from this section that corresponds to the given * column id. If there is no such cell, does nothing. * * @param columnId * the id of the column from which to remove the cell */ protected void removeCell(String columnId) { CELL cell = cells.remove(columnId); if (cell != null) { rowState.cells.remove(columnId); for (Iterator<Set<String>> iterator = rowState.cellGroups .values().iterator(); iterator.hasNext();) { Set<String> group = iterator.next(); group.remove(columnId); if (group.size() < 2) { iterator.remove(); } } cell.detach(); } } /** * Returns the shared state of this row. * * @return the row state */ protected RowState getRowState() { return rowState; } /** * Returns the cell in this section that corresponds to the given column * id. * * @see Column#setId(String) * * @param columnId * the id of the column * @return the cell for the given column * * @throws IllegalArgumentException * if no cell was found for the column id */ public CELL getCell(String columnId) { Column<?, ?> column = section.getGrid().getColumn(columnId); Objects.requireNonNull(column, "No column matching given identifier"); return getCell(column); } /** * Returns the cell in this section that corresponds to the given * column. * * @param column * the column * @return the cell for the given column * * @throws IllegalArgumentException * if no cell was found for the column */ public CELL getCell(Column<?, ?> column) { return internalGetCell(section.getInternalIdForColumn(column)); } /** * Returns the custom style name for this row. * * @return the style name or null if no style name has been set */ public String getStyleName() { return getRowState().styleName; } /** * Sets a custom style name for this row. * * @param styleName * the style name to set or null to not use any style name */ public void setStyleName(String styleName) { getRowState().styleName = styleName; } /** * Returns the cell in this section that corresponds to the given * internal column id. * * @param internalId * the internal id of the column * @return the cell for the given column * * @throws IllegalArgumentException * if no cell was found for the column id */ protected CELL internalGetCell(String internalId) { CELL cell = cells.get(internalId); if (cell == null) { throw new IllegalArgumentException( "No cell found for column id " + internalId); } return cell; } /** * Reads the declarative design from the given table row element. * * @since 7.5.0 * @param trElement * Element to read design from * @param designContext * the design context * @throws DesignException * if the given table row contains unexpected children */ protected void readDesign(Element trElement, DesignContext designContext) throws DesignException { Elements cellElements = trElement.children(); for (int i = 0; i < cellElements.size(); i++) { Element element = cellElements.get(i); if (!element.tagName().equals(getCellTagName())) { throw new DesignException( "Unexpected element in tr while expecting " + getCellTagName() + ": " + element.tagName()); } int colspan = DesignAttributeHandler.readAttribute("colspan", element.attributes(), 1, int.class); String columnIdsString = DesignAttributeHandler.readAttribute( "column-ids", element.attributes(), "", String.class); if (columnIdsString.trim().isEmpty()) { throw new DesignException( "Unexpected 'column-ids' attribute value '" + columnIdsString + "'. It cannot be empty and must " + "be comma separated column identifiers"); } String[] columnIds = columnIdsString.split(","); if (columnIds.length != colspan) { throw new DesignException( "Unexpected 'colspan' attribute value '" + colspan + "' whereas there is " + columnIds.length + " column identifiers specified : '" + columnIdsString + "'"); } Stream.of(columnIds).forEach(this::addCell); Stream<String> idsStream = Stream.of(columnIds); if (colspan > 1) { CELL newCell = createCell(); addMergedCell(createCell(), idsStream.collect(Collectors.toSet())); newCell.readDesign(element, designContext); } else { idsStream.map(this::getCell).forEach( cell -> cell.readDesign(element, designContext)); } } } /** * Writes the declarative design to the given table row element. * * @since 7.5.0 * @param trElement * Element to write design to * @param designContext * the design context */ protected void writeDesign(Element trElement, DesignContext designContext) { Set<String> visited = new HashSet<>(); for (Entry<String, CELL> entry : cells.entrySet()) { if (visited.contains(entry.getKey())) { continue; } visited.add(entry.getKey()); Element cellElement = trElement.appendElement(getCellTagName()); Optional<Entry<CellState, Set<String>>> groupCell = getRowState().cellGroups .entrySet().stream().filter(groupEntry -> groupEntry .getValue().contains(entry.getKey())) .findFirst(); Stream<String> columnIds = Stream.of(entry.getKey()); if (groupCell.isPresent()) { Set<String> orderedSet = new LinkedHashSet<>( cells.keySet()); orderedSet.retainAll(groupCell.get().getValue()); columnIds = orderedSet.stream(); visited.addAll(orderedSet); cellElement.attr("colspan", "" + orderedSet.size()); writeCellState(cellElement, designContext, groupCell.get().getKey()); } else { writeCellState(cellElement, designContext, entry.getValue().getCellState()); } cellElement.attr("column-ids", columnIds.map(section::getColumnByInternalId) .map(Column::getId) .collect(Collectors.joining(","))); } } /** * * Writes declarative design for the cell using its {@code state} to the * given table cell element. * <p> * The method is used instead of StaticCell::writeDesign because * sometimes there is no a reference to the cell which should be written * (merged cell) but only its state is available (the cell is virtual * and is not stored). * * @param cellElement * Element to write design to * @param context * the design context * @param state * a cell state */ protected void writeCellState(Element cellElement, DesignContext context, CellState state) { switch (state.type) { case TEXT: cellElement.attr("plain-text", true); cellElement .appendText(Optional.ofNullable(state.text).orElse("")); break; case HTML: cellElement.append(Optional.ofNullable(state.html).orElse("")); break; case WIDGET: cellElement.appendChild( context.createElement((Component) state.connector)); break; } } void detach() { for (CELL cell : cells.values()) { cell.detach(); } for (CellState cellState : rowState.cellGroups.keySet()) { if (cellState.type == GridStaticCellType.WIDGET && cellState.connector != null) { ((Component) cellState.connector).setParent(null); cellState.connector = null; } } } void checkIfAlreadyMerged(String columnId) { for (Set<String> cellGroup : getRowState().cellGroups.values()) { if (cellGroup.contains(columnId)) { throw new IllegalArgumentException( "Cell " + columnId + " is already merged"); } } if (!cells.containsKey(columnId)) { throw new IllegalArgumentException( "Cell " + columnId + " does not exist on this row"); } } void addMergedCell(CELL newCell, Set<String> columnGroup) { rowState.cellGroups.put(newCell.getCellState(), columnGroup); } public Collection<? extends Component> getComponents() { List<Component> components = new ArrayList<>(); cells.forEach((id, cell) -> { if (cell.getCellType() == GridStaticCellType.WIDGET) { components.add(cell.getComponent()); } }); rowState.cellGroups.forEach((cellState, columnIds) -> { if (cellState.connector != null) { components.add((Component) cellState.connector); } }); return components; } } /** * A header or footer cell. Has a simple textual caption. */ abstract static class StaticCell implements Serializable { private final CellState cellState = new CellState(); private final StaticRow<?> row; protected StaticCell(StaticRow<?> row) { this.row = row; } void setColumnId(String id) { cellState.columnId = id; } public String getColumnId() { return cellState.columnId; } /** * Gets the row where this cell is. * * @return row for this cell */ public StaticRow<?> getRow() { return row; } /** * Returns the shared state of this cell. * * @return the cell state */ protected CellState getCellState() { return cellState; } /** * Sets the textual caption of this cell. * * @param text * a plain text caption, not null */ public void setText(String text) { Objects.requireNonNull(text, "text cannot be null"); removeComponentIfPresent(); cellState.text = text; cellState.type = GridStaticCellType.TEXT; row.section.markAsDirty(); } /** * Returns the textual caption of this cell. * * @return the plain text caption */ public String getText() { return cellState.text; } /** * Returns the HTML content displayed in this cell. * * @return the html * */ public String getHtml() { if (cellState.type != GridStaticCellType.HTML) { throw new IllegalStateException( "Cannot fetch HTML from a cell with type " + cellState.type); } return cellState.html; } /** * Sets the HTML content displayed in this cell. * * @param html * the html to set, not null */ public void setHtml(String html) { Objects.requireNonNull(html, "html cannot be null"); removeComponentIfPresent(); cellState.html = html; cellState.type = GridStaticCellType.HTML; row.section.markAsDirty(); } /** * Returns the component displayed in this cell. * * @return the component */ public Component getComponent() { if (cellState.type != GridStaticCellType.WIDGET) { throw new IllegalStateException( "Cannot fetch Component from a cell with type " + cellState.type); } return (Component) cellState.connector; } /** * Sets the component displayed in this cell. * * @param component * the component to set, not null */ public void setComponent(Component component) { Objects.requireNonNull(component, "component cannot be null"); removeComponentIfPresent(); component.setParent(row.section.getGrid()); cellState.connector = component; cellState.type = GridStaticCellType.WIDGET; row.section.markAsDirty(); } /** * Returns the type of content stored in this cell. * * @return cell content type */ public GridStaticCellType getCellType() { return cellState.type; } /** * Returns the custom style name for this cell. * * @return the style name or null if no style name has been set */ public String getStyleName() { return cellState.styleName; } /** * Sets a custom style name for this cell. * * @param styleName * the style name to set or null to not use any style name */ public void setStyleName(String styleName) { cellState.styleName = styleName; row.section.markAsDirty(); } /** * Reads the declarative design from the given table cell element. * * @since 7.5.0 * @param cellElement * Element to read design from * @param designContext * the design context */ protected void readDesign(Element cellElement, DesignContext designContext) { if (!cellElement.hasAttr("plain-text")) { if (cellElement.children().size() > 0 && cellElement.child(0).tagName().contains("-")) { setComponent( designContext.readDesign(cellElement.child(0))); } else { setHtml(cellElement.html()); } } else { // text – need to unescape HTML entities setText(DesignFormatter.decodeFromTextNode(cellElement.html())); } } private void removeComponentIfPresent() { Component component = (Component) cellState.connector; if (component != null) { component.setParent(null); cellState.connector = null; } } void detach() { removeComponentIfPresent(); } } private final List<ROW> rows = new ArrayList<>(); /** * Creates a new row instance. * * @return the new row */ protected abstract ROW createRow(); /** * Returns the shared state of this section. * * @param markAsDirty * {@code true} to mark the state as modified, {@code false} * otherwise * @return the section state */ protected abstract SectionState getState(boolean markAsDirty); protected abstract Grid<?> getGrid(); protected abstract Column<?, ?> getColumnByInternalId(String internalId); protected abstract String getInternalIdForColumn(Column<?, ?> column); /** * Marks the state of this section as modified. */ protected void markAsDirty() { getState(true); } /** * Adds a new row at the given index. * * @param index * the index of the new row * @return the added row * @throws IndexOutOfBoundsException * if {@code index < 0 || index > getRowCount()} */ public ROW addRowAt(int index) { ROW row = createRow(); rows.add(index, row); getState(true).rows.add(index, row.getRowState()); getGrid().getColumns().stream().forEach(row::addCell); return row; } /** * Removes the row at the given index. * * @param index * the index of the row to remove * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getRowCount()} */ public void removeRow(int index) { ROW row = rows.remove(index); row.detach(); getState(true).rows.remove(index); } /** * Removes the given row from this section. * * @param row * the row to remove, not null * @throws IllegalArgumentException * if this section does not contain the row */ public void removeRow(Object row) { Objects.requireNonNull(row, "row cannot be null"); int index = rows.indexOf(row); if (index < 0) { throw new IllegalArgumentException( "Section does not contain the given row"); } removeRow(index); } /** * Returns the row at the given index. * * @param index * the index of the row * @return the row at the index * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getRowCount()} */ public ROW getRow(int index) { return rows.get(index); } /** * Returns the number of rows in this section. * * @return the number of rows */ public int getRowCount() { return rows.size(); } /** * Adds a cell corresponding to the given column id to this section. * * @param columnId * the id of the column for which to add a cell */ public void addColumn(String columnId) { for (ROW row : rows) { row.internalAddCell(columnId); } } /** * Removes the cell corresponding to the given column id. * * @param columnId * the id of the column whose cell to remove */ public void removeColumn(String columnId) { for (ROW row : rows) { row.removeCell(columnId); } markAsDirty(); } /** * Writes the declarative design to the given table section element. * * @param tableSectionElement * Element to write design to * @param designContext * the design context */ public void writeDesign(Element tableSectionElement, DesignContext designContext) { for (ROW row : getRows()) { Element tr = tableSectionElement.appendElement("tr"); row.writeDesign(tr, designContext); } } /** * Reads the declarative design from the given table section element. * * @since 7.5.0 * @param tableSectionElement * Element to read design from * @param designContext * the design context * @throws DesignException * if the table section contains unexpected children */ public void readDesign(Element tableSectionElement, DesignContext designContext) throws DesignException { while (getRowCount() > 0) { removeRow(0); } for (Element row : tableSectionElement.children()) { if (!row.tagName().equals("tr")) { throw new DesignException("Unexpected element in " + tableSectionElement.tagName() + ": " + row.tagName()); } addRowAt(getRowCount()).readDesign(row, designContext); } } /** * Returns an unmodifiable list of the rows in this section. * * @return the rows in this section */ protected List<ROW> getRows() { return Collections.unmodifiableList(rows); } }