diff options
Diffstat (limited to 'poi')
67 files changed, 1212 insertions, 233 deletions
diff --git a/poi/build.gradle b/poi/build.gradle index b755139a1d..2b1076840e 100644 --- a/poi/build.gradle +++ b/poi/build.gradle @@ -37,7 +37,7 @@ sourceSets { dependencies { api "commons-codec:commons-codec:${commonsCodecVersion}" - api 'org.apache.commons:commons-collections4:4.4' + api 'org.apache.commons:commons-collections4:4.5.0' api "org.apache.commons:commons-math3:${commonsMathVersion}" api "commons-io:commons-io:${commonsIoVersion}" api 'com.zaxxer:SparseBitSet:1.3' diff --git a/poi/src/main/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java b/poi/src/main/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java index f9af178a5a..aa58173db6 100644 --- a/poi/src/main/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java +++ b/poi/src/main/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java @@ -58,7 +58,7 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem; * the XLS2CSVmra example * </p> * - * @see <a href="http://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/hssf/eventusermodel/examples/XLS2CSVmra.java">XLS2CSVmra</a> + * @see <a href="https://github.com/apache/poi/blob/trunk/poi-examples/src/main/java/org/apache/poi/examples/hssf/eventusermodel/XLS2CSVmra.java">XLS2CSVmra</a> */ public class EventBasedExcelExtractor implements POIOLE2TextExtractor, org.apache.poi.ss.extractor.ExcelExtractor { private final POIFSFileSystem poifs; diff --git a/poi/src/main/java/org/apache/poi/hssf/extractor/ExcelExtractor.java b/poi/src/main/java/org/apache/poi/hssf/extractor/ExcelExtractor.java index 3f413b0643..fc0e7aa43c 100644 --- a/poi/src/main/java/org/apache/poi/hssf/extractor/ExcelExtractor.java +++ b/poi/src/main/java/org/apache/poi/hssf/extractor/ExcelExtractor.java @@ -51,7 +51,7 @@ import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; * the XLS2CSVmra example * </p> * - * @see <a href="http://svn.apache.org/repos/asf/poi/trunk/poi-examples/src/main/java/org/apache/poi/hssf/eventusermodel/examples/XLS2CSVmra.java">XLS2CSVmra</a> + * @see <a href="https://github.com/apache/poi/blob/trunk/poi-examples/src/main/java/org/apache/poi/examples/hssf/eventusermodel/XLS2CSVmra.java">XLS2CSVmra</a> */ public class ExcelExtractor implements POIOLE2TextExtractor, org.apache.poi.ss.extractor.ExcelExtractor { private final HSSFWorkbook _wb; diff --git a/poi/src/main/java/org/apache/poi/hssf/record/EscherAggregate.java b/poi/src/main/java/org/apache/poi/hssf/record/EscherAggregate.java index 2c71372ee9..67beb21207 100644 --- a/poi/src/main/java/org/apache/poi/hssf/record/EscherAggregate.java +++ b/poi/src/main/java/org/apache/poi/hssf/record/EscherAggregate.java @@ -1037,11 +1037,7 @@ public final class EscherAggregate extends AbstractEscherHolderRecord { final UnsynchronizedByteArrayOutputStream buffer = UnsynchronizedByteArrayOutputStream.builder().get(); void addBytes(byte[] data) { - try { - buffer.write(data); - } catch (IOException e) { - throw new IllegalStateException("Couldn't get data from drawing/continue records", e); - } + buffer.write(data); } @Override diff --git a/poi/src/main/java/org/apache/poi/hssf/record/TextObjectRecord.java b/poi/src/main/java/org/apache/poi/hssf/record/TextObjectRecord.java index 17bcce71e7..d4a303f873 100644 --- a/poi/src/main/java/org/apache/poi/hssf/record/TextObjectRecord.java +++ b/poi/src/main/java/org/apache/poi/hssf/record/TextObjectRecord.java @@ -212,7 +212,7 @@ public final class TextObjectRecord extends ContinuableRecord { protected void serialize(ContinuableRecordOutput out) { serializeTXORecord(out); - if (_text.getString().length() > 0) { + if (!_text.getString().isEmpty()) { serializeTrailingRecords(out); } } diff --git a/poi/src/main/java/org/apache/poi/hssf/record/WriteAccessRecord.java b/poi/src/main/java/org/apache/poi/hssf/record/WriteAccessRecord.java index 88f8affd2d..712693554c 100644 --- a/poi/src/main/java/org/apache/poi/hssf/record/WriteAccessRecord.java +++ b/poi/src/main/java/org/apache/poi/hssf/record/WriteAccessRecord.java @@ -108,10 +108,19 @@ public final class WriteAccessRecord extends StandardRecord { data = IOUtils.safelyAllocate(in.remaining(), STRING_SIZE); in.readFully(data); if (UTF16FLAG.isSet(is16BitFlag)) { - byteCnt = Math.min(nChars * 2, data.length); + // the spec only allows up to 109 bytes for the string in this record, but it seems some broken + // software out there will generate invalid records + int min = Math.min(nChars * 2, data.length); + + // make sure byteCnt is divisible by 2 as we read UTF-16LE + byteCnt = min - (min % 2); + charset = StandardCharsets.UTF_16LE; } else { + // the spec only allows up to 109 bytes for the string in this record, but it seems some broken + // software out there will generate invalid records byteCnt = Math.min(nChars, data.length); + charset = StandardCharsets.ISO_8859_1; } } @@ -130,7 +139,8 @@ public final class WriteAccessRecord extends StandardRecord { boolean is16bit = StringUtil.hasMultibyte(username); int encodedByteCount = username.length() * (is16bit ? 2 : 1); if (encodedByteCount > STRING_SIZE) { - throw new IllegalArgumentException("Name is too long: " + username); + throw new IllegalArgumentException("Name is too long, expecting up to " + STRING_SIZE + + " bytes, but had: " + encodedByteCount + " bytes: " + username); } field_1_username = username; diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 1569553682..fd4f97365d 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -1020,8 +1020,6 @@ public class HSSFCell extends CellBase { _sheet.getSheet().setActiveCellCol(col); } - private static final DataFormatter DATA_FORMATTER = new DataFormatter(); - /** * Returns a string representation of the cell * @@ -1045,8 +1043,14 @@ public class HSSFCell extends CellBase { case FORMULA: return getCellFormula(); case NUMERIC: + if (DateUtil.isCellDateFormatted(this)) { + DataFormatter df = new DataFormatter(); + df.setUseCachedValuesForFormulaCells(true); + return df.formatCellValue(this); + } + return Double.toString(getNumericCellValue()); case STRING: - return DATA_FORMATTER.formatCellValue(this); + return getRichStringCellValue().toString(); default: return "Unknown Cell Type: " + getCellType(); } diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java index 0983c45497..20d3bcaf6e 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java @@ -35,6 +35,7 @@ import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellUtil; import org.apache.poi.util.Removal; import org.apache.poi.util.ThreadLocalUtil; @@ -49,22 +50,29 @@ public final class HSSFCellStyle implements CellStyle, Duplicatable { private final ExtendedFormatRecord _format; private final short _index; private final InternalWorkbook _workbook; + private final HSSFWorkbook _hssfWorkbook; - - /** Creates new HSSFCellStyle why would you want to do this?? */ protected HSSFCellStyle(short index, ExtendedFormatRecord rec, HSSFWorkbook workbook) { - this(index, rec, workbook.getWorkbook()); + _workbook = workbook.getInternalWorkbook(); + _hssfWorkbook = workbook; + _index = index; + _format = rec; } + + @Deprecated + @Removal(version = "7.0.0") protected HSSFCellStyle(short index, ExtendedFormatRecord rec, InternalWorkbook workbook) { _workbook = workbook; + _hssfWorkbook = null; _index = index; - _format = rec; + _format = rec; } protected HSSFCellStyle(HSSFCellStyle other) { _workbook = other._workbook; + _hssfWorkbook = other._hssfWorkbook; _index = other._index; _format = other._format; } @@ -851,9 +859,10 @@ public final class HSSFCellStyle implements CellStyle, Duplicatable { if(source instanceof HSSFCellStyle) { this.cloneStyleFrom((HSSFCellStyle)source); } else { - throw new IllegalArgumentException("Can only clone from one HSSFCellStyle to another, not between HSSFCellStyle and XSSFCellStyle"); + CellUtil.cloneStyle(source, this, _hssfWorkbook); } } + public void cloneStyleFrom(HSSFCellStyle source) { // First we need to clone the extended format // record diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFName.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFName.java index 75d8c0a303..60fdffa599 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFName.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFName.java @@ -180,7 +180,7 @@ public final class HSSFName implements Name { */ private static void validateName(String name) { - if (name.length() == 0) { + if (name.isEmpty()) { throw new IllegalArgumentException("Name cannot be blank"); } if (name.length() > 255) { diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFSheet.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFSheet.java index bba313f57d..9e56f37714 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFSheet.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFSheet.java @@ -645,7 +645,7 @@ public final class HSSFSheet implements Sheet { } ExtendedFormatRecord xf = _book.getExFormatAt(styleIndex); - return new HSSFCellStyle(styleIndex, xf, _book); + return new HSSFCellStyle(styleIndex, xf, _workbook); } /** diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index 0fea271340..6c7bb93a2b 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -234,7 +234,8 @@ public final class HSSFWorkbook extends POIDocument implements Workbook { * @return the max image length allowed for HSSFWorkbook */ public static int getMaxImageLength() { - return MAX_IMAGE_LENGTH; + final int ioMaxSize = IOUtils.getByteArrayMaxOverride(); + return ioMaxSize < 0 ? MAX_IMAGE_LENGTH : Math.min(MAX_IMAGE_LENGTH, ioMaxSize); } /** @@ -1978,7 +1979,7 @@ public final class HSSFWorkbook extends POIDocument implements Workbook { case PICTURE_TYPE_WMF: // remove first 22 bytes if file starts with the WMF placeable header if (FileMagic.valueOf(pictureData) == FileMagic.WMF) { - pictureData = IOUtils.safelyClone(pictureData, 22, pictureData.length - 22, MAX_IMAGE_LENGTH); + pictureData = IOUtils.safelyClone(pictureData, 22, pictureData.length - 22, getMaxImageLength()); } // fall through case PICTURE_TYPE_EMF: diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/HeaderFooter.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/HeaderFooter.java index aa883dd44e..3884462587 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/HeaderFooter.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/HeaderFooter.java @@ -282,7 +282,7 @@ public abstract class HeaderFooter implements org.apache.poi.ss.usermodel.Header int pos; // Check we really got something to work on - if (pText == null || pText.length() == 0) { + if (pText == null || pText.isEmpty()) { return pText; } diff --git a/poi/src/main/java/org/apache/poi/hssf/usermodel/helpers/HSSFRowColShifter.java b/poi/src/main/java/org/apache/poi/hssf/usermodel/helpers/HSSFRowColShifter.java index 698f1d7279..dc68c77dcb 100644 --- a/poi/src/main/java/org/apache/poi/hssf/usermodel/helpers/HSSFRowColShifter.java +++ b/poi/src/main/java/org/apache/poi/hssf/usermodel/helpers/HSSFRowColShifter.java @@ -62,19 +62,19 @@ import static org.apache.logging.log4j.util.Unbox.box; } /** - * Update the formulas in specified row using the formula shifting policy specified by shifter + * Update the formulas in the specified row using the formula shifting policy specified by shifter * * @param row the row to update the formulas on * @param formulaShifter the formula shifting policy */ /*package*/ static void updateRowFormulas(HSSFRow row, FormulaShifter formulaShifter) { - HSSFSheet sheet = row.getSheet(); - for (Cell c : row) { - HSSFCell cell = (HSSFCell) c; - String formula = cell.getCellFormula(); - if (formula.length() > 0) { - String shiftedFormula = shiftFormula(row, formula, formulaShifter); - cell.setCellFormula(shiftedFormula); + for (Cell cell : row) { + if (cell.getCellType() == CellType.FORMULA) { + String formula = cell.getCellFormula(); + if (formula != null && !formula.isEmpty()) { + String shiftedFormula = shiftFormula(row, formula, formulaShifter); + cell.setCellFormula(shiftedFormula); + } } } } diff --git a/poi/src/main/java/org/apache/poi/hssf/util/HSSFColor.java b/poi/src/main/java/org/apache/poi/hssf/util/HSSFColor.java index 263a7c0da9..107b116798 100644 --- a/poi/src/main/java/org/apache/poi/hssf/util/HSSFColor.java +++ b/poi/src/main/java/org/apache/poi/hssf/util/HSSFColor.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Objects; import org.apache.poi.ss.usermodel.Color; +import org.apache.poi.util.Removal; /** @@ -34,7 +35,7 @@ import org.apache.poi.ss.usermodel.Color; * Each color has an index (for the standard palette in Excel (tm) ), * native (RGB) triplet and string triplet. The string triplet is as the * color would be represented by Gnumeric. Having (string) this here is a bit of a - * collision of function between HSSF and the HSSFSerializer but I think its + * collision of function between HSSF and the HSSFSerializer but I think it's * a reasonable one in this case. */ public class HSSFColor implements Color { @@ -42,7 +43,7 @@ public class HSSFColor implements Color { private static Map<Integer,HSSFColor> indexHash; private static Map<HSSFColorPredefined,HSSFColor> enumList; - private final java.awt.Color color; + private final int rgb; private final int index; private final int index2; @@ -110,7 +111,7 @@ public class HSSFColor implements Color { private final HSSFColor color; HSSFColorPredefined(int index, int index2, int rgb) { - this.color = new HSSFColor(index, index2, new java.awt.Color(rgb)); + this.color = new HSSFColor(index, index2, rgb); } /** @@ -145,7 +146,7 @@ public class HSSFColor implements Color { * @return (a copy of) the HSSFColor assigned to the enum */ public HSSFColor getColor() { - return new HSSFColor(getIndex(), getIndex2(), color.color); + return new HSSFColor(getIndex(), getIndex2(), color.rgb); } } @@ -153,13 +154,40 @@ public class HSSFColor implements Color { /** Creates a new instance of HSSFColor */ public HSSFColor() { // automatic index - this(0x40, -1, java.awt.Color.BLACK); + this(0x40, -1, 0x000000); } + /** Constructs new instance of {@code HSSFColor} by + * extracting RGB from {@link java.awt.Color}. The code is equivalent + * to calling: + * <pre> + * new HSSFColor(index, index2, color.getRGB()); + * </pre> + * or specifying {@link #HSSFColor(int, int, int) RGB directly}. + * + * @param index + * @param index2 + * @param color color to extract RGB from + * @deprecated use {@link #HSSFColor(int, int, int)} instead + */ + @Removal(version = "7.0.0") public HSSFColor(int index, int index2, java.awt.Color color) { + this(index, index2, color.getRGB()); + } + + /** Constructs new instance of {@code HSSFColor} by + * specifying RGB as an {@code int} value. Given {@code blue}, {@code green} and + * {@code blue} being byte values between {@code 0x00 to 0xFF}, then + * {@code rgb = blue + (green >> 8) + (red >> 16)}. + * @param index + * @param index2 + * @param rgb combined value of RGB + * @since POI 5.4.2 + */ + public HSSFColor(int index, int index2, int rgb) { this.index = index; this.index2 = index2; - this.color = color; + this.rgb = 0xFF000000 | rgb; } /** @@ -203,7 +231,7 @@ public class HSSFColor implements Color { } /** - * this function returns all colors in a hastable. It's not implemented as a + * This function returns all colors in a map. It's not implemented as a * static member/statically initialized because that would be dirty in a * server environment as it is intended. This means you'll eat the time * it takes to create it once per request but you will not hold onto it @@ -235,7 +263,7 @@ public class HSSFColor implements Color { private static synchronized Map<HSSFColorPredefined,HSSFColor> mapEnumToColorClass() { if (enumList == null) { enumList = new EnumMap<>(HSSFColorPredefined.class); - // AUTOMATIC is not add to list + // AUTOMATIC is not added to list addHSSFColorPredefined(HSSFColorPredefined.BLACK); addHSSFColorPredefined(HSSFColorPredefined.BROWN); addHSSFColorPredefined(HSSFColorPredefined.OLIVE_GREEN); @@ -315,7 +343,7 @@ public class HSSFColor implements Color { */ public short [] getTriplet() { - return new short[] { (short)color.getRed(), (short)color.getGreen(), (short)color.getBlue() }; + return new short[] { getRed(), getGreen(), getBlue() }; } /** @@ -324,9 +352,19 @@ public class HSSFColor implements Color { */ public String getHexString() { - return (Integer.toHexString(color.getRed()*0x101) + ":" + - Integer.toHexString(color.getGreen()*0x101) + ":" + - Integer.toHexString(color.getBlue()*0x101)).toUpperCase(Locale.ROOT); + return (Integer.toHexString(getRed()*0x101) + ":" + + Integer.toHexString(getGreen()*0x101) + ":" + + Integer.toHexString(getBlue()*0x101)).toUpperCase(Locale.ROOT); + } + + private final short getBlue() { + return (short) (rgb & 0xFF); + } + private final short getGreen() { + return (short) ((rgb >> 8) & 0xFF); + } + private final short getRed() { + return (short) ((rgb >> 16) & 0xFF); } @Override @@ -338,12 +376,12 @@ public class HSSFColor implements Color { if (index != hssfColor.index) return false; if (index2 != hssfColor.index2) return false; - return Objects.equals(color, hssfColor.color); + return Objects.equals(rgb, hssfColor.rgb); } @Override public int hashCode() { - return Objects.hash(color,index,index2); + return Objects.hash(rgb, index, index2); } /** diff --git a/poi/src/main/java/org/apache/poi/poifs/eventfilesystem/POIFSReader.java b/poi/src/main/java/org/apache/poi/poifs/eventfilesystem/POIFSReader.java index b8d443aab4..4cb3ebb4e2 100644 --- a/poi/src/main/java/org/apache/poi/poifs/eventfilesystem/POIFSReader.java +++ b/poi/src/main/java/org/apache/poi/poifs/eventfilesystem/POIFSReader.java @@ -141,22 +141,23 @@ public class POIFSReader * assumed * @param name the document name * - * @throws NullPointerException if listener is null or name is - * null or empty + * @throws NullPointerException if listener is null or name is null * @throws IllegalStateException if read() has already been - * called + * called or name is empty */ - public void registerListener(final POIFSReaderListener listener, final POIFSDocumentPath path, final String name) { - if ((listener == null) || (name == null) || (name.length() == 0)) { - throw new NullPointerException(); + if (listener == null || name == null) { + throw new NullPointerException("invalid null parameter"); + } + if (name.isEmpty()) { + throw new IllegalStateException("name must not be empty"); } if (registryClosed) { - throw new IllegalStateException(); + throw new IllegalStateException("registry closed"); } - registry.registerListener(listener, (path == null) ? new POIFSDocumentPath() : path, name); + registry.registerListener(listener, path == null ? new POIFSDocumentPath() : path, name); } /** diff --git a/poi/src/main/java/org/apache/poi/poifs/filesystem/DocumentDescriptor.java b/poi/src/main/java/org/apache/poi/poifs/filesystem/DocumentDescriptor.java index b8d783638e..40ffdffaec 100644 --- a/poi/src/main/java/org/apache/poi/poifs/filesystem/DocumentDescriptor.java +++ b/poi/src/main/java/org/apache/poi/poifs/filesystem/DocumentDescriptor.java @@ -46,7 +46,7 @@ public class DocumentDescriptor { throw new NullPointerException("name must not be null"); } - if (name.length() == 0) + if (name.isEmpty()) { throw new IllegalArgumentException("name cannot be empty"); } diff --git a/poi/src/main/java/org/apache/poi/poifs/property/PropertyTable.java b/poi/src/main/java/org/apache/poi/poifs/property/PropertyTable.java index 203f23cd46..062627ae52 100644 --- a/poi/src/main/java/org/apache/poi/poifs/property/PropertyTable.java +++ b/poi/src/main/java/org/apache/poi/poifs/property/PropertyTable.java @@ -83,11 +83,14 @@ public final class PropertyTable implements BATManaged { for (ByteBuffer bb : dataSource) { // Turn it into an array - byte[] data; - if (bb.hasArray() && bb.arrayOffset() == 0 && - bb.array().length == _bigBigBlockSize.getBigBlockSize()) { - data = bb.array(); - } else { + byte[] data = null; + if (bb.hasArray() && bb.arrayOffset() == 0) { + final byte[] array = bb.array(); + if (array.length == _bigBigBlockSize.getBigBlockSize()) { + data = array; + } + } + if (data == null) { data = IOUtils.safelyAllocate(_bigBigBlockSize.getBigBlockSize(), POIFSFileSystem.getMaxRecordLength()); int toRead = data.length; diff --git a/poi/src/main/java/org/apache/poi/ss/format/CellFormatPart.java b/poi/src/main/java/org/apache/poi/ss/format/CellFormatPart.java index 990181baad..ea8eaa37b4 100644 --- a/poi/src/main/java/org/apache/poi/ss/format/CellFormatPart.java +++ b/poi/src/main/java/org/apache/poi/ss/format/CellFormatPart.java @@ -251,7 +251,7 @@ public class CellFormatPart { */ private static Color getColor(Matcher m) { String cdesc = m.group(COLOR_GROUP); - if (cdesc == null || cdesc.length() == 0) + if (cdesc == null || cdesc.isEmpty()) return null; Color c = NAMED_COLORS.get(cdesc); if (c == null) { @@ -270,7 +270,7 @@ public class CellFormatPart { */ private CellFormatCondition getCondition(Matcher m) { String mdesc = m.group(CONDITION_OPERATOR_GROUP); - if (mdesc == null || mdesc.length() == 0) + if (mdesc == null || mdesc.isEmpty()) return null; return CellFormatCondition.getInstance(m.group( CONDITION_OPERATOR_GROUP), m.group(CONDITION_VALUE_GROUP)); @@ -509,7 +509,7 @@ public class CellFormatPart { StringBuffer fmt = new StringBuffer(); while (m.find()) { String part = group(m, 0); - if (part.length() > 0) { + if (!part.isEmpty()) { String repl = partHandler.handlePart(m, part, type, fmt); if (repl == null) { switch (part.charAt(0)) { diff --git a/poi/src/main/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java b/poi/src/main/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java index 54199cbfca..3d0e441dbc 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java @@ -357,7 +357,7 @@ public class EvaluationConditionalFormatRule implements Comparable<EvaluationCon String f2 = rule.getFormula2(); ValueEval eval2 = BlankEval.instance; - if (f2 != null && f2.length() > 0) { + if (f2 != null && !f2.isEmpty()) { eval2 = unwrapEval(workbookEvaluator.evaluate(f2, ConditionalFormattingEvaluator.getRef(cell), region)); } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/FormulaParser.java b/poi/src/main/java/org/apache/poi/ss/formula/FormulaParser.java index 020ae38d8c..c748231838 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/FormulaParser.java @@ -483,7 +483,7 @@ public final class FormulaParser { } else { // Is it a named range? String name = parseAsName(); - if (name.length() == 0) { + if (name.isEmpty()) { throw new FormulaParseException("Cell reference or Named Range " + "expected after sheet name at index " + _pointer + "."); } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/poi/src/main/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index 11ae822a21..4b703fc143 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -971,7 +971,7 @@ public final class WorkbookEvaluator { } /** - * Register a ATP function in runtime. + * Register an ATP function in runtime. * * @param name the function name * @param func the function to register diff --git a/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java b/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java index dd165383ee..d7e2db2236 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -1,12 +1,19 @@ -/* - * ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or - * agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ==================================================================== - */ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ package org.apache.poi.ss.formula.atp; @@ -17,6 +24,8 @@ import java.util.Locale; import java.util.Map; import java.util.TreeSet; +import org.apache.logging.log4j.Logger; +import org.apache.poi.logging.PoiLogManager; import org.apache.poi.ss.formula.OperationEvaluationContext; import org.apache.poi.ss.formula.eval.NotImplementedFunctionException; import org.apache.poi.ss.formula.eval.ValueEval; @@ -32,6 +41,8 @@ public final class AnalysisToolPak implements UDFFinder { public static final UDFFinder instance = new AnalysisToolPak(); + private static final Logger LOG = PoiLogManager.getLogger(AnalysisToolPak.class); + private static final class NotImplemented implements FreeRefFunction { private final String _functionName; @@ -180,6 +191,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "RECEIVED", null); r(m, "RTD", null); r(m, "SERIESSUM", null); + r(m, "SHEET", Sheet.instance); r(m, "SINGLE", Single.instance); r(m, "SQRTPI", Sqrtpi.instance); r(m, "STDEV.S", Stdevs.instance); @@ -265,9 +277,26 @@ public final class AnalysisToolPak implements UDFFinder { * @throws IllegalArgumentException if the function is unknown or already registered. * @since 3.8 beta6 */ - public static void registerFunction(String name, FreeRefFunction func){ + public static void registerFunction(String name, FreeRefFunction func) { + registerFunction(name, func, false); + } + + /** + * Register an ATP function in runtime. + * + * @param name the function name + * @param func the function to register + * @param force force registration even if the function is already registered or unknown to POI + * @throws IllegalArgumentException if the function is unknown or already registered (and `force` is not true). + * @since POI 5.4.2 + */ + public static void registerFunction(String name, FreeRefFunction func, boolean force) { AnalysisToolPak inst = (AnalysisToolPak)instance; - if(!isATPFunction(name)) { + if (force) { + // Excel regularly adds new functions, so the ones registered in POI + // can be well out of date - allow users who know what they are doing + // to force their update + } else if(!isATPFunction(name)) { FunctionMetadata metaData = FunctionMetadataRegistry.getFunctionByName(name); if(metaData != null) { throw new IllegalArgumentException(name + " is a built-in Excel function. " + @@ -276,13 +305,20 @@ public final class AnalysisToolPak implements UDFFinder { throw new IllegalArgumentException(name + " is not a function from the Excel Analysis Toolpack."); } + FreeRefFunction f = inst.findFunction(name); if(f != null && !(f instanceof NotImplemented)) { - throw new IllegalArgumentException("POI already implements " + name + - ". You cannot override POI's implementations of Excel functions"); + if (force) { + LOG.info("POI already implements " + name + + ". You are overriding the implementation."); + } else { + throw new IllegalArgumentException("POI already implements " + name + + ". You cannot override POI's implementations of Excel functions"); + } } // FIXME: inconsistent case-sensitivity inst._functionsByName.put(name, func); } + } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/atp/TextJoinFunction.java b/poi/src/main/java/org/apache/poi/ss/formula/atp/TextJoinFunction.java index 5e7ffe8b87..45b3ec31ac 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/atp/TextJoinFunction.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/atp/TextJoinFunction.java @@ -86,7 +86,7 @@ final class TextJoinFunction implements FreeRefFunction { String textValue = OperandResolver.coerceValueToString(textArg); // If we're not ignoring empty values or if our value is not empty, add it to the list - if (!ignoreEmpty || (textValue != null && textValue.length() > 0)) { + if (!ignoreEmpty || (textValue != null && !textValue.isEmpty())) { textValues.add(textValue); } } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/function/FunctionMetadataReader.java b/poi/src/main/java/org/apache/poi/ss/formula/function/FunctionMetadataReader.java index b03c732e89..4b6e6fa2e7 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/function/FunctionMetadataReader.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/function/FunctionMetadataReader.java @@ -124,7 +124,7 @@ final class FunctionMetadataReader { byte returnClassCode = parseReturnTypeCode(parts[4]); byte[] parameterClassCodes = parseOperandTypeCodes(parts[5]); // 6 isVolatile - boolean hasNote = parts[7].length() > 0; + boolean hasNote = !parts[7].isEmpty(); validateFunctionName(functionName); // TODO - make POI use isVolatile @@ -134,7 +134,7 @@ final class FunctionMetadataReader { private static byte parseReturnTypeCode(String code) { - if(code.length() == 0) { + if(code.isEmpty()) { return Ptg.CLASS_REF; // happens for GETPIVOTDATA } return parseOperandTypeCode(code); @@ -155,7 +155,7 @@ final class FunctionMetadataReader { // (all unspecified params are assumed to be the same as the last) nItems --; } - byte[] result = IOUtils.safelyAllocate(nItems, MAX_RECORD_LENGTH); + byte[] result = IOUtils.safelyAllocate(nItems, getMaxRecordLength()); for (int i = 0; i < nItems; i++) { result[i] = parseOperandTypeCode(array[i]); } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/BaseNumberUtils.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/BaseNumberUtils.java index c39d082e10..db583f3229 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/BaseNumberUtils.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/BaseNumberUtils.java @@ -23,7 +23,7 @@ public class BaseNumberUtils { public static double convertToDecimal(String value, int base, int maxNumberOfPlaces) throws IllegalArgumentException { - if (value == null || value.length() == 0) { + if (value == null || value.isEmpty()) { return 0.0; } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Code.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Code.java index 96f2a13204..30f2d3257f 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Code.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Code.java @@ -41,7 +41,7 @@ public class Code extends Fixed1ArgFunction { } String text = OperandResolver.coerceValueToString(veText1); - if (text.length() == 0) { + if (text.isEmpty()) { return ErrorEval.VALUE_INVALID; } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Complex.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Complex.java index a10c54a758..a4fac2685e 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Complex.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Complex.java @@ -90,7 +90,7 @@ public class Complex extends Var2or3ArgFunction implements FreeRefFunction { } String suffixValue = OperandResolver.coerceValueToString(suffix); - if (suffixValue.length() == 0) { + if (suffixValue.isEmpty()) { suffixValue = DEFAULT_SUFFIX; } if (suffixValue.equals(DEFAULT_SUFFIX.toUpperCase(Locale.ROOT)) || diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countif.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countif.java index 93fdf033a4..ecb57e5e1a 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countif.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countif.java @@ -339,7 +339,7 @@ public final class Countif extends Fixed2ArgFunction { switch(getCode()) { case CmpOp.NONE: case CmpOp.EQ: - return _value.length() == 0; + return _value.isEmpty(); case CmpOp.NE: // pred '<>' matches empty string but not blank cell // pred '<>ABC' matches blank and 'not ABC' diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Imaginary.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Imaginary.java index 012aacdd01..f478c95d5b 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Imaginary.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Imaginary.java @@ -74,7 +74,7 @@ public class Imaginary extends Fixed1ArgFunction implements FreeRefFunction { String imaginaryGroup = m.group(5); boolean hasImaginaryPart = imaginaryGroup.equals("i") || imaginaryGroup.equals("j"); - if (imaginaryGroup.length() == 0) { + if (imaginaryGroup.isEmpty()) { return new StringEval(String.valueOf(0)); } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Rept.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Rept.java index c3be177ce3..33b2c46366 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Rept.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Rept.java @@ -40,7 +40,6 @@ import org.apache.poi.ss.formula.eval.ValueEval; */ public class Rept extends Fixed2ArgFunction { - @Override public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval text, ValueEval number_times) { @@ -64,7 +63,7 @@ public class Rept extends Fixed2ArgFunction { strb.append(strText1); } - if (strb.toString().length() > 32767) { + if (strb.length() > 32767) { return ErrorEval.VALUE_INVALID; } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Sheet.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sheet.java new file mode 100644 index 0000000000..37b78dbb89 --- /dev/null +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sheet.java @@ -0,0 +1,89 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.EvaluationWorkbook; +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.*; + +/** + * Implementation for Excel SHEET() function. + * <p> + * <b>Syntax</b>:<br> <b>SHEET</b>([value])<br> + * </p> + * <p> + * Returns the sheet number of the referenced sheet or the current sheet if no argument is provided. + * </p> + * <p> + * Examples: + * </p> + * <ul> + * <li><code>=SHEET()</code> → returns the current sheet number (1-based)</li> + * <li><code>=SHEET(A1)</code> → returns the sheet number of the reference A1</li> + * <li><code>=SHEET(A1:B5)</code> → returns the sheet number of the range A1:B5</li> + * <li><code>=SHEET("Sheet3")</code> → returns the sheet number of the sheet named "Sheet3"</li> + * </ul> + * <p> + * See <a href="https://support.microsoft.com/en-us/office/sheet-function-44718b6f-8b87-47a1-a9d6-b701c06cff24">Microsoft Documentation</a> + * </p> + */ +public class Sheet implements FreeRefFunction { + + public static final Sheet instance = new Sheet(); + + @Override + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + try { + if (args.length == 0) { + // No argument provided → return the current sheet index +1 (Excel uses 1-based index) + return new NumberEval((double) ec.getSheetIndex() + 1); + } else { + ValueEval arg = args[0]; + + if (arg instanceof RefEval) { + // Argument is a single cell reference → return the sheet index of that reference +1 + RefEval ref = (RefEval) arg; + int sheetIndex = ref.getFirstSheetIndex(); + return new NumberEval((double) sheetIndex + 1); + } else if (arg instanceof AreaEval) { + // Argument is a cell range → return the sheet index of that area +1 + AreaEval area = (AreaEval) arg; + int sheetIndex = area.getFirstSheetIndex(); + return new NumberEval((double) sheetIndex + 1); + } else if (arg instanceof StringEval) { + // Argument is a string (sheet name, e.g., "Sheet3") → look up the sheet index by name + String sheetName = ((StringEval) arg).getStringValue(); + EvaluationWorkbook wb = ec.getWorkbook(); + int sheetIndex = wb.getSheetIndex(sheetName); + if (sheetIndex >= 0) { + return new NumberEval((double) sheetIndex + 1); + } else { + // Sheet name not found → return #N/A error + return ErrorEval.NA; + } + } else { + // Unsupported argument type → return #N/A error + return ErrorEval.NA; + } + } + } catch (Exception e) { + // Any unexpected exception (e.g., null pointers) → return #VALUE! error + return ErrorEval.VALUE_INVALID; + } + } +} diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Value.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Value.java index 451f0a2fab..87edae4b0e 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Value.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Value.java @@ -152,7 +152,7 @@ public final class Value extends Fixed1ArgFunction implements ArrayFunction { foundPercentage = true; break; } - if (remainingTextTrimmed.length() > 0) { + if (!remainingTextTrimmed.isEmpty()) { // intervening spaces not allowed once the digits start return null; } diff --git a/poi/src/main/java/org/apache/poi/ss/usermodel/DateUtil.java b/poi/src/main/java/org/apache/poi/ss/usermodel/DateUtil.java index 8018267a26..401b1c47a1 100644 --- a/poi/src/main/java/org/apache/poi/ss/usermodel/DateUtil.java +++ b/poi/src/main/java/org/apache/poi/ss/usermodel/DateUtil.java @@ -70,7 +70,7 @@ public class DateUtil { private static final Pattern date_ptrn2 = Pattern.compile("^\\[[a-zA-Z]+]"); private static final Pattern date_ptrn3a = Pattern.compile("[yYmMdDhHsS]"); // add "\u5e74 \u6708 \u65e5" for Chinese/Japanese date format:2017 \u5e74 2 \u6708 7 \u65e5 - private static final Pattern date_ptrn3b = Pattern.compile("^[\\[\\]yYmMdDhHsS\\-T/\u5e74\u6708\u65e5,. :\"\\\\]+0*[ampAMP/]*$"); + private static final Pattern date_ptrn3b = Pattern.compile("^[\\[\\]yYmMdDhHsS\\-T/\u5e74\u6708\u65e5,. :\"\\\\]+0* ?[ampAMP/]*$"); // elapsed time patterns: [h],[m] and [s] private static final Pattern date_ptrn4 = Pattern.compile("^\\[([hH]+|[mM]+|[sS]+)]"); @@ -548,6 +548,7 @@ public class DateUtil { // avoid re-checking DateUtil.isADateFormat(int, String) if a given format // string represents a date format if the same string is passed multiple times. // see https://issues.apache.org/bugzilla/show_bug.cgi?id=55611 + private static boolean maintainCache = true; private static final ThreadLocal<Integer> lastFormatIndex = ThreadLocal.withInitial(() -> -1); private static final ThreadLocal<String> lastFormatString = new ThreadLocal<>(); private static final ThreadLocal<Boolean> lastCachedResult = new ThreadLocal<>(); @@ -561,22 +562,24 @@ public class DateUtil { } private static boolean isCached(String formatString, int formatIndex) { - return formatIndex == lastFormatIndex.get() + return maintainCache && formatIndex == lastFormatIndex.get() && formatString.equals(lastFormatString.get()); } private static void cache(String formatString, int formatIndex, boolean cached) { - if (formatString == null || "".equals(formatString)) { - lastFormatString.remove(); - } else { - lastFormatString.set(formatString); - } - if (formatIndex == -1) { - lastFormatIndex.remove(); - } else { - lastFormatIndex.set(formatIndex); + if (maintainCache) { + if (formatString == null || "".equals(formatString)) { + lastFormatString.remove(); + } else { + lastFormatString.set(formatString); + } + if (formatIndex == -1) { + lastFormatIndex.remove(); + } else { + lastFormatIndex.set(formatIndex); + } + lastCachedResult.set(cached); } - lastCachedResult.set(cached); } /** @@ -624,7 +627,7 @@ public class DateUtil { } // If we didn't get a real string, don't even cache it as we can always find this out quickly - if(formatString == null || formatString.length() == 0) { + if(formatString == null || formatString.isEmpty()) { return false; } @@ -997,4 +1000,18 @@ public class DateUtil { return tm; } + + /** + * Enable or disable the thread-local cache for date format checking. + * If enabled, the date format checking will be cached per thread, + * which can improve performance when checking the same format multiple times. + * If disabled, the cache will not be used and each check will be performed independently. + * + * @param enable true to enable the cache, false to disable it (enabled, by default) + * @since POI 5.4.2 + */ + public static void enableThreadLocalCache(final boolean enable) { + // enable thread-local cache for date format checking + maintainCache = enable; + } } diff --git a/poi/src/main/java/org/apache/poi/ss/util/AreaReference.java b/poi/src/main/java/org/apache/poi/ss/util/AreaReference.java index fc45d81b4a..fd1cb4ff51 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/AreaReference.java +++ b/poi/src/main/java/org/apache/poi/ss/util/AreaReference.java @@ -326,7 +326,7 @@ public class AreaReference { String currentSegment = ""; StringTokenizer st = new StringTokenizer(reference, ","); while(st.hasMoreTokens()) { - if (currentSegment.length() > 0) { + if (!currentSegment.isEmpty()) { currentSegment += ","; } currentSegment += st.nextToken(); @@ -336,7 +336,7 @@ public class AreaReference { currentSegment = ""; } } - if (currentSegment.length() > 0) { + if (!currentSegment.isEmpty()) { results.add(currentSegment); } return results.toArray(new String[0]); diff --git a/poi/src/main/java/org/apache/poi/ss/util/CellReference.java b/poi/src/main/java/org/apache/poi/ss/util/CellReference.java index 1749878163..da1202c283 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/CellReference.java +++ b/poi/src/main/java/org/apache/poi/ss/util/CellReference.java @@ -116,22 +116,22 @@ public class CellReference implements GenericRecord { _sheetName = parts.sheetName; String colRef = parts.colRef; - _isColAbs = (colRef.length() > 0) && colRef.charAt(0) == '$'; + _isColAbs = (!colRef.isEmpty()) && colRef.charAt(0) == '$'; if (_isColAbs) { colRef = colRef.substring(1); } - if (colRef.length() == 0) { + if (colRef.isEmpty()) { _colIndex = -1; } else { _colIndex = convertColStringToIndex(colRef); } String rowRef=parts.rowRef; - _isRowAbs = (rowRef.length() > 0) && rowRef.charAt(0) == '$'; + _isRowAbs = (!rowRef.isEmpty()) && rowRef.charAt(0) == '$'; if (_isRowAbs) { rowRef = rowRef.substring(1); } - if (rowRef.length() == 0) { + if (rowRef.isEmpty()) { _rowIndex = -1; } else { // throws NumberFormatException if rowRef is not convertible to an int @@ -451,7 +451,7 @@ public class CellReference implements GenericRecord { } /** - * Takes in a 0-based base-10 column and returns a ALPHA-26 + * Takes in a 0-based base-10 column and returns an ALPHA-26 * representation. * eg {@code convertNumToColString(3)} returns {@code "D"} */ diff --git a/poi/src/main/java/org/apache/poi/ss/util/CellUtil.java b/poi/src/main/java/org/apache/poi/ss/util/CellUtil.java index 64ad761e2e..b2879fb7fd 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/CellUtil.java +++ b/poi/src/main/java/org/apache/poi/ss/util/CellUtil.java @@ -18,6 +18,7 @@ package org.apache.poi.ss.util; import java.util.Collections; +import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.Locale; @@ -46,6 +47,7 @@ import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.VerticalAlignment; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.util.Beta; +import org.apache.poi.util.Internal; import org.apache.poi.util.Removal; /** @@ -278,6 +280,7 @@ public final class CellUtil { map.put(LEFT_BORDER_COLOR, CellPropertyType.LEFT_BORDER_COLOR); map.put(RIGHT_BORDER_COLOR, CellPropertyType.RIGHT_BORDER_COLOR); map.put(TOP_BORDER_COLOR, CellPropertyType.TOP_BORDER_COLOR); + map.put(DATA_FORMAT, CellPropertyType.DATA_FORMAT); map.put(FILL_BACKGROUND_COLOR, CellPropertyType.FILL_BACKGROUND_COLOR); map.put(FILL_FOREGROUND_COLOR, CellPropertyType.FILL_FOREGROUND_COLOR); map.put(FILL_BACKGROUND_COLOR_COLOR, CellPropertyType.FILL_BACKGROUND_COLOR_COLOR); @@ -287,10 +290,11 @@ public final class CellUtil { map.put(HIDDEN, CellPropertyType.HIDDEN); map.put(INDENTION, CellPropertyType.INDENTION); map.put(LOCKED, CellPropertyType.LOCKED); + map.put(QUOTE_PREFIXED, CellPropertyType.QUOTE_PREFIXED); map.put(ROTATION, CellPropertyType.ROTATION); - map.put(VERTICAL_ALIGNMENT, CellPropertyType.VERTICAL_ALIGNMENT); map.put(SHRINK_TO_FIT, CellPropertyType.SHRINK_TO_FIT); - map.put(QUOTE_PREFIXED, CellPropertyType.QUOTE_PREFIXED); + map.put(VERTICAL_ALIGNMENT, CellPropertyType.VERTICAL_ALIGNMENT); + map.put(WRAP_TEXT, CellPropertyType.WRAP_TEXT); namePropertyMap = Collections.unmodifiableMap(map); } @@ -570,7 +574,7 @@ public final class CellUtil { @Deprecated @Removal(version = "7.0.0") public static void setCellStyleProperties(Cell cell, Map<String, Object> properties) { - Map<CellPropertyType, Object> strPropMap = new HashMap<>(properties.size()); + final EnumMap<CellPropertyType, Object> strPropMap = new EnumMap<>(CellPropertyType.class); properties.forEach((k, v) -> strPropMap.put(namePropertyMap.get(k), v)); setCellStyleProperties(cell, strPropMap, false); } @@ -610,7 +614,7 @@ public final class CellUtil { CellStyle originalStyle = cell.getCellStyle(); CellStyle newStyle = null; - Map<CellPropertyType, Object> values = getFormatProperties(originalStyle); + EnumMap<CellPropertyType, Object> values = getFormatProperties(originalStyle); if (properties.containsKey(CellPropertyType.FILL_FOREGROUND_COLOR_COLOR) && properties.get(CellPropertyType.FILL_FOREGROUND_COLOR_COLOR) == null) { values.remove(CellPropertyType.FILL_FOREGROUND_COLOR); } @@ -627,11 +631,11 @@ public final class CellUtil { // index seems like what index the cellstyle is in the list of styles for a workbook. // not good to compare on! - int numberCellStyles = workbook.getNumCellStyles(); + final int numberCellStyles = workbook.getNumCellStyles(); for (int i = 0; i < numberCellStyles; i++) { CellStyle wbStyle = workbook.getCellStyleAt(i); - Map<CellPropertyType, Object> wbStyleMap = getFormatProperties(wbStyle); + EnumMap<CellPropertyType, Object> wbStyleMap = getFormatProperties(wbStyle); // the desired style already exists in the workbook. Use the existing style. if (styleMapsMatch(wbStyleMap, values, disableNullColorCheck)) { @@ -651,8 +655,8 @@ public final class CellUtil { private static boolean styleMapsMatch(final Map<CellPropertyType, Object> newProps, final Map<CellPropertyType, Object> storedProps, final boolean disableNullColorCheck) { - final Map<CellPropertyType, Object> map1Copy = new HashMap<>(newProps); - final Map<CellPropertyType, Object> map2Copy = new HashMap<>(storedProps); + final EnumMap<CellPropertyType, Object> map1Copy = new EnumMap<>(newProps); + final EnumMap<CellPropertyType, Object> map2Copy = new EnumMap<>(storedProps); final Object backColor1 = map1Copy.remove(CellPropertyType.FILL_BACKGROUND_COLOR_COLOR); final Object backColor2 = map2Copy.remove(CellPropertyType.FILL_BACKGROUND_COLOR_COLOR); final Object foreColor1 = map1Copy.remove(CellPropertyType.FILL_FOREGROUND_COLOR_COLOR); @@ -683,20 +687,26 @@ public final class CellUtil { * @param cell The cell that is to be changed. * @param property The name of the property that is to be changed. * @param propertyValue The value of the property that is to be changed. - * + * @throws NullPointerException if {@code cell} or {@code property} is null * @since POI 5.4.0 */ public static void setCellStyleProperty(Cell cell, CellPropertyType property, Object propertyValue) { + if (cell == null) { + throw new NullPointerException("Cell must not be null"); + } + if (property == null) { + throw new NullPointerException("CellPropertyType must not be null"); + } boolean disableNullColorCheck = false; final Map<CellPropertyType, Object> propMap; if (CellPropertyType.FILL_FOREGROUND_COLOR_COLOR.equals(property) && propertyValue == null) { disableNullColorCheck = true; - propMap = new HashMap<>(); + propMap = new EnumMap<>(CellPropertyType.class); propMap.put(CellPropertyType.FILL_FOREGROUND_COLOR_COLOR, null); propMap.put(CellPropertyType.FILL_FOREGROUND_COLOR, null); } else if (CellPropertyType.FILL_BACKGROUND_COLOR_COLOR.equals(property) && propertyValue == null) { disableNullColorCheck = true; - propMap = new HashMap<>(); + propMap = new EnumMap<>(CellPropertyType.class); propMap.put(CellPropertyType.FILL_BACKGROUND_COLOR_COLOR, null); propMap.put(CellPropertyType.FILL_BACKGROUND_COLOR, null); } else { @@ -739,8 +749,8 @@ public final class CellUtil { * @return map of format properties (CellPropertyType -> Object) * @see #setFormatProperties(CellStyle, Workbook, Map) */ - private static Map<CellPropertyType, Object> getFormatProperties(CellStyle style) { - Map<CellPropertyType, Object> properties = new HashMap<>(); + private static EnumMap<CellPropertyType, Object> getFormatProperties(CellStyle style) { + EnumMap<CellPropertyType, Object> properties = new EnumMap<>(CellPropertyType.class); put(properties, CellPropertyType.ALIGNMENT, style.getAlignment()); put(properties, CellPropertyType.VERTICAL_ALIGNMENT, style.getVerticalAlignment()); put(properties, CellPropertyType.BORDER_BOTTOM, style.getBorderBottom()); @@ -776,7 +786,6 @@ public final class CellUtil { * * @param src the property map to copy from (read-only) * @param dest the property map to copy into - * @since POI 3.15 beta 3 */ private static void putAll(final Map<CellPropertyType, Object> src, Map<CellPropertyType, Object> dest) { for (final CellPropertyType key : src.keySet()) { @@ -806,7 +815,7 @@ public final class CellUtil { * Sets the format properties of the given style based on the given map. * * @param style cell style - * @param workbook parent workbook + * @param workbook parent workbook (can be null but some fomt info will not be copied if null is passed) * @param properties map of format properties (CellPropertyType -> Object) * @see #getFormatProperties(CellStyle) */ @@ -848,7 +857,9 @@ public final class CellUtil { } } - style.setFont(workbook.getFontAt(getInt(properties, CellPropertyType.FONT))); + if (workbook != null) { + style.setFont(workbook.getFontAt(getInt(properties, CellPropertyType.FONT))); + } style.setHidden(getBoolean(properties, CellPropertyType.HIDDEN)); style.setIndention(getShort(properties, CellPropertyType.INDENTION)); style.setLeftBorderColor(getShort(properties, CellPropertyType.LEFT_BORDER_COLOR)); @@ -862,6 +873,25 @@ public final class CellUtil { } /** + * Clones the style from one cell to another. For internal use only. + * Users should use the cloneStyleFrom method on CellStyle instead. + * + * @param src source cell style + * @param dest destination cell style + * @param destWorkbook destination workbook (can be null but some font info will not be copied if null is passed) + * @throws IllegalArgumentException if source or destination styles are null + * @since POI 5.4.2 + */ + @Internal + public static void cloneStyle(CellStyle src, CellStyle dest, Workbook destWorkbook) { + if (src == null || dest == null) { + throw new IllegalArgumentException("Source and destination styles must not be null"); + } + EnumMap<CellPropertyType, Object> properties = getFormatProperties(src); + setFormatProperties(dest, destWorkbook, properties); + } + + /** * Utility method that returns the named short value from the given map. * * @param properties map of named properties (CellPropertyType -> Object) diff --git a/poi/src/main/java/org/apache/poi/ss/util/PropertyTemplate.java b/poi/src/main/java/org/apache/poi/ss/util/PropertyTemplate.java index 2f74af55d7..e00b785070 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/PropertyTemplate.java +++ b/poi/src/main/java/org/apache/poi/ss/util/PropertyTemplate.java @@ -28,9 +28,11 @@ import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.Removal; +import java.util.EnumMap; +import java.util.EnumSet; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -42,9 +44,9 @@ import java.util.Set; * sheet in any workbook. * * This class requires the full spreadsheet to be in memory, so - * {@link org.apache.poi.xssf.streaming.SXSSFWorkbook} Spreadsheets are not + * <code>SXSSFWorkbook</code> Spreadsheets are not * supported. The same PropertyTemplate can, however, be applied to both - * {@link HSSFWorkbook} and {@link org.apache.poi.xssf.usermodel.XSSFWorkbook} + * {@link HSSFWorkbook} and <code>XSSFWorkbook</code> * objects if necessary. Portions of the border that fall outside the max range * of the {@link Workbook} sheet are ignored. * </p> @@ -59,7 +61,7 @@ public final class PropertyTemplate { * This is a list of cell properties for one shot application to a range of * cells at a later time. */ - private final Map<CellAddress, Map<CellPropertyType, Object>> _propertyTemplate; + private final Map<CellAddress, EnumMap<CellPropertyType, Object>> _propertyTemplate; /** * Create a PropertyTemplate object @@ -75,17 +77,17 @@ public final class PropertyTemplate { */ public PropertyTemplate(PropertyTemplate template) { this(); - for (Map.Entry<CellAddress, Map<CellPropertyType, Object>> entry : template.getTemplate().entrySet()) { + for (Map.Entry<CellAddress, EnumMap<CellPropertyType, Object>> entry : template.getTemplate().entrySet()) { _propertyTemplate.put(new CellAddress(entry.getKey()), cloneCellProperties(entry.getValue())); } } - private Map<CellAddress, Map<CellPropertyType, Object>> getTemplate() { + private Map<CellAddress, EnumMap<CellPropertyType, Object>> getTemplate() { return _propertyTemplate; } - private static Map<CellPropertyType, Object> cloneCellProperties(Map<CellPropertyType, Object> properties) { - return new HashMap<>(properties); + private static EnumMap<CellPropertyType, Object> cloneCellProperties(EnumMap<CellPropertyType, Object> properties) { + return new EnumMap<>(properties); } /** @@ -409,11 +411,11 @@ public final class PropertyTemplate { * @param range - {@link CellRangeAddress} range of cells to remove borders. */ private void removeBorders(CellRangeAddress range) { - Set<CellPropertyType> properties = new HashSet<>(); - properties.add(CellPropertyType.BORDER_TOP); - properties.add(CellPropertyType.BORDER_BOTTOM); - properties.add(CellPropertyType.BORDER_LEFT); - properties.add(CellPropertyType.BORDER_RIGHT); + EnumSet<CellPropertyType> properties = EnumSet.of( + CellPropertyType.BORDER_TOP, + CellPropertyType.BORDER_BOTTOM, + CellPropertyType.BORDER_LEFT, + CellPropertyType.BORDER_RIGHT); for (int row = range.getFirstRow(); row <= range.getLastRow(); row++) { for (int col = range.getFirstColumn(); col <= range .getLastColumn(); col++) { @@ -433,7 +435,7 @@ public final class PropertyTemplate { */ public void applyBorders(Sheet sheet) { Workbook wb = sheet.getWorkbook(); - for (Map.Entry<CellAddress, Map<CellPropertyType, Object>> entry : _propertyTemplate + for (Map.Entry<CellAddress, EnumMap<CellPropertyType, Object>> entry : _propertyTemplate .entrySet()) { CellAddress cellAddress = entry.getKey(); if (cellAddress.getRow() < wb.getSpreadsheetVersion().getMaxRows() @@ -756,11 +758,11 @@ public final class PropertyTemplate { * @param range - {@link CellRangeAddress} range of cells to remove borders. */ private void removeBorderColors(CellRangeAddress range) { - Set<CellPropertyType> properties = new HashSet<>(); - properties.add(CellPropertyType.TOP_BORDER_COLOR); - properties.add(CellPropertyType.BOTTOM_BORDER_COLOR); - properties.add(CellPropertyType.LEFT_BORDER_COLOR); - properties.add(CellPropertyType.RIGHT_BORDER_COLOR); + Set<CellPropertyType> properties = EnumSet.of( + CellPropertyType.TOP_BORDER_COLOR, + CellPropertyType.BOTTOM_BORDER_COLOR, + CellPropertyType.LEFT_BORDER_COLOR, + CellPropertyType.RIGHT_BORDER_COLOR); for (int row = range.getFirstRow(); row <= range.getLastRow(); row++) { for (int col = range.getFirstColumn(); col <= range .getLastColumn(); col++) { @@ -781,9 +783,9 @@ public final class PropertyTemplate { */ private void addProperty(int row, int col, CellPropertyType property, Object value) { CellAddress cell = new CellAddress(row, col); - Map<CellPropertyType, Object> cellProperties = _propertyTemplate.get(cell); + EnumMap<CellPropertyType, Object> cellProperties = _propertyTemplate.get(cell); if (cellProperties == null) { - cellProperties = new HashMap<>(); + cellProperties = new EnumMap<>(CellPropertyType.class); } cellProperties.put(property, value); _propertyTemplate.put(cell, cellProperties); @@ -795,7 +797,7 @@ public final class PropertyTemplate { */ private void removeProperties(int row, int col, Set<CellPropertyType> properties) { CellAddress cell = new CellAddress(row, col); - Map<CellPropertyType, Object> cellProperties = _propertyTemplate.get(cell); + EnumMap<CellPropertyType, Object> cellProperties = _propertyTemplate.get(cell); if (cellProperties != null) { cellProperties.keySet().removeAll(properties); if (cellProperties.isEmpty()) { @@ -946,6 +948,7 @@ public final class PropertyTemplate { * @deprecated See {@link #getTemplateProperty(int, int, CellPropertyType)} */ @Deprecated + @Removal(version = "7.0.0") public short getTemplateProperty(int row, int col, String propertyName) { return getTemplateProperty(new CellAddress(row, col), CellUtil.namePropertyMap.get(propertyName)); } diff --git a/poi/src/main/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java b/poi/src/main/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java index 9b2e711ed4..584f7dec4c 100644 --- a/poi/src/main/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java +++ b/poi/src/main/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java @@ -52,6 +52,9 @@ public class DefaultTempFileCreationStrategy implements TempFileCreationStrategy /** The directory where the temporary files will be created (<code>null</code> to use the default directory). */ private volatile File dir; + /** The directory where that was passed to the constructor (<code>null</code> to use the default directory). */ + private final File initDir; + /** The lock to make dir initialized only once. */ private final Lock dirLock = new ReentrantLock(); @@ -65,14 +68,23 @@ public class DefaultTempFileCreationStrategy implements TempFileCreationStrategy } /** - * Creates the strategy allowing to set the + * Creates the strategy allowing to set a custom directory for the temporary files. + * <p> + * If you provide a non-null dir as input, it must be a directory (if it already exists). + * Since POI 5.4.2, this is checked at construction time. In previous versions, it was checked + * at the first call to {@link #createTempFile(String, String)} or {@link #createTempDirectory(String)}. + * </p> * * @param dir The directory where the temporary files will be created (<code>null</code> to use the default directory). - * + * @throws IllegalArgumentException if the provided directory does not exist or is not a directory * @see Files#createTempFile(Path, String, String, FileAttribute[]) */ public DefaultTempFileCreationStrategy(File dir) { + this.initDir = dir; this.dir = dir; + if (dir != null && dir.exists() && !dir.isDirectory()) { + throw new IllegalArgumentException("The provided file is not a directory: " + dir); + } } @Override @@ -117,7 +129,11 @@ public class DefaultTempFileCreationStrategy implements TempFileCreationStrategy } protected Path getPOIFilesDirectoryPath() throws IOException { - return Paths.get(getJavaIoTmpDir(), POIFILES); + if (initDir == null) { + return Paths.get(getJavaIoTmpDir(), POIFILES); + } else { + return initDir.toPath(); + } } // Create our temp dir only once by double-checked locking diff --git a/poi/src/main/java/org/apache/poi/util/IOUtils.java b/poi/src/main/java/org/apache/poi/util/IOUtils.java index 430e895557..ff86043a54 100644 --- a/poi/src/main/java/org/apache/poi/util/IOUtils.java +++ b/poi/src/main/java/org/apache/poi/util/IOUtils.java @@ -108,6 +108,14 @@ public final class IOUtils { } /** + * @return The maximum number of bytes that should be possible to be allocated in one step. + * @since 5.4.1 + */ + public static int getByteArrayMaxOverride() { + return BYTE_ARRAY_MAX_OVERRIDE; + } + + /** * Peeks at the first 8 bytes of the stream. Returns those bytes, but * with the stream unaffected. Requires a stream that supports mark/reset, * or a PushbackInputStream. If the stream has >0 but <8 bytes, @@ -209,6 +217,27 @@ public final class IOUtils { } /** + * Reads up to {@code length} bytes from the input stream, and returns the bytes read. + * + * @param stream The byte stream of data to read. + * @param length The maximum length to read, use {@link Integer#MAX_VALUE} to read the stream + * until EOF + * @param maxLength if the input is equal to/longer than {@code maxLength} bytes, + * then throw an {@link IOException} complaining about the length. + * use {@link Integer#MAX_VALUE} to disable the check - if {@link #setByteArrayMaxOverride(int)} is + * set then that max of that value and this maxLength is used + * @return A byte array with the read bytes. + * @throws IOException If reading data fails or EOF is encountered too early for the given length. + * @throws RecordFormatException If the requested length is invalid. + * @since POI 5.4.1 + */ + public static byte[] toByteArray(InputStream stream, final long length, final int maxLength) throws IOException { + return toByteArray(stream, + length > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) length, + maxLength, true, length != Integer.MAX_VALUE); + } + + /** * Reads the input stream, and returns the bytes read. * * @param stream The byte stream of data to read. @@ -227,15 +256,12 @@ public final class IOUtils { private static byte[] toByteArray(InputStream stream, final int length, final int maxLength, final boolean checkEOFException, final boolean isLengthKnown) throws IOException { - if (length < 0 || maxLength < 0) { - throw new RecordFormatException("Can't allocate an array of length < 0"); - } final int derivedMaxLength = Math.max(maxLength, BYTE_ARRAY_MAX_OVERRIDE); if ((length != Integer.MAX_VALUE) || (derivedMaxLength != Integer.MAX_VALUE)) { checkLength(length, derivedMaxLength); } - final int derivedLen = isLengthKnown ? Math.min(length, derivedMaxLength) : derivedMaxLength; + final int derivedLen = isLengthKnown && length >= 0 ? Math.min(length, derivedMaxLength) : derivedMaxLength; final int byteArrayInitLen = calculateByteArrayInitLength(isLengthKnown, length, derivedMaxLength); final int internalBufferLen = DEFAULT_BUFFER_SIZE; try (UnsynchronizedByteArrayOutputStream baos = UnsynchronizedByteArrayOutputStream.builder().setBufferSize(byteArrayInitLen).get()) { @@ -254,7 +280,7 @@ public final class IOUtils { throwRecordTruncationException(derivedMaxLength); } - if (checkEOFException && derivedLen != Integer.MAX_VALUE && totalBytes < derivedLen) { + if (checkEOFException && length >= 0 && derivedLen != Integer.MAX_VALUE && totalBytes < derivedLen) { throw new EOFException("unexpected EOF - expected len: " + derivedLen + " - actual len: " + totalBytes); } diff --git a/poi/src/main/java/org/apache/poi/util/TempFile.java b/poi/src/main/java/org/apache/poi/util/TempFile.java index 46bf8ccc8b..2b668dc885 100644 --- a/poi/src/main/java/org/apache/poi/util/TempFile.java +++ b/poi/src/main/java/org/apache/poi/util/TempFile.java @@ -19,6 +19,8 @@ package org.apache.poi.util; import java.io.File; import java.io.IOException; +import java.util.Objects; +import java.util.function.Supplier; /** * Interface for creating temporary files. Collects them all into one directory by default. @@ -27,7 +29,14 @@ public final class TempFile { /** The strategy used by {@link #createTempFile(String, String)} to create the temporary files. */ private static TempFileCreationStrategy strategy = new DefaultTempFileCreationStrategy(); - /** Define a constant for this property as it is sometimes mistypes as "tempdir" otherwise */ + /** If set for the thread, this is used instead of the strategy variable above. */ + private static final ThreadLocal<TempFileCreationStrategy> threadLocalStrategy = new ThreadLocal<>(); + static { + // allow to clear all thread-locals via ThreadLocalUtil + ThreadLocalUtil.registerCleaner(threadLocalStrategy::remove); + } + + /** Define a constant for this property as it is sometimes mistyped as "tempdir" otherwise */ public static final String JAVA_IO_TMPDIR = "java.io.tmpdir"; private TempFile() { @@ -47,6 +56,21 @@ public final class TempFile { } TempFile.strategy = strategy; } + + /** + * Configures the strategy used by {@link #createTempFile(String, String)} to create the temporary files. + * + * @param strategy The new strategy to be used to create the temporary files for this thread. + * <code>null</code> can be used to reset the strategy for this thread to the default one. + * @since POI 5.4.2 + */ + public static void setThreadLocalTempFileCreationStrategy(TempFileCreationStrategy strategy) { + if (strategy == null) { + threadLocalStrategy.remove(); + } else { + threadLocalStrategy.set(strategy); + } + } /** * Creates a new and empty temporary file. By default, files are collected into one directory and are not @@ -64,10 +88,44 @@ public final class TempFile { * @throws IOException If no temporary file could be created. */ public static File createTempFile(String prefix, String suffix) throws IOException { - return strategy.createTempFile(prefix, suffix); + return getStrategy().createTempFile(prefix, suffix); } public static File createTempDirectory(String name) throws IOException { - return strategy.createTempDirectory(name); + return getStrategy().createTempDirectory(name); + } + + /** + * Executes the given task ensuring that POI will use the given temp file creation strategy + * within the scope of the given task. The change of strategy is not visible to other threads, + * and the previous strategy is restored after the task completed (normally or exceptionally). + * + * @param newStrategy the temp file strategy to be used in the scope of the given task + * @param task the task to be executed with the given temp file strategy + * @return the result of the given task + * + * @since POI 5.4.2 + */ + public static <R> R withStrategy(TempFileCreationStrategy newStrategy, Supplier<? extends R> task) { + Objects.requireNonNull(newStrategy, "newStrategy"); + Objects.requireNonNull(task, "task"); + + TempFileCreationStrategy oldStrategy = threadLocalStrategy.get(); + try { + threadLocalStrategy.set(newStrategy); + return task.get(); + } finally { + setThreadLocalTempFileCreationStrategy(oldStrategy); + } + } + + private static TempFileCreationStrategy getStrategy() { + final TempFileCreationStrategy s = threadLocalStrategy.get(); + if (s == null) { + threadLocalStrategy.remove(); + return strategy; + } else { + return s; + } } } diff --git a/poi/src/test/java/org/apache/poi/POIDataSamples.java b/poi/src/test/java/org/apache/poi/POIDataSamples.java index e15843f377..f976cd0c91 100644 --- a/poi/src/test/java/org/apache/poi/POIDataSamples.java +++ b/poi/src/test/java/org/apache/poi/POIDataSamples.java @@ -169,7 +169,7 @@ public final class POIDataSamples { + "' not found in data dir '" + _resolvedDataDir.getAbsolutePath() + "'"); } try { - if(sampleFileName.length() > 0) { + if(!sampleFileName.isEmpty()) { String fn = sampleFileName; if(fn.indexOf('/') > 0) { fn = fn.substring(fn.indexOf('/')+1); diff --git a/poi/src/test/java/org/apache/poi/hssf/eventusermodel/TestFormatTrackingHSSFListener.java b/poi/src/test/java/org/apache/poi/hssf/eventusermodel/TestFormatTrackingHSSFListener.java index 2a99b3b395..3ef75d7f3d 100644 --- a/poi/src/test/java/org/apache/poi/hssf/eventusermodel/TestFormatTrackingHSSFListener.java +++ b/poi/src/test/java/org/apache/poi/hssf/eventusermodel/TestFormatTrackingHSSFListener.java @@ -16,7 +16,9 @@ ==================================================================== */ package org.apache.poi.hssf.eventusermodel; + import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -96,7 +98,7 @@ final class TestFormatTrackingHSSFListener { // Should always give us a string String s = listener.formatNumberDateCell(cvr); assertNotNull(s); - assertTrue(s.length() > 0); + assertNotEquals(0, s.length()); } } diff --git a/poi/src/test/java/org/apache/poi/hssf/model/TestDrawingAggregate.java b/poi/src/test/java/org/apache/poi/hssf/model/TestDrawingAggregate.java index 12854be920..8d07fc4d0e 100644 --- a/poi/src/test/java/org/apache/poi/hssf/model/TestDrawingAggregate.java +++ b/poi/src/test/java/org/apache/poi/hssf/model/TestDrawingAggregate.java @@ -118,11 +118,7 @@ class TestDrawingAggregate { UnsynchronizedByteArrayOutputStream out = UnsynchronizedByteArrayOutputStream.builder().get(); for (RecordBase rb : aggRecords) { Record r = (org.apache.poi.hssf.record.Record) rb; - try { - out.write(r.serialize()); - } catch (IOException e) { - throw new RuntimeException(e); - } + out.write(r.serialize()); } return out.toByteArray(); } @@ -263,11 +259,7 @@ class TestDrawingAggregate { UnsynchronizedByteArrayOutputStream out = UnsynchronizedByteArrayOutputStream.builder().get(); for (RecordBase rb : records) { Record r = (org.apache.poi.hssf.record.Record) rb; - try { - out.write(r.serialize()); - } catch (IOException e) { - throw new RuntimeException(e); - } + out.write(r.serialize()); } return out.toByteArray(); } diff --git a/poi/src/test/java/org/apache/poi/hssf/model/TestEscherRecordFactory.java b/poi/src/test/java/org/apache/poi/hssf/model/TestEscherRecordFactory.java index 091802098c..f641b6eeb3 100644 --- a/poi/src/test/java/org/apache/poi/hssf/model/TestEscherRecordFactory.java +++ b/poi/src/test/java/org/apache/poi/hssf/model/TestEscherRecordFactory.java @@ -42,11 +42,7 @@ class TestEscherRecordFactory { UnsynchronizedByteArrayOutputStream out = UnsynchronizedByteArrayOutputStream.builder().get(); for (RecordBase rb : records) { Record r = (org.apache.poi.hssf.record.Record) rb; - try { - out.write(r.serialize()); - } catch (IOException e) { - throw new RuntimeException(e); - } + out.write(r.serialize()); } return out.toByteArray(); } diff --git a/poi/src/test/java/org/apache/poi/hssf/model/TestWorkbook.java b/poi/src/test/java/org/apache/poi/hssf/model/TestWorkbook.java index 76b306b1d9..5b277e52cc 100644 --- a/poi/src/test/java/org/apache/poi/hssf/model/TestWorkbook.java +++ b/poi/src/test/java/org/apache/poi/hssf/model/TestWorkbook.java @@ -22,10 +22,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; import org.apache.poi.hssf.record.CountryRecord; import org.apache.poi.hssf.record.FontRecord; import org.apache.poi.hssf.record.RecalcIdRecord; @@ -170,4 +172,56 @@ final class TestWorkbook { int newRecordsCount = iwb.getNumRecords(); assertEquals(oldRecordsCount, newRecordsCount, "records count after getWriteAccess"); } + + @Test + void testSetUserName() throws IOException { + // normal username + setAndReadUserName("username", false); + + // 109 characters max if no "multibyte" character + setAndReadUserName("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", false); + + // 54 max if there is at least one "multibyte" character + setAndReadUserName("€23456789012345678901234567890123456789012345678901234", false); + + // also works for very strange characters + setAndReadUserName("\uD801\uDC37\uD852\uDF62€$ⵃⵥꭓꭃꭦ﹄4Uカ\uD800\uDFB1\uD800\uDFC9\uD834\uDF45\uD834\uDF4B\uD83E\uDF22\uD83E\uDF53\uD83E\uDFB5\uD83E\uDFF6\uD801\uDC37\uD852\uDF62€$ⵃⵥꭓꭃꭦ﹄4Uカ\uD800\uDFB1\uD800\uDFC9\uD834\uDF45\uD834\uDF4B", false); + + // fails with longer strings + setAndReadUserName("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", + true); + setAndReadUserName("€234567890123456789012345678901234567890123456789012345", true); + setAndReadUserName("\uD801\uDC37\uD852\uDF62€$ⵃⵥꭓꭃꭦ﹄4Uカ\uD800\uDFB1\uD800\uDFC9\uD834\uDF45\uD834\uDF4B\uD83E\uDF22\uD83E\uDF53\uD83E\uDFB5\uD83E\uDFF6\uD801\uDC37\uD852\uDF62€$ⵃⵥꭓꭃꭦ﹄4Uカ\uD800\uDFB1\uD800\uDFC9\uD834\uDF45\uD834\uDF4B\uDF4B", + true); + } + + private static void setAndReadUserName(String username, boolean fails) throws IOException { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + InternalWorkbook iwb = wb.getInternalWorkbook(); + + String prev = iwb.getWriteAccess().getUsername(); + if (fails) { + assertThrows(IllegalArgumentException.class, + () -> iwb.getWriteAccess().setUsername(username), + "Expected to fail with username " + username); + assertEquals(prev, iwb.getWriteAccess().getUsername(), + "Username should not have been changed, but had: " + prev + " and " + + iwb.getWriteAccess().getUsername()); + + // cannot test more if username is too long + return; + } else { + iwb.getWriteAccess().setUsername(username); + } + + assertEquals(username, iwb.getWriteAccess().getUsername()); + try (UnsynchronizedByteArrayOutputStream os = UnsynchronizedByteArrayOutputStream.builder().get()) { + wb.write(os); + try (HSSFWorkbook wb2 = new HSSFWorkbook(os.toInputStream())) { + InternalWorkbook iwb2 = wb2.getInternalWorkbook(); + assertEquals(username, iwb2.getWriteAccess().getUsername()); + } + } + } + } } diff --git a/poi/src/test/java/org/apache/poi/hssf/record/TestRecordFactory.java b/poi/src/test/java/org/apache/poi/hssf/record/TestRecordFactory.java index a44cce0e42..fb3f44eb6c 100644 --- a/poi/src/test/java/org/apache/poi/hssf/record/TestRecordFactory.java +++ b/poi/src/test/java/org/apache/poi/hssf/record/TestRecordFactory.java @@ -211,11 +211,7 @@ final class TestRecordFactory { }; UnsynchronizedByteArrayOutputStream baos = UnsynchronizedByteArrayOutputStream.builder().get(); for (org.apache.poi.hssf.record.Record rec : recs) { - try { - baos.write(rec.serialize()); - } catch (IOException e) { - throw new RuntimeException(e); - } + baos.write(rec.serialize()); } //simulate the bad padding at the end of the workbook stream in attachment 23483 of bug 46987 baos.write(0x00); diff --git a/poi/src/test/java/org/apache/poi/hssf/record/TestWriteAccessRecord.java b/poi/src/test/java/org/apache/poi/hssf/record/TestWriteAccessRecord.java index ee3cfec38b..8286f442c0 100644 --- a/poi/src/test/java/org/apache/poi/hssf/record/TestWriteAccessRecord.java +++ b/poi/src/test/java/org/apache/poi/hssf/record/TestWriteAccessRecord.java @@ -93,4 +93,47 @@ final class TestWriteAccessRecord { confirmRecordEncoding(WriteAccessRecord.sid, expectedEncoding, rec.serialize()); } + + @Test + void testUTF16LE() { + byte[] data = HexRead.readFromString("" + + "5c 00 70 00 1C 00 01 " + + "44 00 61 00 74 00 61 00 20 00 44 00 79 00 6e 00 " + + "61 00 6d 00 69 00 63 00 73 00 27 00 20 00 53 00 " + + "70 00 72 00 65 00 61 00 64 00 42 00 75 00 69 00 " + + "6c 00 64 00 65 00 72 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00" + ); + RecordInputStream in = TestcaseRecordInputStream.create(data); + + WriteAccessRecord rec = new WriteAccessRecord(in); + + assertEquals("Data Dynamics' SpreadBuilder", rec.getUsername()); + } + + @Test + void testUTF16LE_wrong_size() { + // "0x51" on position 5 is an incorrect size, as it would require 162 bytes to encode as UTF-16LE + // the spec only allows up to 109 bytes for the string in this record, but it seems some broken + // software out there will generate such a file + byte[] data = HexRead.readFromString("" + + "5c 00 70 00 51 00 01 " + + "44 00 61 00 74 00 61 00 20 00 44 00 79 00 6e 00 " + + "61 00 6d 00 69 00 63 00 73 00 27 00 20 00 53 00 " + + "70 00 72 00 65 00 61 00 64 00 42 00 75 00 69 00 " + + "6c 00 64 00 65 00 72 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 " + + "20 00 20 00 20 00" + ); + RecordInputStream in = TestcaseRecordInputStream.create(data); + + WriteAccessRecord rec = new WriteAccessRecord(in); + + assertEquals("Data Dynamics' SpreadBuilder", rec.getUsername()); + } } diff --git a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestBugs.java b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestBugs.java index 3b026b2ce1..df7da70e19 100644 --- a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestBugs.java +++ b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestBugs.java @@ -26,6 +26,8 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -2316,12 +2318,12 @@ final class TestBugs extends BaseTestBugzillaIssues { } @Test - void test46515() throws IOException { + void test46515() throws IOException, URISyntaxException { try (Workbook wb = openSampleWorkbook("46515.xls")) { // Get structure from webservice String urlString = "https://poi.apache.org/components/spreadsheet/images/calendar.jpg"; - URL structURL = new URL(urlString); + URL structURL = new URI(urlString).toURL(); BufferedImage bimage; try { bimage = ImageIO.read(structURL); diff --git a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java index 2949dbeeee..61cd1aba69 100644 --- a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java +++ b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java @@ -27,6 +27,7 @@ import java.util.TimeZone; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.model.InternalWorkbook; +import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.util.LocaleUtil; import org.junit.jupiter.api.AfterAll; @@ -60,7 +61,7 @@ class TestHSSFDateUtil { HSSFWorkbook workbook = HSSFTestDataSamples.openSampleWorkbook("DateFormats.xls"); HSSFSheet sheet = workbook.getSheetAt(0); - InternalWorkbook wb = workbook.getWorkbook(); + InternalWorkbook wb = workbook.getWorkbook(); assertNotNull(wb); HSSFRow row; @@ -115,4 +116,27 @@ class TestHSSFDateUtil { workbook.close(); } + + @Test + void testIsADateFormat() throws IOException { + try (HSSFWorkbook workbook = new HSSFWorkbook()) { + HSSFSheet sheet = workbook.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFCell cell = row.createCell(0); + cell.setCellValue(45825.5); // 2025-06-17 (midday) + HSSFCellStyle style = workbook.createCellStyle(); + style.setDataFormat(workbook.createDataFormat().getFormat("DD MMMM, YYYY hh:mm:ss.000 AM/PM")); + cell.setCellStyle(style); + DateUtil.enableThreadLocalCache(false); + try { + assertTrue(DateUtil.isCellDateFormatted(cell), "cell is date formatted?"); + DataFormatter formatter = new DataFormatter(); + String formattedValue = formatter.formatCellValue(cell); + assertEquals("17 June, 2025 12:00:00.000 PM", formattedValue); + } finally { + DateUtil.enableThreadLocalCache(true); + } + } + } + } diff --git a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFRowCopyRowFrom.java b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFRowCopyRowFrom.java new file mode 100644 index 0000000000..67dcc38f46 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFRowCopyRowFrom.java @@ -0,0 +1,130 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellCopyContext; +import org.apache.poi.ss.usermodel.CellCopyPolicy; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Iterator; + +public class TestHSSFRowCopyRowFrom { + @Test + void testCopyFrom() throws IOException { + CellCopyPolicy cellCopyPolicy = new CellCopyPolicy.Builder() + .cellFormula(false) // NOTE: setting to false allows for copying the evaluated formula value. + .cellStyle(CellCopyPolicy.DEFAULT_COPY_CELL_STYLE_POLICY) + .cellValue(CellCopyPolicy.DEFAULT_COPY_CELL_VALUE_POLICY) + .condenseRows(CellCopyPolicy.DEFAULT_CONDENSE_ROWS_POLICY) + .copyHyperlink(CellCopyPolicy.DEFAULT_COPY_HYPERLINK_POLICY) + .mergeHyperlink(CellCopyPolicy.DEFAULT_MERGE_HYPERLINK_POLICY) + .mergedRegions(CellCopyPolicy.DEFAULT_COPY_MERGED_REGIONS_POLICY) + .rowHeight(CellCopyPolicy.DEFAULT_COPY_ROW_HEIGHT_POLICY) + .build(); + + final LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + final LocalDateTime nonValidExcelDate = LocalDateTime.of(1899, 12, 31, 0, 0, 0); + final Object[][] data = { + {"transaction_id", "transaction_date", "transaction_time"}, + {75, localDateTime, nonValidExcelDate.plusHours(9).plusMinutes(53).plusSeconds(44).toLocalTime()}, + {78, localDateTime, nonValidExcelDate.plusHours(9).plusMinutes(55).plusSeconds(16).toLocalTime()} + }; + + final UnsynchronizedByteArrayOutputStream workbookOutputStream = + UnsynchronizedByteArrayOutputStream.builder().get(); + try (Workbook workbook = new HSSFWorkbook()) { + final Sheet sheet = workbook.createSheet("SomeSheetName"); + populateSheet(sheet, data); + setCellStyles(sheet, workbook); + workbook.write(workbookOutputStream); + } + + try (HSSFWorkbook originalWorkbook = new HSSFWorkbook(workbookOutputStream.toInputStream())) { + final Iterator<Sheet> originalSheetsIterator = originalWorkbook.sheetIterator(); + final CellCopyContext cellCopyContext = new CellCopyContext(); + + while (originalSheetsIterator.hasNext()) { + final HSSFSheet originalSheet = (HSSFSheet) originalSheetsIterator.next(); + final String originalSheetName = originalSheet.getSheetName(); + final Iterator<Row> originalRowsIterator = originalSheet.rowIterator(); + + try (HSSFWorkbook newWorkbook = new HSSFWorkbook()) { + final HSSFSheet newSheet = newWorkbook.createSheet(originalSheetName); + while (originalRowsIterator.hasNext()) { + HSSFRow originalRow = (HSSFRow) originalRowsIterator.next(); + HSSFRow newRow = newSheet.createRow(originalRow.getRowNum()); + newRow.copyRowFrom(originalRow, cellCopyPolicy, cellCopyContext); + } + } + } + } + } + + private static void populateSheet(Sheet sheet, Object[][] data) { + int rowCount = 0; + for (Object[] dataRow : data) { + Row row = sheet.createRow(rowCount++); + int columnCount = 0; + + for (Object field : dataRow) { + Cell cell = row.createCell(columnCount++); + if (field instanceof String) { + cell.setCellValue((String) field); + } else if (field instanceof Integer) { + cell.setCellValue((Integer) field); + } else if (field instanceof Long) { + cell.setCellValue((Long) field); + } else if (field instanceof LocalDateTime) { + cell.setCellValue((LocalDateTime) field); + } else if (field instanceof LocalTime) { + cell.setCellValue(DateUtil.convertTime(DateTimeFormatter.ISO_LOCAL_TIME.format((LocalTime) field))); + } + } + } + } + + void setCellStyles(Sheet sheet, Workbook workbook) { + CreationHelper creationHelper = workbook.getCreationHelper(); + CellStyle dayMonthYearCellStyle = workbook.createCellStyle(); + dayMonthYearCellStyle.setDataFormat(creationHelper.createDataFormat().getFormat("dd/mm/yyyy")); + CellStyle hourMinuteSecond = workbook.createCellStyle(); + hourMinuteSecond.setDataFormat((short) 21); // 21 represents format h:mm:ss + for (int rowNum = sheet.getFirstRowNum() + 1; rowNum < sheet.getLastRowNum() + 1; rowNum++) { + Row row = sheet.getRow(rowNum); + row.getCell(1).setCellStyle(dayMonthYearCellStyle); + row.getCell(2).setCellStyle(hourMinuteSecond); + } + } +} diff --git a/poi/src/test/java/org/apache/poi/hssf/util/TestHSSFColor.java b/poi/src/test/java/org/apache/poi/hssf/util/TestHSSFColor.java index 059dbb28b3..1bde058114 100644 --- a/poi/src/test/java/org/apache/poi/hssf/util/TestHSSFColor.java +++ b/poi/src/test/java/org/apache/poi/hssf/util/TestHSSFColor.java @@ -17,6 +17,7 @@ package org.apache.poi.hssf.util; +import java.awt.Color; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -56,4 +57,15 @@ final class TestHSSFColor { triplets.get(HSSFColorPredefined.YELLOW.getHexString()) ); } + + @Test + void testRgbAndColorWorkTheSame() { + Color color = java.awt.Color.YELLOW; + @SuppressWarnings("deprecation") + HSSFColor fromColor = new HSSFColor(10, 20, color); + HSSFColor fromRgb = new HSSFColor(10, 20, color.getRGB()); + + assertEquals(fromColor, fromRgb, "Both colors are equal"); + assertEquals(fromColor.hashCode(), fromRgb.hashCode(), "Both have the same hash code"); + } } diff --git a/poi/src/test/java/org/apache/poi/ss/formula/atp/TestAnalysisToolPak.java b/poi/src/test/java/org/apache/poi/ss/formula/atp/TestAnalysisToolPak.java new file mode 100644 index 0000000000..2861cab7f0 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/atp/TestAnalysisToolPak.java @@ -0,0 +1,75 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.atp; + +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.FreeRefFunction; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TestAnalysisToolPak { + private static class NullFunction implements FreeRefFunction { + @Override + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + return ErrorEval.DIV_ZERO; + } + } + + @Test + void testOverrideKnownButUnimplemented() { + // JIS is a known function in Excel, but it is not implemented in POI + AnalysisToolPak.registerFunction("JIS", new NullFunction()); + try (HSSFWorkbook workbook = new HSSFWorkbook()) { + HSSFSheet sheet = workbook.createSheet("Sheet1"); + HSSFCell cell = sheet.createRow(0).createCell(0); + cell.setCellFormula("JIS(\"test\")"); + workbook.getCreationHelper().createFormulaEvaluator().evaluateAll(); + assertEquals(ErrorEval.DIV_ZERO.getErrorCode(), cell.getErrorCellValue()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + void testOverrideUnknown() { + assertThrows(IllegalArgumentException.class, () -> { + AnalysisToolPak.registerFunction("UNKNOWN", new NullFunction()); + }); + } + + @Test + void testOverrideUnknownButForceAllowed() { + AnalysisToolPak.registerFunction("FAKE", new NullFunction(), true); + try (HSSFWorkbook workbook = new HSSFWorkbook()) { + HSSFSheet sheet = workbook.createSheet("Sheet1"); + HSSFCell cell = sheet.createRow(0).createCell(0); + cell.setCellFormula("FAKE(\"test\")"); + workbook.getCreationHelper().createFormulaEvaluator().evaluateAll(); + assertEquals(ErrorEval.DIV_ZERO.getErrorCode(), cell.getErrorCellValue()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelCetabFunctionExtractor.java b/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelCetabFunctionExtractor.java index 87c0dcd1f3..3d6f525d2b 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelCetabFunctionExtractor.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelCetabFunctionExtractor.java @@ -149,7 +149,7 @@ public final class ExcelCetabFunctionExtractor { public void addFunction(int funcIx, boolean hasFootnote, String funcName, int minParams, int maxParams, String returnClass, String paramClasses, String volatileFlagStr) { - boolean isVolatile = volatileFlagStr.length() > 0; + boolean isVolatile = !volatileFlagStr.isEmpty(); Integer funcIxKey = Integer.valueOf(funcIx); if(!_groupFunctionIndexes.add(funcIxKey)) { diff --git a/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelFileFormatDocFunctionExtractor.java b/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelFileFormatDocFunctionExtractor.java index b131071abf..11dca94104 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelFileFormatDocFunctionExtractor.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/function/ExcelFileFormatDocFunctionExtractor.java @@ -26,6 +26,8 @@ import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; @@ -169,9 +171,9 @@ public final class ExcelFileFormatDocFunctionExtractor { public void addFunction(int funcIx, boolean hasFootnote, String funcName, int minParams, int maxParams, String returnClass, String paramClasses, String volatileFlagStr) { - boolean isVolatile = volatileFlagStr.length() > 0; + boolean isVolatile = !volatileFlagStr.isEmpty(); - Integer funcIxKey = Integer.valueOf(funcIx); + Integer funcIxKey = funcIx; if(!_groupFunctionIndexes.add(funcIxKey)) { throw new RuntimeException("Duplicate function index (" + funcIx + ")"); } @@ -209,7 +211,7 @@ public final class ExcelFileFormatDocFunctionExtractor { throw new RuntimeException("changing function '" + funcName + "' definition without foot-note"); } - _allFunctionsByIndex.remove(Integer.valueOf(fdPrev.getIndex())); + _allFunctionsByIndex.remove(fdPrev.getIndex()); } } @@ -326,7 +328,7 @@ public final class ExcelFileFormatDocFunctionExtractor { processTableRow(cellData, noteFlags); } else if(matchesRelPath(TABLE_CELL_RELPATH_NAMES)) { _rowData.add(_textNodeBuffer.toString().trim()); - _rowNoteFlags.add(Boolean.valueOf(_cellHasNote)); + _rowNoteFlags.add(_cellHasNote); _textNodeBuffer.setLength(0); } } @@ -350,7 +352,7 @@ public final class ExcelFileFormatDocFunctionExtractor { } int funcIx = parseInt(funcIxStr); - boolean hasFootnote = noteFlags[i + 1].booleanValue(); + boolean hasFootnote = noteFlags[i + 1]; String funcName = cellData[i + 1]; int minParams = parseInt(cellData[i + 2]); int maxParams = parseInt(cellData[i + 3]); @@ -577,8 +579,8 @@ public final class ExcelFileFormatDocFunctionExtractor { private static File downloadSourceFile() { URL url; try { - url = new URL("http://sc.openoffice.org/" + SOURCE_DOC_FILE_NAME); - } catch (MalformedURLException e) { + url = new URI("http://sc.openoffice.org/" + SOURCE_DOC_FILE_NAME).toURL(); + } catch (MalformedURLException | URISyntaxException e) { throw new RuntimeException(e); } diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestNumericFunction.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestNumericFunction.java index 632c53ad39..6343cb1a06 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestNumericFunction.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestNumericFunction.java @@ -69,7 +69,7 @@ final class TestNumericFunction { void testDOLLAR() { Locale defaultLocale = LocaleUtil.getUserLocale(); try { - LocaleUtil.setUserLocale(new Locale("en", "US")); + LocaleUtil.setUserLocale(new Locale.Builder().setLanguage("en").setRegion("US").build()); HSSFWorkbook wb = new HSSFWorkbook(); HSSFCell cell = wb.createSheet().createRow(0).createCell(0); HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); @@ -89,7 +89,7 @@ final class TestNumericFunction { void testDOLLARIreland() { Locale defaultLocale = LocaleUtil.getUserLocale(); try { - LocaleUtil.setUserLocale(new Locale("en", "IE")); + LocaleUtil.setUserLocale(new Locale.Builder().setLanguage("en").setRegion("IE").build()); HSSFWorkbook wb = new HSSFWorkbook(); HSSFCell cell = wb.createSheet().createRow(0).createCell(0); HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); @@ -104,7 +104,7 @@ final class TestNumericFunction { void testDOLLARSpain() { Locale defaultLocale = LocaleUtil.getUserLocale(); try { - LocaleUtil.setUserLocale(new Locale("es", "ES")); + LocaleUtil.setUserLocale(new Locale.Builder().setLanguage("es").setRegion("ES").build()); HSSFWorkbook wb = new HSSFWorkbook(); HSSFCell cell = wb.createSheet().createRow(0).createCell(0); HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); @@ -119,7 +119,7 @@ final class TestNumericFunction { void testDOLLARJapan() { Locale defaultLocale = LocaleUtil.getUserLocale(); try { - LocaleUtil.setUserLocale(new Locale("ja", "JP")); + LocaleUtil.setUserLocale(new Locale.Builder().setLanguage("ja").setRegion("JP").build()); HSSFWorkbook wb = new HSSFWorkbook(); HSSFCell cell = wb.createSheet().createRow(0).createCell(0); HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); @@ -135,7 +135,7 @@ final class TestNumericFunction { void testDOLLARDenmark() { Locale defaultLocale = LocaleUtil.getUserLocale(); try { - LocaleUtil.setUserLocale(new Locale("da", "DK")); + LocaleUtil.setUserLocale(new Locale.Builder().setLanguage("da").setRegion("DK").build()); HSSFWorkbook wb = new HSSFWorkbook(); HSSFCell cell = wb.createSheet().createRow(0).createCell(0); HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestSheet.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestSheet.java new file mode 100644 index 0000000000..894db09814 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestSheet.java @@ -0,0 +1,89 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FormulaError; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class TestSheet { + + private static final OperationEvaluationContext ec = new OperationEvaluationContext(null, null, 2, 0, 2, null); + + @Test + void testSheetFunctionWithRealWorkbook() throws IOException { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + // Add three sheets: Sheet1, Sheet2, Sheet3 + HSSFSheet sheet1 = wb.createSheet("Sheet1"); + HSSFSheet sheet2 = wb.createSheet("Sheet2"); + HSSFSheet sheet3 = wb.createSheet("Sheet3"); + + // Add data + sheet1.createRow(0).createCell(0).setCellValue(123); // A1 in Sheet1 + sheet2.createRow(1).createCell(0).setCellValue(456); // A2 in Sheet2 + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + + // Define formulas and expected results + String[] formulas = { + "SHEET()", + "SHEET(A1)", + "SHEET(A1:B5)", + "SHEET(Sheet2!A2)", + "SHEET(\"Sheet3\")", + "SHEET(\"invalid\")" + }; + + Object[] expected = { + 1.0, // current sheet + 1.0, // A1 in same sheet + 1.0, // A1:B5 in same sheet + 2.0, // Sheet2!A2 + 3.0, // Sheet3 + FormulaError.NA.getCode() // unknown sheet → #N/A + }; + + // Write formulas to separate cells and evaluate + HSSFRow formulaRow = sheet1.createRow(1); + for (int i = 0; i < formulas.length; i++) { + String formula = formulas[i]; + HSSFCell cell = formulaRow.createCell(i); + cell.setCellFormula(formula); + CellType resultType = fe.evaluateFormulaCell(cell); + + if (expected[i] instanceof Double) { + assertEquals(CellType.NUMERIC, resultType, + "Unexpected cell type for formula: " + formula); + assertEquals((Double) expected[i], cell.getNumericCellValue(), + "Unexpected numeric result for formula: " + formula); + } else if (expected[i] instanceof Byte) { + assertEquals(CellType.ERROR, resultType, + "Unexpected cell type for formula: " + formula); + assertEquals((byte) expected[i], cell.getErrorCellValue(), + "Unexpected error code for formula: " + formula); + } + } + } + } +} diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestCell.java b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestCell.java index de2c4ad7c6..e42a662813 100644 --- a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestCell.java +++ b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestCell.java @@ -365,6 +365,9 @@ public abstract class BaseTestCell { dateStyle.setDataFormat(formatId); r.getCell(7).setCellStyle(dateStyle); + // null rich text + r.createCell(8).setCellValue(factory.createRichTextString(null)); // blank + assertEquals("FALSE", r.getCell(0).toString(), "Boolean"); assertEquals("TRUE", r.getCell(1).toString(), "Boolean"); assertEquals("1.5", r.getCell(2).toString(), "Numeric"); @@ -375,6 +378,7 @@ public abstract class BaseTestCell { // toString on a date-formatted cell displays dates as dd-MMM-yyyy, which has locale problems with the month String dateCell1 = r.getCell(7).toString(); assertEquals("2/2/10 0:00", dateCell1); + assertEquals("", r.getCell(8).toString(), "Blank"); //Write out the file, read it in, and then check cell values try (Workbook wb2 = _testDataProvider.writeOutAndReadBack(wb1)) { @@ -388,6 +392,7 @@ public abstract class BaseTestCell { assertEquals("", r.getCell(6).toString(), "Blank"); String dateCell2 = r.getCell(7).toString(); assertEquals(dateCell1, dateCell2, "Date"); + assertEquals("", r.getCell(8).toString(), "Blank"); } } } @@ -1468,6 +1473,36 @@ public abstract class BaseTestCell { verify(cell).setBlank(); } + @Test + protected void setCellNullString() throws IOException { + try (Workbook wb = _testDataProvider.createWorkbook()) { + Cell cell = getInstance(wb); + + cell.setCellValue((String)null); + + // setting string "null" leads to a BLANK cell + assertEquals(CellType.BLANK, cell.getCellType()); + assertEquals("", cell.getStringCellValue()); + assertEquals("", cell.toString()); + + cell.setCellType(CellType.STRING); + + // forcing to string type leads to STRING cell, but still empty strings + assertEquals(CellType.STRING, cell.getCellType()); + assertEquals("", cell.getStringCellValue()); + assertEquals("", cell.toString()); + + try (Workbook wb2 = _testDataProvider.writeOutAndReadBack(wb)) { + // read first sheet, first row, first cell + Cell cellBack = wb2.iterator().next().iterator().next().iterator().next(); + + assertEquals(CellType.STRING, cell.getCellType()); + assertEquals("", cell.getStringCellValue()); + assertEquals("", cell.toString()); + } + } + } + private Cell getInstance(Workbook wb) { return wb.createSheet().createRow(0).createCell(0); } diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java index 29f62da235..64f585608a 100644 --- a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java +++ b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java @@ -531,7 +531,7 @@ public abstract class BaseTestConditionalFormatting { // Sanity check data assertEquals("Values", s.getRow(0).getCell(0).toString()); - assertEquals("10", s.getRow(2).getCell(0).toString()); + assertEquals(10.0, s.getRow(2).getCell(0).getNumericCellValue()); // Check we found all the conditional formatting rules we should have SheetConditionalFormatting sheetCF = s.getSheetConditionalFormatting(); diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/TestDataFormatter.java b/poi/src/test/java/org/apache/poi/ss/usermodel/TestDataFormatter.java index 0d49bee3ab..b1fced30a5 100644 --- a/poi/src/test/java/org/apache/poi/ss/usermodel/TestDataFormatter.java +++ b/poi/src/test/java/org/apache/poi/ss/usermodel/TestDataFormatter.java @@ -937,7 +937,7 @@ class TestDataFormatter { // 2017-12-01 09:54:33 which is 42747.412892397523 as double DataFormatter dfDE = new DataFormatter(Locale.GERMANY); DataFormatter dfZH = new DataFormatter(Locale.PRC); - DataFormatter dfIE = new DataFormatter(new Locale("GA", "IE")); + DataFormatter dfIE = new DataFormatter(new Locale.Builder().setLanguage("GA").setRegion("IE").build()); double date = 42747.412892397523; String format = "dd MMMM yyyy HH:mm:ss"; assertEquals("12 Januar 2017 09:54:33", dfDE.formatRawCellContents(date, -1, format)); diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/TestExcelStyleDateFormatter.java b/poi/src/test/java/org/apache/poi/ss/usermodel/TestExcelStyleDateFormatter.java index b3ccd06a4c..f905470f88 100644 --- a/poi/src/test/java/org/apache/poi/ss/usermodel/TestExcelStyleDateFormatter.java +++ b/poi/src/test/java/org/apache/poi/ss/usermodel/TestExcelStyleDateFormatter.java @@ -58,11 +58,12 @@ class TestExcelStyleDateFormatter { formatter.setDateFormatSymbols(DateFormatSymbols.getInstance(locale)); String result = formatter.format(d, sb, fp).toString(); - String msg = "Failed testDates for locale " + locale + ", provider: " + provider + - " and date " + d + ", having: " + result; int actIdx = localeIndex(locale); + String msg = "Failed testDates for locale " + locale + ", provider: " + provider + + " and date " + d + ", having actIdx: " + actIdx + " and result '" + result + "' (" + result.length() + ")"; + assertNotNull(result, msg); assertTrue(result.length() > actIdx, msg); assertEquals(expected.charAt(month), result.charAt(actIdx), msg); @@ -102,15 +103,15 @@ class TestExcelStyleDateFormatter { public static Stream<Arguments> initializeLocales() throws ParseException { Object[][] locExps = { { Locale.GERMAN, "JFMAMJJASOND" }, - { new Locale("de", "AT"), "JFMAMJJASOND" }, + { new Locale.Builder().setLanguage("de").setRegion("AT").build(), "JFMAMJJASOND" }, { Locale.UK, "JFMAMJJASOND" }, - { new Locale("en", "IN"), "JFMAMJJASOND" }, - { new Locale("in", "ID"), "JFMAMJJASOND" }, + { new Locale.Builder().setLanguage("en").setRegion("IN").build(), "JFMAMJJASOND" }, + { new Locale.Builder().setLanguage("in").setRegion("ID").build(), "JFMAMJJASOND" }, { Locale.FRENCH, "jfmamjjasond" }, - { new Locale("ru", "RU"), "\u044f\u0444\u043c\u0430\u043c\u0438\u0438\u0430\u0441\u043e\u043d\u0434" }, + { new Locale.Builder().setLanguage("ru").setRegion("RU").build(), "\u044f\u0444\u043c\u0430\u043c\u0438\u0438\u0430\u0441\u043e\u043d\u0434" }, { Locale.CHINESE, localeIndex(Locale.CHINESE) == 0 ? "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u5341\u5341" : "123456789111" }, - { new Locale("tr", "TR"), "\u004f\u015e\u004d\u004e\u004d\u0048\u0054\u0041\u0045\u0045\u004b\u0041" }, - { new Locale("hu", "HU"), "\u006a\u0066\u006d\u00e1\u006d\u006a\u006a\u0061\u0073\u006f\u006e\u0064" } + { new Locale.Builder().setLanguage("tr").setRegion("TR").build(), "\u004f\u015e\u004d\u004e\u004d\u0048\u0054\u0041\u0045\u0045\u004b\u0041" }, + { new Locale.Builder().setLanguage("hu").setRegion("HU").build(), "\u006a\u0066\u006d\u00e1\u006d\u006a\u006a\u0061\u0073\u006f\u006e\u0064" } }; String[] dates = { diff --git a/poi/src/test/java/org/apache/poi/ss/util/BaseTestCellUtil.java b/poi/src/test/java/org/apache/poi/ss/util/BaseTestCellUtil.java index 9784e080b5..fb2200ba57 100644 --- a/poi/src/test/java/org/apache/poi/ss/util/BaseTestCellUtil.java +++ b/poi/src/test/java/org/apache/poi/ss/util/BaseTestCellUtil.java @@ -22,6 +22,7 @@ import org.apache.poi.ss.usermodel.*; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -339,7 +340,7 @@ public abstract class BaseTestCellUtil { // Add multiple border properties to cell should create a single new style int styCnt1 = wb.getNumCellStyles(); - Map<CellPropertyType, Object> props = new HashMap<>(); + EnumMap<CellPropertyType, Object> props = new EnumMap<>(CellPropertyType.class); props.put(CellPropertyType.BORDER_TOP, BorderStyle.THIN); props.put(CellPropertyType.BORDER_BOTTOM, BorderStyle.THIN); props.put(CellPropertyType.BORDER_LEFT, BorderStyle.THIN); @@ -574,7 +575,7 @@ public abstract class BaseTestCellUtil { protected void setFillForegroundColorBeforeFillBackgroundColorEnumByEnum() throws IOException { try (Workbook wb1 = _testDataProvider.createWorkbook()) { Cell A1 = wb1.createSheet().createRow(0).createCell(0); - Map<CellPropertyType, Object> properties = new HashMap<>(); + EnumMap<CellPropertyType, Object> properties = new EnumMap<>(CellPropertyType.class); properties.put(CellPropertyType.FILL_PATTERN, FillPatternType.BRICKS); properties.put(CellPropertyType.FILL_FOREGROUND_COLOR, IndexedColors.BLUE.index); properties.put(CellPropertyType.FILL_BACKGROUND_COLOR, IndexedColors.RED.index); diff --git a/poi/src/test/java/org/apache/poi/ss/util/TestCellUtil.java b/poi/src/test/java/org/apache/poi/ss/util/TestCellUtil.java new file mode 100644 index 0000000000..b7defbef99 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/util/TestCellUtil.java @@ -0,0 +1,36 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.ss.util; + +import org.apache.poi.ss.usermodel.CellPropertyType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for CellUtil constants + */ +class TestCellUtil { + @Test + void testNamePropertyMap() { + Arrays.stream(CellPropertyType.values()).forEach(cellPropertyType -> + assertTrue(CellUtil.namePropertyMap.containsValue(cellPropertyType), + "missing " + cellPropertyType)); + } +} diff --git a/poi/src/test/java/org/apache/poi/ss/util/TestDateFormatConverter.java b/poi/src/test/java/org/apache/poi/ss/util/TestDateFormatConverter.java index 059426289e..59c8acf695 100644 --- a/poi/src/test/java/org/apache/poi/ss/util/TestDateFormatConverter.java +++ b/poi/src/test/java/org/apache/poi/ss/util/TestDateFormatConverter.java @@ -130,7 +130,7 @@ final class TestDateFormatConverter { void testJDK8EmptyLocale() { // JDK 8 seems to add an empty locale-string to the list returned via DateFormat.getAvailableLocales() // therefore we now cater for this special locale as well - String prefix = getPrefixForLocale(new Locale("")); + String prefix = getPrefixForLocale(new Locale.Builder().build()); assertEquals("", prefix); } diff --git a/poi/src/test/java/org/apache/poi/util/DefaultTempFileCreationStrategyTest.java b/poi/src/test/java/org/apache/poi/util/DefaultTempFileCreationStrategyTest.java index 4e4f6b779e..d60e4da8ac 100644 --- a/poi/src/test/java/org/apache/poi/util/DefaultTempFileCreationStrategyTest.java +++ b/poi/src/test/java/org/apache/poi/util/DefaultTempFileCreationStrategyTest.java @@ -20,11 +20,14 @@ package org.apache.poi.util; import static org.apache.poi.util.DefaultTempFileCreationStrategy.DELETE_FILES_ON_EXIT; import static org.apache.poi.util.DefaultTempFileCreationStrategy.POIFILES; import static org.apache.poi.util.TempFile.JAVA_IO_TMPDIR; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterEach; @@ -59,16 +62,62 @@ class DefaultTempFileCreationStrategyTest { checkGetFile(strategy); } - private static void checkGetFile(DefaultTempFileCreationStrategy strategy) throws IOException { - File file = strategy.createTempFile("POITest", ".tmp"); + @Test + void testProvidedDir() throws IOException { + DefaultTempFileCreationStrategy parentStrategy = new DefaultTempFileCreationStrategy(); + File dir = parentStrategy.createTempDirectory("testProvidedDir"); + assertNotNull(dir, "Failed to create temp directory"); try { - assertTrue(file.getParentFile().exists(), - "Failed for " + file.getParentFile()); + assertTrue(Files.isDirectory(dir.toPath()), "File is not a directory: " + dir); + DefaultTempFileCreationStrategy testStrategy = new DefaultTempFileCreationStrategy(dir); + checkGetFileAndPath(testStrategy, dir.toPath()); + } finally { + // Clean up the directory after the test + FileUtils.deleteDirectory(dir); + } + } - assertTrue(file.exists(), - "Failed for " + file); + @Test + void testProvidedDirThreadLocal() throws IOException { + DefaultTempFileCreationStrategy parentStrategy = new DefaultTempFileCreationStrategy(); + File dir = parentStrategy.createTempDirectory("testProvidedDir"); + assertNotNull(dir, "Failed to create temp directory"); + try { + assertTrue(Files.isDirectory(dir.toPath()), "File is not a directory: " + dir); + DefaultTempFileCreationStrategy testStrategy = new DefaultTempFileCreationStrategy(dir); + TempFile.setThreadLocalTempFileCreationStrategy(testStrategy); + checkGetFileAndPath(dir.toPath()); } finally { - assertTrue(file.delete()); + // Clean up the directory after the test + FileUtils.deleteDirectory(dir); + TempFile.setThreadLocalTempFileCreationStrategy(null); + } + } + + @Test + void testProvidedDirNotExists() throws IOException { + DefaultTempFileCreationStrategy parentStrategy = new DefaultTempFileCreationStrategy(); + File dir = parentStrategy.createTempDirectory("testProvidedDir"); + assertNotNull(dir, "Failed to create temp directory"); + assertTrue(dir.delete(), "directory not deleted: " + dir); + try { + DefaultTempFileCreationStrategy testStrategy = new DefaultTempFileCreationStrategy(dir); + checkGetFileAndPath(testStrategy, dir.toPath()); + } finally { + // Clean up the directory after the test + FileUtils.deleteDirectory(dir); + } + } + + @Test + void testProvidedDirIsActuallyAPlainFile() throws IOException { + DefaultTempFileCreationStrategy parentStrategy = new DefaultTempFileCreationStrategy(); + File dir = parentStrategy.createTempFile("test123", ".tmp"); + assertNotNull(dir, "Failed to create temp file"); + try { + assertThrows(IllegalArgumentException.class, () -> new DefaultTempFileCreationStrategy(dir)); + } finally { + dir.delete(); } } @@ -106,10 +155,11 @@ class DefaultTempFileCreationStrategyTest { } @Test - void testCustomDir() throws IOException { + void testCustomDirExists() throws IOException { File dirTest = File.createTempFile("POITest", ".dir"); try { assertTrue(dirTest.delete()); + assertTrue(dirTest.mkdir()); DefaultTempFileCreationStrategy strategy = new DefaultTempFileCreationStrategy(dirTest); checkGetFile(strategy); @@ -119,11 +169,12 @@ class DefaultTempFileCreationStrategyTest { } @Test - void testCustomDirExists() throws IOException { + void testCustomDirAndPoiFilesExists() throws IOException { File dirTest = File.createTempFile("POITest", ".dir"); try { assertTrue(dirTest.delete()); assertTrue(dirTest.mkdir()); + assertTrue(new File(dirTest, POIFILES).mkdir()); DefaultTempFileCreationStrategy strategy = new DefaultTempFileCreationStrategy(dirTest); checkGetFile(strategy); @@ -132,18 +183,35 @@ class DefaultTempFileCreationStrategyTest { } } - @Test - void testCustomDirAndPoiFilesExists() throws IOException { - File dirTest = File.createTempFile("POITest", ".dir"); + private static void checkGetFile(DefaultTempFileCreationStrategy strategy) throws IOException { + checkGetFileAndPath(strategy, null); + } + + private static void checkGetFileAndPath(DefaultTempFileCreationStrategy strategy, + Path path) throws IOException { + File file = strategy.createTempFile("POITest", ".tmp"); + testFileAndPath(file, path); + } + + private static void checkGetFileAndPath(Path path) throws IOException { + File file = TempFile.createTempFile("POITest", ".tmp"); + testFileAndPath(file, path); + } + + private static void testFileAndPath(File file, Path path) throws IOException { try { - assertTrue(dirTest.delete()); - assertTrue(dirTest.mkdir()); - assertTrue(new File(dirTest, POIFILES).mkdir()); + if (path != null) { + assertTrue(file.toPath().startsWith(path), + "File path does not start with expected path: " + path); + } - DefaultTempFileCreationStrategy strategy = new DefaultTempFileCreationStrategy(dirTest); - checkGetFile(strategy); + assertTrue(file.getParentFile().exists(), + "Failed for " + file.getParentFile()); + + assertTrue(file.exists(), + "Failed for " + file); } finally { - FileUtils.deleteDirectory(dirTest); + assertTrue(file.delete()); } } -}
\ No newline at end of file +} diff --git a/poi/src/test/java/org/apache/poi/util/ExceptionUtilTest.java b/poi/src/test/java/org/apache/poi/util/ExceptionUtilTest.java index d5d4fdd77a..d78e679789 100644 --- a/poi/src/test/java/org/apache/poi/util/ExceptionUtilTest.java +++ b/poi/src/test/java/org/apache/poi/util/ExceptionUtilTest.java @@ -42,7 +42,7 @@ class ExceptionUtilTest { assertTrue(ExceptionUtil.isFatal(new VirtualMachineError(){})); } - + @SuppressForbidden("Test with ThreadDeath on purpose here") @Test void testThreadDeath() { assertTrue(ExceptionUtil.isFatal(new ThreadDeath())); diff --git a/poi/src/test/java/org/apache/poi/util/TestIOUtils.java b/poi/src/test/java/org/apache/poi/util/TestIOUtils.java index bb12f9932e..7f026adc74 100644 --- a/poi/src/test/java/org/apache/poi/util/TestIOUtils.java +++ b/poi/src/test/java/org/apache/poi/util/TestIOUtils.java @@ -110,8 +110,25 @@ final class TestIOUtils { } @Test - void testToByteArrayNegativeLength() { - assertThrows(RecordFormatException.class, () -> IOUtils.toByteArray(data123(), -1)); + void testToByteArrayNegativeLength() throws IOException { + final byte[] array = new byte[]{1, 2, 3, 4, 5, 6, 7}; + IOUtils.setByteArrayMaxOverride(30 * 1024 * 1024); + try (ByteArrayInputStream is = new ByteArrayInputStream(array)) { + assertArrayEquals(array, IOUtils.toByteArray(is, -1, 100)); + } finally { + IOUtils.setByteArrayMaxOverride(-1); + } + } + + @Test + void testToByteArrayNegativeLength2() throws IOException { + final byte[] array = new byte[]{1, 2, 3, 4, 5, 6, 7}; + IOUtils.setByteArrayMaxOverride(30 * 1024 * 1024); + try (ByteArrayInputStream is = new ByteArrayInputStream(array)) { + assertArrayEquals(array, IOUtils.toByteArray(is, -1)); + } finally { + IOUtils.setByteArrayMaxOverride(-1); + } } @Test diff --git a/poi/src/test/java/org/apache/poi/util/TestThreadLocalTempFile.java b/poi/src/test/java/org/apache/poi/util/TestThreadLocalTempFile.java new file mode 100644 index 0000000000..86161e59ac --- /dev/null +++ b/poi/src/test/java/org/apache/poi/util/TestThreadLocalTempFile.java @@ -0,0 +1,68 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.io.TempDir; + +class TestThreadLocalTempFile { + @AfterEach + void tearDown() { + TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy()); + } + + @RepeatedTest(2) // Repeat it to ensure testing the case + void testThreadLocalStrategy(@TempDir Path tmpDir) { + Path rootDir = tmpDir.toAbsolutePath().normalize(); + Path globalTmpDir = rootDir.resolve("global-tmp-dir"); + Path localTmpDir1 = rootDir.resolve("local-tmp-dir1"); + Path localTmpDir2 = rootDir.resolve("local-tmp-dir2"); + + TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(globalTmpDir.toFile())); + assertTempFileIn(globalTmpDir); + + String result = TempFile.withStrategy(new DefaultTempFileCreationStrategy(localTmpDir1.toFile()), () -> { + assertTempFileIn(localTmpDir1); + String nestedResult = TempFile.withStrategy(new DefaultTempFileCreationStrategy(localTmpDir2.toFile()), () -> { + assertTempFileIn(localTmpDir2); + return "nested-test-result"; + }); + assertTempFileIn(localTmpDir1); + return "my-test-result-" + nestedResult; + }); + assertTempFileIn(globalTmpDir); + assertEquals("my-test-result-nested-test-result", result); + } + + private static void assertTempFileIn(Path expectedDir) { + Path tempFile; + try { + tempFile = TempFile.createTempFile("tmp-prefix", ".tmp").toPath(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + assertTrue(tempFile.startsWith(expectedDir), tempFile.toString()); + } +} |