diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index c054f65be1..ecf151967e 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -37,6 +37,7 @@ + 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas Optimised the FormulaEvaluator to take cell dependencies into account 16936 - Initial support for whole-row cell styling Update hssf.extractor.ExcelExtractor to optionally output blank cells too diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index b416d344fa..e3c9698600 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas Optimised the FormulaEvaluator to take cell dependencies into account 16936 - Initial support for whole-row cell styling Update hssf.extractor.ExcelExtractor to optionally output blank cells too diff --git a/src/java/org/apache/poi/hssf/model/LinkTable.java b/src/java/org/apache/poi/hssf/model/LinkTable.java index 2caab0fb56..998712e5e8 100755 --- a/src/java/org/apache/poi/hssf/model/LinkTable.java +++ b/src/java/org/apache/poi/hssf/model/LinkTable.java @@ -29,6 +29,7 @@ import org.apache.poi.hssf.record.ExternalNameRecord; import org.apache.poi.hssf.record.NameRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.record.SupBookRecord; +import org.apache.poi.hssf.record.UnicodeString; import org.apache.poi.hssf.record.formula.NameXPtg; /** @@ -109,8 +110,8 @@ final class LinkTable { temp.toArray(_crnBlocks); } - public ExternalBookBlock(short numberOfSheets) { - _externalBookRecord = SupBookRecord.createInternalReferences(numberOfSheets); + public ExternalBookBlock(int numberOfSheets) { + _externalBookRecord = SupBookRecord.createInternalReferences((short)numberOfSheets); _externalNameRecords = new ExternalNameRecord[0]; _crnBlocks = new CRNBlock[0]; } @@ -197,7 +198,7 @@ final class LinkTable { return ExternSheetRecord.combine(esrs); } - public LinkTable(short numberOfSheets, WorkbookRecordList workbookRecordList) { + public LinkTable(int numberOfSheets, WorkbookRecordList workbookRecordList) { _workbookRecordList = workbookRecordList; _definedNames = new ArrayList(); _externalBookBlocks = new ExternalBookBlock[] { @@ -303,8 +304,62 @@ final class LinkTable { return lastName.getSheetNumber() == firstName.getSheetNumber(); } - - public int getIndexToSheet(int extRefIndex) { + public String[] getExternalBookAndSheetName(int extRefIndex) { + int ebIx = _externSheetRecord.getExtbookIndexFromRefIndex(extRefIndex); + SupBookRecord ebr = _externalBookBlocks[ebIx].getExternalBookRecord(); + if (!ebr.isExternalReferences()) { + return null; + } + int shIx = _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); + UnicodeString usSheetName = ebr.getSheetNames()[shIx]; + return new String[] { + ebr.getURL(), + usSheetName.getString(), + }; + } + + public int getExternalSheetIndex(String workbookName, String sheetName) { + SupBookRecord ebrTarget = null; + int externalBookIndex = -1; + for (int i=0; i<_externalBookBlocks.length; i++) { + SupBookRecord ebr = _externalBookBlocks[i].getExternalBookRecord(); + if (!ebr.isExternalReferences()) { + continue; + } + if (workbookName.equals(ebr.getURL())) { // not sure if 'equals()' works when url has a directory + ebrTarget = ebr; + externalBookIndex = i; + break; + } + } + if (ebrTarget == null) { + throw new RuntimeException("No external workbook with name '" + workbookName + "'"); + } + int sheetIndex = getSheetIndex(ebrTarget.getSheetNames(), sheetName); + + int result = _externSheetRecord.getRefIxForSheet(externalBookIndex, sheetIndex); + if (result < 0) { + throw new RuntimeException("ExternSheetRecord does not contain combination (" + + externalBookIndex + ", " + sheetIndex + ")"); + } + return result; + } + + private static int getSheetIndex(UnicodeString[] sheetNames, String sheetName) { + for (int i = 0; i < sheetNames.length; i++) { + if (sheetNames[i].getString().equals(sheetName)) { + return i; + } + + } + throw new RuntimeException("External workbook does not contain sheet '" + sheetName + "'"); + } + + /** + * @param extRefIndex as from a {@link Ref3DPtg} or {@link Area3DPtg} + * @return -1 if the reference is to an external book + */ + public int getIndexToInternalSheet(int extRefIndex) { return _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); } @@ -315,20 +370,26 @@ final class LinkTable { return _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); } - public int addSheetIndexToExternSheet(int sheetNumber) { - // TODO - what about the first parameter (extBookIndex)? - return _externSheetRecord.addRef(0, sheetNumber, sheetNumber); - } - - public short checkExternSheet(int sheetIndex) { + public int checkExternSheet(int sheetIndex) { + int thisWbIndex = -1; // this is probably always zero + for (int i=0; i<_externalBookBlocks.length; i++) { + SupBookRecord ebr = _externalBookBlocks[i].getExternalBookRecord(); + if (ebr.isInternalReferences()) { + thisWbIndex = i; + break; + } + } + if (thisWbIndex < 0) { + throw new RuntimeException("Could not find 'internal references' EXTERNALBOOK"); + } //Trying to find reference to this sheet - int i = _externSheetRecord.getRefIxForSheet(sheetIndex); + int i = _externSheetRecord.getRefIxForSheet(thisWbIndex, sheetIndex); if (i>=0) { - return (short)i; + return i; } - //We Haven't found reference to this sheet - return (short)addSheetIndexToExternSheet((short) sheetIndex); + //We haven't found reference to this sheet + return _externSheetRecord.addRef(thisWbIndex, sheetIndex, sheetIndex); } diff --git a/src/java/org/apache/poi/hssf/model/Workbook.java b/src/java/org/apache/poi/hssf/model/Workbook.java index dbf7ecf7e1..0728d0e29f 100644 --- a/src/java/org/apache/poi/hssf/model/Workbook.java +++ b/src/java/org/apache/poi/hssf/model/Workbook.java @@ -26,6 +26,7 @@ import org.apache.poi.ddf.*; import org.apache.poi.hssf.record.*; import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.util.HSSFColor; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -328,9 +329,9 @@ public final class Workbook implements Model { for ( int k = 0; k < nBoundSheets; k++ ) { BoundSheetRecord bsr = retval.createBoundSheet(k); - records.add(bsr); - retval.boundsheets.add(bsr); - retval.records.setBspos(records.size() - 1); + records.add(bsr); + retval.boundsheets.add(bsr); + retval.records.setBspos(records.size() - 1); } // retval.records.supbookpos = retval.records.bspos + 1; // retval.records.namepos = retval.records.supbookpos + 2; @@ -586,19 +587,19 @@ public final class Workbook implements Model { * @param hidden 0 for not hidden, 1 for hidden, 2 for very hidden */ public void setSheetHidden(int sheetnum, int hidden) { - BoundSheetRecord bsr = getBoundSheetRec(sheetnum); - boolean h = false; - boolean vh = false; - if(hidden == 0) { - } else if(hidden == 1) { - h = true; - } else if(hidden == 2) { - vh = true; - } else { - throw new IllegalArgumentException("Invalid hidden flag " + hidden + " given, must be 0, 1 or 2"); - } - bsr.setHidden(h); - bsr.setVeryHidden(vh); + BoundSheetRecord bsr = getBoundSheetRec(sheetnum); + boolean h = false; + boolean vh = false; + if(hidden == 0) { + } else if(hidden == 1) { + h = true; + } else if(hidden == 2) { + vh = true; + } else { + throw new IllegalArgumentException("Invalid hidden flag " + hidden + " given, must be 0, 1 or 2"); + } + bsr.setHidden(h); + bsr.setVeryHidden(vh); } @@ -761,23 +762,23 @@ public final class Workbook implements Model { * have a Style set. */ public StyleRecord getStyleRecord(int xfIndex) { - // Style records always follow after - // the ExtendedFormat records - boolean done = false; - for(int i=records.getXfpos(); i */ final class CellLocation { public static final CellLocation[] EMPTY_ARRAY = { }; + private final EvaluationWorkbook _book; private final int _sheetIndex; private final int _rowIndex; private final int _columnIndex; private final int _hashCode; - public CellLocation(int sheetIndex, int rowIndex, int columnIndex) { + public CellLocation(EvaluationWorkbook book, int sheetIndex, int rowIndex, int columnIndex) { if (sheetIndex < 0) { throw new IllegalArgumentException("sheetIndex must not be negative"); } + _book = book; _sheetIndex = sheetIndex; _rowIndex = rowIndex; _columnIndex = columnIndex; - _hashCode = sheetIndex + 17 * (rowIndex + 17 * columnIndex); + _hashCode = System.identityHashCode(book) + sheetIndex + 17 * (rowIndex + 17 * columnIndex); + } + public Object getBook() { + return _book; } public int getSheetIndex() { return _sheetIndex; @@ -49,15 +56,18 @@ final class CellLocation { public boolean equals(Object obj) { CellLocation other = (CellLocation) obj; - if (getSheetIndex() != other.getSheetIndex()) { - return false; - } if (getRowIndex() != other.getRowIndex()) { return false; } if (getColumnIndex() != other.getColumnIndex()) { return false; } + if (getSheetIndex() != other.getSheetIndex()) { + return false; + } + if (getBook() != other.getBook()) { + return false; + } return true; } public int hashCode() { @@ -68,7 +78,8 @@ final class CellLocation { * @return human readable string for debug purposes */ public String formatAsString() { - return "ShIx=" + getSheetIndex() + " R=" + getRowIndex() + " C=" + getColumnIndex(); + CellReference cr = new CellReference(_rowIndex, _columnIndex, false, false); + return "ShIx=" + getSheetIndex() + " " + cr.formatAsString(); } public String toString() { diff --git a/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java new file mode 100644 index 0000000000..c62d2f182d --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java @@ -0,0 +1,155 @@ +/* ==================================================================== + 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.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Manages a collection of {@link WorkbookEvaluator}s, in order to support evaluation of formulas + * across spreadsheets.

+ * + * For POI internal use only + * + * @author Josh Micich + */ +public final class CollaboratingWorkbooksEnvironment { + + public static final CollaboratingWorkbooksEnvironment EMPTY = new CollaboratingWorkbooksEnvironment(); + + private final Map _evaluatorsByName; + private final WorkbookEvaluator[] _evaluators; + + private boolean _unhooked; + private CollaboratingWorkbooksEnvironment() { + _evaluatorsByName = Collections.EMPTY_MAP; + _evaluators = new WorkbookEvaluator[0]; + } + public static void setup(String[] workbookNames, WorkbookEvaluator[] evaluators) { + int nItems = workbookNames.length; + if (evaluators.length != nItems) { + throw new IllegalArgumentException("Number of workbook names is " + nItems + + " but number of evaluators is " + evaluators.length); + } + if (nItems < 1) { + throw new IllegalArgumentException("Must provide at least one collaborating worbook"); + } + new CollaboratingWorkbooksEnvironment(workbookNames, evaluators, nItems); + } + + private CollaboratingWorkbooksEnvironment(String[] workbookNames, WorkbookEvaluator[] evaluators, int nItems) { + Map m = new HashMap(nItems * 3 / 2); + IdentityHashMap uniqueEvals = new IdentityHashMap(nItems * 3 / 2); + for(int i=0; i0) { + sb.append(", "); + } + sb.append("'").append(i.next()).append("'"); + } + sb.append(")"); + } + throw new RuntimeException(sb.toString()); + } + return result; + } +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCache.java b/src/java/org/apache/poi/ss/formula/EvaluationCache.java index fdc933f6fa..b0c34fd78a 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationCache.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationCache.java @@ -81,13 +81,7 @@ final class EvaluationCache { + cellLoc.formatAsString()); } } - if (_evaluationListener == null) { - // optimisation - don't bother sorting if there is no listener. - } else { - // for testing - // make order of callbacks to listener more deterministic - Arrays.sort(usedCells, CellLocationComparator); - } + sortCellLocationsForLogging(usedCells); CellCacheEntry entry = getEntry(cellLoc); CellLocation[] consumingFormulaCells = entry.getConsumingCells(); CellLocation[] prevUsedCells = entry.getUsedCells(); @@ -110,6 +104,18 @@ final class EvaluationCache { recurseClearCachedFormulaResults(consumingFormulaCells, 0); } + /** + * This method sorts the supplied cellLocs so that the order of call-backs to the evaluation + * listener is more deterministic + */ + private void sortCellLocationsForLogging(CellLocation[] cellLocs) { + if (_evaluationListener == null) { + // optimisation - don't bother sorting if there is no listener. + } else { + Arrays.sort(cellLocs, CellLocationComparator); + } + } + private void unlinkConsumingCells(CellLocation[] prevUsedCells, CellLocation[] usedCells, CellLocation cellLoc) { if (prevUsedCells == null) { @@ -149,6 +155,7 @@ final class EvaluationCache { * @param formulaCells */ private void recurseClearCachedFormulaResults(CellLocation[] formulaCells, int depth) { + sortCellLocationsForLogging(formulaCells); int nextDepth = depth+1; for (int i = 0; i < formulaCells.length; i++) { CellLocation fc = formulaCells[i]; @@ -196,6 +203,10 @@ final class EvaluationCache { CellLocation clB = (CellLocation) b; int cmp; + cmp = System.identityHashCode(clA.getBook()) - System.identityHashCode(clB.getBook()); + if (cmp != 0) { + return cmp; + } cmp = clA.getSheetIndex() - clB.getSheetIndex(); if (cmp != 0) { return cmp; diff --git a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java index 664df2c6ab..41678f1920 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java @@ -31,12 +31,36 @@ import org.apache.poi.hssf.usermodel.HSSFSheet; */ public interface EvaluationWorkbook { String getSheetName(int sheetIndex); + /** + * @return -1 if the specified sheet is from a different book + */ int getSheetIndex(HSSFSheet sheet); + int getSheetIndex(String sheetName); HSSFSheet getSheet(int sheetIndex); + /** + * @return null if externSheetIndex refers to a sheet inside the current workbook + */ + ExternalSheet getExternalSheet(int externSheetIndex); int convertFromExternSheetIndex(int externSheetIndex); EvaluationName getName(NamePtg namePtg); String resolveNameXText(NameXPtg ptg); Ptg[] getFormulaTokens(HSSFCell cell); + + class ExternalSheet { + private final String _workbookName; + private final String _sheetName; + + public ExternalSheet(String workbookName, String sheetName) { + _workbookName = workbookName; + _sheetName = sheetName; + } + public String getWorkbookName() { + return _workbookName; + } + public String getSheetName() { + return _sheetName; + } + } } diff --git a/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java b/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java new file mode 100644 index 0000000000..09262a1320 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java @@ -0,0 +1,29 @@ +/* ==================================================================== + 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; + +/** + * Should be implemented by any {@link Ptg} subclass that needs has an extern sheet index
+ * + * For POI internal use only + * + * @author Josh Micich + */ +public interface ExternSheetReferenceToken { + int getExternSheetIndex(); +} diff --git a/src/java/org/apache/poi/ss/formula/FormulaParser.java b/src/java/org/apache/poi/ss/formula/FormulaParser.java index dd7325a078..0b4b435dff 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParser.java @@ -257,7 +257,7 @@ public final class FormulaParser { */ private Identifier parseIdentifier() { StringBuffer sb = new StringBuffer(); - if (!IsAlpha(look) && look != '\'') { + if (!IsAlpha(look) && look != '\'' && look != '[') { throw expected("Name"); } boolean isQuoted = look == '\''; @@ -276,7 +276,7 @@ public final class FormulaParser { } else { // allow for any sequence of dots and identifier chars // special case of two consecutive dots is best treated in the calling code - while (IsAlNum(look) || look == '.') { + while (IsAlNum(look) || look == '.' || look == '[' || look == ']') { sb.append(look); GetChar(); } @@ -368,7 +368,7 @@ public final class FormulaParser { // 3-D ref // this code assumes iden is a sheetName // TODO - handle ! - int externIdx = book.getExternalSheetIndex(iden.getName()); + int externIdx = getExternalSheetIndex(iden.getName()); String secondIden = parseUnquotedIdentifier(); AreaReference areaRef = parseArea(secondIden); if (areaRef == null) { @@ -418,6 +418,17 @@ public final class FormulaParser { + name + "' is not a range as expected"); } + private int getExternalSheetIndex(String name) { + if (name.charAt(0) == '[') { + // we have a sheet name qualified with workbook name e.g. '[MyData.xls]Sheet1' + int pos = name.lastIndexOf(']'); // safe because sheet names never have ']' + String wbName = name.substring(1, pos); + String sheetName = name.substring(pos+1); + return book.getExternalSheetIndex(wbName, sheetName); + } + return book.getExternalSheetIndex(name); + } + /** * @param name an 'identifier' like string (i.e. contains alphanums, and dots) * @return null if name cannot be split at a dot @@ -656,7 +667,7 @@ public final class FormulaParser { Match('}'); return arrayNode; } - if (IsAlpha(look) || look == '\''){ + if (IsAlpha(look) || look == '\'' || look == '['){ return parseFunctionReferenceOrName(); } // else - assume number diff --git a/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java b/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java index 69431c2c22..e9be7d1d39 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java @@ -32,6 +32,16 @@ public interface FormulaParsingWorkbook { */ EvaluationName getName(String name); - int getExternalSheetIndex(String sheetName); NameXPtg getNameXPtg(String name); + + /** + * gets the externSheet index for a sheet from this workbook + */ + int getExternalSheetIndex(String sheetName); + /** + * gets the externSheet index for a sheet from an external workbook + * @param workbookName e.g. "Budget.xls" + * @param sheetName a name of a sheet in that workbook + */ + int getExternalSheetIndex(String workbookName, String sheetName); } diff --git a/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java b/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java index ac95f4da0f..c9b95f6b1c 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java @@ -19,6 +19,7 @@ package org.apache.poi.ss.formula; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NameXPtg; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; /** * Abstracts a workbook for the purpose of converting formula to text.
@@ -29,6 +30,10 @@ import org.apache.poi.hssf.record.formula.NameXPtg; */ public interface FormulaRenderingWorkbook { + /** + * @return null if externSheetIndex refers to a sheet inside the current workbook + */ + ExternalSheet getExternalSheet(int externSheetIndex); String getSheetNameByExternSheet(int externSheetIndex); String resolveNameXText(NameXPtg nameXPtg); String getNameText(NamePtg namePtg); diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index e958649695..724cf2cf2b 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -63,6 +63,7 @@ import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; /** * Evaluates formula cells.

@@ -75,13 +76,14 @@ import org.apache.poi.hssf.util.CellReference; * * @author Josh Micich */ -public class WorkbookEvaluator { +public final class WorkbookEvaluator { private final EvaluationWorkbook _workbook; - private final EvaluationCache _cache; + private EvaluationCache _cache; private final IEvaluationListener _evaluationListener; private final Map _sheetIndexesBySheet; + private CollaboratingWorkbooksEnvironment _collaboratingWorkbookEnvironment; public WorkbookEvaluator(EvaluationWorkbook workbook) { this (workbook, null); @@ -91,6 +93,7 @@ public class WorkbookEvaluator { _evaluationListener = evaluationListener; _cache = new EvaluationCache(evaluationListener); _sheetIndexesBySheet = new IdentityHashMap(); + _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; } /** @@ -108,7 +111,22 @@ public class WorkbookEvaluator { System.out.println(s); } } + /* package */ void attachToEnvironment(CollaboratingWorkbooksEnvironment collaboratingWorkbooksEnvironment, EvaluationCache cache) { + _collaboratingWorkbookEnvironment = collaboratingWorkbooksEnvironment; + _cache = cache; + } + /* package */ CollaboratingWorkbooksEnvironment getEnvironment() { + return _collaboratingWorkbookEnvironment; + } + /* package */ void detachFromEnvironment() { + _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; + _cache = new EvaluationCache(_evaluationListener); + } + /* package */ IEvaluationListener getEvaluationListener() { + return _evaluationListener; + } + /** * Should be called whenever there are changes to input cells in the evaluated workbook. * Failure to call this method after changing cell values will cause incorrect behaviour @@ -130,7 +148,7 @@ public class WorkbookEvaluator { throw new IllegalArgumentException("value must not be null"); } int sheetIndex = getSheetIndex(sheet); - _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value); + _cache.setValue(new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value); } /** @@ -139,13 +157,17 @@ public class WorkbookEvaluator { */ public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) { int sheetIndex = getSheetIndex(sheet); - _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); + _cache.setValue(new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); } private int getSheetIndex(HSSFSheet sheet) { Integer result = (Integer) _sheetIndexesBySheet.get(sheet); if (result == null) { - result = new Integer(_workbook.getSheetIndex(sheet)); + int sheetIndex = _workbook.getSheetIndex(sheet); + if (sheetIndex < 0) { + throw new RuntimeException("Specified sheet from a different book"); + } + result = new Integer(sheetIndex); _sheetIndexesBySheet.put(sheet, result); } return result.intValue(); @@ -153,7 +175,7 @@ public class WorkbookEvaluator { public ValueEval evaluate(HSSFCell srcCell) { int sheetIndex = getSheetIndex(srcCell.getSheet()); - CellLocation cellLoc = new CellLocation(sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); + CellLocation cellLoc = new CellLocation(_workbook, sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); return internalEvaluate(srcCell, cellLoc, new EvaluationTracker(_cache)); } @@ -342,6 +364,20 @@ public class WorkbookEvaluator { } return operation.evaluate(ops, srcRowNum, (short)srcColNum); } + private SheetRefEvaluator createExternSheetRefEvaluator(EvaluationTracker tracker, + ExternSheetReferenceToken ptg) { + int externSheetIndex = ptg.getExternSheetIndex(); + ExternalSheet externalSheet = _workbook.getExternalSheet(externSheetIndex); + if (externalSheet != null) { + WorkbookEvaluator otherEvaluator = _collaboratingWorkbookEnvironment.getWorkbookEvaluator(externalSheet.getWorkbookName()); + EvaluationWorkbook otherBook = otherEvaluator._workbook; + int otherSheetIndex = otherBook.getSheetIndex(externalSheet.getSheetName()); + return new SheetRefEvaluator(otherEvaluator, tracker, otherBook, otherSheetIndex); + } + int otherSheetIndex = _workbook.convertFromExternSheetIndex(externSheetIndex); + return new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + + } /** * returns an appropriate Eval impl instance for the Ptg. The Ptg must be @@ -350,6 +386,8 @@ public class WorkbookEvaluator { * passed here! */ private Eval getEvalForPtg(Ptg ptg, int sheetIndex, EvaluationTracker tracker) { + // consider converting all these (ptg instanceof XxxPtg) expressions to (ptg.getClass() == XxxPtg.class) + if (ptg instanceof NamePtg) { // named ranges, macro functions NamePtg namePtg = (NamePtg) ptg; @@ -388,14 +426,12 @@ public class WorkbookEvaluator { } if (ptg instanceof Ref3DPtg) { Ref3DPtg refPtg = (Ref3DPtg) ptg; - int otherSheetIndex = _workbook.convertFromExternSheetIndex(refPtg.getExternSheetIndex()); - SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + SheetRefEvaluator sre = createExternSheetRefEvaluator(tracker, refPtg); return new LazyRefEval(refPtg, sre); } if (ptg instanceof Area3DPtg) { Area3DPtg aptg = (Area3DPtg) ptg; - int otherSheetIndex = _workbook.convertFromExternSheetIndex(aptg.getExternSheetIndex()); - SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + SheetRefEvaluator sre = createExternSheetRefEvaluator(tracker, aptg); return new LazyAreaEval(aptg, sre); } SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, sheetIndex); @@ -435,7 +471,7 @@ public class WorkbookEvaluator { } else { cell = row.getCell(columnIndex); } - CellLocation cellLoc = new CellLocation(sheetIndex, rowIndex, columnIndex); + CellLocation cellLoc = new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex); tracker.acceptDependency(cellLoc); return internalEvaluate(cell, cellLoc, tracker); } diff --git a/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls b/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls new file mode 100644 index 0000000000..b844fc1dba Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls b/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls new file mode 100644 index 0000000000..3e4708d6c9 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls differ diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 29de45405f..53061a228b 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -43,11 +43,13 @@ import org.apache.poi.hssf.record.formula.NumberPtg; import org.apache.poi.hssf.record.formula.PercentPtg; import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.Ref3DPtg; import org.apache.poi.hssf.record.formula.RefPtg; import org.apache.poi.hssf.record.formula.StringPtg; import org.apache.poi.hssf.record.formula.SubtractPtg; import org.apache.poi.hssf.record.formula.UnaryMinusPtg; import org.apache.poi.hssf.record.formula.UnaryPlusPtg; +import org.apache.poi.hssf.usermodel.FormulaExtractor; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFErrorConstants; import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; @@ -913,4 +915,33 @@ public final class TestFormulaParser extends TestCase { assertEquals("'true'!B2", cell.getCellFormula()); } + + public void testParseExternalWorkbookReference() { + HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls"); + HSSFCell cell = wbA.getSheetAt(0).getRow(0).getCell(0); + + // make sure formula in sample is as expected + assertEquals("[multibookFormulaB.xls]BSheet1!B1", cell.getCellFormula()); + Ptg[] expectedPtgs = FormulaExtractor.getPtgs(cell); + confirmSingle3DRef(expectedPtgs, 1); + + // now try (re-)parsing the formula + Ptg[] actualPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]BSheet1!B1", wbA); + confirmSingle3DRef(actualPtgs, 1); // externalSheetIndex 1 -> BSheet1 + + // try parsing a formula pointing to a different external sheet + Ptg[] otherPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]AnotherSheet!B1", wbA); + confirmSingle3DRef(otherPtgs, 0); // externalSheetIndex 0 -> AnotherSheet + + // try setting the same formula in a cell + cell.setCellFormula("[multibookFormulaB.xls]AnotherSheet!B1"); + assertEquals("[multibookFormulaB.xls]AnotherSheet!B1", cell.getCellFormula()); + } + private static void confirmSingle3DRef(Ptg[] ptgs, int expectedExternSheetIndex) { + assertEquals(1, ptgs.length); + Ptg ptg0 = ptgs[0]; + assertEquals(Ref3DPtg.class, ptg0.getClass()); + assertEquals(expectedExternSheetIndex, ((Ref3DPtg)ptg0).getExternSheetIndex()); + } + } diff --git a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java index 3727989efb..e2a61dde79 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java +++ b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java @@ -78,7 +78,7 @@ public final class TestSupBookRecord extends TestCase { assertEquals( 34, record.getRecordSize() ); //sid+size+data - assertEquals("testURL", record.getURL().getString()); + assertEquals("testURL", record.getURL()); UnicodeString[] sheetNames = record.getSheetNames(); assertEquals(2, sheetNames.length); assertEquals("Sheet1", sheetNames[0].getString()); diff --git a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java index 20cc9c1787..0d3e93ee7a 100644 --- a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java +++ b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java @@ -19,6 +19,7 @@ package org.apache.poi.ss.formula; import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.record.formula.AreaErrPtg; import org.apache.poi.hssf.record.formula.AttrPtg; import org.apache.poi.hssf.record.formula.DeletedArea3DPtg; @@ -29,36 +30,40 @@ import org.apache.poi.hssf.record.formula.RefErrorPtg; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; /** - * Tests {@link WorkbookEvaluator}. + * Tests {@link WorkbookEvaluator}. * * @author Josh Micich */ public class TestWorkbookEvaluator extends TestCase { - + /** * Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing - * the whole formula which converts tAttrSum to tFuncVar("SUM") ) + * the whole formula which converts tAttrSum to tFuncVar("SUM") ) */ public void testAttrSum() { - + Ptg[] ptgs = { new IntPtg(42), AttrPtg.SUM, }; - + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0); } - + /** * Make sure that the evaluator can directly handle (deleted) ref error tokens - * (instead of relying on re-parsing the whole formula which converts these - * to the error constant #REF! ) + * (instead of relying on re-parsing the whole formula which converts these + * to the error constant #REF! ) */ public void testRefErr() { - + confirmRefErr(new RefErrorPtg()); confirmRefErr(new AreaErrPtg()); confirmRefErr(new DeletedRef3DPtg(0)); @@ -68,25 +73,82 @@ public class TestWorkbookEvaluator extends TestCase { Ptg[] ptgs = { ptg, }; - + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); assertEquals(ErrorEval.REF_INVALID, result); } - + /** * Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing - * the whole formula which converts tAttrSum to tFuncVar("SUM") ) + * the whole formula which converts tAttrSum to tFuncVar("SUM") ) */ public void testMemFunc() { - + Ptg[] ptgs = { new IntPtg(42), AttrPtg.SUM, }; - + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0); } - - + + + public void testEvaluateMultipleWorkbooks() { + HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls"); + HSSFWorkbook wbB = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaB.xls"); + + HSSFFormulaEvaluator evaluatorA = new HSSFFormulaEvaluator(wbA); + HSSFFormulaEvaluator evaluatorB = new HSSFFormulaEvaluator(wbB); + + // Hook up the workbook evaluators to enable evaluation of formulas across books + String[] bookNames = { "multibookFormulaA.xls", "multibookFormulaB.xls", }; + HSSFFormulaEvaluator[] evaluators = { evaluatorA, evaluatorB, }; + HSSFFormulaEvaluator.setupEnvironment(bookNames, evaluators); + + HSSFCell cell; + + HSSFSheet aSheet1 = wbA.getSheetAt(0); + HSSFSheet bSheet1 = wbB.getSheetAt(0); + + // Simple case - single link from wbA to wbB + confirmFormula(wbA, 0, 0, 0, "[multibookFormulaB.xls]BSheet1!B1"); + cell = aSheet1.getRow(0).getCell(0); + confirmEvaluation(35, evaluatorA, cell); + + + // more complex case - back link into wbA + // [wbA]ASheet1!A2 references (among other things) [wbB]BSheet1!B2 + confirmFormula(wbA, 0, 1, 0, "[multibookFormulaB.xls]BSheet1!$B$2+2*A3"); + // [wbB]BSheet1!B2 references (among other things) [wbA]AnotherSheet!A1:B2 + confirmFormula(wbB, 0, 1, 1, "SUM([multibookFormulaA.xls]AnotherSheet!$A$1:$B$2)+B3"); + + cell = aSheet1.getRow(1).getCell(0); + confirmEvaluation(264, evaluatorA, cell); + + // change [wbB]BSheet1!B3 (from 50 to 60) + bSheet1.getRow(2).getCell(1).setCellValue(60); + evaluatorB.setCachedPlainValue(bSheet1, 2, 1, new NumberEval(60)); + confirmEvaluation(274, evaluatorA, cell); + + // change [wbA]ASheet1!A3 (from 100 to 80) + aSheet1.getRow(2).getCell(0).setCellValue(80); + evaluatorA.setCachedPlainValue(aSheet1, 2, 0, new NumberEval(80)); + confirmEvaluation(234, evaluatorA, cell); + + // change [wbA]AnotherSheet!A1 (from 2 to 3) + wbA.getSheetAt(1).getRow(0).getCell(0).setCellValue(3); + evaluatorA.setCachedPlainValue(wbA.getSheetAt(1), 0, 0, new NumberEval(3)); + confirmEvaluation(235, evaluatorA, cell); + } + + private static void confirmEvaluation(double expectedValue, HSSFFormulaEvaluator fe, HSSFCell cell) { + assertEquals(expectedValue, fe.evaluate(cell).getNumberValue(), 0.0); + } + + private static void confirmFormula(HSSFWorkbook wb, int sheetIndex, int rowIndex, int columnIndex, + String expectedFormula) { + HSSFCell cell = wb.getSheetAt(sheetIndex).getRow(rowIndex).getCell(columnIndex); + assertEquals(expectedFormula, cell.getCellFormula()); + } }