1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.rendering.internal.macro.chart.source.table

File AbstractTableBlockDataSource.java

 

Coverage histogram

../../../../../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

64
127
13
1
472
265
59
0.46
9.77
13
4.54

Classes

Class Line # Actions
AbstractTableBlockDataSource 58 127 0% 59 22
0.8921568489.2%
 

Contributing tests

This file is covered by 15 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.rendering.internal.macro.chart.source.table;
21   
22    import java.util.HashSet;
23    import java.util.Map;
24    import java.util.Set;
25    import java.util.regex.Matcher;
26    import java.util.regex.Pattern;
27   
28    import javax.inject.Inject;
29    import javax.inject.Named;
30   
31    import org.apache.commons.lang3.StringUtils;
32    import org.apache.commons.lang3.math.NumberUtils;
33    import org.xwiki.chart.dataset.DatasetType;
34    import org.xwiki.rendering.block.TableBlock;
35    import org.xwiki.rendering.block.TableCellBlock;
36    import org.xwiki.rendering.block.TableRowBlock;
37    import org.xwiki.rendering.internal.macro.chart.source.AbstractDataSource;
38    import org.xwiki.rendering.internal.macro.chart.source.SimpleChartModel;
39    import org.xwiki.rendering.macro.MacroExecutionException;
40    import org.xwiki.rendering.renderer.BlockRenderer;
41    import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
42    import org.xwiki.rendering.renderer.printer.WikiPrinter;
43    import org.xwiki.rendering.transformation.MacroTransformationContext;
44   
45    /**
46    * Data source that extracts values from a table (in any syntax that supports tables). For example in xwiki/2.1 syntax:
47    *
48    * <p>
49    * <code><pre>
50    * |= |= column label 1 |= column label 2
51    * | row label 1| 11 | 12
52    * | row label 2| 21 | 22
53    * </pre></code>
54    *
55    * @version $Id: 7a0330d5a48bf815016d343bb8f70049ab793ce5 $
56    * @since 4.2M1
57    */
 
