123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- /* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You 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 org.apache.poi.ss.formula;
-
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- import org.apache.poi.ss.usermodel.Cell;
- import org.apache.poi.ss.usermodel.ConditionalFormatting;
- import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
- import org.apache.poi.ss.usermodel.Row;
- import org.apache.poi.ss.usermodel.Sheet;
- import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
- import org.apache.poi.ss.usermodel.Workbook;
- import org.apache.poi.ss.util.CellRangeAddress;
- import org.apache.poi.ss.util.CellRangeAddressBase;
- import org.apache.poi.ss.util.CellReference;
- import org.apache.poi.ss.util.SheetUtil;
-
- /**
- * Evaluates Conditional Formatting constraints.<p/>
- *
- * For performance reasons, this class keeps a cache of all previously evaluated rules and cells.
- * Be sure to call {@link #clearAllCachedFormats()} if any conditional formats are modified, added, or deleted,
- * and {@link #clearAllCachedValues()} whenever cell values change.
- * <p/>
- *
- */
- public class ConditionalFormattingEvaluator {
-
- private final WorkbookEvaluator workbookEvaluator;
- private final Workbook workbook;
-
- /**
- * All the underlying structures, for both HSSF and XSSF, repeatedly go to the raw bytes/XML for the
- * different pieces used in the ConditionalFormatting* structures. That's highly inefficient,
- * and can cause significant lag when checking formats for large workbooks.
- * <p/>
- * Instead we need a cached version that is discarded when definitions change.
- * <p/>
- * Sheets don't implement equals, and since its an interface,
- * there's no guarantee instances won't be recreated on the fly by some implementation.
- * So we use sheet name.
- */
- private final Map<String, List<EvaluationConditionalFormatRule>> formats = new HashMap<String, List<EvaluationConditionalFormatRule>>();
-
- /**
- * Evaluating rules for cells in their region(s) is expensive, so we want to cache them,
- * and empty/reevaluate the cache when values change.
- * <p/>
- * Rule lists are in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF)
- * <p/>
- * CellReference implements equals().
- */
- private final Map<CellReference, List<EvaluationConditionalFormatRule>> values = new HashMap<CellReference, List<EvaluationConditionalFormatRule>>();
-
- public ConditionalFormattingEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
- this.workbook = wb;
- this.workbookEvaluator = provider._getWorkbookEvaluator();
- }
-
- protected WorkbookEvaluator getWorkbookEvaluator() {
- return workbookEvaluator;
- }
-
- /**
- * Call this whenever rules are added, reordered, or removed, or a rule formula is changed
- * (not the formula inputs but the formula expression itself)
- */
- public void clearAllCachedFormats() {
- formats.clear();
- }
-
- /**
- * Call this whenever cell values change in the workbook, so condional formats are re-evaluated
- * for all cells.
- * <p/>
- * TODO: eventually this should work like {@link EvaluationCache#notifyUpdateCell(int, int, EvaluationCell)}
- * and only clear values that need recalculation based on the formula dependency tree.
- */
- public void clearAllCachedValues() {
- values.clear();
- }
-
- /**
- * lazy load by sheet since reading can be expensive
- *
- * @param sheet
- * @return unmodifiable list of rules
- */
- protected List<EvaluationConditionalFormatRule> getRules(Sheet sheet) {
- final String sheetName = sheet.getSheetName();
- List<EvaluationConditionalFormatRule> rules = formats.get(sheetName);
- if (rules == null && ! formats.containsKey(sheetName)) {
- final SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
- final int count = scf.getNumConditionalFormattings();
- rules = new ArrayList<EvaluationConditionalFormatRule>(count);
- formats.put(sheetName, rules);
- for (int i=0; i < count; i++) {
- ConditionalFormatting f = scf.getConditionalFormattingAt(i);
- //optimization, as this may be expensive for lots of ranges
- final CellRangeAddress[] regions = f.getFormattingRanges();
- for (int r=0; r < f.getNumberOfRules(); r++) {
- ConditionalFormattingRule rule = f.getRule(r);
- rules.add(new EvaluationConditionalFormatRule(workbookEvaluator, sheet, f, i, rule, r, regions));
- }
- }
- // need them in formatting and priority order so logic works right
- Collections.sort(rules);
- }
- return Collections.unmodifiableList(rules);
- }
-
- /**
- * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
- * in defined "priority" order, returning the matches if any. This is a property currently
- * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
- * <p/>
- * Most cells will have zero or one applied rule, but it is possible to define multiple rules
- * that apply at the same time to the same cell, thus the List result.
- * <p/>
- * Note that to properly apply conditional rules, care must be taken to offset the base
- * formula by the relative position of the current cell, or the wrong value is checked.
- * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
- *
- * @param cell NOTE: if no sheet name is specified, this uses the workbook active sheet
- * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
- * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
- * or null if none apply
- */
- public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(final CellReference cellRef) {
- String sheetName = cellRef.getSheetName();
- Sheet sheet = null;
- if (sheetName == null) sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
- else sheet = workbook.getSheet(sheetName);
-
- final Cell cell = SheetUtil.getCell(sheet, cellRef.getRow(), cellRef.getCol());
-
- if (cell == null) return Collections.emptyList();
-
- return getConditionalFormattingForCell(cell, cellRef);
- }
-
- /**
- * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
- * in defined "priority" order, returning the matches if any. This is a property currently
- * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
- * <p/>
- * Most cells will have zero or one applied rule, but it is possible to define multiple rules
- * that apply at the same time to the same cell, thus the List result.
- * <p/>
- * Note that to properly apply conditional rules, care must be taken to offset the base
- * formula by the relative position of the current cell, or the wrong value is checked.
- * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
- *
- * @param cell
- * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
- * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
- * or null if none apply
- */
- public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell) {
- return getConditionalFormattingForCell(cell, getRef(cell));
- }
-
- /**
- * We need both, and can derive one from the other, but this is to avoid duplicate work
- *
- * @param cell
- * @param ref
- * @return unmodifiable list of matching rules
- */
- private List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell, CellReference ref) {
- List<EvaluationConditionalFormatRule> rules = values.get(ref);
-
- if (rules == null) {
- // compute and cache them
- rules = new ArrayList<EvaluationConditionalFormatRule>();
- /*
- * Per Excel help:
- * https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-e09711a3-48df-4bcb-b82c-9d8b8b22463d#__toc269129417
- * stopIfTrue is true for all rules from HSSF files, and an explicit value for XSSF files.
- * thus the explicit ordering of the rule lists in #getFormattingRulesForSheet(Sheet)
- */
- boolean stopIfTrue = false;
- for (EvaluationConditionalFormatRule rule : getRules(cell.getSheet())) {
-
- if (stopIfTrue) continue; // a previous rule matched and wants no more evaluations
-
- if (rule.matches(cell)) {
- rules.add(rule);
- stopIfTrue = rule.getRule().getStopIfTrue();
- }
- }
- Collections.sort(rules);
- values.put(ref, rules);
- }
-
- return Collections.unmodifiableList(rules);
- }
-
- public static CellReference getRef(Cell cell) {
- return new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex(), false, false);
- }
-
- /**
- * @param sheetName
- * @return unmodifiable list of all Conditional format rules for the given sheet, if any
- */
- public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(String sheetName) {
- return getFormatRulesForSheet(workbook.getSheet(sheetName));
- }
-
- /**
- * @param sheet
- * @return unmodifiable list of all Conditional format rules for the given sheet, if any
- */
- public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(Sheet sheet) {
- return getRules(sheet);
- }
-
- /**
- * Conditional formatting rules can apply only to cells in the sheet to which they are attached.
- * The POI data model does not have a back-reference to the owning sheet, so it must be passed in separately.
- * <p/>
- * We could overload this with convenience methods taking a sheet name and sheet index as well.
- * <p/>
- * @param sheet containing the rule
- * @param index of the {@link ConditionalFormatting} instance in the sheet's array
- * @return unmodifiable List of all cells in the rule's region matching the rule's condition
- */
- public List<Cell> getMatchingCells(Sheet sheet, int conditionalFormattingIndex, int ruleIndex) {
- for (EvaluationConditionalFormatRule rule : getRules(sheet)) {
- if (rule.getSheet().equals(sheet) && rule.getFormattingIndex() == conditionalFormattingIndex && rule.getRuleIndex() == ruleIndex) {
- return getMatchingCells(rule);
- }
- }
- return Collections.emptyList();
- }
-
- /**
- *
- * @param rule
- * @return unmodifiable List of all cells in the rule's region matching the rule's condition
- */
- public List<Cell> getMatchingCells(EvaluationConditionalFormatRule rule) {
- final List<Cell> cells = new ArrayList<Cell>();
- final Sheet sheet = rule.getSheet();
-
- for (CellRangeAddress region : rule.getRegions()) {
- for (int r = region.getFirstRow(); r <= region.getLastRow(); r++) {
- final Row row = sheet.getRow(r);
- if (row == null) continue; // no cells to check
- for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
- final Cell cell = row.getCell(c);
- if (cell == null) continue;
-
- List<EvaluationConditionalFormatRule> cellRules = getConditionalFormattingForCell(cell);
- if (cellRules.contains(rule)) cells.add(cell);
- }
- }
- }
- return Collections.unmodifiableList(cells);
- }
- }
|