| /* ==================================================================== |
| 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.hssf.record.aggregates; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.poi.common.usermodel.GenericRecord; |
| import org.apache.poi.hssf.model.RecordStream; |
| import org.apache.poi.hssf.record.CFHeader12Record; |
| import org.apache.poi.hssf.record.CFHeaderBase; |
| import org.apache.poi.hssf.record.CFHeaderRecord; |
| import org.apache.poi.hssf.record.CFRule12Record; |
| import org.apache.poi.hssf.record.CFRuleBase; |
| import org.apache.poi.hssf.record.CFRuleRecord; |
| import org.apache.poi.hssf.record.Record; |
| import org.apache.poi.ss.formula.FormulaShifter; |
| import org.apache.poi.ss.formula.ptg.Ptg; |
| import org.apache.poi.ss.usermodel.helpers.BaseRowColShifter; |
| import org.apache.poi.ss.util.CellRangeAddress; |
| import org.apache.poi.util.GenericRecordJsonWriter; |
| import org.apache.poi.util.GenericRecordUtil; |
| import org.apache.poi.util.RecordFormatException; |
| |
| import static org.apache.logging.log4j.util.Unbox.box; |
| |
| /** |
| * <p>CFRecordsAggregate - aggregates Conditional Formatting records CFHeaderRecord |
| * and number of up CFRuleRecord records together to simplify access to them.</p> |
| * <p>Note that Excel versions before 2007 can only cope with a maximum of 3 |
| * Conditional Formatting rules per sheet. Excel 2007 or newer can cope with |
| * unlimited numbers, as can Apache OpenOffice. This is an Excel limitation, |
| * not a file format one.</p> |
| */ |
| public final class CFRecordsAggregate extends RecordAggregate implements GenericRecord { |
| /** Excel 97-2003 allows up to 3 conditional formating rules */ |
| private static final int MAX_97_2003_CONDTIONAL_FORMAT_RULES = 3; |
| private static final Logger LOG = LogManager.getLogger(CFRecordsAggregate.class); |
| |
| private final CFHeaderBase header; |
| |
| /** List of CFRuleRecord objects */ |
| private final List<CFRuleBase> rules = new ArrayList<>(); |
| |
| public CFRecordsAggregate(CFRecordsAggregate other) { |
| header = other.header.copy(); |
| other.rules.stream().map(t -> t.copy()).forEach(rules::add); |
| } |
| |
| private CFRecordsAggregate(CFHeaderBase pHeader, CFRuleBase[] pRules) { |
| if(pHeader == null) { |
| throw new IllegalArgumentException("header must not be null"); |
| } |
| if(pRules == null) { |
| throw new IllegalArgumentException("rules must not be null"); |
| } |
| if(pRules.length > MAX_97_2003_CONDTIONAL_FORMAT_RULES) { |
| LOG.atWarn().log("Excel versions before 2007 require that No more than " + |
| MAX_97_2003_CONDTIONAL_FORMAT_RULES + " rules may be specified, {} were found, this file will " + |
| "cause problems with old Excel versions", box(pRules.length)); |
| } |
| if (pRules.length != pHeader.getNumberOfConditionalFormats()) { |
| throw new RecordFormatException("Mismatch number of rules"); |
| } |
| header = pHeader; |
| for (CFRuleBase pRule : pRules) { |
| checkRuleType(pRule); |
| rules.add(pRule); |
| } |
| } |
| |
| public CFRecordsAggregate(CellRangeAddress[] regions, CFRuleBase[] rules) { |
| this(createHeader(regions, rules), rules); |
| } |
| private static CFHeaderBase createHeader(CellRangeAddress[] regions, CFRuleBase[] rules) { |
| final CFHeaderBase header; |
| if (rules.length == 0 || rules[0] instanceof CFRuleRecord) { |
| header = new CFHeaderRecord(regions, rules.length); |
| } else { |
| header = new CFHeader12Record(regions, rules.length); |
| } |
| |
| // set the "needs recalculate" by default to avoid Excel handling conditional formatting incorrectly |
| // see bug 52122 for details |
| header.setNeedRecalculation(true); |
| |
| return header; |
| } |
| |
| /** |
| * Create CFRecordsAggregate from a list of CF Records |
| * @param rs - the stream to read from |
| * @return CFRecordsAggregate object |
| */ |
| public static CFRecordsAggregate createCFAggregate(RecordStream rs) { |
| Record rec = rs.getNext(); |
| if (rec.getSid() != CFHeaderRecord.sid && |
| rec.getSid() != CFHeader12Record.sid) { |
| throw new IllegalStateException("next record sid was " + rec.getSid() |
| + " instead of " + CFHeaderRecord.sid + " or " + |
| CFHeader12Record.sid + " as expected"); |
| } |
| |
| CFHeaderBase header = (CFHeaderBase)rec; |
| int nRules = header.getNumberOfConditionalFormats(); |
| |
| CFRuleBase[] rules = new CFRuleBase[nRules]; |
| for (int i = 0; i < rules.length; i++) { |
| rules[i] = (CFRuleBase) rs.getNext(); |
| } |
| |
| return new CFRecordsAggregate(header, rules); |
| } |
| |
| /** |
| * Create a deep clone of the record |
| * |
| * @return A new object with the same values as this record |
| */ |
| public CFRecordsAggregate cloneCFAggregate() { |
| return new CFRecordsAggregate(this); |
| } |
| |
| /** |
| * @return the header. Never <code>null</code>. |
| */ |
| public CFHeaderBase getHeader() { |
| return header; |
| } |
| |
| private void checkRuleIndex(int idx) { |
| if(idx < 0 || idx >= rules.size()) { |
| throw new IllegalArgumentException("Bad rule record index (" + idx |
| + ") nRules=" + rules.size()); |
| } |
| } |
| private void checkRuleType(CFRuleBase r) { |
| if (header instanceof CFHeaderRecord && |
| r instanceof CFRuleRecord) { |
| return; |
| } |
| if (header instanceof CFHeader12Record && |
| r instanceof CFRule12Record) { |
| return; |
| } |
| throw new IllegalArgumentException("Header and Rule must both be CF or both be CF12, can't mix"); |
| } |
| |
| public CFRuleBase getRule(int idx) { |
| checkRuleIndex(idx); |
| return rules.get(idx); |
| } |
| public void setRule(int idx, CFRuleBase r) { |
| if (r == null) { |
| throw new IllegalArgumentException("r must not be null"); |
| } |
| checkRuleIndex(idx); |
| checkRuleType(r); |
| rules.set(idx, r); |
| } |
| public void addRule(CFRuleBase r) { |
| if (r == null) { |
| throw new IllegalArgumentException("r must not be null"); |
| } |
| if(rules.size() >= MAX_97_2003_CONDTIONAL_FORMAT_RULES) { |
| LOG.atWarn().log("Excel versions before 2007 cannot cope with" |
| + " any more than " + MAX_97_2003_CONDTIONAL_FORMAT_RULES |
| + " - this file will cause problems with old Excel versions"); |
| } |
| checkRuleType(r); |
| rules.add(r); |
| header.setNumberOfConditionalFormats(rules.size()); |
| } |
| public int getNumberOfRules() { |
| return rules.size(); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "header", this::getHeader, |
| "rules", () -> rules |
| ); |
| } |
| |
| /** |
| * String representation of CFRecordsAggregate |
| */ |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| public void visitContainedRecords(RecordVisitor rv) { |
| rv.visitRecord(header); |
| for (CFRuleBase rule : rules) { |
| rv.visitRecord(rule); |
| } |
| } |
| |
| /** |
| * @param shifter The {@link FormulaShifter} to use |
| * @param currentExternSheetIx The index for extern sheets |
| * |
| * @return <code>false</code> if this whole {@link CFHeaderRecord} / {@link CFRuleRecord}s should be deleted |
| */ |
| public boolean updateFormulasAfterCellShift(FormulaShifter shifter, int currentExternSheetIx) { |
| CellRangeAddress[] cellRanges = header.getCellRanges(); |
| boolean changed = false; |
| List<CellRangeAddress> temp = new ArrayList<>(); |
| for (CellRangeAddress craOld : cellRanges) { |
| CellRangeAddress craNew = BaseRowColShifter.shiftRange(shifter, craOld, currentExternSheetIx); |
| if (craNew == null) { |
| changed = true; |
| continue; |
| } |
| temp.add(craNew); |
| if (craNew != craOld) { |
| changed = true; |
| } |
| } |
| |
| if (changed) { |
| int nRanges = temp.size(); |
| if (nRanges == 0) { |
| return false; |
| } |
| CellRangeAddress[] newRanges = new CellRangeAddress[nRanges]; |
| temp.toArray(newRanges); |
| header.setCellRanges(newRanges); |
| } |
| |
| for (CFRuleBase rule : rules) { |
| Ptg[] ptgs; |
| ptgs = rule.getParsedExpression1(); |
| if (ptgs != null && shifter.adjustFormula(ptgs, currentExternSheetIx)) { |
| rule.setParsedExpression1(ptgs); |
| } |
| ptgs = rule.getParsedExpression2(); |
| if (ptgs != null && shifter.adjustFormula(ptgs, currentExternSheetIx)) { |
| rule.setParsedExpression2(ptgs); |
| } |
| if (rule instanceof CFRule12Record) { |
| CFRule12Record rule12 = (CFRule12Record)rule; |
| ptgs = rule12.getParsedExpressionScale(); |
| if (ptgs != null && shifter.adjustFormula(ptgs, currentExternSheetIx)) { |
| rule12.setParsedExpressionScale(ptgs); |
| } |
| } |
| } |
| return true; |
| } |
| } |