How to get rid of "Save changes?" prompt on xlsx-files created with Apache POI XSSF
Asked Answered
S

4

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();
Sculptress answered 9/6, 2016 at 11:25 Comment(0)
W
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.

Wrapper answered 29/6, 2016 at 22:54 Comment(6)
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?Sculptress
.xlsx is really just a simplee zip archíve (see this answer https://mcmap.net/q/41794/-how-to-properly-assemble-a-valid-xlsx-file-from-its-internal-sub-components). 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 archiveWrapper
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.Sculptress
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.Sculptress
it looks like POI is using CTWorkbook implementation to create the mentioned workbook.xml file (see grepcode.com/file/repo1.maven.org/maven2/org.apache.poi/…), you could directly set the properties like XSSFWorkbook.getCTWorkbook().addNewFileVersion();. Get inspired in XSSFWorkbook source where default workbook.xml is createdWrapper
but I haven't actually tested the solution using CTWorkbook, just my thoughts after quick googlingWrapper
A
5

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

wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
Amieeamiel answered 13/10, 2016 at 5:19 Comment(2)
Where did u add it?Jarnagin
Got it, I added it before I save my workbook after all writes done.Jarnagin
W
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.

Wrapper answered 29/6, 2016 at 22:54 Comment(6)
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?Sculptress
.xlsx is really just a simplee zip archíve (see this answer https://mcmap.net/q/41794/-how-to-properly-assemble-a-valid-xlsx-file-from-its-internal-sub-components). 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 archiveWrapper
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.Sculptress
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.Sculptress
it looks like POI is using CTWorkbook implementation to create the mentioned workbook.xml file (see grepcode.com/file/repo1.maven.org/maven2/org.apache.poi/…), you could directly set the properties like XSSFWorkbook.getCTWorkbook().addNewFileVersion();. Get inspired in XSSFWorkbook source where default workbook.xml is createdWrapper
but I haven't actually tested the solution using CTWorkbook, just my thoughts after quick googlingWrapper
E
1

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();
}
Earache answered 5/5, 2022 at 14:35 Comment(0)
O
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.

Ora answered 9/6, 2023 at 4:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.