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.

ConditionalFormattingEvaluator.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.formula;
  16. import java.util.ArrayList;
  17. import java.util.Collections;
  18. import java.util.HashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import org.apache.poi.ss.usermodel.Cell;
  22. import org.apache.poi.ss.usermodel.ConditionalFormatting;
  23. import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
  24. import org.apache.poi.ss.usermodel.Row;
  25. import org.apache.poi.ss.usermodel.Sheet;
  26. import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
  27. import org.apache.poi.ss.usermodel.Workbook;
  28. import org.apache.poi.ss.util.CellRangeAddress;
  29. import org.apache.poi.ss.util.CellRangeAddressBase;
  30. import org.apache.poi.ss.util.CellReference;
  31. import org.apache.poi.ss.util.SheetUtil;
  32. /**
  33. * Evaluates Conditional Formatting constraints.<p/>
  34. *
  35. * For performance reasons, this class keeps a cache of all previously evaluated rules and cells.
  36. * Be sure to call {@link #clearAllCachedFormats()} if any conditional formats are modified, added, or deleted,
  37. * and {@link #clearAllCachedValues()} whenever cell values change.
  38. * <p/>
  39. *
  40. */
  41. public class ConditionalFormattingEvaluator {
  42. private final WorkbookEvaluator workbookEvaluator;
  43. private final Workbook workbook;
  44. /**
  45. * All the underlying structures, for both HSSF and XSSF, repeatedly go to the raw bytes/XML for the
  46. * different pieces used in the ConditionalFormatting* structures. That's highly inefficient,
  47. * and can cause significant lag when checking formats for large workbooks.
  48. * <p/>
  49. * Instead we need a cached version that is discarded when definitions change.
  50. * <p/>
  51. * Sheets don't implement equals, and since its an interface,
  52. * there's no guarantee instances won't be recreated on the fly by some implementation.
  53. * So we use sheet name.
  54. */
  55. private final Map<String, List<EvaluationConditionalFormatRule>> formats = new HashMap<String, List<EvaluationConditionalFormatRule>>();
  56. /**
  57. * Evaluating rules for cells in their region(s) is expensive, so we want to cache them,
  58. * and empty/reevaluate the cache when values change.
  59. * <p/>
  60. * Rule lists are in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF)
  61. * <p/>
  62. * CellReference implements equals().
  63. */
  64. private final Map<CellReference, List<EvaluationConditionalFormatRule>> values = new HashMap<CellReference, List<EvaluationConditionalFormatRule>>();
  65. public ConditionalFormattingEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
  66. this.workbook = wb;
  67. this.workbookEvaluator = provider._getWorkbookEvaluator();
  68. }
  69. protected WorkbookEvaluator getWorkbookEvaluator() {
  70. return workbookEvaluator;
  71. }
  72. /**
  73. * Call this whenever rules are added, reordered, or removed, or a rule formula is changed
  74. * (not the formula inputs but the formula expression itself)
  75. */
  76. public void clearAllCachedFormats() {
  77. formats.clear();
  78. }
  79. /**
  80. * Call this whenever cell values change in the workbook, so condional formats are re-evaluated
  81. * for all cells.
  82. * <p/>
  83. * TODO: eventually this should work like {@link EvaluationCache#notifyUpdateCell(int, int, EvaluationCell)}
  84. * and only clear values that need recalculation based on the formula dependency tree.
  85. */
  86. public void clearAllCachedValues() {
  87. values.clear();
  88. }
  89. /**
  90. * lazy load by sheet since reading can be expensive
  91. *
  92. * @param sheet
  93. * @return unmodifiable list of rules
  94. */
  95. protected List<EvaluationConditionalFormatRule> getRules(Sheet sheet) {
  96. final String sheetName = sheet.getSheetName();
  97. List<EvaluationConditionalFormatRule> rules = formats.get(sheetName);
  98. if (rules == null && ! formats.containsKey(sheetName)) {
  99. final SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
  100. final int count = scf.getNumConditionalFormattings();
  101. rules = new ArrayList<EvaluationConditionalFormatRule>(count);
  102. formats.put(sheetName, rules);
  103. for (int i=0; i < count; i++) {
  104. ConditionalFormatting f = scf.getConditionalFormattingAt(i);
  105. //optimization, as this may be expensive for lots of ranges
  106. final CellRangeAddress[] regions = f.getFormattingRanges();
  107. for (int r=0; r < f.getNumberOfRules(); r++) {
  108. ConditionalFormattingRule rule = f.getRule(r);
  109. rules.add(new EvaluationConditionalFormatRule(workbookEvaluator, sheet, f, i, rule, r, regions));
  110. }
  111. }
  112. // need them in formatting and priority order so logic works right
  113. Collections.sort(rules);
  114. }
  115. return Collections.unmodifiableList(rules);
  116. }
  117. /**
  118. * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
  119. * in defined "priority" order, returning the matches if any. This is a property currently
  120. * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
  121. * <p/>
  122. * Most cells will have zero or one applied rule, but it is possible to define multiple rules
  123. * that apply at the same time to the same cell, thus the List result.
  124. * <p/>
  125. * Note that to properly apply conditional rules, care must be taken to offset the base
  126. * formula by the relative position of the current cell, or the wrong value is checked.
  127. * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
  128. *
  129. * @param cell NOTE: if no sheet name is specified, this uses the workbook active sheet
  130. * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
  131. * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
  132. * or null if none apply
  133. */
  134. public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(final CellReference cellRef) {
  135. String sheetName = cellRef.getSheetName();
  136. Sheet sheet = null;
  137. if (sheetName == null) sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
  138. else sheet = workbook.getSheet(sheetName);
  139. final Cell cell = SheetUtil.getCell(sheet, cellRef.getRow(), cellRef.getCol());
  140. if (cell == null) return Collections.emptyList();
  141. return getConditionalFormattingForCell(cell, cellRef);
  142. }
  143. /**
  144. * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
  145. * in defined "priority" order, returning the matches if any. This is a property currently
  146. * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
  147. * <p/>
  148. * Most cells will have zero or one applied rule, but it is possible to define multiple rules
  149. * that apply at the same time to the same cell, thus the List result.
  150. * <p/>
  151. * Note that to properly apply conditional rules, care must be taken to offset the base
  152. * formula by the relative position of the current cell, or the wrong value is checked.
  153. * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
  154. *
  155. * @param cell
  156. * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
  157. * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
  158. * or null if none apply
  159. */
  160. public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell) {
  161. return getConditionalFormattingForCell(cell, getRef(cell));
  162. }
  163. /**
  164. * We need both, and can derive one from the other, but this is to avoid duplicate work
  165. *
  166. * @param cell
  167. * @param ref
  168. * @return unmodifiable list of matching rules
  169. */
  170. private List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell, CellReference ref) {
  171. List<EvaluationConditionalFormatRule> rules = values.get(ref);
  172. if (rules == null) {
  173. // compute and cache them
  174. rules = new ArrayList<EvaluationConditionalFormatRule>();
  175. /*
  176. * Per Excel help:
  177. * https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-e09711a3-48df-4bcb-b82c-9d8b8b22463d#__toc269129417
  178. * stopIfTrue is true for all rules from HSSF files, and an explicit value for XSSF files.
  179. * thus the explicit ordering of the rule lists in #getFormattingRulesForSheet(Sheet)
  180. */
  181. boolean stopIfTrue = false;
  182. for (EvaluationConditionalFormatRule rule : getRules(cell.getSheet())) {
  183. if (stopIfTrue) continue; // a previous rule matched and wants no more evaluations
  184. if (rule.matches(cell)) {
  185. rules.add(rule);
  186. stopIfTrue = rule.getRule().getStopIfTrue();
  187. }
  188. }
  189. Collections.sort(rules);
  190. values.put(ref, rules);
  191. }
  192. return Collections.unmodifiableList(rules);
  193. }
  194. public static CellReference getRef(Cell cell) {
  195. return new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex(), false, false);
  196. }
  197. /**
  198. * @param sheetName
  199. * @return unmodifiable list of all Conditional format rules for the given sheet, if any
  200. */
  201. public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(String sheetName) {
  202. return getFormatRulesForSheet(workbook.getSheet(sheetName));
  203. }
  204. /**
  205. * @param sheet
  206. * @return unmodifiable list of all Conditional format rules for the given sheet, if any
  207. */
  208. public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(Sheet sheet) {
  209. return getRules(sheet);
  210. }
  211. /**
  212. * Conditional formatting rules can apply only to cells in the sheet to which they are attached.
  213. * The POI data model does not have a back-reference to the owning sheet, so it must be passed in separately.
  214. * <p/>
  215. * We could overload this with convenience methods taking a sheet name and sheet index as well.
  216. * <p/>
  217. * @param sheet containing the rule
  218. * @param index of the {@link ConditionalFormatting} instance in the sheet's array
  219. * @return unmodifiable List of all cells in the rule's region matching the rule's condition
  220. */
  221. public List<Cell> getMatchingCells(Sheet sheet, int conditionalFormattingIndex, int ruleIndex) {
  222. for (EvaluationConditionalFormatRule rule : getRules(sheet)) {
  223. if (rule.getSheet().equals(sheet) && rule.getFormattingIndex() == conditionalFormattingIndex && rule.getRuleIndex() == ruleIndex) {
  224. return getMatchingCells(rule);
  225. }
  226. }
  227. return Collections.emptyList();
  228. }
  229. /**
  230. *
  231. * @param rule
  232. * @return unmodifiable List of all cells in the rule's region matching the rule's condition
  233. */
  234. public List<Cell> getMatchingCells(EvaluationConditionalFormatRule rule) {
  235. final List<Cell> cells = new ArrayList<Cell>();
  236. final Sheet sheet = rule.getSheet();
  237. for (CellRangeAddress region : rule.getRegions()) {
  238. for (int r = region.getFirstRow(); r <= region.getLastRow(); r++) {
  239. final Row row = sheet.getRow(r);
  240. if (row == null) continue; // no cells to check
  241. for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
  242. final Cell cell = row.getCell(c);
  243. if (cell == null) continue;
  244. List<EvaluationConditionalFormatRule> cellRules = getConditionalFormattingForCell(cell);
  245. if (cellRules.contains(rule)) cells.add(cell);
  246. }
  247. }
  248. }
  249. return Collections.unmodifiableList(cells);
  250. }
  251. }