From a5540e0dfbe363de5ca60ba50e33a725fa3efc8f Mon Sep 17 00:00:00 2001 From: Javen O'Neal Date: Sat, 31 Oct 2015 11:57:39 +0000 Subject: [PATCH] bug58452: set cell formulas containing unregistered function names git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1711605 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/ss/formula/FormulaParser.java | 61 +++++++++-- .../poi/ss/formula/TestFormulaParser.java | 96 ++++++++++++++++++ .../poi/hssf/model/TestFormulaParser.java | 86 +++++++++++++--- .../ss/formula/BaseTestExternalFunctions.java | 17 +++- .../apache/poi/ss/usermodel/BaseTestCell.java | 39 +++++++ test-data/spreadsheet/testNames.xlsm | Bin 0 -> 13290 bytes 6 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 test-data/spreadsheet/testNames.xlsm diff --git a/src/java/org/apache/poi/ss/formula/FormulaParser.java b/src/java/org/apache/poi/ss/formula/FormulaParser.java index 18981ec725..dddf2bbbbf 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParser.java @@ -50,6 +50,7 @@ import org.apache.poi.ss.formula.ptg.MissingArgPtg; import org.apache.poi.ss.formula.ptg.MultiplyPtg; import org.apache.poi.ss.formula.ptg.NamePtg; import org.apache.poi.ss.formula.ptg.NameXPtg; +import org.apache.poi.ss.formula.ptg.NameXPxg; import org.apache.poi.ss.formula.ptg.NotEqualPtg; import org.apache.poi.ss.formula.ptg.NumberPtg; import org.apache.poi.ss.formula.ptg.OperandPtg; @@ -67,9 +68,12 @@ import org.apache.poi.ss.formula.ptg.UnaryPlusPtg; import org.apache.poi.ss.formula.ptg.UnionPtg; import org.apache.poi.ss.formula.ptg.ValueOperatorPtg; import org.apache.poi.ss.usermodel.ErrorConstants; +import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.ss.util.CellReference.NameType; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; /** * This class parses a formula string into a List of tokens in RPN order. @@ -85,6 +89,7 @@ import org.apache.poi.ss.util.CellReference.NameType; *

