You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StaticSection.java 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui.components.grid;
  17. import java.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.LinkedHashMap;
  23. import java.util.LinkedHashSet;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Map.Entry;
  27. import java.util.Objects;
  28. import java.util.Optional;
  29. import java.util.Set;
  30. import java.util.stream.Collectors;
  31. import java.util.stream.Stream;
  32. import org.jsoup.nodes.Element;
  33. import org.jsoup.select.Elements;
  34. import com.vaadin.shared.ui.grid.GridStaticCellType;
  35. import com.vaadin.shared.ui.grid.SectionState;
  36. import com.vaadin.shared.ui.grid.SectionState.CellState;
  37. import com.vaadin.shared.ui.grid.SectionState.RowState;
  38. import com.vaadin.ui.Component;
  39. import com.vaadin.ui.Grid;
  40. import com.vaadin.ui.Grid.Column;
  41. import com.vaadin.ui.declarative.DesignAttributeHandler;
  42. import com.vaadin.ui.declarative.DesignContext;
  43. import com.vaadin.ui.declarative.DesignException;
  44. import com.vaadin.ui.declarative.DesignFormatter;
  45. /**
  46. * Represents the header or footer section of a Grid.
  47. *
  48. * @author Vaadin Ltd.
  49. *
  50. * @param <ROW>
  51. * the type of the rows in the section
  52. *
  53. * @since 8.0
  54. */
  55. public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>>
  56. implements Serializable {
  57. /**
  58. * Abstract base class for Grid header and footer rows.
  59. *
  60. * @param <CELL>
  61. * the type of the cells in the row
  62. */
  63. public abstract static class StaticRow<CELL extends StaticCell>
  64. implements Serializable {
  65. private final RowState rowState = new RowState();
  66. private final StaticSection<?> section;
  67. private final Map<String, CELL> cells = new LinkedHashMap<>();
  68. /**
  69. * Creates a new row belonging to the given section.
  70. *
  71. * @param section
  72. * the section of the row
  73. */
  74. protected StaticRow(StaticSection<?> section) {
  75. this.section = section;
  76. }
  77. /**
  78. * Creates and returns a new instance of the cell type.
  79. *
  80. * @return the created cell
  81. */
  82. protected abstract CELL createCell();
  83. /**
  84. * Returns the declarative tag name used for the cells in this row.
  85. *
  86. * @return the cell tag name
  87. */
  88. protected abstract String getCellTagName();
  89. /**
  90. * Adds a cell to this section, corresponding to the given user-defined
  91. * column id.
  92. *
  93. * @param columnId
  94. * the id of the column for which to add a cell
  95. */
  96. protected void addCell(String columnId) {
  97. Column<?, ?> column = section.getGrid().getColumn(columnId);
  98. Objects.requireNonNull(column,
  99. "No column matching given identifier");
  100. addCell(column);
  101. }
  102. /**
  103. * Adds a cell to this section for given column.
  104. *
  105. * @param column
  106. * the column for which to add a cell
  107. */
  108. protected void addCell(Column<?, ?> column) {
  109. if (!section.getGrid().getColumns().contains(column)) {
  110. throw new IllegalArgumentException(
  111. "Given column does not exist in this Grid");
  112. }
  113. internalAddCell(section.getInternalIdForColumn(column));
  114. }
  115. /**
  116. * Adds a cell to this section, corresponding to the given internal
  117. * column id.
  118. *
  119. * @param internalId
  120. * the internal id of the column for which to add a cell
  121. */
  122. protected void internalAddCell(String internalId) {
  123. CELL cell = createCell();
  124. cell.setColumnId(internalId);
  125. cells.put(internalId, cell);
  126. rowState.cells.put(internalId, cell.getCellState());
  127. }
  128. /**
  129. * Removes the cell from this section that corresponds to the given
  130. * column id. If there is no such cell, does nothing.
  131. *
  132. * @param columnId
  133. * the id of the column from which to remove the cell
  134. */
  135. protected void removeCell(String columnId) {
  136. CELL cell = cells.remove(columnId);
  137. if (cell != null) {
  138. rowState.cells.remove(columnId);
  139. for (Iterator<Set<String>> iterator = rowState.cellGroups
  140. .values().iterator(); iterator.hasNext();) {
  141. Set<String> group = iterator.next();
  142. group.remove(columnId);
  143. if (group.size() < 2) {
  144. iterator.remove();
  145. }
  146. }
  147. }
  148. }
  149. /**
  150. * Returns the shared state of this row.
  151. *
  152. * @return the row state
  153. */
  154. protected RowState getRowState() {
  155. return rowState;
  156. }
  157. /**
  158. * Returns the cell in this section that corresponds to the given column
  159. * id.
  160. *
  161. * @see Column#setId(String)
  162. *
  163. * @param columnId
  164. * the id of the column
  165. * @return the cell for the given column
  166. *
  167. * @throws IllegalArgumentException
  168. * if no cell was found for the column id
  169. */
  170. public CELL getCell(String columnId) {
  171. Column<?, ?> column = section.getGrid().getColumn(columnId);
  172. Objects.requireNonNull(column,
  173. "No column matching given identifier");
  174. return getCell(column);
  175. }
  176. /**
  177. * Returns the cell in this section that corresponds to the given
  178. * column.
  179. *
  180. * @param column
  181. * the column
  182. * @return the cell for the given column
  183. *
  184. * @throws IllegalArgumentException
  185. * if no cell was found for the column
  186. */
  187. public CELL getCell(Column<?, ?> column) {
  188. return internalGetCell(section.getInternalIdForColumn(column));
  189. }
  190. /**
  191. * Returns the cell in this section that corresponds to the given
  192. * internal column id.
  193. *
  194. * @param internalId
  195. * the internal id of the column
  196. * @return the cell for the given column
  197. *
  198. * @throws IllegalArgumentException
  199. * if no cell was found for the column id
  200. */
  201. protected CELL internalGetCell(String internalId) {
  202. CELL cell = cells.get(internalId);
  203. if (cell == null) {
  204. throw new IllegalArgumentException(
  205. "No cell found for column id " + internalId);
  206. }
  207. return cell;
  208. }
  209. /**
  210. * Reads the declarative design from the given table row element.
  211. *
  212. * @since 7.5.0
  213. * @param trElement
  214. * Element to read design from
  215. * @param designContext
  216. * the design context
  217. * @throws DesignException
  218. * if the given table row contains unexpected children
  219. */
  220. protected void readDesign(Element trElement,
  221. DesignContext designContext) throws DesignException {
  222. Elements cellElements = trElement.children();
  223. for (int i = 0; i < cellElements.size(); i++) {
  224. Element element = cellElements.get(i);
  225. if (!element.tagName().equals(getCellTagName())) {
  226. throw new DesignException(
  227. "Unexpected element in tr while expecting "
  228. + getCellTagName() + ": "
  229. + element.tagName());
  230. }
  231. int colspan = DesignAttributeHandler.readAttribute("colspan",
  232. element.attributes(), 1, int.class);
  233. String columnIdsString = DesignAttributeHandler.readAttribute(
  234. "column-ids", element.attributes(), "", String.class);
  235. if (columnIdsString.trim().isEmpty()) {
  236. throw new DesignException(
  237. "Unexpected 'column-ids' attribute value '"
  238. + columnIdsString
  239. + "'. It cannot be empty and must "
  240. + "be comma separated column identifiers");
  241. }
  242. String[] columnIds = columnIdsString.split(",");
  243. if (columnIds.length != colspan) {
  244. throw new DesignException(
  245. "Unexpected 'colspan' attribute value '" + colspan
  246. + "' whereas there is " + columnIds.length
  247. + " column identifiers specified : '"
  248. + columnIdsString + "'");
  249. }
  250. Stream.of(columnIds).forEach(this::addCell);
  251. Stream<String> idsStream = Stream.of(columnIds);
  252. if (colspan > 1) {
  253. CELL newCell = createCell();
  254. addMergedCell(createCell(),
  255. idsStream.collect(Collectors.toSet()));
  256. newCell.readDesign(element, designContext);
  257. } else {
  258. idsStream.map(this::getCell).forEach(
  259. cell -> cell.readDesign(element, designContext));
  260. }
  261. }
  262. }
  263. /**
  264. * Writes the declarative design to the given table row element.
  265. *
  266. * @since 7.5.0
  267. * @param trElement
  268. * Element to write design to
  269. * @param designContext
  270. * the design context
  271. */
  272. protected void writeDesign(Element trElement,
  273. DesignContext designContext) {
  274. Set<String> visited = new HashSet<>();
  275. for (Entry<String, CELL> entry : cells.entrySet()) {
  276. if (visited.contains(entry.getKey())) {
  277. continue;
  278. }
  279. visited.add(entry.getKey());
  280. Element cellElement = trElement.appendElement(getCellTagName());
  281. Optional<Entry<CellState, Set<String>>> groupCell = getRowState().cellGroups
  282. .entrySet().stream().filter(groupEntry -> groupEntry
  283. .getValue().contains(entry.getKey()))
  284. .findFirst();
  285. Stream<String> columnIds = Stream.of(entry.getKey());
  286. if (groupCell.isPresent()) {
  287. Set<String> orderedSet = new LinkedHashSet<>(
  288. cells.keySet());
  289. orderedSet.retainAll(groupCell.get().getValue());
  290. columnIds = orderedSet.stream();
  291. visited.addAll(orderedSet);
  292. cellElement.attr("colspan", "" + orderedSet.size());
  293. writeCellState(cellElement, designContext,
  294. groupCell.get().getKey());
  295. } else {
  296. writeCellState(cellElement, designContext,
  297. entry.getValue().getCellState());
  298. }
  299. cellElement.attr("column-ids",
  300. columnIds.map(section::getColumnByInternalId)
  301. .map(Column::getId)
  302. .collect(Collectors.joining(",")));
  303. }
  304. }
  305. /**
  306. *
  307. * Writes declarative design for the cell using its {@code state} to the
  308. * given table cell element.
  309. * <p>
  310. * The method is used instead of StaticCell::writeDesign because
  311. * sometimes there is no a reference to the cell which should be written
  312. * (merged cell) but only its state is available (the cell is virtual
  313. * and is not stored).
  314. *
  315. * @param cellElement
  316. * Element to write design to
  317. * @param context
  318. * the design context
  319. * @param state
  320. * a cell state
  321. */
  322. protected void writeCellState(Element cellElement,
  323. DesignContext context, CellState state) {
  324. switch (state.type) {
  325. case TEXT:
  326. cellElement.attr("plain-text", true);
  327. cellElement
  328. .appendText(Optional.ofNullable(state.text).orElse(""));
  329. break;
  330. case HTML:
  331. cellElement.append(Optional.ofNullable(state.html).orElse(""));
  332. break;
  333. case WIDGET:
  334. cellElement.appendChild(
  335. context.createElement((Component) state.connector));
  336. break;
  337. }
  338. }
  339. void detach() {
  340. for (CELL cell : cells.values()) {
  341. cell.detach();
  342. }
  343. }
  344. void checkIfAlreadyMerged(String columnId) {
  345. for (Set<String> cellGroup : getRowState().cellGroups.values()) {
  346. if (cellGroup.contains(columnId)) {
  347. throw new IllegalArgumentException(
  348. "Cell " + columnId + " is already merged");
  349. }
  350. }
  351. if (!cells.containsKey(columnId)) {
  352. throw new IllegalArgumentException(
  353. "Cell " + columnId + " does not exist on this row");
  354. }
  355. }
  356. void addMergedCell(CELL newCell, Set<String> columnGroup) {
  357. rowState.cellGroups.put(newCell.getCellState(), columnGroup);
  358. }
  359. }
  360. /**
  361. * A header or footer cell. Has a simple textual caption.
  362. */
  363. abstract static class StaticCell implements Serializable {
  364. private final CellState cellState = new CellState();
  365. private final StaticRow<?> row;
  366. protected StaticCell(StaticRow<?> row) {
  367. this.row = row;
  368. }
  369. void setColumnId(String id) {
  370. cellState.columnId = id;
  371. }
  372. public String getColumnId() {
  373. return cellState.columnId;
  374. }
  375. /**
  376. * Gets the row where this cell is.
  377. *
  378. * @return row for this cell
  379. */
  380. public StaticRow<?> getRow() {
  381. return row;
  382. }
  383. /**
  384. * Returns the shared state of this cell.
  385. *
  386. * @return the cell state
  387. */
  388. protected CellState getCellState() {
  389. return cellState;
  390. }
  391. /**
  392. * Sets the textual caption of this cell.
  393. *
  394. * @param text
  395. * a plain text caption, not null
  396. */
  397. public void setText(String text) {
  398. Objects.requireNonNull(text, "text cannot be null");
  399. removeComponentIfPresent();
  400. cellState.text = text;
  401. cellState.type = GridStaticCellType.TEXT;
  402. row.section.markAsDirty();
  403. }
  404. /**
  405. * Returns the textual caption of this cell.
  406. *
  407. * @return the plain text caption
  408. */
  409. public String getText() {
  410. return cellState.text;
  411. }
  412. /**
  413. * Returns the HTML content displayed in this cell.
  414. *
  415. * @return the html
  416. *
  417. */
  418. public String getHtml() {
  419. if (cellState.type != GridStaticCellType.HTML) {
  420. throw new IllegalStateException(
  421. "Cannot fetch HTML from a cell with type "
  422. + cellState.type);
  423. }
  424. return cellState.html;
  425. }
  426. /**
  427. * Sets the HTML content displayed in this cell.
  428. *
  429. * @param html
  430. * the html to set, not null
  431. */
  432. public void setHtml(String html) {
  433. Objects.requireNonNull(html, "html cannot be null");
  434. removeComponentIfPresent();
  435. cellState.html = html;
  436. cellState.type = GridStaticCellType.HTML;
  437. row.section.markAsDirty();
  438. }
  439. /**
  440. * Returns the component displayed in this cell.
  441. *
  442. * @return the component
  443. */
  444. public Component getComponent() {
  445. if (cellState.type != GridStaticCellType.WIDGET) {
  446. throw new IllegalStateException(
  447. "Cannot fetch Component from a cell with type "
  448. + cellState.type);
  449. }
  450. return (Component) cellState.connector;
  451. }
  452. /**
  453. * Sets the component displayed in this cell.
  454. *
  455. * @param component
  456. * the component to set, not null
  457. */
  458. public void setComponent(Component component) {
  459. Objects.requireNonNull(component, "component cannot be null");
  460. removeComponentIfPresent();
  461. component.setParent(row.section.getGrid());
  462. cellState.connector = component;
  463. cellState.type = GridStaticCellType.WIDGET;
  464. row.section.markAsDirty();
  465. }
  466. /**
  467. * Returns the type of content stored in this cell.
  468. *
  469. * @return cell content type
  470. */
  471. public GridStaticCellType getCellType() {
  472. return cellState.type;
  473. }
  474. /**
  475. * Reads the declarative design from the given table cell element.
  476. *
  477. * @since 7.5.0
  478. * @param cellElement
  479. * Element to read design from
  480. * @param designContext
  481. * the design context
  482. */
  483. protected void readDesign(Element cellElement,
  484. DesignContext designContext) {
  485. if (!cellElement.hasAttr("plain-text")) {
  486. if (cellElement.children().size() > 0
  487. && cellElement.child(0).tagName().contains("-")) {
  488. setComponent(
  489. designContext.readDesign(cellElement.child(0)));
  490. } else {
  491. setHtml(cellElement.html());
  492. }
  493. } else {
  494. // text – need to unescape HTML entities
  495. setText(DesignFormatter.decodeFromTextNode(cellElement.html()));
  496. }
  497. }
  498. private void removeComponentIfPresent() {
  499. Component component = (Component) cellState.connector;
  500. if (component != null) {
  501. component.setParent(null);
  502. cellState.connector = null;
  503. }
  504. }
  505. void detach() {
  506. removeComponentIfPresent();
  507. }
  508. }
  509. private final List<ROW> rows = new ArrayList<>();
  510. /**
  511. * Creates a new row instance.
  512. *
  513. * @return the new row
  514. */
  515. protected abstract ROW createRow();
  516. /**
  517. * Returns the shared state of this section.
  518. *
  519. * @param markAsDirty
  520. * {@code true} to mark the state as modified, {@code false}
  521. * otherwise
  522. * @return the section state
  523. */
  524. protected abstract SectionState getState(boolean markAsDirty);
  525. protected abstract Grid<?> getGrid();
  526. protected abstract Column<?, ?> getColumnByInternalId(String internalId);
  527. protected abstract String getInternalIdForColumn(Column<?, ?> column);
  528. /**
  529. * Marks the state of this section as modified.
  530. */
  531. protected void markAsDirty() {
  532. getState(true);
  533. }
  534. /**
  535. * Adds a new row at the given index.
  536. *
  537. * @param index
  538. * the index of the new row
  539. * @return the added row
  540. * @throws IndexOutOfBoundsException
  541. * if {@code index < 0 || index > getRowCount()}
  542. */
  543. public ROW addRowAt(int index) {
  544. ROW row = createRow();
  545. rows.add(index, row);
  546. getState(true).rows.add(index, row.getRowState());
  547. getGrid().getColumns().stream().forEach(row::addCell);
  548. return row;
  549. }
  550. /**
  551. * Removes the row at the given index.
  552. *
  553. * @param index
  554. * the index of the row to remove
  555. * @throws IndexOutOfBoundsException
  556. * if {@code index < 0 || index >= getRowCount()}
  557. */
  558. public void removeRow(int index) {
  559. ROW row = rows.remove(index);
  560. row.detach();
  561. getState(true).rows.remove(index);
  562. }
  563. /**
  564. * Removes the given row from this section.
  565. *
  566. * @param row
  567. * the row to remove, not null
  568. * @throws IllegalArgumentException
  569. * if this section does not contain the row
  570. */
  571. public void removeRow(Object row) {
  572. Objects.requireNonNull(row, "row cannot be null");
  573. int index = rows.indexOf(row);
  574. if (index < 0) {
  575. throw new IllegalArgumentException(
  576. "Section does not contain the given row");
  577. }
  578. removeRow(index);
  579. }
  580. /**
  581. * Returns the row at the given index.
  582. *
  583. * @param index
  584. * the index of the row
  585. * @return the row at the index
  586. * @throws IndexOutOfBoundsException
  587. * if {@code index < 0 || index >= getRowCount()}
  588. */
  589. public ROW getRow(int index) {
  590. return rows.get(index);
  591. }
  592. /**
  593. * Returns the number of rows in this section.
  594. *
  595. * @return the number of rows
  596. */
  597. public int getRowCount() {
  598. return rows.size();
  599. }
  600. /**
  601. * Adds a cell corresponding to the given column id to this section.
  602. *
  603. * @param columnId
  604. * the id of the column for which to add a cell
  605. */
  606. public void addColumn(String columnId) {
  607. for (ROW row : rows) {
  608. row.internalAddCell(columnId);
  609. }
  610. }
  611. /**
  612. * Removes the cell corresponding to the given column id.
  613. *
  614. * @param columnId
  615. * the id of the column whose cell to remove
  616. */
  617. public void removeColumn(String columnId) {
  618. for (ROW row : rows) {
  619. row.removeCell(columnId);
  620. }
  621. markAsDirty();
  622. }
  623. /**
  624. * Writes the declarative design to the given table section element.
  625. *
  626. * @param tableSectionElement
  627. * Element to write design to
  628. * @param designContext
  629. * the design context
  630. */
  631. public void writeDesign(Element tableSectionElement,
  632. DesignContext designContext) {
  633. for (ROW row : getRows()) {
  634. Element tr = tableSectionElement.appendElement("tr");
  635. row.writeDesign(tr, designContext);
  636. }
  637. }
  638. /**
  639. * Reads the declarative design from the given table section element.
  640. *
  641. * @since 7.5.0
  642. * @param tableSectionElement
  643. * Element to read design from
  644. * @param designContext
  645. * the design context
  646. * @throws DesignException
  647. * if the table section contains unexpected children
  648. */
  649. public void readDesign(Element tableSectionElement,
  650. DesignContext designContext) throws DesignException {
  651. while (getRowCount() > 0) {
  652. removeRow(0);
  653. }
  654. for (Element row : tableSectionElement.children()) {
  655. if (!row.tagName().equals("tr")) {
  656. throw new DesignException("Unexpected element in "
  657. + tableSectionElement.tagName() + ": " + row.tagName());
  658. }
  659. addRowAt(getRowCount()).readDesign(row, designContext);
  660. }
  661. }
  662. /**
  663. * Returns an unmodifiable list of the rows in this section.
  664. *
  665. * @return the rows in this section
  666. */
  667. protected List<ROW> getRows() {
  668. return Collections.unmodifiableList(rows);
  669. }
  670. }