58    public abstract class AbstractTableBlockDataSource extends AbstractDataSource
59    {
60    /**
61    * The number of letters that can be used in the column identifier.
62    */
63    private static final int LETTER_RANGE_LENGTH = 'Z' - 'A' + 1;
64   
65    /**
66    * Identifies the data range to be used for plotting.
67    */
68    private static final String RANGE_PARAM = "range";
69   
70    /**
71    * Indicates how the table values should be mapped to the dataset.
72    */
73    private static final String SERIES_PARAM = "series";
74   
75    /**
76    * The columns value for the series parameter.
77    */
78    private static final String SERIES_COLUMNS = "columns";
79   
80    /**
81    * The rows value for the series parameter.
82    */
83    private static final String SERIES_ROWS = "rows";
84   
85    /**
86    * No-limit indicator symbol in ranges.
87    */
88    private static final String NO_LIMIT_SYMBOL = ".";
89   
90    /**
91    * Pattern matching the cell range.
92    */
93    private static final Pattern RANGE_PATTERN =
94    Pattern.compile("^([A-Z]+|\\.)([0-9]+|\\.)-([A-Z]+|\\.)([0-9]+|\\.)$");
95   
96    /**
97    * The name of the category dataset.
98    */
99    private static final String CATEGORY_DATASET = "category";
100   
101    /**
102    * The name of the time series dataset.
103    */
104    private static final String TIME_SERIES_DATASET = "timeseries";
105   
106    /**
107    * The name of the pie dataset.
108    */
109    private static final String PIE_DATASET = "pie";
110   
111    /**
112    * The default dataset.
113    */
114    private static final String DEFAULT_DATASET = CATEGORY_DATASET;
115   
116    /**
117    * The range parameter.
118    */
119    private String range;
120   
121    /**
122    * The series parameter.
123    */
124    private String series;
125   
126    /**
127    * Used to convert cell blocks in plain text so that it can be converted to numbers.
128    */
129    @Inject
130    @Named("plain/1.0")
131    private BlockRenderer plainTextBlockRenderer;
132   
 
133  21 toggle @Override
134    public void buildDataset(String macroContent, Map<String, String> parameters, MacroTransformationContext context)
135    throws MacroExecutionException
136    {
137  21 validateParameters(parameters);
138   
139  21 TableBlock tableBlock = getTableBlock(macroContent, context);
140   
141  21 int[] dataRange = getDataRange(tableBlock);
142   
143  21 TableDatasetBuilder datasetBuilder;
144  21 setChartModel(new SimpleChartModel());
145   
146  21 switch (getDatasetType()) {
147  7 case CATEGORY:
148  7 datasetBuilder = new TableCategoryDatasetBuilder();
149  7 break;
150  9 case PIE:
151  9 datasetBuilder = new TablePieDatasetBuilder();
152  9 break;
153  5 case TIMETABLE_XY:
154  5 datasetBuilder = new TableTimeTableXYDatasetBuilder();
155  5 break;
156  0 default:
157  0 throw new MacroExecutionException(String.format("Unsupported dataset type [%s]",
158    getDatasetType().getName()));
159    }
160   
161  21 setAxes();
162   
163  21 datasetBuilder.setLocaleConfiguration(getLocaleConfiguration());
164  21 datasetBuilder.setParameters(parameters);
165   
166  21 if (SERIES_COLUMNS.equals(series)) {
167  16 datasetBuilder.setTranspose(true);
168    }
169   
170  21 buildDataset(tableBlock, dataRange, datasetBuilder);
171   
172  21 setDataset(datasetBuilder.getDataset());
173    }
174   
175    /**
176    * @param tableBlock The table block to parse.
177    * @param startRow The first row to include.
178    * @param endRow The last row to include.
179    * @param startColumn The first column to include.
180    * @param datasetBuilder The dataset builder.
181    * @throws MacroExecutionException if there are any errors in the table.
182    */
 
183  21 toggle private void getRowKeys(TableBlock tableBlock, int startRow, int endRow, int startColumn,
184    TableDatasetBuilder datasetBuilder) throws MacroExecutionException
185    {
186   
187  21 datasetBuilder.setNumberOfRows(endRow - startRow + 1);
188   
189  21 if (startColumn > 0) {
190  17 Set<String> rowKeySet = new HashSet<String>();
191  86 for (int i = startRow; i <= endRow; i++) {
192  69 TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(i);
193  69 String key = cellContentAsString((TableCellBlock) tableRow.getChildren().get(startColumn - 1));
194  69 datasetBuilder.setRowHeading(i - startRow, key);
195    }
196    } else {
197  12 for (int i = startRow; i <= endRow; i++) {
198  8 datasetBuilder.setRowHeading(i - startRow, "R" + i);
199    }
200    }
201    }
202   
203    /**
204    * @param tableBlock The table block to parse.
205    * @param startColumn The first column to include.
206    * @param endColumn The last column to include.
207    * @param startRow The first row to include.
208    * @param datasetBuilder The dataset builder.
209    * @throws MacroExecutionException if there are any errors in the table.
210    */
 
211  21 toggle private void getColumnKeys(TableBlock tableBlock, int startColumn, int endColumn, int startRow,
212    TableDatasetBuilder datasetBuilder)
213    throws MacroExecutionException
214    {
215  21 datasetBuilder.setNumberOfColumns(endColumn - startColumn + 1);
216   
217  21 if (startRow > 0) {
218  17 TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(startRow - 1);
219  52 for (int i = startColumn; i <= endColumn; i++) {
220  35 String key = cellContentAsString((TableCellBlock) tableRow.getChildren().get(i));
221  35 datasetBuilder.setColumnHeading(i - startColumn, key);
222    }
223    } else {
224  16 for (int i = startColumn; i <= endColumn; i++) {
225  12 datasetBuilder.setColumnHeading(i - startColumn, "C" + i);
226    }
227    }
228    }
229   
230    /**
231    * Build a category dataset.
232    *
233    * @param tableBlock The table block to parse.
234    * @param dataRange The data range.
235    * @param datasetBuilder The dataset builder.
236    * @throws MacroExecutionException if there are any errors.
237    */
 
238  21 toggle private void buildDataset(TableBlock tableBlock, int[] dataRange,
239    TableDatasetBuilder datasetBuilder) throws MacroExecutionException
240    {
241  21 int startRow = dataRange[0];
242  21 int startColumn = dataRange[1];
243  21 int endRow = dataRange[2];
244  21 int endColumn = dataRange[3];
245   
246  21 if (startRow == 0 && datasetBuilder.forceRowHeadings()) {
247  0 startRow = 1;
248    }
249   
250  21 if (startColumn == 0 && datasetBuilder.forceColumnHeadings()) {
251  2 startColumn = 1;
252    }
253   
254  21 getRowKeys(tableBlock, startRow, endRow, startColumn, datasetBuilder);
255   
256  21 getColumnKeys(tableBlock, startColumn, endColumn, startRow, datasetBuilder);
257   
258  98 for (int i = startRow; i <= endRow; i++) {
259  77 if (i < tableBlock.getChildren().size()) {
260  77 TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(i);
261  255 for (int j = startColumn; j <= endColumn; j++) {
262  178 if (j < tableRow.getChildren().size()) {
263  178 Number value = cellContentAsNumber((TableCellBlock) tableRow.getChildren().get(j));
264  178 datasetBuilder.setValue(i - startRow, j - startColumn, value);
265    } else {
266  0 throw new MacroExecutionException("Data range (columns) overflow.");
267    }
268    }
269    } else {
270  0 throw new MacroExecutionException("Data range (rows) overflow.");
271    }
272    }
273    }
274   
275    /**
276    * Converts a column identifier ('G') into a column index number (6).
277    *
278    * @param identifier the cell identifier, containing a column ([A-Z]{1,2})
279    * @return the column number extracted from the identifier
280    */
 
281  45 toggle protected static Integer getColumnNumberFromIdentifier(String identifier)
282    {
283  45 if (NO_LIMIT_SYMBOL.equals(identifier)) {
284  1 return null;
285    }
286  44 int i = 0;
287  44 int result = -1;
288  44 char j;
289  93 while (i < identifier.length()) {
290  57 j = identifier.charAt(i++);
291  57 if (!Character.isUpperCase(j)) {
292  8 break;
293    }
294  49 result = (result + 1) * LETTER_RANGE_LENGTH + j - 'A';
295    }
296  44 return result;
297    }
298   
299    /**
300    * Converts a row identifier ('6') into a column index number (6).
301    *
302    * @param identifier the cell identifier, containing a column.
303    * @return the column number extracted from the identifier
304    */
 
305  36 toggle private static Integer getRowNumberFromIdentifier(String identifier)
306    {
307  36 if (NO_LIMIT_SYMBOL.equals(identifier)) {
308  7 return null;
309    }
310  29 return Integer.parseInt(identifier) - 1;
311    }
312   
313    /**
314    * Tries to parse the cell content as a number.
315    *
316    * @param cell the {@link TableCellBlock}.
317    * @return cell content parsed as a {@link Number}.
318    * @throws MacroExecutionException if the cell does not represent a number.
319    */
 
320  178 toggle private Number cellContentAsNumber(TableCellBlock cell) throws MacroExecutionException
321    {
322  178 String stringContent = cellContentAsString(cell);
323  178 try {
324  178 return NumberUtils.createNumber(StringUtils.trim(stringContent));
325    } catch (NumberFormatException ex) {
326  0 throw new MacroExecutionException(String.format("Invalid number: [%s].", stringContent));
327    }
328    }
329   
330    /**
331    * Renders the cell content as a string.
332    *
333    * @param cell the {@link TableCellBlock}.
334    * @return cell content rendered as a string.
335    */
 
336  282 toggle private String cellContentAsString(TableCellBlock cell)
337    {
338  282 WikiPrinter printer = new DefaultWikiPrinter();
339  282 this.plainTextBlockRenderer.render(cell.getChildren(), printer);
340  282 return printer.toString();
341    }
342   
343    /**
344    * Parses the range parameter and return an array on the form [startRow, startColumn, endRow, endColumn]. Any
345    * element in the array may be {@code null} to indicate no bound.
346    *
347    * @return An array as described above.
348    * @throws MacroExecutionException if the range parameter cannot be parsed.
349    */
 
350  21 toggle private Integer[] getDataRangeFromParameter()
351    throws MacroExecutionException
352    {
353  21 Integer startColumn = null;
354  21 Integer endColumn = null;
355  21 Integer startRow = null;
356  21 Integer endRow = null;
357   
358  21 if (range != null) {
359   
360  18 Matcher m = RANGE_PATTERN.matcher(range);
361   
362  18 if (!m.matches()) {
363  0 throw new MacroExecutionException(String.format("Invalid range specification: [%s].", range));
364    }
365   
366  18 startColumn = getColumnNumberFromIdentifier(m.group(1));
367  18 startRow = getRowNumberFromIdentifier(m.group(2));
368  18 endColumn = getColumnNumberFromIdentifier(m.group(3));
369  18 endRow = getRowNumberFromIdentifier(m.group(4));
370   
371  18 if (startColumn != null && endColumn != null && startColumn > endColumn) {
372  0 throw new MacroExecutionException(
373    String.format("Invalid data range, end column mustn't come before start column: [%s].", range));
374    }
375   
376  18 if (startRow != null && endRow != null && startRow > endRow) {
377  0 throw new MacroExecutionException(
378    String.format("Invalid data range, end row mustn't come before start row: [%s].", range));
379    }
380    }
381   
382  21 return new Integer[]{startRow, startColumn, endRow, endColumn};
383    }
384   
385    /**
386    * Calculates the data-range that is to be used for plotting the chart.
387    *
388    * @param tableBlock the {@link TableBlock}.
389    * @return an integer array consisting of start-row, start-column, end-row and end-column of the data range.
390    * @throws MacroExecutionException if it's not possible to determine the data range correctly.
391    */
 
392  21 toggle protected int[] getDataRange(TableBlock tableBlock)
393    throws MacroExecutionException
394    {
395   
396  21 Integer[] r = getDataRangeFromParameter();
397   
398  21 int rowCount = tableBlock.getChildren().size();
399  21 if (rowCount > 0) {
400  21 TableRowBlock firstRow = (TableRowBlock) tableBlock.getChildren().get(0);
401  21 int columnCount = firstRow.getChildren().size();
402  21 if (columnCount > 0) {
403  21 return new int[]{r[0] != null ? r[0] : 0,
404  21 r[1] != null ? r[1] : 0,
405  21 r[2] != null ? r[2] : rowCount - 1,
406  21 r[3] != null ? r[3] : columnCount - 1};
407    }
408    }
409   
410  0 throw new MacroExecutionException("Data table is incomplete.");
411    }
412   
 
413  21 toggle @Override
414    public void validateDatasetType() throws MacroExecutionException
415    {
416  21 super.validateDatasetType();
417   
418  21 switch (getDatasetType()) {
419  7 case CATEGORY:
420  7 break;
421  9 case PIE:
422  9 break;
423  5 case TIMETABLE_XY:
424  5 break;
425  0 default:
426  0 throw new MacroExecutionException(
427    String.format("Dataset type [%s] is not supported by the table data source.",
428    getDatasetType().getName()));
429    }
430    }
431   
432    /**
433    * Returns the {@link TableBlock} which contains the data to be plotted.
434    *
435    * @param macroContent macro content.
436    * @param context the macro transformation context, used for example to find out the current document reference
437    * @return the {@link TableBlock} containing the data to be plotted.
438    * @throws MacroExecutionException if it's not possible to locate the {@link TableBlock} specified by the user.
439    */
440    protected abstract TableBlock getTableBlock(String macroContent, MacroTransformationContext context)
441    throws MacroExecutionException;
442   
 
443  114 toggle @Override
444    protected boolean setParameter(String key, String value) throws MacroExecutionException
445    {
446  114 if (RANGE_PARAM.equals(key)) {
447  18 range = value;
448  18 return true;
449    }
450  96 if (SERIES_PARAM.equals(key)) {
451  11 series = value;
452  11 return true;
453    }
454  85 return false;
455    }
456   
 
457  21 toggle @Override
458    protected void validateParameters() throws MacroExecutionException
459    {
460  21 if (series == null) {
461    // Backwards compliancy: The "series" parameter doesn't really make sense for category datasets, but setting
462    // it to "columns" indicates that the table should be transposed. Adding to oddities, it also takes on
463    // different default values depending on the dataset type.
464  10 series = getDatasetType() == DatasetType.CATEGORY ? SERIES_ROWS : SERIES_COLUMNS;
465    } else {
466  11 if (!(SERIES_COLUMNS.equals(series) || SERIES_ROWS.equals(series))) {
467  0 throw new MacroExecutionException(String.format("Unsupported value for parameter [%s]: [%s]",
468    SERIES_PARAM, series));
469    }
470    }
471    }
472    }