4

After opening and immediately closing an xlsx-file, created with Apache POI XSSF, I get prompted to save unsaved changes. As far as i can tell, this is happening because I am using formulas within the xlsx-file.

According to the javadoc, this should be bypassed by setting XSSFWorkbook.setForceFormulaRecalculation(true) However, this doesn't solve the problem.

I also tried to manually recalculate the formulas before saving the file without success.

SSCCE:

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

public class XSSFExample {

    public static void main(String[] args) {
        // Create workbook and sheet
        Workbook wb = new XSSFWorkbook();
        Sheet sheet = wb.createSheet("Sheet 1");

        // Create a row and put some cells in it.
        Row row = sheet.createRow((short) 0);
        row.createCell(0).setCellValue(5.0);
        row.createCell(1).setCellValue(5.0);
        row.createCell(2).setCellFormula("A1/B1");


        // Write the output to a file
        try (FileOutputStream fileOut = new FileOutputStream("XSSFExample.xlsx")) {
            wb.setForceFormulaRecalculation(false);
            System.out.println(wb.getForceFormulaRecalculation()); // prints "false"
            XSSFFormulaEvaluator.evaluateAllFormulaCells((XSSFWorkbook) wb); // this doesn't seem to make any difference
            wb.write(fileOut);
        } catch (IOException ex) {
            Logger.getLogger(XSSFExample.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

What can I do to create the file and not get prompted to save it after I opened it the first time?

Update:
As stated here (https://poi.apache.org/spreadsheet/eval.html#recalculation) I also tried another method to manually recalculate with no success. Even re-reading the file after save, recalc and save as a second file doesn't work.

Update 2:
Considering the accepted answer, I was able to solve the problem by adding following lines of code to the above SSCCE:
(Please note that this was just a "quick and dirty" attempt to solve the problem. There are probably a lot of improvements possible).

ZipFile zipFile = new ZipFile("XSSFExample.xlsx");
final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("XSSFExample_NoSave.xlsx"));
for (Enumeration e = zipFile.entries(); e.hasMoreElements();) {
    ZipEntry entryIn = (ZipEntry) e.nextElement();
    if (!entryIn.getName().equalsIgnoreCase("xl/workbook.xml")) {
        zos.putNextEntry(entryIn);
        InputStream is = zipFile.getInputStream(entryIn);
        byte[] buf = new byte[1024];
        int len;
        while ((len = (is.read(buf))) > 0) {
            zos.write(buf, 0, len);
        }
    } else {
        zos.putNextEntry(new ZipEntry("xl/workbook.xml"));
        InputStream is = zipFile.getInputStream(entryIn);
        byte[] buf = new byte[1024];
        int len;
        while (is.read(buf) > 0) {
            String s = new String(buf);
            String searchFileVersion = "/relationships\"><workbookPr";
            String replaceFileVersion = "/relationships\"><fileVersion appName=\"xl\" lastEdited=\"5\" lowestEdited=\"5\" rupBuild=\"9303\"/><workbookPr";
            String searchCalcId = "<calcPr calcId=\"0\"/>";
            String replaceCalcId = "<calcPr calcId=\"" + String.valueOf(Integer.MAX_VALUE) + "\"/>";
            if (s.contains(searchFileVersion)) {
                s = s.replaceAll(searchFileVersion, replaceFileVersion);
            }
            if (s.contains(searchCalcId)) {
                s = s.replaceAll(searchCalcId, replaceCalcId);
            }
            len = s.trim().length();
            buf = s.getBytes();
            zos.write(buf, 0, (len < buf.length) ? len : buf.length);
        }
    }
    zos.closeEntry();
}
zos.close();
sobrino
  • 153
  • 12

4 Answers4

5

Even I was facing the same issue but after adding the below line, the issue has been resolved.

wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
Raghav2580
  • 256
  • 2
  • 10
2

PROBLEM

The problem could lie in MS Excel itself (once you are sure that all formulas were calculated and saved in the .xlsx file). According to my testing, Excel will recalculate all formulas during opening if it finds out that the file was last saved by older version of Excel or other application (the point is that the version numbers doesn't match and/or are lower than current version of Excel opening the file) to maintain good compatibility.

SOLUTION

(making Excel think that the .xlsx file was generated by the same Excel version to avoid recalculation)

Excel reads all file versioning info from workbook.xml file located in xl directory inside .xlsx archive (.xlsx is just a zipped archive).

workbook.xml file generated by Apache POI could look like this:

<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <workbookPr date1904="false"/>
  <bookViews><workbookView activeTab="0"/></bookViews>
  <sheets>
    <sheet name="new sheet" r:id="rId3" sheetId="1"/>
  </sheets>
  <calcPr calcId="0"/>
</workbook>

The file generated by Excel 2010 looks like this:

<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="9303"/>
  <workbookPr defaultThemeVersion="124226"/>
  <bookViews><workbookView xWindow="630" yWindow="510" windowWidth="27495" windowHeight="14505"/></bookViews>
  <sheets>
    <sheet name="new sheet" sheetId="1" r:id="rId1"/>
  </sheets>
  <calcPr calcId="145621"/>
</workbook>

Notice the <fileVersion> tag completely missing in POI generated file and <calcPr> tag with calcId set to some real value in Excel generated file.

I was able to avoid Excel 2010 automatic formula recalculation (and annoying "Save changes" dialog) by inserting correlated <fileVersion> tag and setting calcId to equal or greater number than the number generated by my current version of Excel to the workbook.xml generated by POI.

More information regarding the workbook.xml format can be found on MSDN Open XML SDK documentation.

betatester07
  • 667
  • 8
  • 16
  • This really is a nice answer. Not only I believe this will lead me to solving the problem, but also I learned a bit about how Excel works in terms of file versioning. Right now i am looking for a way to extract the `workbook.xml`, edit it, and merge it back to the xlsx. Do you know a proper way to do that? – sobrino Jun 30 '16 at 07:38
  • .xlsx is really just a simplee zip archíve (see this answer http://stackoverflow.com/a/11116875/1351175). Therefore, you shold be able to open it using any zip extracting software, edit the needed XML files and put edited files back to the archive – betatester07 Jun 30 '16 at 11:47
  • I guess I was not clear on what I wanted to know. I successfully used your information to solve my problem by unzip, edit, rezip it in Windows. I wanted to know if you know about a proper way to do it Java. If not, I'm sure I'll find a way. I just don't want to "reinvent the wheel" if there is already a proper way to do it. – sobrino Jun 30 '16 at 11:57
  • Meanwhile I found a way to change the xml in Java Code. I'll update my question with my solution. Anyways, if you know a better way, I'd appreciate any suggestions. – sobrino Jun 30 '16 at 12:53
  • it looks like POI is using CTWorkbook implementation to create the mentioned workbook.xml file (see http://grepcode.com/file/repo1.maven.org/maven2/org.apache.poi/ooxml-schemas/1.1/org/openxmlformats/schemas/spreadsheetml/x2006/main/CTWorkbook.java), you could directly set the properties like `XSSFWorkbook.getCTWorkbook().addNewFileVersion();`. Get inspired in [XSSFWorkbook source](http://grepcode.com/file/repo1.maven.org/maven2/org.apache.poi/poi-ooxml/3.7-beta2/org/apache/poi/xssf/usermodel/XSSFWorkbook.java#XSSFWorkbook.onWorkbookCreate%28%29) where default workbook.xml is created – betatester07 Jun 30 '16 at 23:15
  • but I haven't actually tested the solution using CTWorkbook, just my thoughts after quick googling – betatester07 Jun 30 '16 at 23:17
0

I am using Apache POI 5.2.2, open template.xlsx file with just one sheet, clone 1..n new sheets, write cells, delete 1st template sheet, save .xlsx file.

Opening a file in Excel and close gives Save changes? prompt even if did nothing, no @formula cells, no external links or objects in a workbook. I realized if the number of worksheets is different than the original file then prompt is shown.

All sheets had xl/worksheets/sheet1.xml@xr:uid={00000000-0001-0000-0000-000000000000} zero guid.

Text editing sheetX.xml@xr:uid values to {11111111-1111-1111-1111-112233440001}, {11111111-1111-1111-1111-112233440002}, {11111111-1111-1111-1111-112233440003}, .. unique guids fixed a problem.

Using @sobrino's answer this is a modified unzip-zip fix.

public void fixFile(File inputFile, File outputFile) throws IOException {
    int count=0;
    ZipFile zipFile = new ZipFile(inputFile);       
    final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputFile));
    for (Enumeration<? extends ZipEntry> en = zipFile.entries(); en.hasMoreElements();) {
        ZipEntry entryIn = (ZipEntry)en.nextElement();
        String name = entryIn.getName();
        if(!( name.startsWith("xl/worksheets/") && name.endsWith(".xml")
                && name.indexOf('/', 14)<0 ))  {
            zos.putNextEntry(entryIn);
            InputStream is = zipFile.getInputStream(entryIn);
            byte[] buf = new byte[2*1024];
            int len;
            while ((len = (is.read(buf))) > 0) {
                zos.write(buf, 0, len);
            }
        } else {
            // fix xr:uid="{00000000-0001-0000-0000-000000000000}" zero GUID to avoid "save changes" prompt
            // <worksheet ... xr:uid="{11111111-1111-1111-1111-112233440001" ...
            count++;
            zos.putNextEntry(new ZipEntry(name));
            InputStream is = zipFile.getInputStream(entryIn);
            byte[] buf = new byte[2*1024];
            int len;
            boolean firstRead=true;
            while ( (len=is.read(buf)) > 0) {
                if(firstRead) {
                    firstRead=false;
                    String sData=new String(buf,0,len, "UTF-8");
                    int delimS=sData.indexOf("xr:uid=\"");
                    int delimE=sData.indexOf('"', delimS+8);
                    int delimG=sData.indexOf("-000000000000}", delimS+8);
                    if(delimG>0 && delimG<=delimE && delimS>0) {
                        // found zero GUID, replace value
                        sData=sData.substring(0, delimS+8)
                            + String.format("{11111111-1111-1111-1111-11223344%04x}", count)
                            + sData.substring(delimE);
                        zos.write(sData.getBytes("UTF-8"));
                    } else {
                        zos.write(buf, 0, len);
                    }
                } else {
                    zos.write(buf, 0, len);
                }
            }
        }
        zos.closeEntry();
    }
    zos.close();
    zipFile.close();
}
Whome
  • 10,181
  • 6
  • 53
  • 65
0

I have a simple resolution for XSSF user model:

public static void fixXSSFSheetUid(XSSFSheet sheet) {
    Node node = (Node) ((CTWorksheetImpl) sheet.getCTWorksheet()).get_store();
    node = node.getAttributes().getNamedItem("xr:uid");
    if (node instanceof Attr) {
        ((Attr) node).setValue("{" + UUID.randomUUID().toString().toUpperCase() + "}");
    }        
}

I expect that POI will include similar logic in Workbook#cloneSheet(int sheetNum, String newName) to avoid the anoyance.

Feng Dihai
  • 416
  • 3
  • 9