diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CellValueManager.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CellValueManager.java index ce06ab22a..a288d3b8e 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CellValueManager.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CellValueManager.java @@ -64,6 +64,7 @@ import org.apache.poi.xssf.usermodel.XSSFHyperlink; import org.apache.poi.xssf.usermodel.XSSFSheet; +import com.vaadin.addon.spreadsheet.ConditionalFormatter.ConditionalFormatterEvaluator; import com.vaadin.addon.spreadsheet.Spreadsheet.CellDeletionHandler; import com.vaadin.addon.spreadsheet.Spreadsheet.CellValueChangeEvent; import com.vaadin.addon.spreadsheet.Spreadsheet.CellValueHandler; @@ -223,7 +224,12 @@ private String getCachedFormulaCellValue(Cell formulaCell) { return result; } - protected CellData createCellDataForCell(Cell cell) { + /** + * @param cell + * @param conditionalFormatter evaluator from a batch run of conditional formatting checks + * @return a new CellData instance + */ + protected CellData createCellDataForCell(Cell cell, ConditionalFormatterEvaluator conditionalFormatter) { CellData cellData = new CellData(); cellData.row = cell.getRowIndex() + 1; cellData.col = cell.getColumnIndex() + 1; @@ -333,9 +339,8 @@ && isGenerallCell(cell)) { // conditional formatting might be applied even if there isn't a // value (such as borders for the cell to the right) - Set cellFormattingIndexes = spreadsheet - .getConditionalFormatter().getCellFormattingIndex(cell); - if (cellFormattingIndexes != null) { + Set cellFormattingIndexes = conditionalFormatter.getCellFormattingIndex(cell); + if (cellFormattingIndexes != null && !cellFormattingIndexes.isEmpty()) { for (Integer i : cellFormattingIndexes) { cellData.cellStyle = cellData.cellStyle + " cf" + i; @@ -1126,32 +1131,37 @@ protected ArrayList loadCellDataForRowAndColumnRange( @SuppressWarnings("unchecked") final Collection customComponentCells = (Collection) (componentIDtoCellKeysMap == null ? Collections .emptyList() : componentIDtoCellKeysMap.values()); - for (int r = firstRow - 1; r < lastRow; r++) { - Row row = activeSheet.getRow(r); - if (row != null && row.getLastCellNum() != -1 - && row.getLastCellNum() >= firstColumn) { - for (int c = firstColumn - 1; c < lastColumn; c++) { - final String key = SpreadsheetUtil.toKey(c + 1, r + 1); - if (!customComponentCells.contains(key) - && !sentCells.contains(key) - && !sentFormulaCells.contains(key)) { - Cell cell = row.getCell(c); - if (cell != null) { - final CellData cd = createCellDataForCell(cell); - if (cd != null) { - CellType cellType = cell.getCellType(); - if (cellType == CellType.FORMULA) { - sentFormulaCells.add(key); - } else { - sentCells.add(key); + + // iterate in reverse row/column order (bottom right to top left) to match CSS order for border calculations, + // to avoid issues like #651 where cell values are not updated in the proper sequence. + spreadsheet.getConditionalFormatter().evaluateBatch(formatter -> { + for (int r = lastRow - 1; r >= firstRow -1; r--) { + Row row = activeSheet.getRow(r); + if (row != null && row.getLastCellNum() != -1 + && row.getLastCellNum() >= firstColumn) { + for (int c = lastColumn - 1; c >= firstColumn -1; c--) { + final String key = SpreadsheetUtil.toKey(c + 1, r + 1); + if (!customComponentCells.contains(key) + && !sentCells.contains(key) + && !sentFormulaCells.contains(key)) { + Cell cell = row.getCell(c); + if (cell != null) { + final CellData cd = createCellDataForCell(cell, formatter); + if (cd != null) { + CellType cellType = cell.getCellType(); + if (cellType == CellType.FORMULA) { + sentFormulaCells.add(key); + } else { + sentCells.add(key); + } + cellData.add(cd); + } } - cellData.add(cd); } } } } - } - } + }); return cellData; } @@ -1182,42 +1192,48 @@ protected void updateMarkedCellValues() { // update all cached formula cell values on client side, because they // might have changed. also make sure all marked cells are updated Iterator rows = sheet.rowIterator(); - while (rows.hasNext()) { - final Row r = rows.next(); - final Iterator cells = r.cellIterator(); - while (cells.hasNext()) { - final Cell cell = cells.next(); - int rowIndex = cell.getRowIndex(); - int columnIndex = cell.getColumnIndex(); - final String key = SpreadsheetUtil.toKey(columnIndex + 1, - rowIndex + 1); - CellData cd = createCellDataForCell(cell); - // update formula cells - if (cell.getCellType() == CellType.FORMULA) { - if (cd != null) { - if (sentFormulaCells.contains(key) - || markedCells.contains(key)) { + // iterate in reverse row/column order (bottom right to top left) to match CSS order for border calculations, + // to avoid issues like #651 where cell values are not updated in the proper sequence. + spreadsheet.getConditionalFormatter().evaluateBatch(formatter -> { + for (int ri = sheet.getLastRowNum(); ri >= 0; ri--) { + final Row r = sheet.getRow(ri); + if (r == null) continue; + for (int ci = r.getLastCellNum() - 1; ci >= 0; ci--) { + final Cell cell = r.getCell(ci); + if (cell == null) continue; + int rowIndex = cell.getRowIndex(); + int columnIndex = cell.getColumnIndex(); + final String key = SpreadsheetUtil.toKey(columnIndex + 1, + rowIndex + 1); + CellData cd = createCellDataForCell(cell, formatter); + // update formula cells + if (cell.getCellType() == CellType.FORMULA) { + if (cd != null) { + if (sentFormulaCells.contains(key) + || markedCells.contains(key)) { + sentFormulaCells.add(key); + updatedCellData.add(cd); + } + } else if (sentFormulaCells.contains(key)) { + // in case the formula cell value has changed to null or + // empty; this case is probably quite rare, formula cell + // pointing to a cell that was removed or had its value + // cleared ??? sentFormulaCells.add(key); + cd = new CellData(); + cd.col = columnIndex + 1; + cd.row = rowIndex + 1; + cd.cellStyle = "" + cell.getCellStyle().getIndex(); updatedCellData.add(cd); } - } else if (sentFormulaCells.contains(key)) { - // in case the formula cell value has changed to null or - // empty; this case is probably quite rare, formula cell - // pointing to a cell that was removed or had its value - // cleared ??? - sentFormulaCells.add(key); - cd = new CellData(); - cd.col = columnIndex + 1; - cd.row = rowIndex + 1; - cd.cellStyle = "" + cell.getCellStyle().getIndex(); + } else if (markedCells.contains(key)) { + sentCells.add(key); updatedCellData.add(cd); } - } else if (markedCells.contains(key)) { - sentCells.add(key); - updatedCellData.add(cd); } } - } + }); + if (!changedFormulaCells.isEmpty()) { fireFormulaValueChangeEvent(changedFormulaCells); changedFormulaCells = new HashSet(); diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ColorConverter.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ColorConverter.java index 64acc8279..4e5dcc77f 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ColorConverter.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ColorConverter.java @@ -21,7 +21,9 @@ import org.apache.poi.ss.usermodel.BorderFormatting; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Color; import org.apache.poi.ss.usermodel.ConditionalFormattingRule; +import org.apache.poi.ss.usermodel.DifferentialStyleProvider; import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder.BorderSide; /** @@ -70,6 +72,17 @@ String getBorderColorCSS(BorderSide borderSide, String attribute, String getBorderColorCSS(BorderSide borderSide, String attribute, BorderFormatting format); + /** + * Returns CSS border definitions for the given attribute and color + * + * @param attr + * border CSS attribute + * @param colorInstance + * POI color + * @return CSS color string + */ + String getBorderColorCSS(String attr, Color colorInstance); + /** * Writes the default background and foreground colors as CSS styles from * the given cell style to the given string buffer. @@ -101,6 +114,16 @@ String getBorderColorCSS(BorderSide borderSide, String attribute, public String getBackgroundColorCSS(ConditionalFormattingRule rule); /** + * Create a CSS color string for the background in the given style. + * + * @param styleProvider + * conditional format rule, table style, etc. + * @return valid color string with semicolon or null if no + * color matches. + */ + public String getBackgroundColorCSS(DifferentialStyleProvider styleProvider); + + /** * Create a CSS color string for the font in the given rule. * * @param rule @@ -109,4 +132,14 @@ String getBorderColorCSS(BorderSide borderSide, String attribute, * color matches. */ public String getFontColorCSS(ConditionalFormattingRule rule); + + /** + * Create a CSS color string for the font in the given style provider. + * + * @param styleProvider + * conditional format rule, table style, etc. + * @return valid color string with semicolon or null if no + * color matches. + */ + public String getFontColorCSS(DifferentialStyleProvider styleProvider); } diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ConditionalFormatter.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ConditionalFormatter.java index 24e8c0459..2697d629d 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ConditionalFormatter.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/ConditionalFormatter.java @@ -18,56 +18,30 @@ */ import java.io.Serializable; -import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.poi.hssf.usermodel.HSSFSheetConditionalFormatting; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.ss.formula.FormulaParser; -import org.apache.poi.ss.formula.FormulaType; -import org.apache.poi.ss.formula.WorkbookEvaluatorUtil; -import org.apache.poi.ss.formula.eval.BoolEval; -import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.formula.BaseFormulaEvaluator; +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.formula.EvaluationConditionalFormatRule; import org.apache.poi.ss.formula.eval.NotImplementedException; -import org.apache.poi.ss.formula.eval.NumberEval; -import org.apache.poi.ss.formula.eval.NumericValueEval; -import org.apache.poi.ss.formula.eval.StringEval; -import org.apache.poi.ss.formula.eval.ValueEval; -import org.apache.poi.ss.formula.ptg.Ptg; -import org.apache.poi.ss.formula.ptg.RefPtgBase; +import org.apache.poi.ss.usermodel.BorderFormatting; +import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.ComparisonOperator; -import org.apache.poi.ss.usermodel.ConditionType; -import org.apache.poi.ss.usermodel.ConditionalFormatting; -import org.apache.poi.ss.usermodel.ConditionalFormattingRule; -import org.apache.poi.ss.usermodel.FontFormatting; import org.apache.poi.ss.usermodel.FormulaEvaluator; -import org.apache.poi.ss.usermodel.PatternFormatting; -import org.apache.poi.ss.usermodel.SheetConditionalFormatting; +import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.XSSFBorderFormatting; -import org.apache.poi.xssf.usermodel.XSSFConditionalFormatting; -import org.apache.poi.xssf.usermodel.XSSFConditionalFormattingRule; -import org.apache.poi.xssf.usermodel.XSSFFontFormatting; +import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder.BorderSide; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBooleanProperty; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBorder; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCfRule; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont; - -import com.vaadin.addon.spreadsheet.SpreadsheetStyleFactory.BorderStyle; /** * ConditionalFormatter is an utility class of Spreadsheet, which handles all @@ -76,16 +50,39 @@ * Rules are parsed into CSS rules with individual class names. Class names for * each cell can then be fetched from this class. *

- * For now, only XSSF formatting rules are supported because of bugs in POI. - * - * @author Thomas Mattsson / Vaadin Ltd. */ @SuppressWarnings("serial") public class ConditionalFormatter implements Serializable { - private static final Logger LOGGER = Logger - .getLogger(ConditionalFormatter.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ConditionalFormatter.class.getName()); + /** + * Interface for a callback that evaluates the conditional format state of a collection of cells at once. + * Use this to avoid re-calculating format styles multiple times for the same cell when no values are changing + * in the mean time. + */ + @FunctionalInterface + public static interface ConditionalFormattingBatchEvaluator extends Serializable { + /** + * called by {@link ConditionalFormatter} to evaluate cells using cached results for efficiency + * @param formatter the conditional formatter to use - don't use another reference, it may have a different cache! + */ + public void evaluate(ConditionalFormatterEvaluator formatter); + } + + /** + * Interface for batch processing wrapping calls to {@link #getCellFormattingIndex(Cell)}. + */ + @FunctionalInterface + public static interface ConditionalFormatterEvaluator extends Serializable { + /** + * define the set of CSS rule indexes that apply to this cell. + * @param cell + * @return set of CSS rule IDs for applicable conditional formatting + */ + public Set getCellFormattingIndex(Cell cell); + } + /* * Slight hack. This style is used when a CF rule defines 'no border', in * which case the border should be empty. However, since we use cell DIV @@ -96,17 +93,41 @@ public class ConditionalFormatter implements Serializable { */ private static String BORDER_STYLE_DEFAULT = "1pt solid #d6d6d6;"; + /** + * starting index for conditional formatting CSS styles. + * Should be high enough to avoid conflicts with other types. + */ + public static final int BASE_CONDITIONAL_FORMAT_CSS_INDEX = 1000000000; + private Spreadsheet spreadsheet; + /* + * cache cell CSS style ID lists (1:N), style ID# determines CSS priority order + */ + private Map> cellToCssIndex = new HashMap>(); + /** - * Cache of styles for each cell. One cell may have several styles. + * Excel colors to CSS color definitions */ - private Map> cellToIndex = new HashMap>(); + protected ColorConverter colorConverter; - private Map topBorders = new HashMap(); - private Map leftBorders = new HashMap(); + /** + * Evaluator to cache formats and conditional evaluations + */ + private ConditionalFormattingEvaluator cfEvaluator; - protected ColorConverter colorConverter; + /** + * Stored here as a convenience, and because we need to notice changes to this + * from reloading a spreadsheet or any other path that could change it out from + * under the cfEvaluator. + */ + private FormulaEvaluator formulaEvaluator; + + /** + * common class for converting Excel formatting to CSS formatting. + * Used by both conditional formatting and table style formatting code. + */ + private IncrementalStyleBuilder styleBuilder; /** * Constructs a new ConditionalFormatter targeting the given Spreadsheet. @@ -123,756 +144,226 @@ public ConditionalFormatter(Spreadsheet spreadsheet) { } else { colorConverter = new XSSFColorConverter((XSSFWorkbook) workbook); } + + // need to re-evaluate all styles when any value changes + // - no way to predict dependencies + spreadsheet.addCellValueChangeListener(e -> { if (cfEvaluator != null) cfEvaluator.clearAllCachedValues(); }); + } /** - * Each cell can have multiple matching rules, hence a collection. Order - * doesn't matter here, CSS is applied in correct order on the client side. - * - * @param cell - * Target cell - * @return indexes of the rules that match this Cell (to be used in class - * names) + * this may be called after the component workbook has been swapped out from under us. + * When that happens, the formula evaluator is recreated, so we can check for object equivalence. + * + * @return true if the formula evaluator changed, and we have to reinitialize everything */ - public Set getCellFormattingIndex(Cell cell) { - Set index = cellToIndex.get(SpreadsheetUtil.toKey(cell)); - return index; + protected boolean init() { + if (formulaEvaluator != spreadsheet.getFormulaEvaluator()) { + formulaEvaluator = spreadsheet.getFormulaEvaluator(); + cfEvaluator = new ConditionalFormattingEvaluator(spreadsheet.getWorkbook(), (BaseFormulaEvaluator) formulaEvaluator); + styleBuilder = new IncrementalStyleBuilder(spreadsheet, BORDER_STYLE_DEFAULT); + return true; + } + return false; } - + /** - * Creates the necessary CSS rules and runs evaluations on all affected - * cells. + * @return the cfEvaluator */ - public void createConditionalFormatterRules() { - - // make sure old styles are cleared - if (cellToIndex != null) { - for (String key : cellToIndex.keySet()) { - int col = SpreadsheetUtil.getColumnIndexFromKey(key) - 1; - int row = SpreadsheetUtil.getRowFromKey(key) - 1; - Cell cell = spreadsheet.getCell(row, col); - if (cell != null) { - spreadsheet.markCellAsUpdated(cell, true); - } - } - } - - cellToIndex.clear(); - topBorders.clear(); - leftBorders.clear(); - spreadsheet.getState().conditionalFormattingStyles = new HashMap(); - - SheetConditionalFormatting cfs = spreadsheet.getActiveSheet() - .getSheetConditionalFormatting(); - - if (cfs instanceof HSSFSheetConditionalFormatting) { - // disable formatting for HSSF, since formulas are read incorrectly - // and we would return incorrect results. - return; - } - - for (int i = 0; i < cfs.getNumConditionalFormattings(); i++) { - ConditionalFormatting cf = cfs.getConditionalFormattingAt(i); - - List cfRuleList = getOrderedRuleList(cf); - - // rules are listen bottom up, but we want top down so that we can - // stop when we need to. Rule indexes follow original order, because - // that is the order CSS is applied on client side. - for (int ruleIndex = cf.getNumberOfRules() - 1; ruleIndex >= 0; ruleIndex--) { - - ConditionalFormattingRule rule = cfRuleList.get(ruleIndex); - - // first formatting object gets 0-999, second 1000-1999... - // should be enough. - int cssIndex = i * 1000000 + ruleIndex * 1000; - - // build style - - // TODO: some of this code will override all old values on each - // iteration. POI API will return the default value for nulls, - // which is not what we want. - - StringBuilder css = new StringBuilder(); - - FontFormatting fontFormatting = rule.getFontFormatting(); - - if (fontFormatting != null) { - String fontColorCSS = colorConverter.getFontColorCSS(rule); - if (fontColorCSS != null) { - css.append("color:" + fontColorCSS); - } - - // we can't have both underline and line-through in the same - // DIV element, so use the first one that matches. - - // HSSF might return 255 for 'none'... - if (fontFormatting.getUnderlineType() != FontFormatting.U_NONE - && fontFormatting.getUnderlineType() != 255) { - css.append("text-decoration: underline;"); - } - if (hasStrikeThrough(fontFormatting)) { - css.append("text-decoration: line-through;"); - } - - if (fontFormatting.getFontHeight() != -1) { - // POI returns height in 1/20th points, convert - int fontHeight = fontFormatting.getFontHeight() / 20; - css.append("font-size:" + fontHeight + "pt;"); - } - - // excel has a setting for bold italic, otherwise bold - // overrides - // italic and vice versa - if (fontFormatting.isItalic() && fontFormatting.isBold()) { - css.append("font-style: italic;"); - css.append("font-weight: bold;"); - } else if (fontFormatting.isItalic()) { - css.append("font-style: italic;"); - css.append("font-weight: initial;"); - } else if (fontFormatting.isBold()) { - css.append("font-style: normal;"); - css.append("font-weight: bold;"); - } - } - - PatternFormatting patternFormatting = rule - .getPatternFormatting(); - if (patternFormatting != null) { - String colorCSS = colorConverter - .getBackgroundColorCSS(rule); - - if (colorCSS != null) { - css.append("background-color:" + colorCSS); - } - } - - cssIndex = addBorderFormatting(cf, rule, css, cssIndex); - - spreadsheet.getState().conditionalFormattingStyles.put( - cssIndex, css.toString()); - - // check actual cells - runCellMatcher(cf, rule, cssIndex); - - // stop here if defined in rules - if (stopHere(rule)) { - break; - } - } - - } + public ConditionalFormattingEvaluator getConditionalFormattingEvaluator() { + return cfEvaluator; } - + /** - * Get the common {@link FormulaEvaluator} instance from {@link Spreadsheet} + * @return the styleBuilder */ - protected FormulaEvaluator getFormulaEvaluator() { - return spreadsheet.getFormulaEvaluator(); + public IncrementalStyleBuilder getStyleBuilder() { + return styleBuilder; } /** - * Excel uses a field called 'priority' to re-order rules. Just calling - * {@link XSSFConditionalFormatting#getRule(int)} will result in wrong - * order. So, instead, get the list and reorder it according to the priority - * field. - * - * @return The list of conditional formatting rules in reverse order (same - * order Excel processes them). + * @return the spreadsheet */ - private List getOrderedRuleList( - ConditionalFormatting cf) { - - // get the list - XSSFConditionalFormatting xcf = (XSSFConditionalFormatting) cf; - List rules = new ArrayList(); - for (int i = 0; i < xcf.getNumberOfRules(); i++) { - rules.add(xcf.getRule(i)); - } - - // reorder with hidden field - Collections.sort(rules, - new Comparator() { - - @Override - public int compare(XSSFConditionalFormattingRule o1, - XSSFConditionalFormattingRule o2) { - - CTCfRule object = (CTCfRule) getFieldValWithReflection( - o1, "_cfRule"); - CTCfRule object2 = (CTCfRule) getFieldValWithReflection( - o2, "_cfRule"); - - if (object != null && object2 != null) { - // reverse order - return object2.getPriority() - object.getPriority(); - } - - return 0; - } - }); - - return rules; + public Spreadsheet getSpreadsheet() { + return spreadsheet; } - + /** - * @return the new cssIndex + * this may be called after the component workbook has been swapped out from under us. + * When that happens, the formula evaluator is recreated, so we can check for object equivalence. + *

+ * Conditional formats have multiple rules, with an order and possible "stop-if-true" logic. + * A single workbook can have multiple conditional formats. + * Everything is keyed by array index # in the Excel file formats, so we use those indexes + * as the basis for unique CSS ID values via a simple deterministic algorithm. This way + * we don't have to track which CSS goes with which format and rule, we can recreate that + * if needed from the ID. */ - private int addBorderFormatting(ConditionalFormatting cf, - ConditionalFormattingRule rule, StringBuilder css, int cssIndex) { - - if (!(rule instanceof XSSFConditionalFormattingRule)) { - // HSSF not supported - return cssIndex; + public void createConditionalFormatterRules() { + + // only do this if the formula evaluator instance doesn't match (i.e. first call, and if the spreadsheet workbook ref. is different) + if (! init()) { + return; } - - XSSFBorderFormatting borderFormatting = (XSSFBorderFormatting) rule - .getBorderFormatting(); - if (borderFormatting != null) { - - BorderStyle borderLeft = SpreadsheetStyleFactory.BORDER - .get(borderFormatting.getBorderLeftEnum()); - BorderStyle borderRight = SpreadsheetStyleFactory.BORDER - .get(borderFormatting.getBorderRightEnum()); - BorderStyle borderTop = SpreadsheetStyleFactory.BORDER - .get(borderFormatting.getBorderTopEnum()); - BorderStyle borderBottom = SpreadsheetStyleFactory.BORDER - .get(borderFormatting.getBorderBottomEnum()); - - // In Excel, we can set a border to 'none', which overrides previous - // rules. Default is 'not set', in which case we add no CSS. - boolean isLeftSet = isBorderSet(borderFormatting, BorderSide.LEFT); - boolean isTopSet = isBorderSet(borderFormatting, BorderSide.TOP); - boolean isRightSet = isBorderSet(borderFormatting, BorderSide.RIGHT); - boolean isBottomSet = isBorderSet(borderFormatting, - BorderSide.BOTTOM); - - if (isRightSet) { - css.append("border-right:"); - if (borderRight != BorderStyle.NONE) { - css.append(borderRight.getBorderAttributeValue()); - css.append(colorConverter.getBorderColorCSS( - BorderSide.RIGHT, "border-right-color", - borderFormatting)); - } else { - css.append(BORDER_STYLE_DEFAULT); - } - } - if (isBottomSet) { - css.append("border-bottom:"); - if (borderBottom != BorderStyle.NONE) { - css.append(borderBottom.getBorderAttributeValue()); - css.append(colorConverter.getBorderColorCSS( - BorderSide.BOTTOM, "border-bottom-color", - borderFormatting)); - } else { - css.append(BORDER_STYLE_DEFAULT); - } - } - - // top and left borders might be applied to another cell, so store - // them with a different index - if (isTopSet) { - // bottom border for cell above - final StringBuilder sb2 = new StringBuilder("border-bottom:"); - if (borderTop != BorderStyle.NONE) { - sb2.append(borderTop.getBorderAttributeValue()); - sb2.append(colorConverter.getBorderColorCSS(BorderSide.TOP, - "border-bottom-color", borderFormatting)); - - spreadsheet.getState().conditionalFormattingStyles.put( - cssIndex, sb2.toString()); - topBorders.put(cf, cssIndex++); - } else { - css.append(BORDER_STYLE_DEFAULT); - } - } - - if (isLeftSet) { - // right border for cell to the left - final StringBuilder sb2 = new StringBuilder("border-right:"); - if (borderLeft != BorderStyle.NONE) { - sb2.append(borderLeft.getBorderAttributeValue()); - sb2.append(colorConverter.getBorderColorCSS( - BorderSide.LEFT, "border-right-color", - borderFormatting)); - - spreadsheet.getState().conditionalFormattingStyles.put( - cssIndex, sb2.toString()); - leftBorders.put(cf, cssIndex++); - } else { - css.append(BORDER_STYLE_DEFAULT); + + // no-op on first call, nothing's been done yet + cfEvaluator.clearAllCachedValues(); + // make sure old styles are cleared + if (cellToCssIndex != null) { + for (CellReference key : cellToCssIndex.keySet()) { + int col = key.getCol(); + int row = key.getRow(); + + // note: this is for the active sheet! If that has changed, it's the right coordinates + // for the UI, but not the cell specified by the key! + Cell cell = spreadsheet.getCell(row, col); + if (cell != null) { + spreadsheet.getCellValueManager().markCellForUpdate(cell); } } } + + cellToCssIndex.clear(); + + // build rules properly for all sheets, but don't evaluate cells yet + + // remove previous styles, if any(should not be needed now that we do all sheets at once) + spreadsheet.getState().conditionalFormattingStyles = new HashMap(); - return cssIndex; - } - - /** - * Checks if this rule has 'stop if true' defined. - */ - private boolean stopHere(ConditionalFormattingRule rule) { - if (rule instanceof XSSFConditionalFormattingRule) { - - // No POI API for this particular data, but it is present in XML. - CTCfRule ctRule = (CTCfRule) getFieldValWithReflection(rule, - "_cfRule"); - if (ctRule != null) { - return ctRule.getStopIfTrue(); - } + for (int s=0; s < spreadsheet.getWorkbook().getNumberOfSheets(); s++) { + buildStylesForSheet(spreadsheet.getWorkbook().getSheetAt(s)); } - return false; } - + /** - * Helper for the very common case of having to get underlying XML data. + * define the set of CSS rule indexes that apply to this cell. + * NOTE: this does not use caching, use {@link #evaluateBatch(ConditionalFormattingBatchEvaluator)} if possible + * @param cell + * @return set of CSS rule IDs for applicable conditional formatting */ - private Object getFieldValWithReflection(Object owner, String fieldName) { - Field f = null; - Object val = null; - try { - f = owner.getClass().getDeclaredField(fieldName); - f.setAccessible(true); - - val = f.get(owner); - return val; - - } catch (NoSuchFieldException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation, unable to parse conditional formatting rule", - e); - } catch (SecurityException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation, unable to parse conditional formatting rule", - e); - } catch (IllegalArgumentException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation, unable to parse conditional formatting rule", - e); - } catch (IllegalAccessException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation, unable to parse conditional formatting rule", - e); - } finally { - if (f != null) { - f.setAccessible(false); - } - } - - return null; + public Set getCellFormattingIndex(Cell cell) { + return getCellFormattingIndex(cell, new HashSet<>()); } - + /** - * @param i - * 0 - left, 1 - top, 2 - right, 3 - bottom + * define the set of CSS rule indexes that apply to this cell, with caching. + * @param cell + * @param cellsEvaluatedInThisRun + * @return set of CSS rule IDs for applicable conditional formatting */ - private boolean isBorderSet(XSSFBorderFormatting borderFormatting, - BorderSide b) { - - CTBorder ctBorder = (CTBorder) getFieldValWithReflection( - borderFormatting, "_border"); - - if (ctBorder == null) { - return false; - } - - switch (b) { - case LEFT: - return ctBorder.isSetLeft(); - case TOP: - return ctBorder.isSetTop(); - case RIGHT: - return ctBorder.isSetRight(); - case BOTTOM: - return ctBorder.isSetBottom(); + protected Set getCellFormattingIndex(Cell cell, Set cellsEvaluatedInThisRun) { + /* + * Why use Integer CSS IDs? Looking at uses, there is no reason they can't be String instead. + * Or even an array of ints, so we can use sheet/row/column/border index arrays and return Set + */ + if (cell == null) return Collections.emptySet(); + + // calculate for cells to the right and below first, so this can have the proper border IDs if needed + if (cell.getRowIndex() < cell.getSheet().getLastRowNum() && cell.getRowIndex() < SpreadsheetVersion.EXCEL2007.getLastRowIndex() -1) { + getCellFormattingIndexInternal(new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex() + 1, cell.getColumnIndex(), false, false), cellsEvaluatedInThisRun); } - - return false; - } - - /** - * Checks if this formatting has strike-through enabled or not. - */ - private boolean hasStrikeThrough(FontFormatting fontFormatting) { - if (fontFormatting instanceof XSSFFontFormatting) { - - // No POI API for this particular data, but it is present in XML. - - CTFont font = (CTFont) getFieldValWithReflection(fontFormatting, - "_font"); - - if (font == null) { - return false; - } - - List strikeList = font.getStrikeList(); - - if (strikeList != null) { - for (CTBooleanProperty p : strikeList) { - if (p.getVal()) { - return true; - } - } - } - + if (cell.getColumnIndex() < SpreadsheetVersion.EXCEL2007.getLastColumnIndex() -1) { + getCellFormattingIndexInternal(new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex() + 1, false, false), cellsEvaluatedInThisRun); } - return false; + + return getCellFormattingIndexInternal(new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex(), false, false), cellsEvaluatedInThisRun); } /** - * Goes through the cells specified in the given formatting, and checks if - * each rule matches. Style ids from resulting matches are put in - * {@link #cellToIndex}. - * - * @param cf - * {@link ConditionalFormatting} that specifies the affected - * cells - * @param rule - * The rule to be evaluated - * @param classNameIndex - * The index of the class name that was generated for this rule, - * to be added to {@link #cellToIndex} + * @param ref + * @param cellsEvaluatedInThisRun + * @return IDs of applied styles */ - protected void runCellMatcher(ConditionalFormatting cf, - ConditionalFormattingRule rule, int classNameIndex) { - final int firstColumn = cf.getFormattingRanges()[0].getFirstColumn(); - final int firstRow = cf.getFormattingRanges()[0].getFirstRow(); - for (CellRangeAddress cra : cf.getFormattingRanges()) { - - for (int row = cra.getFirstRow(); row <= cra.getLastRow(); row++) { - for (int col = cra.getFirstColumn(); col <= cra.getLastColumn(); col++) { - - Cell cell = spreadsheet.getCell(row, col); - if (cell == null) { - cell = spreadsheet.createCell(row, col, ""); + protected Set getCellFormattingIndexInternal(CellReference ref, Set cellsEvaluatedInThisRun) { + // performance optimization - only evaluate a cell once per response loop + if (cellsEvaluatedInThisRun.contains(ref)) return cellToCssIndex.get(ref); + + Set styles = new TreeSet<>(); + // always recalculate, but track previous values to see if the cell style changed or not + Set currentStyles = cellToCssIndex.put(ref, styles); + + try { + List rules = cfEvaluator.getConditionalFormattingForCell(ref); + if (rules == null) rules = Collections.emptyList(); + for (EvaluationConditionalFormatRule rule : rules) { + styles.add(Integer.valueOf(getCssIndex(rule, IncrementalStyleBuilder.StyleType.BASE))); + styles.add(Integer.valueOf(getCssIndex(rule, IncrementalStyleBuilder.StyleType.BOTTOM))); + styles.add(Integer.valueOf(getCssIndex(rule, IncrementalStyleBuilder.StyleType.RIGHT))); + + // LEFT border CSS goes on the cell to the left! + final BorderFormatting borderFormatting = rule.getRule().getBorderFormatting(); + if (borderFormatting != null) { + if (borderFormatting.getBorderLeft() != BorderStyle.NONE) { + addBorderStyleId(getCssIndex(rule, IncrementalStyleBuilder.StyleType.LEFT), ref.getRow(), ref.getCol() - 1, cellsEvaluatedInThisRun); } - if (matches(cell, rule, col - firstColumn, row - - firstRow)) { - Set list = cellToIndex.get(SpreadsheetUtil - .toKey(cell)); - if (list == null) { - list = new HashSet(); - cellToIndex.put(SpreadsheetUtil.toKey(cell), list); - } - list.add(classNameIndex); - - // if the rule contains borders, we need to add styles - // to other cells too - if (leftBorders.containsKey(cf)) { - int ruleIndex = leftBorders.get(cf); - - // left border for col 0 isn't rendered - if (col != 0) { - Cell cellToLeft = spreadsheet.getCell(row, - col - 1); - if (cellToLeft == null) { - cellToLeft = spreadsheet.createCell(row, - col - 1, ""); - } - list = cellToIndex.get(SpreadsheetUtil - .toKey(cellToLeft)); - if (list == null) { - list = new HashSet(); - cellToIndex.put( - SpreadsheetUtil.toKey(cellToLeft), - list); - } - list.add(ruleIndex); - } - } - if (topBorders.containsKey(cf)) { - int ruleIndex = topBorders.get(cf); - - // top border for row 0 isn't rendered - if (row != 0) { - Cell cellOnTop = spreadsheet.getCell(row - 1, - col); - if (cellOnTop == null) { - cellOnTop = spreadsheet.createCell(row - 1, - col, ""); - } - list = cellToIndex.get(SpreadsheetUtil - .toKey(cellOnTop)); - if (list == null) { - list = new HashSet(); - cellToIndex.put( - SpreadsheetUtil.toKey(cellOnTop), - list); - } - list.add(ruleIndex); - } - } + // TOP border CSS goes on the cell above! + if (borderFormatting.getBorderTop() != BorderStyle.NONE) { + addBorderStyleId(getCssIndex(rule, IncrementalStyleBuilder.StyleType.TOP), ref.getRow() - 1, ref.getCol(), cellsEvaluatedInThisRun); } } } - } - } - - /** - * Checks if the given cell value matches the given conditional formatting - * rule. - * - * @param cell - * Target cell - * @param rule - * Conditional formatting rule to check against - * @return Whether the given rule evaluates to true for the - * given cell. - */ - protected boolean matches(Cell cell, ConditionalFormattingRule rule, - int deltaColumn, int deltaRow) { - /* - * Formula type is the default for most rules in modern excel files. - * - * There are a couple of issues with this. - * - * 1. the condition type seems to be '0' in all xlsx files, which is an - * illegal value according to the API. The formula is still correct, and - * can be accessed. - * - * 2. in xls-files the type is correct, but the formula is not: it - * references the wrong cell. - * - * 3. the formula is a String. POIs FormulaEvaluation only takes Cell - * arguments. So, to use it, we need to copy the formula to an existing - * cell temporarily, and run the eval. - */ - try { - if (rule.getConditionType().equals(ConditionType.CELL_VALUE_IS)) { - return matchesValue(cell, rule, deltaColumn, deltaRow); - } else { - return matchesFormula(cell, rule, deltaColumn, deltaRow); - } } catch (NotImplementedException e) { + // treat formulas we can't evaluate as non-matches, log in case consumers want to see what happened LOGGER.log(Level.FINEST, e.getMessage(), e); - return false; } - + + // if previously calculated (not null) and has changed, mark cell as having styles updated + if (currentStyles != null && ! currentStyles.equals(styles)) { + // style IDs changed for the cell, mark as updated + cellToCssIndex.put(ref, styles); + final Cell cell = spreadsheet.getCell(ref); + // don't need to update formula cache, just tell the framework to send the new styles + if (cell != null) spreadsheet.getCellValueManager().markCellForUpdate(cell); + } + cellsEvaluatedInThisRun.add(ref); + return styles; } /** - * Checks if the formula in the given rule evaluates to true. - *

- * - * NOTE: Does not support HSSF files currently. - * - * @param cell - * Cell with conditional formatting - * @param rule - * Conditional formatting rule based on formula - * @return Formula value, if the formula is of boolean formula type - * Formula value != 0, if the formula is of numeric formula type - * and false otherwise + * Adds the proper border style ID to the given cell. + * @param ruleCSSIndex + * @param row + * @param col + * @param cellsEvaluatedInThisRun */ - protected boolean matchesFormula(Cell cell, ConditionalFormattingRule rule, int deltaColumn, int deltaRow) { - if ( ! (rule instanceof XSSFConditionalFormattingRule)) { - // TODO Does not support HSSF files for now, since HSSF does not - // read cell references in the file correctly.Since HSSF formulas - // are read completely wrong, that boolean formula above is useless. - return false; - } - String booleanFormula = rule.getFormula1(); - - if (booleanFormula == null || booleanFormula.isEmpty()) { - return false; - } - - ValueEval eval = getValueEvalFromFormula(booleanFormula, cell, deltaColumn, deltaRow); - - if (eval instanceof ErrorEval){ - LOGGER.log(Level.FINEST, ((ErrorEval) eval).getErrorString(), eval); - } - - if (eval instanceof BoolEval) { - return eval == null ? false : ((BoolEval) eval).getBooleanValue(); - } else { - if (eval instanceof NumericValueEval) { - return ((NumberEval) eval).getNumberValue() != 0; - } else { - return false; - } - } + protected void addBorderStyleId(int ruleCSSIndex, int row, int col, Set cellsEvaluatedInThisRun) { + if (row < 0 || col < 0) return; // out of bounds + Set styles = getCellFormattingIndexInternal(new CellReference(spreadsheet.getActiveSheet().getSheetName(), row, col, false, false), cellsEvaluatedInThisRun); + styles.add(new Integer(ruleCSSIndex)); } - - private ValueEval getValueEvalFromFormula(String formula, Cell cell, int deltaColumn, int deltaRow) { - // Parse formula and use deltas to get relative cell references to work - // (#18702) - Ptg[] ptgs = FormulaParser.parse(formula, WorkbookEvaluatorUtil.getEvaluationWorkbook(spreadsheet), - FormulaType.CELL, spreadsheet.getActiveSheetIndex()); - - for (Ptg ptg : ptgs) { - // base class for cell reference "things" - if (ptg instanceof RefPtgBase) { - RefPtgBase ref = (RefPtgBase) ptg; - // re-calculate cell references - if (ref.isColRelative()) { - ref.setColumn(ref.getColumn() + deltaColumn); - } - if (ref.isRowRelative()) { - ref.setRow(ref.getRow() + deltaRow); - } - } + + /** + * @param sheet + */ + protected void buildStylesForSheet(Sheet sheet) { + for (EvaluationConditionalFormatRule rule : cfEvaluator.getFormatRulesForSheet(sheet)) { + styleBuilder.addStyleForRule(rule.getRule(), getCssIndex(rule, IncrementalStyleBuilder.StyleType.BASE)); } - return WorkbookEvaluatorUtil.evaluate(spreadsheet, ptgs, cell); } /** - * Checks if the given cell value matches a - * {@link ConditionalFormattingRule} of VALUE_IS type. Covers - * all cell types and comparison operations. - * - * @param cell - * Target cell + * CSS index value is deterministic, so just calculate it as needed rather than keeping extra maps around * @param rule - * Conditional formatting rule to match against. - * @param deltaColumn - * delta (on column axis) between cell and the origin cell - * @param deltaRow - * delta (on row axis) between cell and the origin cell - * @return True if the given cells value matches the given - * VALUE_IS rule, false otherwise + * @param type + * @return index for the given formatting, rule, and type */ - protected boolean matchesValue(Cell cell, ConditionalFormattingRule rule, int deltaColumn, int deltaRow) { - - boolean isFormulaType = cell.getCellType() == CellType.FORMULA; - - if (isFormulaType) { - // make sure we have the latest value for formula cells - getFormulaEvaluator().evaluateFormulaCell(cell); - } - - boolean isFormulaStringType = isFormulaType - && cell.getCachedFormulaResultType() == CellType.STRING; - boolean isFormulaBooleanType = isFormulaType - && cell.getCachedFormulaResultType() == CellType.BOOLEAN; - boolean isFormulaNumericType = isFormulaType - && cell.getCachedFormulaResultType() == CellType.NUMERIC; - - String formula = rule.getFormula1(); - byte comparisonOperation = rule.getComparisonOperation(); - ValueEval eval = getValueEvalFromFormula(formula, cell, deltaColumn, deltaRow); - - if (eval instanceof ErrorEval){ - LOGGER.log(Level.FINEST, ((ErrorEval) eval).getErrorString(), eval); - return false; - } - - if (!hasCoherentType(eval, cell.getCellType(), isFormulaStringType, - isFormulaBooleanType, isFormulaNumericType)) { - // Comparison between different types (e.g. Bool vs String) - return (comparisonOperation == ComparisonOperator.NOT_EQUAL); - } - - // other than numerical types - if (cell.getCellType() == CellType.STRING || isFormulaStringType) { - - String formulaValue = ((StringEval)eval).getStringValue(); - String stringValue = cell.getStringCellValue(); - - // Excel string comparison ignores case - switch (comparisonOperation) { - case ComparisonOperator.EQUAL: - return stringValue.equalsIgnoreCase(formulaValue); - case ComparisonOperator.NOT_EQUAL: - return !stringValue.equalsIgnoreCase(formulaValue); - } - } - if (cell.getCellType() == CellType.BOOLEAN - || isFormulaBooleanType) { - // not sure if this is used, since no boolean option exists in - // Excel.. - - boolean formulaVal = ((BoolEval)eval).getBooleanValue(); - - switch (comparisonOperation) { - case ComparisonOperator.EQUAL: - return cell.getBooleanCellValue() == formulaVal; - case ComparisonOperator.NOT_EQUAL: - return cell.getBooleanCellValue() != formulaVal; - } - } - - // numerical types - if (cell.getCellType() == CellType.NUMERIC - || isFormulaNumericType) { - - double formula1Val = ((NumericValueEval)eval).getNumberValue(); - - switch (comparisonOperation) { - - case ComparisonOperator.EQUAL: - return cell.getNumericCellValue() == formula1Val; - case ComparisonOperator.NOT_EQUAL: - return cell.getNumericCellValue() != formula1Val; - - case ComparisonOperator.LT: - return cell.getNumericCellValue() < formula1Val; - case ComparisonOperator.LE: - return cell.getNumericCellValue() <= formula1Val; - case ComparisonOperator.GT: - return cell.getNumericCellValue() > formula1Val; - case ComparisonOperator.GE: - return cell.getNumericCellValue() >= formula1Val; - - case ComparisonOperator.BETWEEN: - boolean lt = cell.getNumericCellValue() >= formula1Val; - boolean gt = cell.getNumericCellValue() <= Double.valueOf(rule - .getFormula2()); - return lt && gt; - - case ComparisonOperator.NOT_BETWEEN: - lt = cell.getNumericCellValue() <= formula1Val; - gt = cell.getNumericCellValue() >= Double.valueOf(rule - .getFormula2()); - return lt && gt; - } - } - - return false; + protected int getCssIndex(EvaluationConditionalFormatRule rule, IncrementalStyleBuilder.StyleType type) { + // each rule has 3 possible styles (StyleType). Start at 9M just in case. + return BASE_CONDITIONAL_FORMAT_CSS_INDEX + + spreadsheet.getWorkbook().getSheetIndex(rule.getSheet().getSheetName()) * 100000 // 899 sheets + + rule.getFormattingIndex() * 10000 // room for 99 formatting indexes per sheet (mostly 1:1 with regions) + + rule.getRuleIndex() * 10 // room for 999 rules per formatting (HSSF max 3, XSSF unlimited) + + type.ordinal(); // 0-2 } /** - * @param eval - * Value of a formula - * @param cellType - * Type of a cell - * @param isFormulaStringType - * true if eval is a formula of type String, false otherwise - * @param isFormulaBooleanType - * true if eval is a formula of type Boolean, false otherwise - * @param isFormulaNumericType - * true if eval is a formula of type Numeric, false otherwise - * @return true if eval is coherent with cellType, false otherwise + * For performance, since we always check cells next to a given cell to manage borders, don't evaluate each one multiple times + * per pass. Passes calling this with a {@link ConditionalFormattingBatchEvaluator} allows caching of calculated style results + * while the batch is processed. + * @param evaluator */ - private boolean hasCoherentType(ValueEval eval, CellType cellType, - boolean isFormulaStringType, boolean isFormulaBooleanType, - boolean isFormulaNumericType) { - switch (cellType) { - case STRING: - return eval instanceof StringEval; - case BOOLEAN: - return eval instanceof BoolEval; - case NUMERIC: - return eval instanceof NumericValueEval || isFormulaNumericType; - case FORMULA: - return isCoherentTypeFormula(eval, isFormulaStringType, - isFormulaBooleanType, isFormulaNumericType); - default: - return false; - } - } - - private boolean isCoherentTypeFormula(ValueEval eval, - boolean isFormulaStringType, boolean isFormulaBooleanType, - boolean isFormulaNumericType) { - boolean coherentString = eval instanceof StringEval && isFormulaStringType; - boolean coherentBoolean = eval instanceof BoolEval && isFormulaBooleanType; - boolean coherentNumeric = eval instanceof NumericValueEval && isFormulaNumericType; - return coherentString || coherentBoolean || coherentNumeric; + public void evaluateBatch(ConditionalFormattingBatchEvaluator evaluator) { + Set cellsEvaluatedInThisRun = new HashSet<>(); + evaluator.evaluate((cell) -> getCellFormattingIndex(cell, cellsEvaluatedInThisRun)); } } diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CustomDataFormatter.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CustomDataFormatter.java index 412597aee..7cd463cf9 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CustomDataFormatter.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/CustomDataFormatter.java @@ -6,9 +6,11 @@ import org.apache.poi.ss.format.CellFormat; import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.formula.EvaluationConditionalFormatRule; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.ExcelNumberFormat; import org.apache.poi.ss.usermodel.FormulaEvaluator; /** @@ -80,13 +82,24 @@ public String formatCellValue(Cell cell, FormulaEvaluator evaluator, Conditional return super.formatCellValue(cell, evaluator, cfEvaluator); } - final String[] parts = dataFormatString.split(";", -1); + ExcelNumberFormat numberFormat = null; + for (EvaluationConditionalFormatRule rule : cfEvaluator.getConditionalFormattingForCell(cell)) { + numberFormat = rule.getNumberFormat(); + if (numberFormat != null) { + break; + } + } + + String[] parts = dataFormatString.split(";", -1); final CellType cellType = getCellType(cell, evaluator); if (cellType == CellType.NUMERIC) { final double value = cell.getNumericCellValue(); + if (numberFormat != null) { + parts = numberFormat.getFormat().split(";", -1); + } return formatNumericValueUsingFormatPart(cell, value, parts); } else if (cellType == CellType.STRING && parts.length == 4) { diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/HSSFColorConverter.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/HSSFColorConverter.java index 8510214ce..a6333aa64 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/HSSFColorConverter.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/HSSFColorConverter.java @@ -24,7 +24,11 @@ import org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined; import org.apache.poi.ss.usermodel.BorderFormatting; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Color; import org.apache.poi.ss.usermodel.ConditionalFormattingRule; +import org.apache.poi.ss.usermodel.DifferentialStyleProvider; +import org.apache.poi.ss.usermodel.FontFormatting; +import org.apache.poi.ss.usermodel.PatternFormatting; import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder.BorderSide; /** @@ -152,16 +156,34 @@ public boolean hasBackgroundColor(CellStyle cellStyle) { @Override public String getBackgroundColorCSS(ConditionalFormattingRule rule) { - short index = rule.getFontFormatting().getFontColorIndex(); - String styleColor = styleColor(index); - return styleColor; + return getBackgroundColorCSS((DifferentialStyleProvider) rule); + } + + @Override + public String getBackgroundColorCSS(DifferentialStyleProvider styleProvider) { + PatternFormatting fillFmt = styleProvider.getPatternFormatting(); + if (fillFmt == null) return null; + HSSFColor color = (HSSFColor) fillFmt.getFillBackgroundColorColor(); + return styleColor(color.getIndex()); + } + + /** + * @param styleProvider + * @return CSS or null + */ + public String getFontColorCSS(DifferentialStyleProvider styleProvider) { + + FontFormatting font = styleProvider.getFontFormatting(); + + if (font == null) return null; + + HSSFColor color = (HSSFColor) font.getFontColor(); + return styleColor(color.getIndex()); } @Override public String getFontColorCSS(ConditionalFormattingRule rule) { - short color = rule.getPatternFormatting().getFillForegroundColor(); - String styleColor = styleColor(color); - return styleColor; + return getFontColorCSS((DifferentialStyleProvider) rule); } @Override @@ -180,6 +202,17 @@ private String styleColor(short index) { return null; } + /** + * @see com.vaadin.addon.spreadsheet.ColorConverter#getBorderColorCSS(java.lang.String, org.apache.poi.ss.usermodel.Color) + */ + @Override + public String getBorderColorCSS(String attr, Color colorInstance) { + StringBuilder sb = new StringBuilder(); + + styleBorderColor(sb, attr, ((HSSFColor) colorInstance).getIndex()); + return sb.toString(); + } + private void styleBorderColor(final StringBuilder sb, String attr, short index) { HSSFColor color = colors.getColor(index); diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/IncrementalStyleBuilder.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/IncrementalStyleBuilder.java new file mode 100644 index 000000000..21e627b40 --- /dev/null +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/IncrementalStyleBuilder.java @@ -0,0 +1,272 @@ +package com.vaadin.addon.spreadsheet; + +import java.io.Serializable; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/* + * #%L + * Vaadin Spreadsheet + * %% + * Copyright (C) 2013 - 2015 Vaadin Ltd + * %% + * This program is available under Commercial Vaadin Add-On License 3.0 + * (CVALv3). + * + * See the file license.html distributed with this software for more + * information about licensing. + * + * You should have received a copy of the CVALv3 along with this program. + * If not, see . + * #L% + */ + +import org.apache.poi.ss.usermodel.BorderFormatting; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.DifferentialStyleProvider; +import org.apache.poi.ss.usermodel.FontFormatting; +import org.apache.poi.ss.usermodel.PatternFormatting; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder.BorderSide; + +/** + * IncrementalStyleBuilder converts a POI {@link DifferentialStyleProvider} (conditional format rule or table style element) to CSS. + * note: Create a new instance if the workbook changes, since the colorConverter field relies on the workbook theme + */ +public class IncrementalStyleBuilder implements Serializable { + + /** default */ + private static final long serialVersionUID = 1L; + + /** + * used to generate unique CSS style IDs for the different types of styles needed + */ + static enum StyleType { + BOTTOM, + RIGHT, + TOP, + LEFT, + HORIZONTAL, // table internal row bottom border + VERTICAL, // table internal column right border + BASE, + ; + + public int offsetFromBase() { + return BASE.ordinal() - this.ordinal(); + } + } + + /* + * Slight hack. This style is used when a CF rule defines 'no border', in + * which case the border should be empty. However, since we use cell DIV + * borders for the grid structure, empty borders are in fact grey. So, if + * one rule says red, and the next says no border, then we need to know what + * 'no border' means in CSS. Of course, if the default CSS changes, this + * needs to change too, typically by creating a new instance with the updated value. + */ + private String defaultBorderStyleCSS; + + private final Spreadsheet ss; + + private final ColorConverter colorConverter; + + private DifferentialStyleProvider styleProvider; + private int baseCSSIndex; + private StringBuilder css; + + /** + * @param ss + */ + public IncrementalStyleBuilder(Spreadsheet ss, String defaultBorderCSS) { + this.ss = ss; + this.defaultBorderStyleCSS = defaultBorderCSS; + if (ss.getWorkbook() instanceof HSSFWorkbook) { + colorConverter = new HSSFColorConverter((HSSFWorkbook) ss.getWorkbook()); + } else { + colorConverter = new XSSFColorConverter((XSSFWorkbook) ss.getWorkbook()); + } + } + + private ColorConverter getColorConverter() { + return colorConverter; + } + + /** + * @param provider + * @param baseIndex border indexes are based off this, which should be at least 3 digits different than any other kind of style index + */ + public void addStyleForRule(DifferentialStyleProvider provider, int baseIndex) { + this.styleProvider = provider; + this.baseCSSIndex = baseIndex; + css = new StringBuilder(); + + FontFormatting fontFormatting = provider.getFontFormatting(); + + if (fontFormatting != null) { + String fontColorCSS = getColorConverter().getFontColorCSS(provider); + if (fontColorCSS != null) { + css.append("color:" + fontColorCSS); + } + + // we can't have both underline and line-through in the same + // DIV element, so use the first one that matches. + + // HSSF might return 255 for 'none'... + if (fontFormatting.getUnderlineType() != FontFormatting.U_NONE && fontFormatting.getUnderlineType() != 255) { + css.append("text-decoration: underline;"); + } + if (fontFormatting.isStruckout()) { + css.append("text-decoration: line-through;"); + } + + if (fontFormatting.getFontHeight() != -1) { + // Excel stores height in 1/20th points, convert + int fontHeight = fontFormatting.getFontHeight() / 20; + css.append("font-size:" + fontHeight + "pt;"); + } + + // excel has a setting for bold italic, otherwise bold + // overrides + // italic and vice versa + if (fontFormatting.isItalic() && fontFormatting.isBold()) { + css.append("font-style: italic;"); + css.append("font-weight: bold;"); + } else if (fontFormatting.isItalic()) { + css.append("font-style: italic;"); + css.append("font-weight: initial;"); + } else if (fontFormatting.isBold()) { + css.append("font-style: normal;"); + css.append("font-weight: bold;"); + } + } + + PatternFormatting patternFormatting = provider.getPatternFormatting(); + if (patternFormatting != null) { + String colorCSS = getColorConverter().getBackgroundColorCSS(provider); + + if (colorCSS != null) { + css.append("background-color:" + colorCSS); + } + } + + addBorderFormatting(); + + addCssToComponentState(baseCSSIndex, css); + } + + private void addBorderFormatting() { + BorderFormatting borderFormatting = styleProvider.getBorderFormatting(); + + if (borderFormatting == null) return; + + BorderStyle borderLeft = borderFormatting.getBorderLeftEnum(); + BorderStyle borderRight = borderFormatting.getBorderRightEnum(); + BorderStyle borderTop = borderFormatting.getBorderTopEnum(); + BorderStyle borderBottom = borderFormatting.getBorderBottomEnum(); + // for range styles, like tables, these are the internal grid lines, bottom and left + BorderStyle borderHorizontal = borderFormatting.getBorderHorizontalEnum(); + BorderStyle borderVertical = borderFormatting.getBorderVerticalEnum(); + + // In Excel, we can set a border to 'none', which overrides previous + // rules. Default is 'not set', in which case we add no CSS. + boolean isLeftSet = borderLeft != BorderStyle.NONE; + boolean isTopSet = borderTop != BorderStyle.NONE; + boolean isRightSet = borderRight != BorderStyle.NONE; + boolean isBottomSet = borderBottom != BorderStyle.NONE; + boolean isHorizontalSet = borderHorizontal != BorderStyle.NONE; + boolean isVerticalSet = borderVertical != BorderStyle.NONE; + + // bottom/right/horizontal/vertical go on the cell itself but are defined as their own styles for flexibility - for table styles, not all cells get all styles + if (isRightSet) { + final StringBuilder sb2 = new StringBuilder("border-right:"); + if (borderRight != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderRight)); + sb2.append(getColorConverter().getBorderColorCSS(BorderSide.RIGHT, "border-right-color", borderFormatting)); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.RIGHT.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + if (isBottomSet) { + final StringBuilder sb2 = new StringBuilder("border-bottom:"); + if (borderBottom != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderBottom)); + sb2.append(getColorConverter().getBorderColorCSS(BorderSide.BOTTOM, "border-bottom-color", borderFormatting)); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.BOTTOM.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + if (isVerticalSet) { + final StringBuilder sb2 = new StringBuilder("border-right:"); + if (borderVertical != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderVertical)); + sb2.append(getColorConverter().getBorderColorCSS(BorderSide.RIGHT, "border-right-color", borderFormatting)); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.VERTICAL.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + if (isHorizontalSet) { + final StringBuilder sb2 = new StringBuilder("border-bottom:"); + if (borderHorizontal != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderHorizontal)); + sb2.append(getColorConverter().getBorderColorCSS("border-bottom-color", XSSFColor.toXSSFColor(borderFormatting.getHorizontalBorderColorColor()))); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.HORIZONTAL.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + + // top and left borders might be applied to another cell, so store + // them with a different index + if (isTopSet) { + // bottom border for cell above + final StringBuilder sb2 = new StringBuilder("border-bottom:"); + if (borderTop != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderTop)); + sb2.append(getColorConverter().getBorderColorCSS(BorderSide.TOP, "border-bottom-color", borderFormatting)); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.TOP.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + + if (isLeftSet) { + // right border for cell to the left + final StringBuilder sb2 = new StringBuilder("border-right:"); + if (borderLeft != BorderStyle.NONE) { + sb2.append(getBorderStyleCss(borderLeft)); + sb2.append(getColorConverter().getBorderColorCSS(BorderSide.LEFT, "border-right-color", borderFormatting)); + + addCssToComponentState(baseCSSIndex - IncrementalStyleBuilder.StyleType.LEFT.offsetFromBase(), sb2); + } else { + css.append(defaultBorderStyleCSS); + } + } + } + + /** + * @param border + * @return border style CSS string + */ + protected String getBorderStyleCss(BorderStyle border) { + return SpreadsheetStyleFactory.BORDER.get(border).getBorderAttributeValue(); + } + + /** + * @param index + * @param cssString - may not be the main CSS, may be a distinct border CSS for a neighboring cell + */ + protected void addCssToComponentState(int index, StringBuilder cssString) { + ss.getState().conditionalFormattingStyles.put(new Integer(index), cssString.toString()); + } + + +} diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/SpreadsheetStyleFactory.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/SpreadsheetStyleFactory.java index 84e26b816..9682228fe 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/SpreadsheetStyleFactory.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/SpreadsheetStyleFactory.java @@ -500,7 +500,10 @@ public void loadCustomBorderStylesToState() { * Reloads all styles for the currently active sheet. */ public void reloadActiveSheetCellStyles() { - // need to remove the cell identifiers (css selectors from the shifted + // conditional formatting, needs to be run first, or shifted borders may not get displayed. + spreadsheet.getConditionalFormatter().createConditionalFormatterRules(); + + // need to remove the cell identifiers (css selectors from the shifted // border style rules for (Entry entry : shiftedBorderLeftStyles.entrySet()) { String value = entry.getValue(); @@ -551,10 +554,6 @@ public void reloadActiveSheetCellStyles() { entry.getValue())); } - - // conditional formatting - spreadsheet.getConditionalFormatter().createConditionalFormatterRules(); - } @SuppressWarnings({ "unchecked" }) diff --git a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/XSSFColorConverter.java b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/XSSFColorConverter.java index 6c9e21746..a34d87a90 100644 --- a/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/XSSFColorConverter.java +++ b/vaadin-spreadsheet/src/main/java/com/vaadin/addon/spreadsheet/XSSFColorConverter.java @@ -18,27 +18,27 @@ */ import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined; import org.apache.poi.ss.usermodel.BorderFormatting; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Color; import org.apache.poi.ss.usermodel.ConditionalFormattingRule; +import org.apache.poi.ss.usermodel.DifferentialStyleProvider; +import org.apache.poi.ss.usermodel.FontFormatting; +import org.apache.poi.ss.usermodel.PatternFormatting; import org.apache.poi.xssf.model.ThemesTable; import org.apache.poi.xssf.usermodel.XSSFBorderFormatting; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFColor; -import org.apache.poi.xssf.usermodel.XSSFConditionalFormattingRule; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder; import org.apache.poi.xssf.usermodel.extensions.XSSFCellBorder.BorderSide; import org.apache.poi.xssf.usermodel.extensions.XSSFCellFill; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBorder; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCfRule; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTColor; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDxf; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXf; /** @@ -111,12 +111,14 @@ public String getBorderColorCSS(BorderSide borderSide, String attr, sb.append(attr); sb.append(":"); - if (color == null || color.isAuto()) { - sb.append("#000;"); - return sb.toString(); + + if (color == null || color.isAuto() || (color.isIndexed() && + color.getIndex() == HSSFColorPredefined.AUTOMATIC.getIndex()) ) { + sb.append("#000;"); + return sb.toString(); } - byte[] argb = color.getARGB(); + byte[] argb = color.getARGB(); if (argb == null) { sb.append("#000;"); return sb.toString(); @@ -142,32 +144,19 @@ public String getBorderColorCSS(BorderSide borderSide, String attr, } @Override - public String getBorderColorCSS(BorderSide borderSide, String attr, - BorderFormatting format) { - - XSSFBorderFormatting casted = (XSSFBorderFormatting) format; - - // getXBorderColor methods are useless with XSSF, so we need to dig - // deeper. - CTColor color = getBorderColor(casted, borderSide); + public String getBorderColorCSS(String attr, Color colorInstance) { + final XSSFColor color = (XSSFColor) colorInstance; StringBuilder sb = new StringBuilder(); sb.append(attr); sb.append(":"); - if (color == null || color.getAuto()) { + if (color == null || color.isAuto()) { sb.append("#000;"); return sb.toString(); } - byte[] argb; - if (color.isSetTheme()) { - XSSFColor themeColor = workbook.getTheme().getThemeColor( - (int) color.getTheme()); - argb = themeColor.getARGB(); - } else { - argb = color.getRgb(); - } + byte[] argb = color.getRGB(); if (argb == null) { sb.append("#000;"); @@ -193,6 +182,25 @@ public String getBorderColorCSS(BorderSide borderSide, String attr, return sb.toString(); } + @Override + public String getBorderColorCSS(BorderSide borderSide, String attr, + BorderFormatting format) { + + switch (borderSide) { + case BOTTOM: + return getBorderColorCSS(attr, XSSFColor.toXSSFColor(format.getBottomBorderColorColor())); + case LEFT: + return getBorderColorCSS(attr, XSSFColor.toXSSFColor(format.getLeftBorderColorColor())); + case RIGHT: + return getBorderColorCSS(attr, XSSFColor.toXSSFColor(format.getRightBorderColorColor())); + case TOP: + return getBorderColorCSS(attr, XSSFColor.toXSSFColor(format.getTopBorderColorColor())); + default: + return ""; // unused, but needed for compilation + } + + } + private CTColor getBorderColor(XSSFBorderFormatting casted, BorderSide borderSide) { @@ -217,22 +225,7 @@ private CTColor getBorderColor(XSSFBorderFormatting casted, default: break; } - } catch (IllegalArgumentException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation; unable to parse border color", - e); - } catch (IllegalAccessException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation; unable to parse border color", - e); - } catch (NoSuchFieldException e) { - LOGGER.log( - Level.SEVERE, - "Incompatible POI implementation; unable to parse border color", - e); - } catch (SecurityException e) { + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { LOGGER.log( Level.SEVERE, "Incompatible POI implementation; unable to parse border color", @@ -286,54 +279,58 @@ public boolean hasBackgroundColor(CellStyle cellStyle) { } @Override - public String getBackgroundColorCSS(ConditionalFormattingRule rule) { - - XSSFConditionalFormattingRule r = (XSSFConditionalFormattingRule) rule; - CTDxf dxf = getXMLColorDataWithReflection(r); - if (dxf.isSetFill() && dxf.getFill().isSetPatternFill() - && dxf.getFill().getPatternFill().isSetBgColor()) { - CTColor bgColor = dxf.getFill().getPatternFill().getBgColor(); - - if (bgColor.isSetTheme()) { - XSSFColor themeColor = workbook.getTheme() - .getThemeColor((int) bgColor.getTheme()); + /** + * @param styleProvider general interface to support conditional format rules and table styles + * @return CSS color string, or null if none found + */ + public String getBackgroundColorCSS(DifferentialStyleProvider styleProvider) { + PatternFormatting fillFmt = styleProvider.getPatternFormatting(); + if (fillFmt == null) return null; + + XSSFColor color = (XSSFColor) fillFmt.getFillBackgroundColorColor(); - // CF rules have tint in bgColor but not the XSSFColor. - return styleColor(themeColor, bgColor.getTint()); - } else { - byte[] rgb = bgColor.getRgb(); - return rgb == null ? null : ColorConverterUtil.toRGBA(rgb); - } - } else { - return null; - } + return getColorCSS(color); } @Override - public String getFontColorCSS(ConditionalFormattingRule rule) { + public String getBackgroundColorCSS(ConditionalFormattingRule rule) { + return getBackgroundColorCSS((DifferentialStyleProvider) rule); + } - XSSFConditionalFormattingRule r = (XSSFConditionalFormattingRule) rule; + /** + * @param styleProvider + * @return CSS or null + */ + public String getFontColorCSS(DifferentialStyleProvider styleProvider) { - CTDxf dxf = getXMLColorDataWithReflection(r); - CTFont font = dxf.getFont(); + FontFormatting font = styleProvider.getFontFormatting(); - if (font.getColorList() == null || font.getColorList().isEmpty()) { - // default color - return null; - } + if (font == null) return null; + + XSSFColor color = (XSSFColor) font.getFontColor(); + return getColorCSS(color); + } - CTColor ctColor = font.getColorList().get(0); + @Override + public String getFontColorCSS(ConditionalFormattingRule rule) { + return getFontColorCSS((DifferentialStyleProvider) rule); + } - if (ctColor.isSetTheme()) { - XSSFColor themeColor = workbook.getTheme().getThemeColor( - (int) ctColor.getTheme()); + /** + * @param color + * @return CSS or null + */ + public String getColorCSS(XSSFColor color) { + if (color == null || color.getCTColor() == null) return null; - return styleColor(themeColor, ctColor.getTint()); + if (color.isThemed()) { + XSSFColor themeColor = workbook.getTheme().getThemeColor(color.getTheme()); + // apply tint from the style, it isn't in the theme. + return styleColor(themeColor, color.getTint()); } else { - byte[] rgb = ctColor.getRgb(); - return rgb == null ? null : ColorConverterUtil.toRGBA(rgb); + byte[] rgb = color.getARGB(); + return rgb == null ? null : ColorConverterUtil.toRGBA(rgb); } - } private XSSFColor getFillColor(XSSFCellStyle cs) { @@ -368,7 +365,7 @@ private String styleColor(XSSFColor color) { return styleColor(color, color == null ? 0.0 : color.getTint()); } - private String styleColor(XSSFColor color, double tint) { + protected String styleColor(XSSFColor color, double tint) { if (color == null || color.isAuto()) { return null; } @@ -404,34 +401,4 @@ private byte applyTint(int lum, double tint) { } } - /** - * XSSF doesn't have an API to get this value, so brute force it is.. - * - * @param rule - * The rule that has color data defined - * @return OpenXML data format that contains the real defined styles - */ - private CTDxf getXMLColorDataWithReflection( - XSSFConditionalFormattingRule rule) { - CTCfRule realRule = null; - - Method declaredMethod = null; - try { - declaredMethod = rule.getClass().getDeclaredMethod("getCTCfRule"); - declaredMethod.setAccessible(true); - realRule = (CTCfRule) declaredMethod.invoke(rule); - } catch (Exception e) { - LOGGER.fine(e.getMessage()); - } finally { - if (declaredMethod != null) { - declaredMethod.setAccessible(false); - } - } - - CTDxf dxf = workbook.getStylesSource().getCTStylesheet().getDxfs() - .getDxfArray((int) realRule.getDxfId()); - - return dxf; - } - } \ No newline at end of file diff --git a/vaadin-spreadsheet/src/test/java/com/vaadin/addon/spreadsheet/test/ConditionalFormattingFormulaUpdateOrderTest.java b/vaadin-spreadsheet/src/test/java/com/vaadin/addon/spreadsheet/test/ConditionalFormattingFormulaUpdateOrderTest.java new file mode 100644 index 000000000..45d5024d2 --- /dev/null +++ b/vaadin-spreadsheet/src/test/java/com/vaadin/addon/spreadsheet/test/ConditionalFormattingFormulaUpdateOrderTest.java @@ -0,0 +1,31 @@ +package com.vaadin.addon.spreadsheet.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import com.vaadin.addon.spreadsheet.test.pageobjects.SpreadsheetPage; + +public class ConditionalFormattingFormulaUpdateOrderTest + extends AbstractSpreadsheetTestCase { + + private static final String FALSE_CONDITION_COLOR = "rgba(255, 255, 255, 1)"; + private static final String TRUE_CONDITION_COLOR = "rgba(255, 199, 206, 1)"; + + private SpreadsheetPage spreadsheetPage; + + @Override + public void setUp() throws Exception { + super.setUp(); + spreadsheetPage = headerPage.loadFile( + "conditional_formatting_formula_cells_update_order.xlsx", this); + spreadsheetPage.selectSheetAt(0); + } + + @Test + public void loadSpreadsheetWithConditionalFormatting_MakeConditionTrue_CellB1FilledRed() { + assertEquals("4", spreadsheetPage.getCellValue("B1")); + spreadsheetPage.setCellValue("A1", "6"); + assertEquals(TRUE_CONDITION_COLOR, spreadsheetPage.getCellColor("B1")); + } + +} diff --git a/vaadin-spreadsheet/src/test/resources/test_sheets/conditional_formatting_formula_cells_update_order.xlsx b/vaadin-spreadsheet/src/test/resources/test_sheets/conditional_formatting_formula_cells_update_order.xlsx new file mode 100644 index 000000000..1a156118e Binary files /dev/null and b/vaadin-spreadsheet/src/test/resources/test_sheets/conditional_formatting_formula_cells_update_order.xlsx differ