*/ public final class FormulaParser { + private final static POILogger log = POILogFactory.getLogger(FormulaParser.class); private final String _formulaString; private final int _formulaLength; /** points at the next character to be read (after the {@link #look} char) */ @@ -108,10 +113,10 @@ public final class FormulaParser { */ private boolean _inIntersection = false; - private FormulaParsingWorkbook _book; - private SpreadsheetVersion _ssVersion; + private final FormulaParsingWorkbook _book; + private final SpreadsheetVersion _ssVersion; - private int _sheetIndex; + private final int _sheetIndex; /** @@ -137,6 +142,7 @@ public final class FormulaParser { /** * Parse a formula into a array of tokens + * Side effect: creates name (Workbook.createName) if formula contains unrecognized names (names are likely UDFs) * * @param formula the formula to parse * @param workbook the parent workbook @@ -927,6 +933,8 @@ public final class FormulaParser { * Note - Excel function names are 'case aware but not case sensitive'. This method may end * up creating a defined name record in the workbook if the specified name is not an internal * Excel function, and has not been encountered before. + * + * Side effect: creates workbook name if name is not recognized (name is probably a UDF) * * @param name case preserved function name (as it was entered/appeared in the formula). */ @@ -940,22 +948,42 @@ public final class FormulaParser { // Only test cases omit the book (expecting it not to be needed) throw new IllegalStateException("Need book to evaluate name '" + name + "'"); } + // Check to see if name is a named range in the workbook EvaluationName hName = _book.getName(name, _sheetIndex); - if (hName == null) { - nameToken = _book.getNameXPtg(name, null); - if (nameToken == null) { - throw new FormulaParseException("Name '" + name - + "' is completely unknown in the current workbook"); - } - } else { + if (hName != null) { if (!hName.isFunctionName()) { throw new FormulaParseException("Attempt to use name '" + name + "' as a function, but defined name in workbook does not refer to a function"); } - + // calls to user-defined functions within the workbook // get a Name token which points to a defined name record nameToken = hName.createPtg(); + } else { + // Check if name is an external names table + nameToken = _book.getNameXPtg(name, null); + if (nameToken == null) { + // name is not an internal or external name + if (log.check(POILogger.WARN)) { + log.log(POILogger.WARN, + "FormulaParser.function: Name '" + name + "' is completely unknown in the current workbook."); + } + // name is probably the name of an unregistered User-Defined Function + switch (_book.getSpreadsheetVersion()) { + case EXCEL97: + // HSSFWorkbooks require a name to be added to Workbook defined names table + addName(name); + hName = _book.getName(name, _sheetIndex); + nameToken = hName.createPtg(); + break; + case EXCEL2007: + // XSSFWorkbooks store formula names as strings. + nameToken = new NameXPxg(name); + break; + default: + throw new IllegalStateException("Unexpected spreadsheet version: " + _book.getSpreadsheetVersion().name()); + } + } } } @@ -965,6 +993,17 @@ public final class FormulaParser { return getFunction(name, nameToken, args); } + + /** + * Adds a name (named range or user defined function) to underlying workbook's names table + * @param functionName + */ + private final void addName(String functionName) { + final Name name = _book.createName(); + name.setFunction(true); + name.setNameName(functionName); + name.setSheetIndex(_sheetIndex); + } /** * Generates the variable function ptg for the formula. diff --git a/src/ooxml/testcases/org/apache/poi/ss/formula/TestFormulaParser.java b/src/ooxml/testcases/org/apache/poi/ss/formula/TestFormulaParser.java index f90c653a98..ade19a3846 100644 --- a/src/ooxml/testcases/org/apache/poi/ss/formula/TestFormulaParser.java +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/TestFormulaParser.java @@ -18,8 +18,17 @@ */ package org.apache.poi.ss.formula; +import java.io.File; +import java.io.FileOutputStream; +import java.util.Locale; + import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.formula.ptg.AbstractFunctionPtg; +import org.apache.poi.ss.formula.ptg.NameXPxg; +import org.apache.poi.ss.formula.ptg.Ptg; +import org.apache.poi.ss.formula.ptg.StringPtg; +import org.apache.poi.xssf.XSSFTestDataSamples; import org.apache.poi.xssf.usermodel.XSSFEvaluationWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; @@ -62,5 +71,92 @@ public class TestFormulaParser extends TestCase { catch (FormulaParseException expected) { } } + + // copied from org.apache.poi.hssf.model.TestFormulaParser + public void testMacroFunction() throws Exception { + // testNames.xlsm contains a VB function called 'myFunc' + final String testFile = "testNames.xlsm"; + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook(testFile); + try { + XSSFEvaluationWorkbook workbook = XSSFEvaluationWorkbook.create(wb); + + //Expected ptg stack: [NamePtg(myFunc), StringPtg(arg), (additional operands would go here...), FunctionPtg(myFunc)] + Ptg[] ptg = FormulaParser.parse("myFunc(\"arg\")", workbook, FormulaType.CELL, -1); + assertEquals(3, ptg.length); + + // the name gets encoded as the first operand on the stack + NameXPxg tname = (NameXPxg) ptg[0]; + assertEquals("myFunc", tname.toFormulaString()); + + // the function's arguments are pushed onto the stack from left-to-right as OperandPtgs + StringPtg arg = (StringPtg) ptg[1]; + assertEquals("arg", arg.getValue()); + + // The external FunctionPtg is the last Ptg added to the stack + // During formula evaluation, this Ptg pops off the the appropriate number of + // arguments (getNumberOfOperands()) and pushes the result on the stack + AbstractFunctionPtg tfunc = (AbstractFunctionPtg) ptg[2]; + assertTrue(tfunc.isExternalFunction()); + + // confirm formula parsing is case-insensitive + FormulaParser.parse("mYfUnC(\"arg\")", workbook, FormulaType.CELL, -1); + + // confirm formula parsing doesn't care about argument count or type + // this should only throw an error when evaluating the formula. + FormulaParser.parse("myFunc()", workbook, FormulaType.CELL, -1); + FormulaParser.parse("myFunc(\"arg\", 0, TRUE)", workbook, FormulaType.CELL, -1); + + // A completely unknown formula name (not saved in workbook) should still be parseable and renderable + // but will throw an NotImplementedFunctionException or return a #NAME? error value if evaluated. + FormulaParser.parse("yourFunc(\"arg\")", workbook, FormulaType.CELL, -1); + + // Make sure workbook can be written and read + XSSFTestDataSamples.writeOutAndReadBack(wb).close(); + + // Manually check to make sure file isn't corrupted + final File fileIn = XSSFTestDataSamples.getSampleFile(testFile); + final File reSavedFile = new File(fileIn.getParentFile(), fileIn.getName().replace(".xlsm", "-saved.xlsm")); + final FileOutputStream fos = new FileOutputStream(reSavedFile); + wb.write(fos); + fos.close(); + } finally { + wb.close(); + } + } + + public void testParserErrors() throws Exception { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("testNames.xlsm"); + try { + XSSFEvaluationWorkbook workbook = XSSFEvaluationWorkbook.create(wb); + + parseExpectedException("("); + parseExpectedException(")"); + parseExpectedException("+"); + parseExpectedException("42+"); + parseExpectedException("IF()"); + parseExpectedException("IF("); //no closing paren + parseExpectedException("myFunc(", workbook); //no closing paren + } finally { + wb.close(); + } + } + + private static void parseExpectedException(String formula) { + parseExpectedException(formula, null); + } + + /** confirm formula has invalid syntax and parsing the formula results in FormulaParseException + * @param formula + * @param wb + */ + private static void parseExpectedException(String formula, FormulaParsingWorkbook wb) { + try { + FormulaParser.parse(formula, wb, FormulaType.CELL, -1); + fail("Expected FormulaParseException: " + formula); + } catch (final FormulaParseException e) { + // expected during successful test + assertNotNull(e.getMessage()); + } + } } diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 09bf5a9b6a..35576cac6d 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -19,7 +19,10 @@ package org.apache.poi.hssf.model; import static org.junit.Assert.assertArrayEquals; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.util.Locale; import junit.framework.AssertionFailedError; import junit.framework.TestCase; @@ -111,20 +114,68 @@ public final class TestFormulaParser extends TestCase { assertEquals("TOTAL[", ((StringPtg)ptgs[0]).getValue()); } - public void testMacroFunction() { + public void testMacroFunction() throws IOException { // testNames.xls contains a VB function called 'myFunc' - HSSFWorkbook w = HSSFTestDataSamples.openSampleWorkbook("testNames.xls"); - HSSFEvaluationWorkbook book = HSSFEvaluationWorkbook.create(w); - - Ptg[] ptg = HSSFFormulaParser.parse("myFunc()", w); - // myFunc() actually takes 1 parameter. Don't know if POI will ever be able to detect this problem - - // the name gets encoded as the first arg - NamePtg tname = (NamePtg) ptg[0]; - assertEquals("myFunc", tname.toFormulaString(book)); - - AbstractFunctionPtg tfunc = (AbstractFunctionPtg) ptg[1]; - assertTrue(tfunc.isExternalFunction()); + final String testFile = "testNames.xls"; + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook(testFile); + try { + HSSFEvaluationWorkbook book = HSSFEvaluationWorkbook.create(wb); + + //Expected ptg stack: [NamePtg(myFunc), StringPtg(arg), (additional operands go here...), FunctionPtg(myFunc)] + Ptg[] ptg = FormulaParser.parse("myFunc(\"arg\")", book, FormulaType.CELL, -1); + assertEquals(3, ptg.length); + + // the name gets encoded as the first operand on the stack + NamePtg tname = (NamePtg) ptg[0]; + assertEquals("myFunc", tname.toFormulaString(book)); + + // the function's arguments are pushed onto the stack from left-to-right as OperandPtgs + StringPtg arg = (StringPtg) ptg[1]; + assertEquals("arg", arg.getValue()); + + // The external FunctionPtg is the last Ptg added to the stack + // During formula evaluation, this Ptg pops off the the appropriate number of + // arguments (getNumberOfOperands()) and pushes the result on the stack + AbstractFunctionPtg tfunc = (AbstractFunctionPtg) ptg[2]; //FuncVarPtg + assertTrue(tfunc.isExternalFunction()); + + // confirm formula parsing is case-insensitive + FormulaParser.parse("mYfUnC(\"arg\")", book, FormulaType.CELL, -1); + + // confirm formula parsing doesn't care about argument count or type + // this should only throw an error when evaluating the formula. + FormulaParser.parse("myFunc()", book, FormulaType.CELL, -1); + FormulaParser.parse("myFunc(\"arg\", 0, TRUE)", book, FormulaType.CELL, -1); + + // A completely unknown formula name (not saved in workbook) should still be parseable and renderable + // but will throw an NotImplementedFunctionException or return a #NAME? error value if evaluated. + FormulaParser.parse("yourFunc(\"arg\")", book, FormulaType.CELL, -1); + + // Verify that myFunc and yourFunc were successfully added to Workbook names + HSSFWorkbook wb2 = HSSFTestDataSamples.writeOutAndReadBack(wb); + try { + // HSSFWorkbook/EXCEL97-specific side-effects user-defined function names must be added to Workbook's defined names in order to be saved. + assertNotNull(wb2.getName("myFunc")); + assertEqualsIgnoreCase("myFunc", wb2.getName("myFunc").getNameName()); + assertNotNull(wb2.getName("yourFunc")); + assertEqualsIgnoreCase("yourFunc", wb2.getName("yourFunc").getNameName()); + + // Manually check to make sure file isn't corrupted + final File fileIn = HSSFTestDataSamples.getSampleFile(testFile); + final File reSavedFile = new File(fileIn.getParentFile(), fileIn.getName().replace(".xls", "-saved.xls")); + FileOutputStream fos = new FileOutputStream(reSavedFile); + wb2.write(fos); + fos.close(); + } finally { + wb2.close(); + } + } finally { + wb.close(); + } + } + + private final static void assertEqualsIgnoreCase(String expected, String actual) { + assertEquals(expected.toLowerCase(Locale.ROOT), actual.toLowerCase(Locale.ROOT)); } public void testEmbeddedSlash() { @@ -713,12 +764,19 @@ public final class TestFormulaParser extends TestCase { parseExpectedException("IF(TRUE)"); parseExpectedException("countif(A1:B5, C1, D1)"); + + parseExpectedException("("); + parseExpectedException(")"); + parseExpectedException("+"); + parseExpectedException("42+"); + + parseExpectedException("IF("); } private static void parseExpectedException(String formula) { try { parseFormula(formula); - throw new AssertionFailedError("expected parse exception"); + throw new AssertionFailedError("Expected FormulaParseException: " + formula); } catch (FormulaParseException e) { // expected during successful test assertNotNull(e.getMessage()); diff --git a/src/testcases/org/apache/poi/ss/formula/BaseTestExternalFunctions.java b/src/testcases/org/apache/poi/ss/formula/BaseTestExternalFunctions.java index 87da6100db..d40bdca57a 100644 --- a/src/testcases/org/apache/poi/ss/formula/BaseTestExternalFunctions.java +++ b/src/testcases/org/apache/poi/ss/formula/BaseTestExternalFunctions.java @@ -19,6 +19,8 @@ package org.apache.poi.ss.formula; import junit.framework.TestCase; import org.apache.poi.ss.ITestDataProvider; import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NotImplementedException; +import org.apache.poi.ss.formula.eval.NotImplementedFunctionException; import org.apache.poi.ss.formula.eval.StringEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.functions.FreeRefFunction; @@ -84,6 +86,7 @@ public class BaseTestExternalFunctions extends TestCase { public void testExternalFunctions() { Workbook wb = _testDataProvider.createWorkbook(); + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); Sheet sh = wb.createSheet(); @@ -92,11 +95,16 @@ public class BaseTestExternalFunctions extends TestCase { assertEquals("ISODD(1)+ISEVEN(2)", cell1.getCellFormula()); Cell cell2 = sh.createRow(1).createCell(0); + cell2.setCellFormula("MYFUNC(\"B1\")"); //unregistered functions are parseable and renderable, but may not be evaluateable try { - cell2.setCellFormula("MYFUNC(\"B1\")"); - fail("Should fail because MYFUNC is an unknown function"); - } catch (FormulaParseException e){ - ; //expected + evaluator.evaluate(cell2); + fail("Expected NotImplementedFunctionException/NotImplementedException"); + } catch (final NotImplementedException e) { + if (!(e.getCause() instanceof NotImplementedFunctionException)) + throw e; + // expected + // Alternatively, a future implementation of evaluate could return #NAME? error to align behavior with Excel + // assertEquals(ErrorEval.NAME_INVALID, ErrorEval.valueOf(evaluator.evaluate(cell2).getErrorValue())); } wb.addToolPack(customToolpack); @@ -108,7 +116,6 @@ public class BaseTestExternalFunctions extends TestCase { cell3.setCellFormula("MYFUNC2(\"C1\")&\"-\"&A2"); //where A2 is defined above assertEquals("MYFUNC2(\"C1\")&\"-\"&A2", cell3.getCellFormula()); - FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); assertEquals(2.0, evaluator.evaluate(cell1).getNumberValue()); assertEquals("B1abc", evaluator.evaluate(cell2).getStringValue()); assertEquals("C1abc2-B1abc", evaluator.evaluate(cell3).getStringValue()); diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestCell.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestCell.java index 9cdaf3d18d..ebefb4358c 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestCell.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestCell.java @@ -298,6 +298,45 @@ public abstract class BaseTestCell { private Cell createACell() { return _testDataProvider.createWorkbook().createSheet("Sheet1").createRow(0).createCell(0); } + + /** + * bug 58452: Copy cell formulas containing unregistered function names + * Make sure that formulas with unknown/unregistered UDFs can be written to and read back from a file. + * + * @throws IOException + */ + @Test + public void testFormulaWithUnknownUDF() throws IOException { + final Workbook wb1 = _testDataProvider.createWorkbook(); + final FormulaEvaluator evaluator1 = wb1.getCreationHelper().createFormulaEvaluator(); + try { + final Cell cell1 = wb1.createSheet().createRow(0).createCell(0); + final String formula = "myFunc(\"arg\")"; + cell1.setCellFormula(formula); + confirmFormulaWithUnknownUDF(formula, cell1, evaluator1); + + final Workbook wb2 = _testDataProvider.writeOutAndReadBack(wb1); + final FormulaEvaluator evaluator2 = wb2.getCreationHelper().createFormulaEvaluator(); + try { + final Cell cell2 = wb2.getSheetAt(0).getRow(0).getCell(0); + confirmFormulaWithUnknownUDF(formula, cell2, evaluator2); + } finally { + wb2.close(); + } + } finally { + wb1.close(); + } + } + + private static void confirmFormulaWithUnknownUDF(String expectedFormula, Cell cell, FormulaEvaluator evaluator) { + assertEquals(expectedFormula, cell.getCellFormula()); + try { + evaluator.evaluate(cell); + fail("Expected NotImplementedFunctionException/NotImplementedException"); + } catch (final org.apache.poi.ss.formula.eval.NotImplementedException e) { + // expected + } + } @Test public void testChangeTypeStringToBool() { diff --git a/test-data/spreadsheet/testNames.xlsm b/test-data/spreadsheet/testNames.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..f4e7db8f22829aaef3454021cbb4409f3255723d GIT binary patch literal 13290 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