diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..03720813 Binary files /dev/null and b/.DS_Store differ diff --git a/fastexcel-core/pom.xml b/fastexcel-core/pom.xml new file mode 100644 index 00000000..81a56719 --- /dev/null +++ b/fastexcel-core/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + org.dhatim + fastexcel-parent + 0-SNAPSHOT + ../pom.xml + + + fastexcel-core + Fastexcel Core + jar + diff --git a/fastexcel-core/src/main/java/module-info.java b/fastexcel-core/src/main/java/module-info.java new file mode 100644 index 00000000..883e0347 --- /dev/null +++ b/fastexcel-core/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.dhatim.fastexcel.core { + exports org.dhatim.fastexcel.common; +} diff --git a/fastexcel-core/src/main/java/org/dhatim/fastexcel/common/CellAddressUtil.java b/fastexcel-core/src/main/java/org/dhatim/fastexcel/common/CellAddressUtil.java new file mode 100644 index 00000000..a56b479a --- /dev/null +++ b/fastexcel-core/src/main/java/org/dhatim/fastexcel/common/CellAddressUtil.java @@ -0,0 +1,31 @@ +package org.dhatim.fastexcel.common; + +import java.nio.charset.StandardCharsets; + +public final class CellAddressUtil { + + private static final int COL_RADIX = 'Z' - 'A' + 1; + private static final int MAX_COL_CHARS = 3; + + private CellAddressUtil() { + } + + public static String convertNumToColString(int col) { + int excelColNum = col + 1; + + final byte[] colRef = new byte[MAX_COL_CHARS]; + int colRemain = excelColNum; + int pos = MAX_COL_CHARS - 1; + while (colRemain > 0) { + int thisPart = colRemain % COL_RADIX; + if (thisPart == 0) { + thisPart = COL_RADIX; + } + colRemain = (colRemain - thisPart) / COL_RADIX; + + colRef[pos--] = (byte) (thisPart + (int) 'A' - 1); + } + pos++; + return new String(colRef, pos, (MAX_COL_CHARS - pos), StandardCharsets.ISO_8859_1); + } +} diff --git a/fastexcel-reader/pom.xml b/fastexcel-reader/pom.xml index 32ca9029..7ae66612 100644 --- a/fastexcel-reader/pom.xml +++ b/fastexcel-reader/pom.xml @@ -11,6 +11,10 @@ https://github.com/dhatim/fastexcel + + org.dhatim + fastexcel-core + com.fasterxml aalto-xml diff --git a/fastexcel-reader/src/main/java/module-info.java b/fastexcel-reader/src/main/java/module-info.java index 7ecee45d..409e61e9 100644 --- a/fastexcel-reader/src/main/java/module-info.java +++ b/fastexcel-reader/src/main/java/module-info.java @@ -1,7 +1,8 @@ module org.dhatim.fastexcel.reader { + requires org.dhatim.fastexcel.core; requires java.xml; requires java.logging; requires org.apache.commons.compress; requires com.fasterxml.aalto; exports org.dhatim.fastexcel.reader; -} \ No newline at end of file +} diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/BaseFormulaCell.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/BaseFormulaCell.java index 3706616d..e988a70f 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/BaseFormulaCell.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/BaseFormulaCell.java @@ -1,6 +1,6 @@ package org.dhatim.fastexcel.reader; -public class BaseFormulaCell { +class BaseFormulaCell { private final CellAddress baseCelAddr; private final String formula; diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/Cell.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/Cell.java index b720d715..6cb873e6 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/Cell.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/Cell.java @@ -24,8 +24,8 @@ public class Cell { private static final long DAY_MILLISECONDS = 86_400_000L; - private final ReadableWorkbook workbook; - private final Object value; + private final boolean date1904; + private final CellValue value; private final String formula; private final CellType type; private final CellAddress address; @@ -39,9 +39,9 @@ public class Cell { Cell(ReadableWorkbook workbook, CellType type, Object value, CellAddress address, String formula, String rawValue, String dataFormatId, String dataFormatString) { - this.workbook = workbook; + this.date1904 = workbook != null && workbook.isDate1904(); this.type = type; - this.value = value; + this.value = CellValue.of(type, value); this.address = address; this.formula = formula; this.rawValue = rawValue; @@ -62,7 +62,7 @@ public CellAddress getAddress() { } public Object getValue() { - return value; + return value.asObject(); } /** @@ -78,7 +78,7 @@ public String getFormula() { public BigDecimal asNumber() { requireType(CellType.NUMBER); - return (BigDecimal) value; + return value.asNumber(); } /** @@ -107,7 +107,7 @@ private LocalDateTime convertToDate(double value) { int startYear = 1900; int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't - if (workbook.isDate1904()) { + if (date1904) { startYear = 1904; dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day } else if (wholeDays < 61) { @@ -122,7 +122,7 @@ private LocalDateTime convertToDate(double value) { public Boolean asBoolean() { requireType(CellType.BOOLEAN); - return (Boolean) value; + return value.asBoolean(); } /** @@ -132,7 +132,7 @@ public Boolean asBoolean() { */ public String asString() { requireType(CellType.STRING); - return value == null ? "" : (String) value; + return value.asString(); } private void requireType(CellType requiredType) { @@ -146,7 +146,7 @@ private void requireType(CellType requiredType) { * @see #asString() */ public String getText() { - return value == null ? "" : value.toString(); + return value.asText(); } public Integer getDataFormatId() { @@ -167,12 +167,95 @@ public String getDataFormatString() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append('[').append(type).append(' '); - if (value == null) { + if (value.asObject() == null) { sb.append("null"); } else { - sb.append('"').append(value).append('"'); + sb.append('"').append(value.asObject()).append('"'); } return sb.append(']').toString(); } + private interface CellValue { + Object asObject(); + + default BigDecimal asNumber() { + return (BigDecimal) asObject(); + } + + default Boolean asBoolean() { + return (Boolean) asObject(); + } + + default String asString() { + Object value = asObject(); + return value == null ? "" : (String) value; + } + + default String asText() { + Object value = asObject(); + return value == null ? "" : value.toString(); + } + + static CellValue of(CellType type, Object value) { + switch (type) { + case NUMBER: + return new NumberCellValue((BigDecimal) value); + case BOOLEAN: + return new BooleanCellValue((Boolean) value); + case STRING: + return new StringCellValue((String) value); + default: + return new ObjectCellValue(value); + } + } + } + + private static final class ObjectCellValue implements CellValue { + private final Object value; + + private ObjectCellValue(Object value) { + this.value = value; + } + + public Object asObject() { + return value; + } + } + + private static final class NumberCellValue implements CellValue { + private final BigDecimal value; + + private NumberCellValue(BigDecimal value) { + this.value = value; + } + + public Object asObject() { + return value; + } + } + + private static final class BooleanCellValue implements CellValue { + private final Boolean value; + + private BooleanCellValue(Boolean value) { + this.value = value; + } + + public Object asObject() { + return value; + } + } + + private static final class StringCellValue implements CellValue { + private final String value; + + private StringCellValue(String value) { + this.value = value; + } + + public Object asObject() { + return value; + } + } + } diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/CellAddress.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/CellAddress.java index 46c0dd64..5227dd6e 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/CellAddress.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/CellAddress.java @@ -15,7 +15,8 @@ */ package org.dhatim.fastexcel.reader; -import java.nio.charset.StandardCharsets; +import org.dhatim.fastexcel.common.CellAddressUtil; + import java.util.Objects; public final class CellAddress implements Comparable { @@ -114,25 +115,7 @@ static StringBuilder format(StringBuilder sb, int row, int col) { } public static String convertNumToColString(int col) { - // Excel counts column A as the 1st column, we - // treat it as the 0th one - int excelColNum = col + 1; - - final int MAX_COL_CHARS = 3; - final byte[] colRef = new byte[MAX_COL_CHARS]; - int colRemain = excelColNum; - int pos = 2; - while (colRemain > 0) { - int thisPart = colRemain % COL_RADIX; - if (thisPart == 0) { - thisPart = COL_RADIX; - } - colRemain = (colRemain - thisPart) / COL_RADIX; - - colRef[pos--] = (byte) (thisPart + (int) 'A' - 1); - } - pos++; - return new String(colRef, pos, (MAX_COL_CHARS - pos), StandardCharsets.ISO_8859_1); + return CellAddressUtil.convertNumToColString(col); } diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/DefaultXMLInputFactory.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/DefaultXMLInputFactory.java index 92828574..958870d4 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/DefaultXMLInputFactory.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/DefaultXMLInputFactory.java @@ -2,8 +2,14 @@ import javax.xml.stream.XMLInputFactory; -public class DefaultXMLInputFactory { - static final XMLInputFactory factory = defaultXmlInputFactory(); +final class DefaultXMLInputFactory { + + private DefaultXMLInputFactory() { + } + + static XMLInputFactory create() { + return defaultXmlInputFactory(); + } private static XMLInputFactory defaultXmlInputFactory() { XMLInputFactory factory = new com.fasterxml.aalto.stax.InputFactoryImpl(); diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormatCatalog.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormatCatalog.java new file mode 100644 index 00000000..ff7efe1c --- /dev/null +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormatCatalog.java @@ -0,0 +1,34 @@ +package org.dhatim.fastexcel.reader; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +final class FormatCatalog { + + static final FormatCatalog EMPTY = new FormatCatalog(Collections.emptyList(), Collections.emptyMap()); + + private final List formatIdsByStyleIndex; + private final Map formatsById; + + FormatCatalog(List formatIdsByStyleIndex, Map formatsById) { + this.formatIdsByStyleIndex = Collections.unmodifiableList(formatIdsByStyleIndex); + this.formatsById = Collections.unmodifiableMap(formatsById); + } + + String getFormatId(int styleIndex) { + return styleIndex < formatIdsByStyleIndex.size() ? formatIdsByStyleIndex.get(styleIndex) : null; + } + + String getFormatString(String formatId) { + return formatsById.get(formatId); + } + + List getFormatIdsByStyleIndex() { + return formatIdsByStyleIndex; + } + + Map getFormatsById() { + return formatsById; + } +} diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormulaResolver.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormulaResolver.java new file mode 100644 index 00000000..abac09e8 --- /dev/null +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/FormulaResolver.java @@ -0,0 +1,106 @@ +package org.dhatim.fastexcel.reader; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +final class FormulaResolver { + + private final Map sharedFormula = new HashMap<>(); + private final Map arrayFormula = new HashMap<>(); + + void registerArrayFormula(String ref, String formula) { + arrayFormula.put(CellRangeAddress.valueOf(ref), formula); + } + + void registerSharedFormula(Integer sharedIndex, CellAddress address, String formula, String ref) { + sharedFormula.put(sharedIndex, new BaseFormulaCell(address, formula, CellRangeAddress.valueOf(ref))); + } + + String resolveSharedFormula(Integer sharedIndex, CellAddress address) { + BaseFormulaCell baseFormulaCell = sharedFormula.get(sharedIndex); + int dRow = address.getRow() - baseFormulaCell.getBaseCelAddr().getRow(); + int dCol = address.getColumn() - baseFormulaCell.getBaseCelAddr().getColumn(); + return translateSharedFormula(dCol, dRow, baseFormulaCell.getFormula()); + } + + Optional resolveArrayFormula(CellAddress address) { + for (Map.Entry entry : arrayFormula.entrySet()) { + if (entry.getKey().isInRange(address.getRow(), address.getColumn())) { + return Optional.of(entry.getValue()); + } + } + return Optional.empty(); + } + + /** + * @see here + */ + private String translateSharedFormula(Integer dCol, Integer dRow, String baseFormula) { + StringBuilder res = new StringBuilder(); + int start = 0; + boolean stringLiteral = false; + for (int end = 0; end < baseFormula.length(); end++) { + char c = baseFormula.charAt(end); + if ('"' == c) { + stringLiteral = !stringLiteral; + } + if (stringLiteral) { + continue;// Skip characters in quotes + } + if (c >= 'A' && c <= 'Z' || c == '$') { + + res.append(baseFormula.substring(start, end)); + start = end; + end++; + boolean foundNum = false; + for (; end < baseFormula.length(); end++) { + char idc = baseFormula.charAt(end); + if (idc >= '0' && idc <= '9' || idc == '$') { + foundNum = true; + } else if (idc >= 'A' && idc <= 'Z') { + if (foundNum) { + break; + } + } else { + break; + } + } + if (foundNum) { + String cellID = baseFormula.substring(start, end); + res.append(shiftCell(cellID, dCol, dRow)); + start = end; + } + } + } + + if (start < baseFormula.length()) { + res.append(baseFormula.substring(start)); + } + + return res.toString(); + } + + /** + * @see here + */ + private String shiftCell(String cellID, Integer dCol, Integer dRow) { + CellAddress cellAddress = new CellAddress(cellID); + int fCol = cellAddress.getColumn(); + int fRow = cellAddress.getRow(); + + String signCol = "", signRow = ""; + if (cellID.indexOf("$") == 0) { + signCol = "$"; + } else { + fCol += dCol; + } + if (cellID.lastIndexOf("$") > 0) { + signRow = "$"; + } else { + fRow += dRow; + } + String colName = CellAddress.convertNumToColString(fCol); + return signCol + colName + signRow + (++fRow); + } +} diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/OPCPackage.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/OPCPackage.java index 94372f9d..d7c09a7f 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/OPCPackage.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/OPCPackage.java @@ -6,6 +6,7 @@ import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -20,8 +21,6 @@ import java.util.regex.Pattern; import static java.lang.String.format; -import static org.dhatim.fastexcel.reader.DefaultXMLInputFactory.factory; - class OPCPackage implements AutoCloseable { private static final Pattern filenameRegex = Pattern.compile("^(.*/)([^/]+)$"); // Following is a listing of number formats whose formatCode @@ -57,31 +56,32 @@ class OPCPackage implements AutoCloseable { put("49", "@"); }}; private final ZipFile zip; + private final XMLInputFactory xmlInputFactory; private final Map workbookPartsById; private final PartEntryNames parts; - private final List formatIdList; - private Map fmtIdToFmtString; + private final FormatCatalog formatCatalog; private OPCPackage(File zipFile) throws IOException { this(zipFile, false); } private OPCPackage(File zipFile, boolean withFormat) throws IOException { - this(new ZipFile(zipFile), withFormat); + this(new ZipFile(zipFile), withFormat, DefaultXMLInputFactory.create()); } private OPCPackage(SeekableInMemoryByteChannel channel, boolean withStyle) throws IOException { - this(new ZipFile(channel), withStyle); + this(new ZipFile(channel), withStyle, DefaultXMLInputFactory.create()); } - private OPCPackage(ZipFile zip, boolean withFormat) throws IOException { + private OPCPackage(ZipFile zip, boolean withFormat, XMLInputFactory xmlInputFactory) throws IOException { try { this.zip = zip; + this.xmlInputFactory = xmlInputFactory; this.parts = extractPartEntriesFromContentTypes(); if (withFormat) { - this.formatIdList = extractFormat(parts.style); + this.formatCatalog = extractFormat(parts.style); } else { - this.formatIdList = Collections.emptyList(); + this.formatCatalog = FormatCatalog.EMPTY; } this.workbookPartsById = readWorkbookPartsIds(relsNameFor(parts.workbook)); } catch (XMLStreamException e) { @@ -96,7 +96,7 @@ private static String relsNameFor(String entryName) { private Map readWorkbookPartsIds(String workbookRelsEntryName) throws IOException, XMLStreamException { String xlFolder = workbookRelsEntryName.substring(0, workbookRelsEntryName.indexOf("_rel")); Map partsIdById = new HashMap<>(); - SimpleXmlReader rels = new SimpleXmlReader(factory, getRequiredEntryContent(workbookRelsEntryName)); + SimpleXmlReader rels = new SimpleXmlReader(xmlInputFactory, getRequiredEntryContent(workbookRelsEntryName)); while (rels.goTo("Relationship")) { String id = rels.getAttribute("Id"); String target = rels.getAttribute("Target"); @@ -112,7 +112,7 @@ private Map readWorkbookPartsIds(String workbookRelsEntryName) t private PartEntryNames extractPartEntriesFromContentTypes() throws XMLStreamException, IOException { PartEntryNames entries = new PartEntryNames(); final String contentTypesXml = "[Content_Types].xml"; - try (SimpleXmlReader reader = new SimpleXmlReader(factory, getRequiredEntryContent(contentTypesXml))) { + try (SimpleXmlReader reader = new SimpleXmlReader(xmlInputFactory, getRequiredEntryContent(contentTypesXml))) { while (reader.goTo(() -> reader.isStartElement("Override"))) { String contentType = reader.getAttributeRequired("ContentType"); if (PartEntryNames.WORKBOOK_MAIN_CONTENT_TYPE.equals(contentType) @@ -136,10 +136,10 @@ private PartEntryNames extractPartEntriesFromContentTypes() throws XMLStreamExce return entries; } - private List extractFormat(String styleXml) throws XMLStreamException, IOException { + private FormatCatalog extractFormat(String styleXml) throws XMLStreamException, IOException { List fmtIdList = new ArrayList<>(); - fmtIdToFmtString = new HashMap<>(); - try (SimpleXmlReader reader = new SimpleXmlReader(factory, getRequiredEntryContent(styleXml))) { + Map fmtIdToFmtString = new HashMap<>(); + try (SimpleXmlReader reader = new SimpleXmlReader(xmlInputFactory, getRequiredEntryContent(styleXml))) { AtomicBoolean insideCellXfs = new AtomicBoolean(false); while (reader.goTo(() -> reader.isStartElement("numFmt") || reader.isStartElement("xf") || reader.isStartElement("cellXfs") || reader.isEndElement("cellXfs"))) { @@ -160,7 +160,7 @@ private List extractFormat(String styleXml) throws XMLStreamException, I } } } - return fmtIdList; + return new FormatCatalog(fmtIdList, fmtIdToFmtString); } private InputStream getRequiredEntryContent(String name) throws IOException { @@ -231,11 +231,19 @@ public InputStream getSheetContent(Sheet sheet) throws IOException { } public List getFormatList() { - return formatIdList; + return formatCatalog.getFormatIdsByStyleIndex(); } public Map getFmtIdToFmtString() { - return fmtIdToFmtString; + return formatCatalog.getFormatsById(); + } + + FormatCatalog getFormatCatalog() { + return formatCatalog; + } + + XMLInputFactory getXmlInputFactory() { + return xmlInputFactory; } private static class PartEntryNames { diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/ReadableWorkbook.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/ReadableWorkbook.java index 9a1f1182..072d2d16 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/ReadableWorkbook.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/ReadableWorkbook.java @@ -16,6 +16,7 @@ package org.dhatim.fastexcel.reader; import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; import java.io.*; import java.util.ArrayList; import java.util.List; @@ -24,13 +25,12 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static org.dhatim.fastexcel.reader.DefaultXMLInputFactory.factory; - public class ReadableWorkbook implements Closeable { private final OPCPackage pkg; private final SST sst; private final ReadingOptions readingOptions; + private final XMLInputFactory xmlInputFactory; private boolean date1904; private final List sheets = new ArrayList<>(); @@ -64,11 +64,12 @@ private ReadableWorkbook(OPCPackage pkg, ReadingOptions readingOptions) throws I try { this.pkg = pkg; - sst = SST.fromInputStream(pkg.getSharedStrings()); + this.xmlInputFactory = pkg.getXmlInputFactory(); + sst = SST.fromInputStream(pkg.getSharedStrings(), xmlInputFactory); } catch (XMLStreamException e) { throw new ExcelReaderException(e); } - try (SimpleXmlReader workbookReader = new SimpleXmlReader(factory, pkg.getWorkbookContent())) { + try (SimpleXmlReader workbookReader = new SimpleXmlReader(xmlInputFactory, pkg.getWorkbookContent())) { readWorkbook(workbookReader); } catch (XMLStreamException e) { throw new ExcelReaderException(e); @@ -148,7 +149,7 @@ private void createSheet(SimpleXmlReader r) { Stream openStream(Sheet sheet) throws IOException { try { InputStream inputStream = pkg.getSheetContent(sheet); - Stream stream = StreamSupport.stream(new RowSpliterator(this, inputStream), false); + Stream stream = StreamSupport.stream(new RowSpliterator(this, inputStream, xmlInputFactory), false); return stream.onClose(asUncheckedRunnable(inputStream)); } catch (XMLStreamException e) { throw new IOException(e); @@ -163,6 +164,14 @@ public Map getNumFmtIdToFormat() { return pkg.getFmtIdToFmtString(); } + String getFormatId(int styleIndex) { + return pkg.getFormatCatalog().getFormatId(styleIndex); + } + + String getFormatString(String formatId) { + return pkg.getFormatCatalog().getFormatString(formatId); + } + SST getSharedStringsTable() { return sst; } diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/RowSpliterator.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/RowSpliterator.java index 6e48f77a..3196838f 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/RowSpliterator.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/RowSpliterator.java @@ -15,20 +15,16 @@ */ package org.dhatim.fastexcel.reader; -import static org.dhatim.fastexcel.reader.DefaultXMLInputFactory.factory; - import java.io.InputStream; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; -import java.util.Optional; import java.util.Spliterator; import java.util.function.Consumer; import java.util.function.Function; +import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; class RowSpliterator implements Spliterator { @@ -36,14 +32,17 @@ class RowSpliterator implements Spliterator { private final SimpleXmlReader r; private final ReadableWorkbook workbook; - private final HashMap sharedFormula = new HashMap<>(); - private final HashMap arrayFormula = new HashMap<>(); + private final FormulaResolver formulaResolver = new FormulaResolver(); private int rowCapacity = 16; private int trackedRowIndex = 0; public RowSpliterator(ReadableWorkbook workbook, InputStream inputStream) throws XMLStreamException { + this(workbook, inputStream, DefaultXMLInputFactory.create()); + } + + public RowSpliterator(ReadableWorkbook workbook, InputStream inputStream, XMLInputFactory xmlInputFactory) throws XMLStreamException { this.workbook = workbook; - this.r = new SimpleXmlReader(factory, inputStream); + this.r = new SimpleXmlReader(xmlInputFactory, inputStream); r.goTo("sheetData"); } @@ -138,9 +137,9 @@ private Cell parseCell(int trackedColIndex) throws XMLStreamException { String formatString = null; if (styleString != null) { int index = Integer.parseInt(styleString); - if (index < workbook.getFormats().size()) { - formatId = workbook.getFormats().get(index); - formatString = workbook.getNumFmtIdToFormat().get(formatId); + formatId = workbook.getFormatId(index); + if (formatId != null) { + formatString = workbook.getFormatString(formatId); } } @@ -157,7 +156,18 @@ private Cell parseOther(CellAddress addr, String type, String dataFormatId, Stri throws XMLStreamException { CellType definedType = parseType(type); Function parser = getParserForType(definedType); + ParsedCellContent content = parseCellContent(addr, definedType, parser); + + if (content.formula == null && content.value == null && content.type == CellType.NUMBER) { + return new Cell(workbook, CellType.EMPTY, null, addr, null, content.rawValue); + } else { + CellType cellType = content.formula != null ? CellType.FORMULA : content.type; + return new Cell(workbook, cellType, content.value, addr, content.formula, content.rawValue, dataFormatId, dataFormatString); + } + } + private ParsedCellContent parseCellContent(CellAddress addr, CellType definedType, Function parser) + throws XMLStreamException { Object value = null; String formula = null; String rawValue = null; @@ -180,15 +190,13 @@ private Cell parseOther(CellAddress addr, String type, String dataFormatId, Stri Integer siInt = si == null ? null : Integer.parseInt(si); formula = r.getValueUntilEndElement("f"); if ("array".equals(t) && ref != null) { - CellRangeAddress range = CellRangeAddress.valueOf(ref); - arrayFormula.put(range, formula); + formulaResolver.registerArrayFormula(ref, formula); } if ("shared".equals(t)) { if (ref != null) { - CellRangeAddress range = CellRangeAddress.valueOf(ref); - sharedFormula.put(siInt, new BaseFormulaCell(addr, formula, range)); + formulaResolver.registerSharedFormula(siInt, addr, formula, ref); } else { - formula = parseSharedFormula(siInt, addr); + formula = formulaResolver.resolveSharedFormula(siInt, addr); } } } else { @@ -197,99 +205,12 @@ private Cell parseOther(CellAddress addr, String type, String dataFormatId, Stri } if (formula == null || "".equals(formula)) { - formula = getArrayFormula(addr).orElse(null); - } - - if (formula == null && value == null && definedType == CellType.NUMBER) { - return new Cell(workbook, CellType.EMPTY, null, addr, null, rawValue); - } else { - CellType cellType = formula != null ? CellType.FORMULA : definedType; - return new Cell(workbook, cellType, value, addr, formula, rawValue, dataFormatId, dataFormatString); + formula = formulaResolver.resolveArrayFormula(addr).orElse(null); } - } - private String parseSharedFormula(Integer si, CellAddress addr) { - BaseFormulaCell baseFormulaCell = sharedFormula.get(si); - int dRow = addr.getRow() - baseFormulaCell.getBaseCelAddr().getRow(); - int dCol = addr.getColumn() - baseFormulaCell.getBaseCelAddr().getColumn(); - String baseFormula = baseFormulaCell.getFormula(); - return parseSharedFormula(dCol, dRow, baseFormula); + return new ParsedCellContent(definedType, value, formula, rawValue); } - /** - * @see here - */ - private String parseSharedFormula(Integer dCol, Integer dRow, String baseFormula) { - StringBuilder res = new StringBuilder(); - int start = 0; - boolean stringLiteral = false; - for (int end = 0; end < baseFormula.length(); end++) { - char c = baseFormula.charAt(end); - if ('"' == c) { - stringLiteral = !stringLiteral; - } - if (stringLiteral) { - continue;// Skip characters in quotes - } - if (c >= 'A' && c <= 'Z' || c == '$') { - - res.append(baseFormula.substring(start, end)); - start = end; - end++; - boolean foundNum = false; - for (; end < baseFormula.length(); end++) { - char idc = baseFormula.charAt(end); - if (idc >= '0' && idc <= '9' || idc == '$') { - foundNum = true; - } else if (idc >= 'A' && idc <= 'Z') { - if (foundNum) { - break; - } - } else { - break; - } - } - if (foundNum) { - String cellID = baseFormula.substring(start, end); - res.append(shiftCell(cellID, dCol, dRow)); - start = end; - } - } - } - - if (start < baseFormula.length()) { - res.append(baseFormula.substring(start)); - } - - return res.toString(); - } - - /** - * @see here - */ - private String shiftCell(String cellID, Integer dCol, Integer dRow) { - CellAddress cellAddress = new CellAddress(cellID); - int fCol = cellAddress.getColumn(); - int fRow = cellAddress.getRow(); - - String signCol = "", signRow = ""; - if (cellID.indexOf("$") == 0) { - signCol = "$"; - } else { - // Shift column - fCol += dCol; - } - if (cellID.lastIndexOf("$") > 0) { - signRow = "$"; - } else { - // Shift row - fRow += dRow; - } - String colName = CellAddress.convertNumToColString(fCol); - return signCol + colName + signRow + (++fRow); - } - - private Cell parseString(CellAddress addr) throws XMLStreamException { r.goTo(() -> r.isStartElement("v") || r.isEndElement("c")); if (r.isEndElement("c")) { @@ -329,15 +250,6 @@ private Cell parseInlineStr(CellAddress addr) throws XMLStreamException { return new Cell(workbook, cellType, value, addr, formula, rawValue); } - private Optional getArrayFormula(CellAddress addr) { - for (Map.Entry entry : arrayFormula.entrySet()) { - if (entry.getKey().isInRange(addr.getRow(), addr.getColumn())) { - return Optional.of(entry.getValue()); - } - } - return Optional.empty(); - } - private CellType parseType(String type) { switch (type) { case "b": @@ -396,4 +308,18 @@ private static void ensureSize(List list, int newSize) { } } + private static final class ParsedCellContent { + private final CellType type; + private final Object value; + private final String formula; + private final String rawValue; + + private ParsedCellContent(CellType type, Object value, String formula, String rawValue) { + this.type = type; + this.value = value; + this.formula = formula; + this.rawValue = rawValue; + } + } + } diff --git a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/SST.java b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/SST.java index d105ee7d..fa9d0ec7 100644 --- a/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/SST.java +++ b/fastexcel-reader/src/main/java/org/dhatim/fastexcel/reader/SST.java @@ -2,12 +2,11 @@ import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import static org.dhatim.fastexcel.reader.DefaultXMLInputFactory.factory; - class SST { private static final SST EMPTY = new SST(); private final SimpleXmlReader reader; @@ -17,12 +16,12 @@ private SST() { reader = null; } - SST(InputStream in) throws XMLStreamException { - reader = new SimpleXmlReader(factory, in); + SST(InputStream in, XMLInputFactory xmlInputFactory) throws XMLStreamException { + reader = new SimpleXmlReader(xmlInputFactory, in); } - static SST fromInputStream(InputStream in) throws XMLStreamException { - return in == null ? EMPTY : new SST(in); + static SST fromInputStream(InputStream in, XMLInputFactory xmlInputFactory) throws XMLStreamException { + return in == null ? EMPTY : new SST(in, xmlInputFactory); } String getItemAt(int index) throws XMLStreamException { diff --git a/fastexcel-writer/.DS_Store b/fastexcel-writer/.DS_Store new file mode 100644 index 00000000..fc3cb7b9 Binary files /dev/null and b/fastexcel-writer/.DS_Store differ diff --git a/fastexcel-writer/pom.xml b/fastexcel-writer/pom.xml index 10eb61fb..3f101da7 100644 --- a/fastexcel-writer/pom.xml +++ b/fastexcel-writer/pom.xml @@ -15,6 +15,10 @@ https://github.com/dhatim/fastexcel + + org.dhatim + fastexcel-core + com.github.rzymek opczip diff --git a/fastexcel-writer/src/main/java/module-info.java b/fastexcel-writer/src/main/java/module-info.java index ac068a38..67798128 100644 --- a/fastexcel-writer/src/main/java/module-info.java +++ b/fastexcel-writer/src/main/java/module-info.java @@ -1,5 +1,6 @@ module org.dhatim.fastexcel { + requires org.dhatim.fastexcel.core; requires opczip; exports org.dhatim.fastexcel; -} \ No newline at end of file +} diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/AbstractDataValidation.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/AbstractDataValidation.java new file mode 100644 index 00000000..8a005c09 --- /dev/null +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/AbstractDataValidation.java @@ -0,0 +1,81 @@ +package org.dhatim.fastexcel; + +import java.io.IOException; + +abstract class AbstractDataValidation> implements DataValidation { + + private final Range range; + private final String type; + + private boolean allowBlank = true; + private boolean showDropdown = true; + private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION; + private boolean showErrorMessage = false; + private String errorTitle; + private String error; + + AbstractDataValidation(Range range, String type) { + this.range = range; + this.type = type; + } + + public T allowBlank(boolean allowBlank) { + this.allowBlank = allowBlank; + return self(); + } + + public T showDropdown(boolean showDropdown) { + this.showDropdown = showDropdown; + return self(); + } + + public T errorStyle(DataValidationErrorStyle errorStyle) { + this.errorStyle = errorStyle; + return self(); + } + + public T showErrorMessage(boolean showErrorMessage) { + this.showErrorMessage = showErrorMessage; + return self(); + } + + public T errorTitle(String errorTitle) { + this.errorTitle = errorTitle; + return self(); + } + + public T error(String error) { + this.error = error; + return self(); + } + + @Override + public final void write(Writer w) throws IOException { + w.append(""); + writeFormula(w); + w.append(""); + } + + protected abstract void writeFormula(Writer w) throws IOException; + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } +} diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Cell.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Cell.java index 37f1ec2c..486508c2 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Cell.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Cell.java @@ -32,7 +32,7 @@ class Cell implements Ref { /** * Cell value. */ - private Object value; + private CellValue value; /** * Cached style index. @@ -53,79 +53,47 @@ void write(Writer w, int r, int c) throws IOException { if (style != 0) { w.append(" s=\"").append(style).append('\"'); } - if (value != null && !(value instanceof Formula)) { - w.append(" t=\"").append(getCellType(value)).append('\"'); + if (value != null && value.type() != null) { + w.append(" t=\"").append(value.type()).append('\"'); } w.append(">"); - if (value instanceof Formula) { - w.append("").append(((Formula) value).getExpression()).append(""); - } else if (value instanceof RichText) { - ((RichText) value).write(w); - } else if (value instanceof String) { - w.append("").appendEscaped((String) value).append(""); - } else if (value != null) { - w.append(""); - if (value instanceof CachedString) { - w.append(((CachedString) value).getIndex()); - } else if (value instanceof Integer) { - w.append((int) value); - } else if (value instanceof Long) { - w.append((long) value); - } else if (value instanceof Double) { - w.append((double) value); - } else if (value instanceof Boolean) { - w.append((Boolean) value ? '1' : '0'); - } else { - w.append(value.toString()); - } - w.append(""); + if (value != null) { + value.write(w); } w.append(""); } } - static String getCellType(Object value) { - if (value instanceof CachedString) { - return "s"; - } else if (value instanceof Boolean) { - return "b"; - } else if (value instanceof String || value instanceof RichText) { - return "inlineStr"; - } else { - return "n"; - } - } - void setValue(Workbook wb, String v) { - value = v == null ? null : wb.cacheString(v); + value = v == null ? null : new CachedStringValue(wb.cacheString(v)); } void setValue(Number v) { - value = v; + value = v == null ? null : new NumericValue(v); } void setValue(Boolean v) { - value = v; + value = v == null ? null : new BooleanValue(v); } void setValue(Date v) { - value = v == null ? null : TimestampUtil.convertDate(v); + setValue(v == null ? null : TimestampUtil.convertDate(v)); } void setValue(LocalDateTime v) { - value = v == null ? null : TimestampUtil.convertDate(v); + setValue(v == null ? null : TimestampUtil.convertDate(v)); } void setValue(LocalDate v) { - value = v == null ? null : TimestampUtil.convertDate(v); + setValue(v == null ? null : TimestampUtil.convertDate(v)); } void setValue(ZonedDateTime v) { - value = v == null ? null : TimestampUtil.convertZonedDateTime(v); + setValue(v == null ? null : TimestampUtil.convertZonedDateTime(v)); } void setValue(Instant v) { - value = v == null ? null : TimestampUtil.convertInstant(v); + setValue(v == null ? null : TimestampUtil.convertInstant(v)); } /** @@ -134,13 +102,7 @@ void setValue(Instant v) { * @return Value or {@link Formula}, or {@code null}. */ Object getValue() { - Object result; - if (value instanceof CachedString) { - result = ((CachedString) value).getString(); - } else { - result = value; - } - return result; + return value == null ? null : value.value(); } /** @@ -149,7 +111,7 @@ Object getValue() { * @param expression Formula expression. */ void setFormula(String expression) { - value = new Formula(expression); + value = new FormulaValue(new Formula(expression)); } /** @@ -158,7 +120,7 @@ void setFormula(String expression) { * @param v String value. */ void setInlineString(String v) { - value = v; + value = v == null ? null : new InlineStringValue(v); } /** @@ -167,7 +129,7 @@ void setInlineString(String v) { * @param v Rich inline string value. */ void setInlineString(RichText v) { - value = v; + value = v == null ? null : new RichTextValue(v); } /** @@ -188,4 +150,132 @@ void setStyle(int style) { this.style = style; } + private interface CellValue { + String type(); + + void write(Writer w) throws IOException; + + Object value(); + } + + private static final class FormulaValue implements CellValue { + private final Formula formula; + + private FormulaValue(Formula formula) { + this.formula = formula; + } + + public String type() { + return null; + } + + public void write(Writer w) throws IOException { + w.append("").append(formula.getExpression()).append(""); + } + + public Object value() { + return formula; + } + } + + private static final class RichTextValue implements CellValue { + private final RichText richText; + + private RichTextValue(RichText richText) { + this.richText = richText; + } + + public String type() { + return "inlineStr"; + } + + public void write(Writer w) throws IOException { + richText.write(w); + } + + public Object value() { + return richText; + } + } + + private static final class InlineStringValue implements CellValue { + private final String string; + + private InlineStringValue(String string) { + this.string = string; + } + + public String type() { + return "inlineStr"; + } + + public void write(Writer w) throws IOException { + w.append("").appendEscaped(string).append(""); + } + + public Object value() { + return string; + } + } + + private static final class CachedStringValue implements CellValue { + private final CachedString cachedString; + + private CachedStringValue(CachedString cachedString) { + this.cachedString = cachedString; + } + + public String type() { + return "s"; + } + + public void write(Writer w) throws IOException { + w.append("").append(cachedString.getIndex()).append(""); + } + + public Object value() { + return cachedString.getString(); + } + } + + private static final class NumericValue implements CellValue { + private final Number number; + + private NumericValue(Number number) { + this.number = number; + } + + public String type() { + return "n"; + } + + public void write(Writer w) throws IOException { + w.append("").append(number.toString()).append(""); + } + + public Object value() { + return number; + } + } + + private static final class BooleanValue implements CellValue { + private final Boolean bool; + + private BooleanValue(Boolean bool) { + this.bool = bool; + } + + public String type() { + return "b"; + } + + public void write(Writer w) throws IOException { + w.append("").append(bool ? '1' : '0').append(""); + } + + public Object value() { + return bool; + } + } + } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CellAddress.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CellAddress.java index 8f505ba4..25617348 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CellAddress.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CellAddress.java @@ -15,10 +15,9 @@ */ package org.dhatim.fastexcel; -import java.nio.charset.StandardCharsets; +import org.dhatim.fastexcel.common.CellAddressUtil; final class CellAddress { - private static final int COL_RADIX = 'Z' - 'A' + 1; private static final String[] CACHED_COLS = new String[1024]; static { @@ -45,24 +44,6 @@ static String convertNumToColString(int col) { } private static String convertNumToColStringImpl(int col) { - // Excel counts column A as the 1st column, we - // treat it as the 0th one - int excelColNum = col + 1; - - final int MAX_COL_CHARS = 3; - final byte[] colRef = new byte[MAX_COL_CHARS]; - int colRemain = excelColNum; - int pos = 2; - while (colRemain > 0) { - int thisPart = colRemain % COL_RADIX; - if (thisPart == 0) { - thisPart = COL_RADIX; - } - colRemain = (colRemain - thisPart) / COL_RADIX; - - colRef[pos--] = (byte) (thisPart + (int) 'A' - 1); - } - pos++; - return new String(colRef, pos, (MAX_COL_CHARS - pos), StandardCharsets.ISO_8859_1); + return CellAddressUtil.convertNumToColString(col); } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ColumnStyleSetter.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ColumnStyleSetter.java index e7a9b0e7..0b9a5008 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ColumnStyleSetter.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ColumnStyleSetter.java @@ -51,7 +51,7 @@ public class ColumnStyleSetter extends GenericStyleSetter { * done otherwise style changes are lost! */ public void set() { - super.setStyle(false, new HashSet<>(Collections.singletonList(column.getStyle())), column::applyStyle); + super.setStyle(true, new HashSet<>(Collections.singletonList(column.getStyle())), column::applyStyle); } @Override diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CustomDataValidation.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CustomDataValidation.java index 7d281461..c3f03aa9 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CustomDataValidation.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/CustomDataValidation.java @@ -5,18 +5,10 @@ /** * A CustomDataValidation defines a DataValidation for a worksheet of type = "custom" */ -public class CustomDataValidation implements DataValidation { +public class CustomDataValidation extends AbstractDataValidation { private final static String TYPE = "custom"; - private final Range range; private final Formula formula; - private boolean allowBlank = true; - private boolean showDropdown = true; - private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION; - private boolean showErrorMessage = false; - private String errorTitle; - private String error; - /** * Constructor * @@ -24,104 +16,17 @@ public class CustomDataValidation implements DataValidation { * @param formula The Formula of the custom validation */ CustomDataValidation(Range range, Formula formula) { - this.range = range; + super(range, TYPE); this.formula = formula; } - /** - * whether blank cells should pass the validation - * - * @param allowBlank whether or not to allow blank values - * @return this CustomDataValidation - */ - public CustomDataValidation allowBlank(boolean allowBlank) { - this.allowBlank = allowBlank; - return this; - } - - /** - * Whether Excel will show an in-cell dropdown list - * containing the validation list - * - * @param showDropdown whether or not to show the dropdown - * @return this CustomDataValidation - */ - public CustomDataValidation showDropdown(boolean showDropdown) { - this.showDropdown = showDropdown; - return this; - } - - /** - * The style of error alert used for this data validation. - * - * @param errorStyle The DataValidationErrorStyle for this DataValidation - * @return this CustomDataValidation - */ - public CustomDataValidation errorStyle(DataValidationErrorStyle errorStyle) { - this.errorStyle = errorStyle; - return this; - } - - /** - * Whether to display the error alert message when an invalid value has been entered. - * - * @param showErrorMessage whether to display the error message - * @return this CustomDataValidation - */ - public CustomDataValidation showErrorMessage(boolean showErrorMessage) { - this.showErrorMessage = showErrorMessage; - return this; - } - - /** - * Title bar text of error alert. - * - * @param errorTitle The error title - * @return this CustomDataValidation - */ - public CustomDataValidation errorTitle(String errorTitle) { - this.errorTitle = errorTitle; - return this; - } - - /** - * Message text of error alert. - * - * @param error The error message - * @return this CustomDataValidation - */ - public CustomDataValidation error(String error) { - this.error = error; - return this; - } - /** * Write this dataValidation as an XML element. * * @param w Output writer. * @throws IOException If an I/O error occurs. */ - @Override - public void write(Writer w) throws IOException { - w - .append("") - .append(formula.getExpression()) - .append(""); + protected void writeFormula(Writer w) throws IOException { + w.append(formula.getExpression()); } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/DynamicBitMatrix.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/DynamicBitMatrix.java index 04614f32..c14ea729 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/DynamicBitMatrix.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/DynamicBitMatrix.java @@ -30,38 +30,43 @@ void setRegion(int top, int left, int bottom, int right) { int leftBitMatrixColIndex = left / UNIT_WEITH; int topBitMatrixRowIndex = top / UNIT_HIGHT; int bottomBitMatrixRowIndex = bottom / UNIT_HIGHT; - if (rightBitMatrixColIndex >= bitMatrixData.size()) { - for (int i = bitMatrixData.size() - 1; i < rightBitMatrixColIndex; i++) { - bitMatrixData.add(null); - } - } for (int i = leftBitMatrixColIndex; i <= rightBitMatrixColIndex; i++) { - if (bitMatrixData.get(i) == null || bitMatrixData.get(i).isEmpty()) { - bitMatrixData.set(i, new CopyOnWriteArrayList<>()); - } - CopyOnWriteArrayList colBitMatrices = bitMatrixData.get(i); - if (bottomBitMatrixRowIndex >= colBitMatrices.size()) { - for (int j = colBitMatrices.size() - 1; j < bottomBitMatrixRowIndex; j++) { - colBitMatrices.add(null); - } - } + CopyOnWriteArrayList colBitMatrices = ensureColumn(i); for (int j = topBitMatrixRowIndex; j <= bottomBitMatrixRowIndex; j++) { - if (colBitMatrices.get(j) == null || colBitMatrices.isEmpty()) { - colBitMatrices.set(j, new BitMatrix(UNIT_WEITH, UNIT_HIGHT)); - } - BitMatrix bitMatrix = colBitMatrices.get(j); + BitMatrix bitMatrix = ensureChunk(colBitMatrices, j); + MatrixRegion region = MatrixRegion.intersection(i, j, top, left, bottom, right); + bitMatrix.setRegion(region.left, region.top, region.width(), region.height()); + } - int l = Math.max(i * UNIT_WEITH, left) - i * UNIT_WEITH; - int t = Math.max(j * UNIT_HIGHT, top) - j * UNIT_HIGHT; - int r = Math.min((i + 1) * UNIT_WEITH - 1, right) - i * UNIT_WEITH; - int b = Math.min((j + 1) * UNIT_HIGHT - 1, bottom) - j * UNIT_HIGHT; - bitMatrix.setRegion(l, t, r - l + 1, b - t + 1); - } + } + + } + private CopyOnWriteArrayList ensureColumn(int columnIndex) { + ensureSize(bitMatrixData, columnIndex); + CopyOnWriteArrayList colBitMatrices = bitMatrixData.get(columnIndex); + if (colBitMatrices == null || colBitMatrices.isEmpty()) { + colBitMatrices = new CopyOnWriteArrayList<>(); + bitMatrixData.set(columnIndex, colBitMatrices); + } + return colBitMatrices; + } + private BitMatrix ensureChunk(CopyOnWriteArrayList colBitMatrices, int rowIndex) { + ensureSize(colBitMatrices, rowIndex); + BitMatrix bitMatrix = colBitMatrices.get(rowIndex); + if (bitMatrix == null) { + bitMatrix = new BitMatrix(UNIT_WEITH, UNIT_HIGHT); + colBitMatrices.set(rowIndex, bitMatrix); } + return bitMatrix; + } + private static void ensureSize(CopyOnWriteArrayList list, int index) { + for (int i = list.size() - 1; i < index; i++) { + list.add(null); + } } boolean isConflict(int top, int left, int bottom, int right) { @@ -114,11 +119,7 @@ public String buildToString(String setString, String unsetString, String fillNul for (int i = 0; i < maxBitMatrixRow; i++) { for (int h = 0; h < UNIT_HIGHT; h++) { for (int j = 0; j < maxBitMatrixCol; j++) { - boolean inNullArea = isInNullArea(i, j); - for (int k = 0; k < UNIT_WEITH; k++) { - builder.append(inNullArea ? fillNullString : bitMatrixData.get(j).get(i).get(k, h) ? setString : unsetString); - builder.append(j == maxBitMatrixCol - 1 && k == UNIT_WEITH - 1 ? lineSeparator : ','); - } + appendChunkRow(builder, i, h, j, maxBitMatrixCol, setString, unsetString, fillNullString, lineSeparator); } } @@ -126,4 +127,45 @@ public String buildToString(String setString, String unsetString, String fillNul return builder.toString(); } + private void appendChunkRow(StringBuilder builder, int chunkRow, int localRow, int chunkCol, int maxBitMatrixCol, + String setString, String unsetString, String fillNullString, String lineSeparator) { + boolean inNullArea = isInNullArea(chunkRow, chunkCol); + for (int k = 0; k < UNIT_WEITH; k++) { + builder.append(inNullArea ? fillNullString : bitMatrixData.get(chunkCol).get(chunkRow).get(k, localRow) ? setString : unsetString); + builder.append(chunkCol == maxBitMatrixCol - 1 && k == UNIT_WEITH - 1 ? lineSeparator : ','); + } + } + + private static final class MatrixRegion { + private final int left; + private final int top; + private final int right; + private final int bottom; + + private MatrixRegion(int left, int top, int right, int bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + private static MatrixRegion intersection(int chunkCol, int chunkRow, int top, int left, int bottom, int right) { + int chunkLeft = chunkCol * UNIT_WEITH; + int chunkTop = chunkRow * UNIT_HIGHT; + return new MatrixRegion( + Math.max(chunkLeft, left) - chunkLeft, + Math.max(chunkTop, top) - chunkTop, + Math.min((chunkCol + 1) * UNIT_WEITH - 1, right) - chunkLeft, + Math.min((chunkRow + 1) * UNIT_HIGHT - 1, bottom) - chunkTop); + } + + private int width() { + return right - left + 1; + } + + private int height() { + return bottom - top + 1; + } + } + } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Font.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Font.java index bb104b69..de792306 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Font.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Font.java @@ -30,7 +30,7 @@ class Font { /** * Default font. */ - public static Font DEFAULT = build(false, false, false, "Calibri", BigDecimal.valueOf(11.0), "FF000000", false); + public static final Font DEFAULT = new Font(false, false, false, "Calibri", BigDecimal.valueOf(11.0), "FF000000", false); /** * Bold flag. @@ -98,7 +98,17 @@ class Font { * @return New font object. */ public static Font build(Boolean bold, Boolean italic, Boolean underlined, String name, BigDecimal size, String rgbColor, Boolean strikethrough) { - return new Font(bold != null? bold : DEFAULT.bold, italic != null ? italic : DEFAULT.italic , underlined != null ? underlined : DEFAULT.underlined, name != null ? name : DEFAULT.name, size != null ? size:DEFAULT.size, rgbColor != null ? rgbColor: DEFAULT.rgbColor, strikethrough != null ? strikethrough : DEFAULT.strikethrough); + return build(DEFAULT, bold, italic, underlined, name, size, rgbColor, strikethrough); + } + + static Font build(Font defaults, Boolean bold, Boolean italic, Boolean underlined, String name, BigDecimal size, String rgbColor, Boolean strikethrough) { + return new Font(bold != null ? bold : defaults.bold, + italic != null ? italic : defaults.italic, + underlined != null ? underlined : defaults.underlined, + name != null ? name : defaults.name, + size != null ? size : defaults.size, + rgbColor != null ? rgbColor : defaults.rgbColor, + strikethrough != null ? strikethrough : defaults.strikethrough); } @Override diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/FontSpec.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/FontSpec.java new file mode 100644 index 00000000..66868fac --- /dev/null +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/FontSpec.java @@ -0,0 +1,33 @@ +package org.dhatim.fastexcel; + +import java.math.BigDecimal; + +final class FontSpec { + + private final Boolean bold; + private final Boolean italic; + private final Boolean underlined; + private final String name; + private final BigDecimal size; + private final String rgbColor; + private final Boolean strikethrough; + + FontSpec(Boolean bold, Boolean italic, Boolean underlined, String name, BigDecimal size, String rgbColor, Boolean strikethrough) { + this.bold = bold; + this.italic = italic; + this.underlined = underlined; + this.name = name; + this.size = size; + this.rgbColor = rgbColor; + this.strikethrough = strikethrough; + } + + Font toFont(Font defaults) { + return Font.build(defaults, bold, italic, underlined, name, size, rgbColor, strikethrough); + } + + boolean hasDifferentialOverrides() { + return Boolean.TRUE.equals(bold) || Boolean.TRUE.equals(italic) || Boolean.TRUE.equals(underlined) + || Boolean.TRUE.equals(strikethrough) || name != null || size != null || rgbColor != null; + } +} diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java index 6cb0776d..a63b9adf 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java @@ -469,76 +469,79 @@ public STYLE_SETTER protectionOption(ProtectionOption option, Boolean value) { */ protected void setStyle(boolean shadingEnabled, Set currentStyles, StylesFunction stylesFunction) { - Alignment alignment; - if (horizontalAlignment != null || verticalAlignment != null || wrapText || rotation != 0 || indent != 0) { - alignment = new Alignment(horizontalAlignment, verticalAlignment, wrapText, rotation, indent); - } else { - alignment = null; - } - Font font; - if (!Font.equalsDefault(bold,italic,underlined,fontName,fontSize,fontColor, strikethrough)) { - font = Font.build(bold, italic, underlined, fontName, fontSize, fontColor, strikethrough); - } else { - font = Font.DEFAULT; - } - Fill fill; - if (fillColor == null) { - fill = Fill.NONE; - } else { - fill = Fill.fromColor(fillColor); - } - if (border == null) { - border = Border.NONE; - } - - Protection protection; - if (protectionOptions != null) { - protection = new Protection(protectionOptions); - } else { - protection = null; - } + StyleSpec styleSpec = regularStyleSpec(); // Compute a map giving new styles for current styles - Map newStyles = currentStyles.stream().collect(Collectors.toMap(Function.identity(), s -> worksheet.getWorkbook().mergeAndCacheStyle(s, valueFormatting, font, fill, border, alignment, protection))); + Map newStyles = currentStyles.stream().collect(Collectors.toMap(Function.identity(), s -> worksheet.getWorkbook().mergeAndCacheStyle(s, styleSpec))); // Apply styles stylesFunction.applyStyles(newStyles); if (shadingEnabled) { - // Shading color for alternate rows is cached separately - if (alternateShadingFillColor != null) { - getRange().shadeAlternateRows(Fill.fromColor(alternateShadingFillColor, false)); - } - - if (shadingFillColor != null) { - getRange().shadeRows(Fill.fromColor(shadingFillColor, false), eachNRows); - } + applyShading(); } } - /** - * Apply style elements conditionally - * @param conditionalFormattingRule Conditional formatting rule to apply - */ - public void set(ConditionalFormattingRule conditionalFormattingRule) { - Alignment alignment = null; - if (horizontalAlignment != null || verticalAlignment != null || wrapText || rotation != 0 || indent != 0) { - alignment = new Alignment(horizontalAlignment, verticalAlignment, wrapText, rotation, indent); - } - Font font = null; - if (bold != null && bold || italic != null && italic || underlined != null && underlined || fontColor != null || fontName != null || fontSize != null || strikethrough != null && strikethrough) { - font = Font.build(bold, italic, underlined, fontName, fontSize, fontColor, strikethrough); + private StyleSpec regularStyleSpec() { + return new StyleSpec(valueFormatting, regularFont(), fill(Fill.NONE, true), border(Border.NONE), alignment(), protection()); + } + + private Alignment alignment() { + if (horizontalAlignment != null || verticalAlignment != null || wrapText || rotation != 0 || indent != 0) { + return new Alignment(horizontalAlignment, verticalAlignment, wrapText, rotation, indent); } - Fill fill = null; + return null; + } + + private Font regularFont() { + return fontSpec().toFont(worksheet.getWorkbook().getDefaultFont()); + } + + private Font differentialFont() { + FontSpec fontSpec = fontSpec(); + return fontSpec.hasDifferentialOverrides() ? fontSpec.toFont(worksheet.getWorkbook().getDefaultFont()) : null; + } + + private FontSpec fontSpec() { + return new FontSpec(bold, italic, underlined, fontName, fontSize, fontColor, strikethrough); + } + + private Fill fill(Fill defaultFill, boolean indexed) { if (fillColor != null) { - fill = Fill.fromColor(fillColor, false); + return Fill.fromColor(fillColor, indexed); } - Protection protection = null; + return defaultFill; + } + + private Border border(Border defaultBorder) { + return border != null ? border : defaultBorder; + } + + private Protection protection() { if (protectionOptions != null) { - protection = new Protection(protectionOptions); + return new Protection(protectionOptions); + } + return null; + } + + private void applyShading() { + // Shading color for alternate rows is cached separately + if (alternateShadingFillColor != null) { + getRange().shadeAlternateRows(Fill.fromColor(alternateShadingFillColor, false)); } - int dxfId = worksheet.getWorkbook().cacheDifferentialFormat(new DifferentialFormat(valueFormatting, font, fill, border, alignment, protection)); + if (shadingFillColor != null) { + getRange().shadeRows(Fill.fromColor(shadingFillColor, false), eachNRows); + } + } + + /** + * Apply style elements conditionally + * @param conditionalFormattingRule Conditional formatting rule to apply + */ + public void set(ConditionalFormattingRule conditionalFormattingRule) { + int dxfId = worksheet.getWorkbook().cacheDifferentialFormat(new DifferentialFormat(valueFormatting, + differentialFont(), fill(null, false), border, alignment(), protection())); conditionalFormattingRule.setDxfId(dxfId); ConditionalFormatting conditionalFormatting = new ConditionalFormatting(getRange(), conditionalFormattingRule); worksheet.addConditionalFormatting(conditionalFormatting); diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/LegacyProtectionHash.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/LegacyProtectionHash.java new file mode 100644 index 00000000..f176515d --- /dev/null +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/LegacyProtectionHash.java @@ -0,0 +1,27 @@ +package org.dhatim.fastexcel; + +final class LegacyProtectionHash { + + private LegacyProtectionHash() { + } + + static String hashPassword(String password) { + byte[] passwordCharacters = password.getBytes(); + int hash = 0; + if (passwordCharacters.length > 0) { + int charIndex = passwordCharacters.length; + while (charIndex-- > 0) { + hash = rotateHash(hash); + hash ^= passwordCharacters[charIndex]; + } + hash = rotateHash(hash); + hash ^= passwordCharacters.length; + hash ^= (0x8000 | ('N' << 8) | 'K'); + } + return Integer.toHexString(hash & 0xffff); + } + + private static int rotateHash(int hash) { + return ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); + } +} diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListDataValidation.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListDataValidation.java index 8da8b5ca..aa473a34 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListDataValidation.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListDataValidation.java @@ -5,18 +5,10 @@ /** * A ListDataValidation defines a DataValidation for a worksheet of type = "list" */ -public class ListDataValidation implements DataValidation { +public class ListDataValidation extends AbstractDataValidation { private final static String TYPE = "list"; - private final Range range; private final Range listRange; - private boolean allowBlank = true; - private boolean showDropdown = true; - private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION; - private boolean showErrorMessage = false; - private String errorTitle; - private String error; - /** * Constructor * @@ -24,106 +16,19 @@ public class ListDataValidation implements DataValidation { * @param listRange The Range of the list this validation references */ ListDataValidation(Range range, Range listRange) { - this.range = range; + super(range, TYPE); this.listRange = listRange; } - /** - * whether blank cells should pass the validation - * - * @param allowBlank whether or not to allow blank values - * @return this ListDataValidation - */ - public ListDataValidation allowBlank(boolean allowBlank) { - this.allowBlank = allowBlank; - return this; - } - - /** - * Whether Excel will show an in-cell dropdown list - * containing the validation list - * - * @param showDropdown whether or not to show the dropdown - * @return this ListDataValidation - */ - public ListDataValidation showDropdown(boolean showDropdown) { - this.showDropdown = showDropdown; - return this; - } - - /** - * The style of error alert used for this data validation. - * - * @param errorStyle The DataValidationErrorStyle for this DataValidation - * @return this ListDataValidation - */ - public ListDataValidation errorStyle(DataValidationErrorStyle errorStyle) { - this.errorStyle = errorStyle; - return this; - } - - /** - * Whether to display the error alert message when an invalid value has been entered. - * - * @param showErrorMessage whether to display the error message - * @return this ListDataValidation - */ - public ListDataValidation showErrorMessage(boolean showErrorMessage) { - this.showErrorMessage = showErrorMessage; - return this; - } - - /** - * Title bar text of error alert. - * - * @param errorTitle The error title - * @return this ListDataValidation - */ - public ListDataValidation errorTitle(String errorTitle) { - this.errorTitle = errorTitle; - return this; - } - - /** - * Message text of error alert. - * - * @param error The error message - * @return this ListDataValidation - */ - public ListDataValidation error(String error) { - this.error = error; - return this; - } - /** * Write this dataValidation as an XML element. * * @param w Output writer. * @throws IOException If an I/O error occurs. */ - @Override - public void write(Writer w) throws IOException { - w - .append("'") - .appendEscaped(listRange.getWorksheet().getName()) + protected void writeFormula(Writer w) throws IOException { + w.append("'").appendEscaped(listRange.getWorksheet().getName()) .append("'!") - .append(listRange.toAbsoluteString()) - .append(""); + .append(listRange.toAbsoluteString()); } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListFormulaDataValidation.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListFormulaDataValidation.java index e951b38c..70030e95 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListFormulaDataValidation.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/ListFormulaDataValidation.java @@ -5,18 +5,10 @@ /** * A ListDataValidation defines a DataValidation for a worksheet of type = "list" */ -public class ListFormulaDataValidation implements DataValidation { +public class ListFormulaDataValidation extends AbstractDataValidation { private final static String TYPE = "list"; - private final Range range; private final Formula formula; - private boolean allowBlank = true; - private boolean showDropdown = true; - private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION; - private boolean showErrorMessage = false; - private String errorTitle; - private String error; - /** * Constructor * @@ -24,104 +16,17 @@ public class ListFormulaDataValidation implements DataValidation { * @param formula The Formula of this validation to retrieve the list */ ListFormulaDataValidation(Range range, Formula formula) { - this.range = range; + super(range, TYPE); this.formula = formula; } - /** - * whether blank cells should pass the validation - * - * @param allowBlank whether or not to allow blank values - * @return this ListDataValidation - */ - public ListFormulaDataValidation allowBlank(boolean allowBlank) { - this.allowBlank = allowBlank; - return this; - } - - /** - * Whether Excel will show an in-cell dropdown list - * containing the validation list - * - * @param showDropdown whether or not to show the dropdown - * @return this ListDataValidation - */ - public ListFormulaDataValidation showDropdown(boolean showDropdown) { - this.showDropdown = showDropdown; - return this; - } - - /** - * The style of error alert used for this data validation. - * - * @param errorStyle The DataValidationErrorStyle for this DataValidation - * @return this ListDataValidation - */ - public ListFormulaDataValidation errorStyle(DataValidationErrorStyle errorStyle) { - this.errorStyle = errorStyle; - return this; - } - - /** - * Whether to display the error alert message when an invalid value has been entered. - * - * @param showErrorMessage whether to display the error message - * @return this ListDataValidation - */ - public ListFormulaDataValidation showErrorMessage(boolean showErrorMessage) { - this.showErrorMessage = showErrorMessage; - return this; - } - - /** - * Title bar text of error alert. - * - * @param errorTitle The error title - * @return this ListDataValidation - */ - public ListFormulaDataValidation errorTitle(String errorTitle) { - this.errorTitle = errorTitle; - return this; - } - - /** - * Message text of error alert. - * - * @param error The error message - * @return this ListDataValidation - */ - public ListFormulaDataValidation error(String error) { - this.error = error; - return this; - } - /** * Write this dataValidation as an XML element. * * @param w Output writer. * @throws IOException If an I/O error occurs. */ - @Override - public void write(Writer w) throws IOException { - w - .append("") - .append(formula.getExpression()) - .append(""); + protected void writeFormula(Writer w) throws IOException { + w.append(formula.getExpression()); } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/PictureAnchor.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/PictureAnchor.java index 0d8c656a..c407976c 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/PictureAnchor.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/PictureAnchor.java @@ -46,20 +46,7 @@ public class PictureAnchor { */ public static final int EMU_PER_CM = 360000; - private final int fromCol; - private final int fromColOff; // offset in EMUs - private final int fromRow; - private final int fromRowOff; // offset in EMUs - - // For two-cell anchor - private final Integer toCol; - private final Integer toColOff; - private final Integer toRow; - private final Integer toRowOff; - - // For one-cell anchor (explicit size in EMUs) - private final Long widthEmu; - private final Long heightEmu; + private final AnchorGeometry geometry; /** * Create a one-cell anchor with explicit size in pixels. @@ -71,8 +58,8 @@ public class PictureAnchor { * @return A new PictureAnchor configured for one-cell anchoring */ public static PictureAnchor oneCellAnchor(int row, int col, int widthPx, int heightPx) { - return new PictureAnchor(col, 0, row, 0, null, null, null, null, - (long) widthPx * EMU_PER_PIXEL, (long) heightPx * EMU_PER_PIXEL); + return new PictureAnchor(new OneCellAnchorGeometry(new AnchorMarker(row, col, 0, 0), + new AnchorExtent((long) widthPx * EMU_PER_PIXEL, (long) heightPx * EMU_PER_PIXEL))); } /** @@ -88,9 +75,9 @@ public static PictureAnchor oneCellAnchor(int row, int col, int widthPx, int hei */ public static PictureAnchor oneCellAnchor(int row, int col, int colOffPx, int rowOffPx, int widthPx, int heightPx) { - return new PictureAnchor(col, colOffPx * EMU_PER_PIXEL, row, rowOffPx * EMU_PER_PIXEL, - null, null, null, null, - (long) widthPx * EMU_PER_PIXEL, (long) heightPx * EMU_PER_PIXEL); + return new PictureAnchor(new OneCellAnchorGeometry(new AnchorMarker(row, col, + colOffPx * EMU_PER_PIXEL, rowOffPx * EMU_PER_PIXEL), + new AnchorExtent((long) widthPx * EMU_PER_PIXEL, (long) heightPx * EMU_PER_PIXEL))); } /** @@ -103,7 +90,8 @@ public static PictureAnchor oneCellAnchor(int row, int col, int colOffPx, int ro * @return A new PictureAnchor configured for two-cell anchoring */ public static PictureAnchor twoCellAnchor(int fromRow, int fromCol, int toRow, int toCol) { - return new PictureAnchor(fromCol, 0, fromRow, 0, toCol, 0, toRow, 0, null, null); + return new PictureAnchor(new TwoCellAnchorGeometry(new AnchorMarker(fromRow, fromCol, 0, 0), + new AnchorMarker(toRow, toCol, 0, 0))); } /** @@ -121,23 +109,13 @@ public static PictureAnchor twoCellAnchor(int fromRow, int fromCol, int toRow, i */ public static PictureAnchor twoCellAnchor(int fromRow, int fromCol, int fromColOffPx, int fromRowOffPx, int toRow, int toCol, int toColOffPx, int toRowOffPx) { - return new PictureAnchor(fromCol, fromColOffPx * EMU_PER_PIXEL, fromRow, fromRowOffPx * EMU_PER_PIXEL, - toCol, toColOffPx * EMU_PER_PIXEL, toRow, toRowOffPx * EMU_PER_PIXEL, null, null); + return new PictureAnchor(new TwoCellAnchorGeometry(new AnchorMarker(fromRow, fromCol, + fromColOffPx * EMU_PER_PIXEL, fromRowOffPx * EMU_PER_PIXEL), + new AnchorMarker(toRow, toCol, toColOffPx * EMU_PER_PIXEL, toRowOffPx * EMU_PER_PIXEL))); } - private PictureAnchor(int fromCol, int fromColOff, int fromRow, int fromRowOff, - Integer toCol, Integer toColOff, Integer toRow, Integer toRowOff, - Long widthEmu, Long heightEmu) { - this.fromCol = fromCol; - this.fromColOff = fromColOff; - this.fromRow = fromRow; - this.fromRowOff = fromRowOff; - this.toCol = toCol; - this.toColOff = toColOff; - this.toRow = toRow; - this.toRowOff = toRowOff; - this.widthEmu = widthEmu; - this.heightEmu = heightEmu; + private PictureAnchor(AnchorGeometry geometry) { + this.geometry = geometry; } /** @@ -146,51 +124,160 @@ private PictureAnchor(int fromCol, int fromColOff, int fromRow, int fromRowOff, * @return true if two-cell anchor, false if one-cell anchor */ public boolean isTwoCellAnchor() { - return toCol != null; + return geometry.isTwoCellAnchor(); } /** * Write the "from" position element. */ void writeFrom(Writer w) throws IOException { - w.append(""); - w.append("").append(fromCol).append(""); - w.append("").append(fromColOff).append(""); - w.append("").append(fromRow).append(""); - w.append("").append(fromRowOff).append(""); - w.append(""); + geometry.writeFrom(w); } /** * Write the "to" position element (for two-cell anchors). */ void writeTo(Writer w) throws IOException { - w.append(""); - w.append("").append(toCol).append(""); - w.append("").append(toColOff).append(""); - w.append("").append(toRow).append(""); - w.append("").append(toRowOff).append(""); - w.append(""); + geometry.writeTo(w); } /** * Write the extent element (for one-cell anchors). */ void writeExt(Writer w) throws IOException { - w.append(""); + geometry.writeExt(w); } /** * Get width in EMUs (for one-cell anchors). */ public Long getWidthEmu() { - return widthEmu; + return geometry.getWidthEmu(); } /** * Get height in EMUs (for one-cell anchors). */ public Long getHeightEmu() { - return heightEmu; + return geometry.getHeightEmu(); + } + + private interface AnchorGeometry { + boolean isTwoCellAnchor(); + + void writeFrom(Writer w) throws IOException; + + void writeTo(Writer w) throws IOException; + + void writeExt(Writer w) throws IOException; + + Long getWidthEmu(); + + Long getHeightEmu(); + } + + private static final class OneCellAnchorGeometry implements AnchorGeometry { + private final AnchorMarker from; + private final AnchorExtent extent; + + private OneCellAnchorGeometry(AnchorMarker from, AnchorExtent extent) { + this.from = from; + this.extent = extent; + } + + public boolean isTwoCellAnchor() { + return false; + } + + public void writeFrom(Writer w) throws IOException { + from.write(w, "from"); + } + + public void writeTo(Writer w) { + throw new UnsupportedOperationException("One-cell anchors do not have a to marker."); + } + + public void writeExt(Writer w) throws IOException { + extent.write(w); + } + + public Long getWidthEmu() { + return extent.widthEmu; + } + + public Long getHeightEmu() { + return extent.heightEmu; + } + } + + private static final class TwoCellAnchorGeometry implements AnchorGeometry { + private final AnchorMarker from; + private final AnchorMarker to; + + private TwoCellAnchorGeometry(AnchorMarker from, AnchorMarker to) { + this.from = from; + this.to = to; + } + + public boolean isTwoCellAnchor() { + return true; + } + + public void writeFrom(Writer w) throws IOException { + from.write(w, "from"); + } + + public void writeTo(Writer w) throws IOException { + to.write(w, "to"); + } + + public void writeExt(Writer w) { + throw new UnsupportedOperationException("Two-cell anchors do not have an extent."); + } + + public Long getWidthEmu() { + return null; + } + + public Long getHeightEmu() { + return null; + } + } + + private static final class AnchorMarker { + private final int row; + private final int col; + private final int colOffsetEmu; + private final int rowOffsetEmu; + + private AnchorMarker(int row, int col, int colOffsetEmu, int rowOffsetEmu) { + this.row = row; + this.col = col; + this.colOffsetEmu = colOffsetEmu; + this.rowOffsetEmu = rowOffsetEmu; + } + + private void write(Writer w, String elementName) throws IOException { + w.append(""); + w.append("").append(col).append(""); + w.append("").append(colOffsetEmu).append(""); + w.append("").append(row).append(""); + w.append("").append(rowOffsetEmu).append(""); + w.append(""); + } + } + + private static final class AnchorExtent { + private final long widthEmu; + private final long heightEmu; + + private AnchorExtent(long widthEmu, long heightEmu) { + this.widthEmu = widthEmu; + this.heightEmu = heightEmu; + } + + private void write(Writer w) throws IOException { + w.append(""); + } } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Properties.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Properties.java index 281ca448..ca3ef02b 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Properties.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Properties.java @@ -9,6 +9,8 @@ public class Properties { + private static final int MAX_METADATA_LENGTH = 65536; + //***** core properties ***** private String title; private String subject; @@ -30,10 +32,7 @@ String getTitle() { } public Properties setTitle(String title) { - if (title != null && title.length() > 65536) { - throw new IllegalStateException("The length of title must be less than or equal to 65536: " + title.length()); - } - this.title = title; + this.title = validateMetadataValue("title", title); return this; } @@ -42,10 +41,7 @@ String getSubject() { } public Properties setSubject(String subject) { - if (subject != null && subject.length() > 65536) { - throw new IllegalStateException("The length of subject must be less than or equal to 65536: " + subject.length()); - } - this.subject = subject; + this.subject = validateMetadataValue("subject", subject); return this; } @@ -54,10 +50,7 @@ String getKeywords() { } public Properties setKeywords(String keywords) { - if (keywords != null && keywords.length() > 65536) { - throw new IllegalStateException("The length of keywords must be less than or equal to 65536: " + keywords.length()); - } - this.keywords = keywords; + this.keywords = validateMetadataValue("keywords", keywords); return this; } @@ -66,10 +59,7 @@ String getDescription() { } public Properties setDescription(String description) { - if (description != null && description.length() > 65536) { - throw new IllegalStateException("The length of description must be less than or equal to 65536: " + description.length()); - } - this.description = description; + this.description = validateMetadataValue("description", description); return this; } @@ -78,10 +68,7 @@ String getCategory() { } public Properties setCategory(String category) { - if (category != null && category.length() > 65536) { - throw new IllegalStateException("The length of category must be less than or equal to 65536: " + category.length()); - } - this.category = category; + this.category = validateMetadataValue("category", category); return this; } @@ -90,10 +77,7 @@ String getManager() { } public Properties setManager(String manager) { - if (manager != null && manager.length() > 65536) { - throw new IllegalStateException("The length of manager must be less than or equal to 65536: " + manager.length()); - } - this.manager = manager; + this.manager = validateMetadataValue("manager", manager); return this; } @@ -102,10 +86,7 @@ String getCompany() { } public Properties setCompany(String company) { - if (company != null && company.length() > 65536) { - throw new IllegalStateException("The length of company must be less than or equal to 65536: " + title.length()); - } - this.company = company; + this.company = validateMetadataValue("company", company); return this; } @@ -114,13 +95,17 @@ String getHyperlinkBase() { } public Properties setHyperlinkBase(String hyperlinkBase) { - if (hyperlinkBase != null && hyperlinkBase.length() > 65536) { - throw new IllegalStateException("The length of hyperlinkBase must be less than or equal to 65536: " + title.length()); - } - this.hyperlinkBase = hyperlinkBase; + this.hyperlinkBase = validateMetadataValue("hyperlinkBase", hyperlinkBase); return this; } + private static String validateMetadataValue(String fieldName, String value) { + if (value != null && value.length() > MAX_METADATA_LENGTH) { + throw new IllegalStateException("The length of " + fieldName + " must be less than or equal to " + MAX_METADATA_LENGTH + ": " + value.length()); + } + return value; + } + interface CustomProty { void write(Writer w, int pid) throws IOException; } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java index 4b71ad55..97fb1644 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java @@ -16,12 +16,9 @@ public class Relationships { private final AtomicInteger maxIndex = new AtomicInteger(1); - final Worksheet worksheet; - private ArrayList relationship = new ArrayList<>(); - public Relationships(Worksheet worksheet) { - this.worksheet = worksheet; + public Relationships() { } String setHyperLinkRels(String target, String targetMode) { diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java index c200de1e..d3829be3 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java @@ -43,7 +43,7 @@ final class StyleCache { * Default constructor. Pre-cache Excel-reserved stuff. */ StyleCache() { - mergeAndCacheStyle(0, null, Font.DEFAULT, Fill.NONE, Border.NONE, null, null); + mergeAndCacheStyle(0, new StyleSpec(null, Font.DEFAULT, Fill.NONE, Border.NONE, null, null)); cacheFill(Fill.GRAY125); } @@ -140,8 +140,18 @@ int cacheDxf(DifferentialFormat f) { } int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, Alignment alignment, Protection protection) { + return mergeAndCacheStyle(currentStyle, new StyleSpec(numberingFormat, font, fill, border, alignment, protection)); + } + + int mergeAndCacheStyle(int currentStyle, StyleSpec styleSpec) { Style original = styleIndexToStyle.get(currentStyle); - Style s = new Style(original, cacheValueFormatting(numberingFormat), cacheFont(font), cacheFill(fill), cacheBorder(border), alignment, protection); + Style s = new Style(original, + cacheValueFormatting(styleSpec.getNumberingFormat()), + cacheFont(styleSpec.getFont()), + cacheFill(styleSpec.getFill()), + cacheBorder(styleSpec.getBorder()), + styleSpec.getAlignment(), + styleSpec.getProtection()); return cacheStyle(s, k -> styles.size()); } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleSpec.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleSpec.java new file mode 100644 index 00000000..b3d82918 --- /dev/null +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleSpec.java @@ -0,0 +1,44 @@ +package org.dhatim.fastexcel; + +final class StyleSpec { + + private final String numberingFormat; + private final Font font; + private final Fill fill; + private final Border border; + private final Alignment alignment; + private final Protection protection; + + StyleSpec(String numberingFormat, Font font, Fill fill, Border border, Alignment alignment, Protection protection) { + this.numberingFormat = numberingFormat; + this.font = font; + this.fill = fill; + this.border = border; + this.alignment = alignment; + this.protection = protection; + } + + String getNumberingFormat() { + return numberingFormat; + } + + Font getFont() { + return font; + } + + Fill getFill() { + return fill; + } + + Border getBorder() { + return border; + } + + Alignment getAlignment() { + return alignment; + } + + Protection getProtection() { + return protection; + } +} diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java index d79f39d7..a73c4271 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java @@ -48,6 +48,7 @@ public class Workbook implements Closeable { private final OpcOutputStream os; private final Writer writer; private final AtomicInteger maxTableIndex = new AtomicInteger(1); + private Font defaultFont = Font.DEFAULT; /** * Constructor. @@ -101,28 +102,7 @@ public void setActiveTab(int tabIndex) { * @param password The password to use. */ public void protectStructure(String password) { - this.workbookPasswordHash = password != null ? hashPassword(password) : null; - } - - /** - * Hash the password using the same algorithm as worksheet protection. - * @param password The password to hash. - * @return The password hash as a hex string. - */ - private static String hashPassword(String password) { - byte[] passwordCharacters = password.getBytes(); - int hash = 0; - if (passwordCharacters.length > 0) { - int charIndex = passwordCharacters.length; - while (charIndex-- > 0) { - hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); - hash ^= passwordCharacters[charIndex]; - } - hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); - hash ^= passwordCharacters.length; - hash ^= (0x8000 | ('N' << 8) | 'K'); - } - return Integer.toHexString(hash & 0xffff); + this.workbookPasswordHash = password != null ? LegacyProtectionHash.hashPassword(password) : null; } @@ -130,14 +110,18 @@ private static String hashPassword(String password) { public void setGlobalDefaultFont(String fontName, double fontSize) { - this.setGlobalDefaultFont(Font.build(null, null, null, fontName, BigDecimal.valueOf(fontSize), null, null)); + this.setGlobalDefaultFont(Font.build(defaultFont, null, null, null, fontName, BigDecimal.valueOf(fontSize), null, null)); } public void setGlobalDefaultFont(Font font) { - Font.DEFAULT = font; + this.defaultFont = font; this.styleCache.replaceDefaultFont(font); } + Font getDefaultFont() { + return defaultFont; + } + public Properties properties() { return this.properties; } @@ -171,10 +155,32 @@ public void finish() throws IOException { throw new IllegalArgumentException("A workbook must contain at least one worksheet."); } + closeWorksheets(); + writePackageParts(); + this.os.finish(); + finished = true; + } + + private void closeWorksheets() throws IOException { for (Worksheet ws : worksheets) { ws.close(); } + } + + private void writePackageParts() throws IOException { + writeContentTypes(); + writeProperties(); + if (properties.hasCustomProperties()) { + writeFile("docProps/custom.xml", properties::writeCustomProperties); + } + writeRootRelationships(); + writeWorkbookFile(); + writeWorkbookRelationships(); + writeFile("xl/sharedStrings.xml", stringCache::write); + writeFile("xl/styles.xml", styleCache::write); + } + private void writeContentTypes() throws IOException { writeFile("[Content_Types].xml", w -> { w.append(""); if (hasComments()) { @@ -212,11 +218,9 @@ public void finish() throws IOException { } w.append(""); }); - writeProperties(); - if (properties.hasCustomProperties()) { - writeFile("docProps/custom.xml", properties::writeCustomProperties); - } + } + private void writeRootRelationships() throws IOException { writeFile("_rels/.rels", w -> { w.append(""); w.append(""); @@ -228,9 +232,9 @@ public void finish() throws IOException { } w.append(""); }); + } - writeWorkbookFile(); - + private void writeWorkbookRelationships() throws IOException { writeFile("xl/_rels/workbook.xml.rels", w -> { w.append(""); for (Worksheet ws : worksheets) { @@ -238,10 +242,6 @@ public void finish() throws IOException { } w.append(""); }); - writeFile("xl/sharedStrings.xml", stringCache::write); - writeFile("xl/styles.xml", styleCache::write); - this.os.finish(); - finished = true; } private void writeProperties() throws IOException { @@ -393,7 +393,7 @@ private void writeWorkbookFile() throws IOException { w.append(""); } /** define specifically named ranges **/ - for (Map.Entry nr : ws.getNamedRanges().entrySet()) { + for (Map.Entry nr : ws.namedRanges().entrySet()) { String rangeName = nr.getKey(); Range range = nr.getValue(); w.append("