package Xlsx; import Utils.*; import java.io.FileOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.text.DateFormat; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Locale; import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.zip.ZipOutputStream; import java.util.zip.ZipEntry; public class SpreadsheetWriter { public static class Sheet { public String name = ""; public RefVector formattedRows = new RefVector<>(String.class); public RefVector valueRows = new RefVector<>(Object[].class); public int colsInWidestRow = 0; } public DateFormat dateFormatter; public DateFormat timeFormatter; public final File parentFolder; public final String fileName; HashMap stringsMap; RefVector stringsList; int totalStringCount; public RefVector sheets; public int currentSheetIdx = 0; public boolean shouldSaveAfterEveryRow = true; public SpreadsheetWriter(File absPath) { this(absPath.getParentFile(), absPath.getName()); } public SpreadsheetWriter(File parentFolder, String name) { this.parentFolder = parentFolder; this.fileName = name; this.dateFormatter = DateFormat.getDateInstance(2, Locale.US); this.timeFormatter = DateFormat.getTimeInstance(2, Locale.US); this.stringsMap = new HashMap<>(); this.stringsList = new RefVector<>(String.class); this.totalStringCount = 0; this.sheets = new RefVector<>(Sheet.class); this.currentSheetIdx = 0; } public static SpreadsheetWriter fromExistingXlsx(String path) throws IOException { ArrayList existingSheets = XlsxReader.loadXlsxFromFile(path); SpreadsheetWriter writer = new SpreadsheetWriter(new File(path)); writer.addSheets(existingSheets); return writer; } public void addSheets(ArrayList existingSheets) throws IOException { if (existingSheets == null || existingSheets.isEmpty()) return; boolean wasSaving = this.shouldSaveAfterEveryRow; this.shouldSaveAfterEveryRow = false; for (XlsxReader.SheetCells s : existingSheets) { if (s.rows <= 0 || s.cols <= 0) continue; if (s.cells != null) { for (int i = 0; i < s.rows; i++) writeRowFromObjectArray(s.cells, i * s.cols, s.cols); } else if (s.valueRows != null) { for (int i = 0; i < s.rows; i++) { Object[] row = s.valueRows.buf[i]; writeRowFromObjectArray(row, 0, row.length); } } currentSheetIdx++; } this.shouldSaveAfterEveryRow = wasSaving; } public static String makeColumnNumber(int n) { if (n <= 1) return "A"; byte[] buf = new byte[16]; int idx = buf.length; while (n > 0) { n--; buf[--idx] = (byte)(0x41 + (n % 26)); n /= 26; } return new String(buf, idx, buf.length - idx); } private int lookupOrAddCellString(String str) { Integer idx = stringsMap.get(str); if (idx == null) { int len = stringsList.size; stringsList.add(str); stringsMap.put(str, len); return len; } return idx; } public void writeRow(Object... values) throws IOException { Object[] valuesList = values; writeRowFromObjectArray(valuesList, 0, valuesList.length); } public void writeRowFromObjectArray(Object[] values, int off, int len) throws IOException { int sheetIdx = currentSheetIdx; while (sheetIdx >= sheets.size) sheets.add(new Sheet()); Sheet s = sheets.buf[sheetIdx]; int rowNumber = s.formattedRows.size + 1; ByteVectorOutputStream sb = new ByteVectorOutputStream(); sb.add(""); int colNumber = 0; for (int i = 0; i < len; i++) { colNumber++; Object value = values[off+i]; if (value == null) continue; sb.add(""); sb.add(value.toString()); sb.add(""); } else { int idx = lookupOrAddCellString(value.toString()); totalStringCount++; sb.add(" t=\"s\">"); sb.add("" + idx); sb.add(""); } } sb.add(""); if (colNumber > s.colsInWidestRow) s.colsInWidestRow = colNumber; Object[] valueRow = off == 0 && len == values.length ? values : Arrays.copyOfRange(values, off, off+len); s.valueRows.add(valueRow); s.formattedRows.add(sb.toString()); if (shouldSaveAfterEveryRow) save(); } public void save() throws IOException { ByteVectorOutputStream vector = new ByteVectorOutputStream(); ZipOutputStream zip = new ZipOutputStream(vector); String dateStr = LocalDateTime.now().atZone(ZoneId.of("GMT")).format(DateTimeFormatter.ISO_INSTANT); int sheetCount = sheets.size; for (int i = 0; i < sheetCount; i++) { Sheet s = sheets.buf[i]; if (s.name == null || s.name.isEmpty()) s.name = "Sheet" + (i+1); } String[] folders = new String[] { "_rels/", "docProps/", "xl/", "xl/_rels/", "xl/worksheets/" }; for (String folder : folders) { zip.putNextEntry(new ZipEntry(folder)); zip.closeEntry(); } writeStringBuilderToZip(zip, "[Content_Types].xml", generateContentTypesXml(sheetCount)); writeStringBuilderToZip(zip, "_rels/.rels", generateTopRelsXml()); writeStringBuilderToZip(zip, "docProps/app.xml", generateAppXml(sheets)); writeStringBuilderToZip(zip, "docProps/core.xml", generateCoreXml("AngioTool-Batch", dateStr)); writeStringBuilderToZip(zip, "xl/_rels/workbook.xml.rels", generateWorkbookRels(sheetCount)); writeStringBuilderToZip(zip, "xl/styles.xml", generateStylesXml()); writeStringBuilderToZip(zip, "xl/workbook.xml", generateWorkbookXml(sheets, dateStr)); writeStringBuilderToZip(zip, "xl/sharedStrings.xml", generateSharedStringsXml(stringsList, totalStringCount)); for (int i = 0; i < sheetCount; i++) writeStringBuilderToZip(zip, "xl/worksheets/sheet" + (i+1) + ".xml", generateSheetXml(sheets.buf[i], dateStr)); zip.close(); File outPath = new File(parentFolder, fileName); FileOutputStream fileStream = new FileOutputStream(outPath); fileStream.write(vector.buf, 0, vector.size); fileStream.close(); } static String escapeXmlString(String input) { ByteVectorOutputStream sb = new ByteVectorOutputStream(); byte[] bytes = input.getBytes(); int start = 0; String esc = null; for (int i = 0; i < bytes.length; i++) { byte c = bytes[i]; switch (c) { case '&': esc = "&"; break; case '<': esc = "<"; break; case '>': esc = ">"; break; case '\'': esc = "'"; break; case '"': esc = """; break; default: esc = null; } if (esc != null) { if (start < i) sb.add(bytes, start, i - start); sb.add(esc); start = i + 1; } } if (start < bytes.length) sb.add(bytes, start, bytes.length - start); return sb.toString(); } static String generateExcelUuid(String seed) { byte[] seedBytes = seed.getBytes(); long hash1 = murmurHash64a(seedBytes, 0, seedBytes.length, 1337); long hash2 = murmurHash64a(seedBytes, 0, seedBytes.length, 420); byte[] uuid = new byte[36]; int pos = 0; for (int i = 0; i < 32; i++) { long h = i < 16 ? hash1 : hash2; int d = i == 12 ? 4 : (int)((h >>> ((i&15L)*4L)) & 15L); uuid[pos++] = (byte)(d + (d < 10 ? 0x30 : 0x57)); if (i == 7 || i == 11 || i == 15 || i == 19) uuid[pos++] = '-'; } return new String(uuid, 0, 36); } static void writeStringBuilderToZip(ZipOutputStream zip, String path, StringBuilder sb) throws IOException { zip.putNextEntry(new ZipEntry(path)); zip.write(sb.toString().getBytes()); } static StringBuilder generateContentTypesXml(int sheetCount) { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" + "" + "" + "" ); for (int i = 0; i < sheetCount; i++) { sb.append(""); } sb.append( "" + "" + "" + "" + "" ); return sb; } static StringBuilder generateTopRelsXml() { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" + "" + "" + "" + "" ); return sb; } static StringBuilder generateAppXml(RefVector sheets) { StringBuilder sb = new StringBuilder(); int sheetCount = sheets.size; sb.append( "\n" + "" + "" + "Microsoft Excel" + "0" + "false" + "" + "Worksheets" + "" ); sb.append(sheetCount); sb.append("" + "" + ""); for (int i = 0; i < sheetCount; i++) { sb.append(""); sb.append(escapeXmlString(sheets.buf[i].name)); sb.append(""); } sb.append( "" + "false" + "false" + "false" + "16.0300" ); return sb; } static StringBuilder generateCoreXml(String creator, String dateStr) { StringBuilder sb = new StringBuilder(); sb.append("\n" + "" + "" ); sb.append(creator); sb.append(""); sb.append(creator); sb.append(""); sb.append(dateStr); sb.append(""); sb.append(dateStr); sb.append(""); return sb; } static StringBuilder generateWorkbookRels(int sheetCount) { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" ); for (int i = 0; i < sheetCount; i++) { sb.append(""); } sb.append("" ); sb.append("" ); sb.append(""); return sb; } static StringBuilder generateStylesXml() { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" ); return sb; } static StringBuilder generateWorkbookXml(RefVector sheets, String dateStr) { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" + "" + "" + "" + "" + ""); int sheetCount = sheets.size; for (int i = 0; i < sheetCount; i++) { sb.append(""); } sb.append( "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" ); return sb; } static StringBuilder generateSharedStringsXml(RefVector uniqueStrings, int nTotalStrings) { StringBuilder sb = new StringBuilder(); sb.append( "\n" + ""); for (String str : uniqueStrings) { sb.append(""); sb.append(escapeXmlString(str)); sb.append(""); } sb.append(""); return sb; } static StringBuilder generateSheetXml(Sheet sheet, String dateStr) { StringBuilder sb = new StringBuilder(); sb.append( "\n" + "" + "" + "" + "" + "" + "" + "" + "" ); // maybe add ... to specify column widths sb.append(""); for (String row : sheet.formattedRows) sb.append(row); sb.append(""); // ... goes here sb.append( "" + "" ); return sb; } static long murmurHash64a(byte[] key, int off, int len, long seed) { if (off < 0 || len <= 0 || off+len > key.length) return 0L; final long m = 0xc6a4a7935bd1e995L; final int r = 47; long h = seed ^ ((long)len * m); int blocks = len >> 3; for (int i = 0; i < len - 7; i += 8) { long k = (key[off+i] & 0xffL) | ((key[off+i+1] & 0xffL) << 8L) | ((key[off+i+2] & 0xffL) << 16L) | ((key[off+i+3] & 0xffL) << 24L) | ((key[off+i+4] & 0xffL) << 32L) | ((key[off+i+5] & 0xffL) << 40L) | ((key[off+i+6] & 0xffL) << 48L) | ((key[off+i+7] & 0xffL) << 56L); k *= m; k ^= k >>> r; k *= m; h ^= k; h *= m; } int tail = off + (blocks << 3); switch (len & 7) { case 7: h ^= (key[tail+6] & 0xffL) << 48L; case 6: h ^= (key[tail+5] & 0xffL) << 40L; case 5: h ^= (key[tail+4] & 0xffL) << 32L; case 4: h ^= (key[tail+3] & 0xffL) << 24L; case 3: h ^= (key[tail+2] & 0xffL) << 16L; case 2: h ^= (key[tail+1] & 0xffL) << 8L; case 1: h ^= (key[tail] & 0xffL); h *= m; }; h ^= h >>> r; h *= m; h ^= h >>> r; return h; } }