View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.api.markup.asciidoc;
18  
19  import static org.forgerock.api.markup.asciidoc.AsciiDocSymbols.*;
20  import static org.forgerock.api.util.ValidationUtil.isEmpty;
21  
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  import org.forgerock.util.Reject;
28  
29  /**
30   * AsciiDoc table builder [<a href="http://asciidoctor.org/docs/user-manual/#tables">ref</a>], which defers insertion
31   * of the table, at the end of the parent document, until {@link #tableEnd()} is called.
32   * <p>
33   * This class is not thread-safe.
34   * </p>
35   */
36  public class AsciiDocTable {
37  
38      /**
39       * <em>Small</em> column-width for use with {@link #columnWidths(int...)}.
40       */
41      public static final int COLUMN_WIDTH_SMALL = 1;
42  
43      /**
44       * <em>Medium</em> column-width (2x {@link #COLUMN_WIDTH_SMALL}) for use with {@link #columnWidths(int...)}.
45       */
46      public static final int COLUMN_WIDTH_MEDIUM = 2;
47  
48      private static final Pattern TABLE_CELL_SYMBOL_PATTERN = Pattern.compile("\\|");
49  
50      private final AsciiDoc asciiDoc;
51      private final StringBuilder builder;
52      private final List<String> cells;
53      private int[] columnWidths;
54      private Integer columnsPerRow;
55      private String title;
56      private boolean hasHeader;
57  
58      AsciiDocTable(final AsciiDoc asciiDoc, final StringBuilder builder) {
59          this.asciiDoc = Reject.checkNotNull(asciiDoc);
60          this.builder = Reject.checkNotNull(builder);
61          cells = new ArrayList<>();
62      }
63  
64      /**
65       * Sets a table-title.
66       *
67       * @param title Table-title
68       * @return Table builder
69       */
70      public AsciiDocTable title(final String title) {
71          if (isEmpty(title)) {
72              throw new AsciiDocException("title required");
73          }
74          if (this.title != null) {
75              throw new AsciiDocException("title already defined");
76          }
77          this.title = title;
78          return this;
79      }
80  
81      /**
82       * Sets the column headers, where blank entries can be null/empty, but the length of the headers array must
83       * be equal to the number of columns in the table.
84       *
85       * @param columnHeaders Column headers
86       * @return Table builder
87       */
88      public AsciiDocTable headers(final List<String> columnHeaders) {
89          return headers(columnHeaders.toArray(new String[columnHeaders.size()]));
90      }
91  
92      /**
93       * Sets the column headers, where blank entries can be null/empty, but the length of the headers array must
94       * be equal to the number of columns in the table.
95       *
96       * @param columnHeaders Column headers
97       * @return Table builder
98       */
99      public AsciiDocTable headers(final String... columnHeaders) {
100         if (isEmpty(columnHeaders)) {
101             throw new AsciiDocException("columnHeaders required");
102         }
103         if (hasHeader) {
104             throw new AsciiDocException("headers already defined");
105         }
106         if (columnsPerRow == null) {
107             columnsPerRow = columnHeaders.length;
108         } else if (columnsPerRow != columnHeaders.length) {
109             throw new AsciiDocException("columnHeaders.length != columnsPerRow");
110         }
111         hasHeader = true;
112 
113         // add to front of cells
114         cells.add(null);
115         for (int i = columnHeaders.length - 1; i > -1; --i) {
116             cells.add(0, TABLE_CELL + normalizeColumnCell(columnHeaders[i]));
117         }
118         return this;
119     }
120 
121     /**
122      * Sets number of columns per row, which is implicitly set by {@link #headers(String...)} and
123      * {@link #columnWidths(int...)}.
124      * <p>
125      * This value can only be set once.
126      * </p>
127      *
128      * @param columnsPerRow Columns per row
129      * @return Table builder
130      */
131     public AsciiDocTable columnsPerRow(final int columnsPerRow) {
132         if (this.columnsPerRow != null) {
133             throw new AsciiDocException("columnsPerRow already defined");
134         }
135         if (this.columnsPerRow < 1) {
136             throw new AsciiDocException("columnsPerRow < 1");
137         }
138         this.columnsPerRow = columnsPerRow;
139         return this;
140     }
141 
142     /**
143      * Sets the widths for all columns-per-row, which can be a proportional integer (the default is 1) or a
144      * percentage (1 to 99).
145      *
146      * @param columnWidths An entry for each column-per row in value range [1,99]
147      * @return Table builder
148      */
149     public AsciiDocTable columnWidths(final List<Integer> columnWidths) {
150         final int[] array = new int[columnWidths.size()];
151         for (int i = 0; i < array.length; ++i) {
152             array[i] = columnWidths.get(i);
153         }
154         return columnWidths(array);
155     }
156 
157     /**
158      * Sets the widths for all columns-per-row, which can be a proportional integer (the default is 1) or a
159      * percentage (1 to 99).
160      *
161      * @param columnWidths An entry for each column-per row in value range [1,99]
162      * @return Table builder
163      */
164     public AsciiDocTable columnWidths(final int... columnWidths) {
165         if (columnWidths == null || columnWidths.length == 0) {
166             throw new AsciiDocException("columnWidths required");
167         }
168         for (final int w : columnWidths) {
169             if (w < 1 || w > 99) {
170                 throw new AsciiDocException("columnWidths values must be within range [1,99]");
171             }
172         }
173         if (columnsPerRow != null) {
174             if (columnsPerRow != columnWidths.length) {
175                 throw new AsciiDocException("columnWidths.length != columnsPerRow");
176             }
177         } else {
178             columnsPerRow = columnWidths.length;
179         }
180         this.columnWidths = columnWidths;
181         return this;
182     }
183 
184     /**
185      * Inserts a column-cell.
186      *
187      * @param columnCell Column-cell or {@code null} for empty cell
188      * @return Table builder
189      */
190     public AsciiDocTable columnCell(final String columnCell) {
191         cells.add(TABLE_CELL + normalizeColumnCell(columnCell));
192         return this;
193     }
194 
195     /**
196      * Inserts a column-cell, with a style.
197      *
198      * @param columnCell Column-cell or {@code null} for empty cell
199      * @param style Column-style
200      * @return Table builder
201      */
202     public AsciiDocTable columnCell(final String columnCell, final AsciiDocTableColumnStyles style) {
203         cells.add(style.toString() + TABLE_CELL + normalizeColumnCell(columnCell));
204         return this;
205     }
206 
207     /**
208      * Adds an optional space to visually delineate the end of a row in the generated markup. The intention is that
209      * this method would be called after adding all columns for a given row.
210      *
211      * @return table builder
212      */
213     public AsciiDocTable rowEnd() {
214         cells.add(null);
215         return this;
216     }
217 
218     private String normalizeColumnCell(final String columnCell) {
219         if (isEmpty(columnCell)) {
220             // allow for empty cells
221             return "";
222         }
223         // escape TABLE_CELL symbols
224         final Matcher m = TABLE_CELL_SYMBOL_PATTERN.matcher(columnCell);
225         return m.find() ? m.replaceAll("\\" + TABLE_CELL) : columnCell;
226     }
227 
228     /**
229      * Completes the table being built, and inserts it at the end of the parent document.
230      *
231      * @return Doc builder
232      */
233     public AsciiDoc tableEnd() {
234         if (columnsPerRow == null) {
235             throw new AsciiDocException("columnsPerRow has not be defined");
236         }
237 
238         // table configuration (e.g., [cols="2*", caption="", options="header"])
239         builder.append("[cols=\"");
240         if (columnWidths != null) {
241             // unique column widths
242             builder.append(columnWidths[0]);
243             for (int i = 1; i < columnWidths.length; ++i) {
244                 builder.append(',').append(columnWidths[i]);
245             }
246         } else {
247             // each column same width
248             builder.append(columnsPerRow).append("*");
249         }
250         builder.append("\", caption=\"\", options=\"");
251         if (hasHeader) {
252             builder.append("header");
253         }
254         builder.append("\"]").append(NEWLINE);
255 
256         // optional title
257         if (title != null) {
258             builder.append(".").append(title).append(NEWLINE);
259         }
260 
261         // cells
262         builder.append(TABLE).append(NEWLINE);
263         if (cells.get(cells.size() - 1) == null) {
264             // remove trailing "row spacer"
265             cells.remove(cells.size() - 1);
266         }
267         for (final String item : cells) {
268             if (item != null) {
269                 // null is an optional "row spacer" (see endRow), otherwise cells will be non-null
270                 builder.append(item);
271             }
272             builder.append(NEWLINE);
273         }
274         builder.append(TABLE).append(NEWLINE);
275 
276         return asciiDoc;
277     }
278 
279 }