From ab36704d3e9ccb3f289c7f4126773fc7ff3ee3d5 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sun, 24 Jul 2016 11:07:06 +0000 Subject: [PATCH 1/9] Create branch to unify crypto handling in HSSF and support CryptoAPI for HSSF git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1753906 13f79535-47bb-0310-9956-ffa450edef68 From c4ac2e77586574d9e799b54277cea325465e5b60 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Wed, 3 Aug 2016 23:54:01 +0000 Subject: [PATCH 2/9] Preparations for hssf_cryptoapi: - Add cloneable - Change existing hslf cryptoapi to streaming git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755127 13f79535-47bb-0310-9956-ffa450edef68 --- src/java/org/apache/poi/POIDocument.java | 20 +- .../poi/hssf/record/RecordInputStream.java | 36 +- .../poifs/crypt/ChunkedCipherInputStream.java | 125 ++++-- .../crypt/ChunkedCipherOutputStream.java | 163 +++++--- .../org/apache/poi/poifs/crypt/Decryptor.java | 74 +++- .../poi/poifs/crypt/EncryptionHeader.java | 10 +- .../poi/poifs/crypt/EncryptionInfo.java | 56 ++- .../poifs/crypt/EncryptionInfoBuilder.java | 20 - .../poi/poifs/crypt/EncryptionVerifier.java | 13 +- .../org/apache/poi/poifs/crypt/Encryptor.java | 20 +- .../crypt/binaryrc4/BinaryRC4Decryptor.java | 66 +++- .../binaryrc4/BinaryRC4EncryptionHeader.java | 11 +- .../BinaryRC4EncryptionInfoBuilder.java | 54 +-- .../BinaryRC4EncryptionVerifier.java | 10 +- .../crypt/binaryrc4/BinaryRC4Encryptor.java | 141 +++---- .../crypt/cryptoapi/CryptoAPIDecryptor.java | 140 +++---- .../CryptoAPIDocumentInputStream.java | 86 ++++ .../CryptoAPIDocumentOutputStream.java | 74 ++++ .../cryptoapi/CryptoAPIEncryptionHeader.java | 8 +- .../CryptoAPIEncryptionInfoBuilder.java | 67 ++-- .../CryptoAPIEncryptionVerifier.java | 10 +- .../crypt/cryptoapi/CryptoAPIEncryptor.java | 125 +++--- .../crypt/standard/StandardDecryptor.java | 24 +- .../standard/StandardEncryptionHeader.java | 29 +- .../StandardEncryptionInfoBuilder.java | 53 +-- .../standard/StandardEncryptionVerifier.java | 11 +- .../crypt/standard/StandardEncryptor.java | 30 +- .../LittleEndianByteArrayInputStream.java | 126 +++--- .../LittleEndianByteArrayOutputStream.java | 45 ++- .../poi/poifs/crypt/agile/AgileDecryptor.java | 64 +-- .../crypt/agile/AgileEncryptionHeader.java | 12 +- .../agile/AgileEncryptionInfoBuilder.java | 62 +-- .../crypt/agile/AgileEncryptionVerifier.java | 21 +- .../poi/poifs/crypt/agile/AgileEncryptor.java | 50 ++- .../crypt/TestAgileEncryptionParameters.java | 7 +- .../usermodel/HSLFSlideShowEncrypted.java | 374 +++++++++++------- .../poi/hslf/usermodel/HSLFSlideShowImpl.java | 32 +- .../hslf/record/TestDocumentEncryption.java | 4 +- 38 files changed, 1417 insertions(+), 856 deletions(-) create mode 100644 src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentInputStream.java create mode 100644 src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentOutputStream.java diff --git a/src/java/org/apache/poi/POIDocument.java b/src/java/org/apache/poi/POIDocument.java index 87c6a14f6..d742c5572 100644 --- a/src/java/org/apache/poi/POIDocument.java +++ b/src/java/org/apache/poi/POIDocument.java @@ -32,6 +32,7 @@ import org.apache.poi.hpsf.PropertySet; import org.apache.poi.hpsf.PropertySetFactory; import org.apache.poi.hpsf.SummaryInformation; import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; @@ -59,6 +60,8 @@ public abstract class POIDocument implements Closeable { /* Have the property streams been read yet? (Only done on-demand) */ private boolean initialized = false; + + private static final String[] encryptedStreamNames = { "EncryptedSummary" }; /** * Constructs a POIDocument with the given directory node. @@ -195,13 +198,18 @@ public abstract class POIDocument implements Closeable { try { if (encryptionInfo != null) { step = "getting encrypted"; - InputStream is = encryptionInfo.getDecryptor().getDataStream(directory); - try { - encPoifs = new NPOIFSFileSystem(is); - dirNode = encPoifs.getRoot(); - } finally { - is.close(); + String encryptedStream = null; + for (String s : encryptedStreamNames) { + if (dirNode.hasEntry(s)) { + encryptedStream = s; + } } + if (encryptedStream == null) { + throw new EncryptedDocumentException("can't find matching encrypted property stream"); + } + CryptoAPIDecryptor dec = (CryptoAPIDecryptor)encryptionInfo.getDecryptor(); + encPoifs = dec.getSummaryEntries(dirNode, encryptedStream); + dirNode = encPoifs.getRoot(); } //directory can be null when creating new documents diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index f9212c798..8991eb770 100644 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -18,13 +18,14 @@ package org.apache.poi.hssf.record; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.util.Locale; import org.apache.poi.hssf.dev.BiffViewer; import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; -import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; @@ -91,6 +92,10 @@ public final class RecordInputStream implements LittleEndianInput { * index within the data section of the current BIFF record */ private int _currentDataOffset; + /** + * index within the data section when mark() was called + */ + private int _markedDataOffset; private static final class SimpleHeaderInput implements BiffHeaderInput { @@ -123,8 +128,8 @@ public final class RecordInputStream implements LittleEndianInput { _bhi = new SimpleHeaderInput(in); } else { Biff8DecryptingStream bds = new Biff8DecryptingStream(in, initialOffset, key); + _dataInput = bds; _bhi = bds; - _dataInput = bds; } _nextSid = readNextSid(); } @@ -491,4 +496,31 @@ public final class RecordInputStream implements LittleEndianInput { public int getNextSid() { return _nextSid; } + + /** + * Mark the stream position - experimental function + * + * @param readlimit the read ahead limit + * + * @see InputStream#mark(int) + */ + @Internal + public void mark(int readlimit) { + ((InputStream)_dataInput).mark(readlimit); + _markedDataOffset = _currentDataOffset; + } + + /** + * Resets the stream position to the previously marked position. + * Experimental function - this only works, when nextRecord() wasn't called in the meantime. + * + * @throws IOException if marking is not supported + * + * @see InputStream#reset() + */ + @Internal + public void reset() throws IOException { + ((InputStream)_dataInput).reset(); + _currentDataOffset = _markedDataOffset; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java index 7d695a1eb..255494d6a 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java @@ -16,6 +16,7 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -29,54 +30,81 @@ import org.apache.poi.util.LittleEndianInputStream; @Internal public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { - private final int chunkSize; - private final int chunkMask; - private final int chunkBits; + private final int _chunkSize; + private final int _chunkBits; - private int _lastIndex = 0; - private long _pos = 0; - private long _size; - private byte[] _chunk; - private Cipher _cipher; + private final long _size; + private final byte[] _chunk; + private final Cipher _cipher; + + private int _lastIndex; + private long _pos; + private boolean _chunkIsValid = false; public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize) - throws GeneralSecurityException { + throws GeneralSecurityException { + this(stream, size, chunkSize, 0); + } + + public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize, int initialPos) + throws GeneralSecurityException { super((InputStream)stream); _size = size; - this.chunkSize = chunkSize; - chunkMask = chunkSize-1; - chunkBits = Integer.bitCount(chunkMask); + _pos = initialPos; + this._chunkSize = chunkSize; + if (chunkSize == -1) { + _chunk = new byte[4096]; + } else { + _chunk = new byte[chunkSize]; + } + _chunkBits = Integer.bitCount(_chunk.length-1); + _lastIndex = (int)(_pos >> _chunkBits); + _cipher = initCipherForBlock(null, _lastIndex); + } + + public final Cipher initCipherForBlock(int block) throws IOException, GeneralSecurityException { + if (_chunkSize != -1) { + throw new GeneralSecurityException("the cipher block can only be set for streaming encryption, e.g. CryptoAPI..."); + } - _cipher = initCipherForBlock(null, 0); + _chunkIsValid = false; + return initCipherForBlock(_cipher, block); } protected abstract Cipher initCipherForBlock(Cipher existing, int block) throws GeneralSecurityException; + @Override public int read() throws IOException { byte[] b = new byte[1]; - if (read(b) == 1) + if (read(b) == 1) { return b[0]; + } return -1; } // do not implement! -> recursion // public int read(byte[] b) throws IOException; + @Override public int read(byte[] b, int off, int len) throws IOException { int total = 0; - if (available() <= 0) return -1; + if (available() <= 0) { + return -1; + } + final int chunkMask = getChunkMask(); while (len > 0) { - if (_chunk == null) { + if (!_chunkIsValid) { try { - _chunk = nextChunk(); + nextChunk(); + _chunkIsValid = true; } catch (GeneralSecurityException e) { throw new EncryptedDocumentException(e.getMessage(), e); } } - int count = (int)(chunkSize - (_pos & chunkMask)); + int count = (int)(_chunk.length - (_pos & chunkMask)); int avail = available(); if (avail == 0) { return total; @@ -86,8 +114,9 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { off += count; len -= count; _pos += count; - if ((_pos & chunkMask) == 0) - _chunk = null; + if ((_pos & chunkMask) == 0) { + _chunkIsValid = false; + } total += count; } @@ -95,18 +124,28 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { } @Override - public long skip(long n) throws IOException { + public long skip(final long n) throws IOException { long start = _pos; - long skip = Math.min(available(), n); + long skip = Math.min(remainingBytes(), n); - if ((((_pos + skip) ^ start) & ~chunkMask) != 0) - _chunk = null; + if ((((_pos + skip) ^ start) & ~getChunkMask()) != 0) { + _chunkIsValid = false; + } _pos += skip; return skip; } @Override public int available() { + return remainingBytes(); + } + + /** + * Helper method for forbidden available call - we know the size beforehand, so it's ok ... + * + * @return the remaining byte until EOF + */ + private int remainingBytes() { return (int)(_size - _pos); } @@ -125,17 +164,37 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { throw new UnsupportedOperationException(); } - private byte[] nextChunk() throws GeneralSecurityException, IOException { - int index = (int)(_pos >> chunkBits); - initCipherForBlock(_cipher, index); + private int getChunkMask() { + return _chunk.length-1; + } + + private void nextChunk() throws GeneralSecurityException, IOException { + if (_chunkSize != -1) { + int index = (int)(_pos >> _chunkBits); + initCipherForBlock(_cipher, index); - if (_lastIndex != index) { - super.skip((index - _lastIndex) << chunkBits); + if (_lastIndex != index) { + super.skip((index - _lastIndex) << _chunkBits); + } + + _lastIndex = index + 1; } - byte[] block = new byte[Math.min(super.available(), chunkSize)]; - super.read(block, 0, block.length); - _lastIndex = index + 1; - return _cipher.doFinal(block); + final int todo = (int)Math.min(_size, _chunk.length); + int readBytes = 0, totalBytes = 0; + do { + readBytes = super.read(_chunk, totalBytes, todo-totalBytes); + totalBytes += Math.max(0, readBytes); + } while (readBytes != -1 && totalBytes < todo); + + if (readBytes == -1 && _pos+totalBytes < _size) { + throw new EOFException("buffer underrun"); + } + + if (_chunkSize == -1) { + _cipher.update(_chunk, 0, totalBytes, _chunk); + } else { + _cipher.doFinal(_chunk, 0, totalBytes, _chunk); + } } } diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java index f0b14bffc..573cbdeb6 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java @@ -32,6 +32,7 @@ import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.POIFSWriterEvent; import org.apache.poi.poifs.filesystem.POIFSWriterListener; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianConsts; @@ -41,137 +42,177 @@ import org.apache.poi.util.TempFile; @Internal public abstract class ChunkedCipherOutputStream extends FilterOutputStream { - private static final POILogger logger = POILogFactory.getLogger(ChunkedCipherOutputStream.class); - - protected final int chunkSize; - protected final int chunkMask; - protected final int chunkBits; - + private static final POILogger LOG = POILogFactory.getLogger(ChunkedCipherOutputStream.class); + private static final int STREAMING = -1; + + protected final int _chunkSize; + protected final int _chunkBits; + private final byte[] _chunk; - private final File fileOut; - private final DirectoryNode dir; + private final File _fileOut; + private final DirectoryNode _dir; private long _pos = 0; private Cipher _cipher; - + public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException { super(null); - this.chunkSize = chunkSize; - chunkMask = chunkSize-1; - chunkBits = Integer.bitCount(chunkMask); - _chunk = new byte[chunkSize]; - - fileOut = TempFile.createTempFile("encrypted_package", "crypt"); - fileOut.deleteOnExit(); - this.out = new FileOutputStream(fileOut); - this.dir = dir; + this._chunkSize = chunkSize; + int cs = chunkSize == STREAMING ? 4096 : chunkSize; + _chunk = new byte[cs]; + _chunkBits = Integer.bitCount(cs-1); + _fileOut = TempFile.createTempFile("encrypted_package", "crypt"); + _fileOut.deleteOnExit(); + this.out = new FileOutputStream(_fileOut); + this._dir = dir; _cipher = initCipherForBlock(null, 0, false); } + public ChunkedCipherOutputStream(OutputStream stream, int chunkSize) throws IOException, GeneralSecurityException { + super(stream); + this._chunkSize = chunkSize; + int cs = chunkSize == STREAMING ? 4096 : chunkSize; + _chunk = new byte[cs]; + _chunkBits = Integer.bitCount(cs-1); + _fileOut = null; + _dir = null; + _cipher = initCipherForBlock(null, 0, false); + } + + public final Cipher initCipherForBlock(int block, boolean lastChunk) throws IOException, GeneralSecurityException { + return initCipherForBlock(_cipher, block, lastChunk); + } + protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk) - throws GeneralSecurityException; - - @SuppressWarnings("hiding") + throws IOException, GeneralSecurityException; + protected abstract void calculateChecksum(File fileOut, int oleStreamSize) throws GeneralSecurityException, IOException; - - @SuppressWarnings("hiding") + protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) throws IOException, GeneralSecurityException; + @Override public void write(int b) throws IOException { write(new byte[]{(byte)b}); } + @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } + @Override public void write(byte[] b, int off, int len) throws IOException { - if (len == 0) return; - + if (len == 0) { + return; + } + if (len < 0 || b.length < off+len) { throw new IOException("not enough bytes in your input buffer"); } - + + final int chunkMask = getChunkMask(); while (len > 0) { int posInChunk = (int)(_pos & chunkMask); - int nextLen = Math.min(chunkSize-posInChunk, len); + int nextLen = Math.min(_chunk.length-posInChunk, len); System.arraycopy(b, off, _chunk, posInChunk, nextLen); _pos += nextLen; off += nextLen; len -= nextLen; if ((_pos & chunkMask) == 0) { - try { - writeChunk(); - } catch (GeneralSecurityException e) { - throw new IOException(e); - } + writeChunk(len > 0); } } } - protected void writeChunk() throws IOException, GeneralSecurityException { - int posInChunk = (int)(_pos & chunkMask); + private int getChunkMask() { + return _chunk.length-1; + } + + protected void writeChunk(boolean continued) throws IOException { + if (_pos == 0) { + return; + } + + int posInChunk = (int)(_pos & getChunkMask()); + // normally posInChunk is 0, i.e. on the next chunk (-> index-1) // but if called on close(), posInChunk is somewhere within the chunk data - int index = (int)(_pos >> chunkBits); + int index = (int)(_pos >> _chunkBits); boolean lastChunk; if (posInChunk==0) { index--; - posInChunk = chunkSize; + posInChunk = _chunk.length; lastChunk = false; } else { // pad the last chunk lastChunk = true; } - _cipher = initCipherForBlock(_cipher, index, lastChunk); + int ciLen; + try { + if (_chunkSize == STREAMING) { + if (continued) { + ciLen = _cipher.update(_chunk, 0, posInChunk, _chunk); + } else { + ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + } + + // reset stream (not only) in case we were interrupted by plain stream parts + _pos = 0; + } else { + _cipher = initCipherForBlock(_cipher, index, lastChunk); + ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + } + } catch (GeneralSecurityException e) { + throw new IOException("can't re-/initialize cipher", e); + } - int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); out.write(_chunk, 0, ciLen); } - + + @Override public void close() throws IOException { try { - writeChunk(); + writeChunk(false); super.close(); - - int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE); - calculateChecksum(fileOut, (int)_pos); - dir.createDocument(DEFAULT_POIFS_ENTRY, oleStreamSize, new EncryptedPackageWriter()); - createEncryptionInfoEntry(dir, fileOut); + + if (_fileOut != null) { + int oleStreamSize = (int)(_fileOut.length()+LittleEndianConsts.LONG_SIZE); + calculateChecksum(_fileOut, (int)_pos); + _dir.createDocument(DEFAULT_POIFS_ENTRY, oleStreamSize, new EncryptedPackageWriter()); + createEncryptionInfoEntry(_dir, _fileOut); + } } catch (GeneralSecurityException e) { throw new IOException(e); } } private class EncryptedPackageWriter implements POIFSWriterListener { + @Override public void processPOIFSWriterEvent(POIFSWriterEvent event) { try { OutputStream os = event.getStream(); - byte buf[] = new byte[chunkSize]; - - // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data - // encrypted within the EncryptedData field, not including the size of the StreamSize field. - // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this - // value, depending on the block size of the chosen encryption algorithm - LittleEndian.putLong(buf, 0, _pos); - os.write(buf, 0, LittleEndian.LONG_SIZE); - FileInputStream fis = new FileInputStream(fileOut); - int readBytes; - while ((readBytes = fis.read(buf)) != -1) { - os.write(buf, 0, readBytes); - } + // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data + // encrypted within the EncryptedData field, not including the size of the StreamSize field. + // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this + // value, depending on the block size of the chosen encryption algorithm + byte buf[] = new byte[LittleEndianConsts.LONG_SIZE]; + LittleEndian.putLong(buf, 0, _pos); + os.write(buf); + + FileInputStream fis = new FileInputStream(_fileOut); + IOUtils.copy(fis, os); fis.close(); os.close(); - - if (!fileOut.delete()) { - logger.log(POILogger.ERROR, "Can't delete temporary encryption file: "+fileOut); + + if (!_fileOut.delete()) { + LOG.log(POILogger.ERROR, "Can't delete temporary encryption file: "+_fileOut); } } catch (IOException e) { throw new EncryptedDocumentException(e); diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index bec436a88..41621853e 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -20,24 +20,26 @@ import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import javax.crypto.Cipher; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.LittleEndianInput; -public abstract class Decryptor { +public abstract class Decryptor implements Cloneable { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; public static final String DEFAULT_POIFS_ENTRY="EncryptedPackage"; - protected final EncryptionInfoBuilder builder; + protected EncryptionInfo encryptionInfo; private SecretKey secretKey; private byte[] verifier, integrityHmacKey, integrityHmacValue; - protected Decryptor(EncryptionInfoBuilder builder) { - this.builder = builder; + protected Decryptor() { } /** @@ -54,6 +56,45 @@ public abstract class Decryptor { public abstract InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException; + /** + * Wraps a stream for decryption

+ * + * As we are handling streams and don't know the total length beforehand, + * it's the callers duty to care for the length of the entries. + * + * @param stream the stream to be wrapped + * @param initialPos initial/current byte position within the stream + * @return decrypted stream + */ + public InputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + throws IOException, GeneralSecurityException { + throw new RuntimeException("this decryptor doesn't support reading from a stream"); + } + + /** + * Sets the chunk size of the data stream. + * Needs to be set before the data stream is requested. + * When not set, the implementation uses method specific default values + * + * @param chunkSize the chunk size, i.e. the block size with the same encryption key + */ + public void setChunkSize(int chunkSize) { + throw new RuntimeException("this decryptor doesn't support changing the chunk size"); + } + + /** + * Initializes a cipher object for a given block index for encryption + * + * @param cipher may be null, otherwise the given instance is reset to the new block index + * @param block the block index, e.g. the persist/slide id (hslf) + * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher + * @throws GeneralSecurityException + */ + public Cipher initCipherForBlock(Cipher cipher, int block) + throws GeneralSecurityException { + throw new RuntimeException("this decryptor doesn't support initCipherForBlock"); + } + public abstract boolean verifyPassword(String password) throws GeneralSecurityException; @@ -85,9 +126,11 @@ public abstract class Decryptor { public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException { return getDataStream(fs.getRoot()); } + public InputStream getDataStream(OPOIFSFileSystem fs) throws IOException, GeneralSecurityException { return getDataStream(fs.getRoot()); } + public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException { return getDataStream(fs.getRoot()); } @@ -126,10 +169,29 @@ public abstract class Decryptor { } protected int getBlockSizeInBytes() { - return builder.getHeader().getBlockSize(); + return encryptionInfo.getHeader().getBlockSize(); } protected int getKeySizeInBytes() { - return builder.getHeader().getKeySize()/8; + return encryptionInfo.getHeader().getKeySize()/8; + } + + public EncryptionInfo getEncryptionInfo() { + return encryptionInfo; + } + + public void setEncryptionInfo(EncryptionInfo encryptionInfo) { + this.encryptionInfo = encryptionInfo; + } + + @Override + public Decryptor clone() throws CloneNotSupportedException { + Decryptor other = (Decryptor)super.clone(); + other.integrityHmacKey = integrityHmacKey.clone(); + other.integrityHmacValue = integrityHmacValue.clone(); + other.verifier = verifier.clone(); + other.secretKey = new SecretKeySpec(secretKey.getEncoded(), secretKey.getAlgorithm()); + // encryptionInfo is set from outside + return other; } } \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java index b6a0e2d0c..c52e87c62 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java @@ -16,12 +16,11 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; - /** * Reads and processes OOXML Encryption Headers * The constants are largely based on ZIP constants. */ -public abstract class EncryptionHeader { +public abstract class EncryptionHeader implements Cloneable { public static final int ALGORITHM_RC4 = CipherAlgorithm.rc4.ecmaId; public static final int ALGORITHM_AES_128 = CipherAlgorithm.aes128.ecmaId; public static final int ALGORITHM_AES_192 = CipherAlgorithm.aes192.ecmaId; @@ -132,4 +131,11 @@ public abstract class EncryptionHeader { protected void setCspName(String cspName) { this.cspName = cspName; } + + @Override + public EncryptionHeader clone() throws CloneNotSupportedException { + EncryptionHeader other = (EncryptionHeader)super.clone(); + other.keySalt = (keySalt == null) ? null : keySalt.clone(); + return other; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java index 780409e19..34b83cb8c 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -34,15 +34,15 @@ import org.apache.poi.util.LittleEndianInput; /** */ -public class EncryptionInfo { +public class EncryptionInfo implements Cloneable { private final int versionMajor; private final int versionMinor; private final int encryptionFlags; - private final EncryptionHeader header; - private final EncryptionVerifier verifier; - private final Decryptor decryptor; - private final Encryptor encryptor; + private EncryptionHeader header; + private EncryptionVerifier verifier; + private Decryptor decryptor; + private Encryptor encryptor; /** * A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption @@ -96,11 +96,10 @@ public class EncryptionInfo { public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException { final EncryptionMode encryptionMode; - versionMajor = dis.readShort(); - versionMinor = dis.readShort(); + versionMajor = dis.readUShort(); + versionMinor = dis.readUShort(); - if (!isCryptoAPI - && versionMajor == binaryRC4.versionMajor + if ( versionMajor == binaryRC4.versionMajor && versionMinor == binaryRC4.versionMinor) { encryptionMode = binaryRC4; encryptionFlags = -1; @@ -138,10 +137,6 @@ public class EncryptionInfo { } eib.initialize(this, dis); - header = eib.getHeader(); - verifier = eib.getVerifier(); - decryptor = eib.getDecryptor(); - encryptor = eib.getEncryptor(); } /** @@ -187,11 +182,6 @@ public class EncryptionInfo { } eib.initialize(this, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - - header = eib.getHeader(); - verifier = eib.getVerifier(); - decryptor = eib.getDecryptor(); - encryptor = eib.getEncryptor(); } protected static EncryptionInfoBuilder getBuilder(EncryptionMode encryptionMode) @@ -229,4 +219,32 @@ public class EncryptionInfo { public Encryptor getEncryptor() { return encryptor; } -} + + public void setHeader(EncryptionHeader header) { + this.header = header; + } + + public void setVerifier(EncryptionVerifier verifier) { + this.verifier = verifier; + } + + public void setDecryptor(Decryptor decryptor) { + this.decryptor = decryptor; + } + + public void setEncryptor(Encryptor encryptor) { + this.encryptor = encryptor; + } + + @Override + public EncryptionInfo clone() throws CloneNotSupportedException { + EncryptionInfo other = (EncryptionInfo)super.clone(); + other.header = header.clone(); + other.verifier = verifier.clone(); + other.decryptor = decryptor.clone(); + other.decryptor.setEncryptionInfo(other); + other.encryptor = encryptor.clone(); + other.encryptor.setEncryptionInfo(other); + return other; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java index e36d44da9..24371dfdd 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java @@ -30,24 +30,4 @@ public interface EncryptionInfoBuilder { * initialize the builder from scratch */ void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode); - - /** - * @return the header data - */ - EncryptionHeader getHeader(); - - /** - * @return the verifier data - */ - EncryptionVerifier getVerifier(); - - /** - * @return the decryptor - */ - Decryptor getDecryptor(); - - /** - * @return the encryptor - */ - Encryptor getEncryptor(); } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java index 2688b50f4..6e8059261 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java @@ -16,11 +16,10 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; - /** * Used when checking if a key is valid for a document */ -public abstract class EncryptionVerifier { +public abstract class EncryptionVerifier implements Cloneable { private byte[] salt; private byte[] encryptedVerifier; private byte[] encryptedVerifierHash; @@ -105,5 +104,13 @@ public abstract class EncryptionVerifier { this.hashAlgorithm = hashAlgorithm; } - + @Override + public EncryptionVerifier clone() throws CloneNotSupportedException { + EncryptionVerifier other = (EncryptionVerifier)super.clone(); + other.salt = (salt == null) ? null : salt.clone(); + other.encryptedVerifier = (encryptedVerifier == null) ? null : encryptedVerifier.clone(); + other.encryptedVerifierHash = (encryptedVerifierHash == null) ? null : encryptedVerifierHash.clone(); + other.encryptedKey = (encryptedKey == null) ? null : encryptedKey.clone(); + return other; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/Encryptor.java b/src/java/org/apache/poi/poifs/crypt/Encryptor.java index d40f8ae5d..d92fc3097 100644 --- a/src/java/org/apache/poi/poifs/crypt/Encryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Encryptor.java @@ -21,14 +21,16 @@ import java.io.OutputStream; import java.security.GeneralSecurityException; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; -public abstract class Encryptor { +public abstract class Encryptor implements Cloneable { protected static final String DEFAULT_POIFS_ENTRY = Decryptor.DEFAULT_POIFS_ENTRY; + private EncryptionInfo encryptionInfo; private SecretKey secretKey; /** @@ -66,4 +68,20 @@ public abstract class Encryptor { protected void setSecretKey(SecretKey secretKey) { this.secretKey = secretKey; } + + public EncryptionInfo getEncryptionInfo() { + return encryptionInfo; + } + + public void setEncryptionInfo(EncryptionInfo encryptionInfo) { + this.encryptionInfo = encryptionInfo; + } + + @Override + public Encryptor clone() throws CloneNotSupportedException { + Encryptor other = (Encryptor)super.clone(); + other.secretKey = new SecretKeySpec(secretKey.getEncoded(), secretKey.getAlgorithm()); + // encryptionInfo is set from outside + return other; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java index 5fb39d921..1fdf3a982 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java @@ -29,36 +29,45 @@ import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.crypt.*; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.StringUtil; -public class BinaryRC4Decryptor extends Decryptor { +public class BinaryRC4Decryptor extends Decryptor implements Cloneable { private long _length = -1L; + private int _chunkSize = 512; private class BinaryRC4CipherInputStream extends ChunkedCipherInputStream { + @Override protected Cipher initCipherForBlock(Cipher existing, int block) throws GeneralSecurityException { - return BinaryRC4Decryptor.initCipherForBlock(existing, block, builder, getSecretKey(), Cipher.DECRYPT_MODE); + return BinaryRC4Decryptor.this.initCipherForBlock(existing, block); } public BinaryRC4CipherInputStream(DocumentInputStream stream, long size) throws GeneralSecurityException { - super(stream, size, 512); + super(stream, size, _chunkSize); } + + public BinaryRC4CipherInputStream(LittleEndianInput stream) + throws GeneralSecurityException { + super(stream, Integer.MAX_VALUE, _chunkSize); + } } - protected BinaryRC4Decryptor(BinaryRC4EncryptionInfoBuilder builder) { - super(builder); + protected BinaryRC4Decryptor() { } + @Override public boolean verifyPassword(String password) { - EncryptionVerifier ver = builder.getVerifier(); + EncryptionVerifier ver = getEncryptionInfo().getVerifier(); SecretKey skey = generateSecretKey(password, ver); try { - Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE); + Cipher cipher = initCipherForBlock(null, 0, getEncryptionInfo(), skey, Cipher.DECRYPT_MODE); byte encryptedVerifier[] = ver.getEncryptedVerifier(); byte verifier[] = new byte[encryptedVerifier.length]; cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier); @@ -78,17 +87,23 @@ public class BinaryRC4Decryptor extends Decryptor { return false; } - protected static Cipher initCipherForBlock(Cipher cipher, int block, - EncryptionInfoBuilder builder, SecretKey skey, int encryptMode) + @Override + public Cipher initCipherForBlock(Cipher cipher, int block) throws GeneralSecurityException { - EncryptionVerifier ver = builder.getVerifier(); + return initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.DECRYPT_MODE); + } + + protected static Cipher initCipherForBlock(Cipher cipher, int block, + EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode) + throws GeneralSecurityException { + EncryptionVerifier ver = encryptionInfo.getVerifier(); HashAlgorithm hashAlgo = ver.getHashAlgorithm(); byte blockKey[] = new byte[4]; LittleEndian.putUInt(blockKey, 0, block); byte encKey[] = CryptoFunctions.generateKey(skey.getEncoded(), hashAlgo, blockKey, 16); SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm()); if (cipher == null) { - EncryptionHeader em = builder.getHeader(); + EncryptionHeader em = encryptionInfo.getHeader(); cipher = CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), null, null, encryptMode); } else { cipher.init(encryptMode, key); @@ -96,10 +111,10 @@ public class BinaryRC4Decryptor extends Decryptor { return cipher; } - protected static SecretKey generateSecretKey(String password, - EncryptionVerifier ver) { - if (password.length() > 255) + protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver) { + if (password.length() > 255) { password = password.substring(0, 255); + } HashAlgorithm hashAlgo = ver.getHashAlgorithm(); MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); byte hash[] = hashAlg.digest(StringUtil.getToUnicodeLE(password)); @@ -116,15 +131,22 @@ public class BinaryRC4Decryptor extends Decryptor { return skey; } + @Override @SuppressWarnings("resource") - public InputStream getDataStream(DirectoryNode dir) throws IOException, + public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY); _length = dis.readLong(); - BinaryRC4CipherInputStream cipherStream = new BinaryRC4CipherInputStream(dis, _length); - return cipherStream; + return new BinaryRC4CipherInputStream(dis, _length); } + + public InputStream getDataStream(LittleEndianInput stream) + throws IOException, GeneralSecurityException { + return new BinaryRC4CipherInputStream(stream); + } + + @Override public long getLength() { if (_length == -1L) { throw new IllegalStateException("Decryptor.getDataStream() was not called"); @@ -132,4 +154,14 @@ public class BinaryRC4Decryptor extends Decryptor { return _length; } + + @Override + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + + @Override + public BinaryRC4Decryptor clone() throws CloneNotSupportedException { + return (BinaryRC4Decryptor)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java index 1b811a103..b9022017b 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java @@ -24,8 +24,7 @@ import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.util.LittleEndianByteArrayOutputStream; -public class BinaryRC4EncryptionHeader extends EncryptionHeader implements - EncryptionRecord { +public class BinaryRC4EncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable { protected BinaryRC4EncryptionHeader() { setCipherAlgorithm(CipherAlgorithm.rc4); @@ -39,6 +38,14 @@ public class BinaryRC4EncryptionHeader extends EncryptionHeader implements setChainingMode(null); } + @Override public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) { } + + @Override + public BinaryRC4EncryptionHeader clone() throws CloneNotSupportedException { + return (BinaryRC4EncryptionHeader)super.clone(); + } + + } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java index 10bf58d83..94ddde72a 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java @@ -23,55 +23,37 @@ import org.apache.poi.util.LittleEndianInput; public class BinaryRC4EncryptionInfoBuilder implements EncryptionInfoBuilder { - EncryptionInfo info; - BinaryRC4EncryptionHeader header; - BinaryRC4EncryptionVerifier verifier; - BinaryRC4Decryptor decryptor; - BinaryRC4Encryptor encryptor; - public BinaryRC4EncryptionInfoBuilder() { } + @Override public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException { - this.info = info; int vMajor = info.getVersionMajor(); int vMinor = info.getVersionMinor(); assert (vMajor == 1 && vMinor == 1); - header = new BinaryRC4EncryptionHeader(); - verifier = new BinaryRC4EncryptionVerifier(dis); - decryptor = new BinaryRC4Decryptor(this); - encryptor = new BinaryRC4Encryptor(this); + info.setHeader(new BinaryRC4EncryptionHeader()); + info.setVerifier(new BinaryRC4EncryptionVerifier(dis)); + Decryptor dec = new BinaryRC4Decryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new BinaryRC4Encryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } + @Override public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { - this.info = info; - header = new BinaryRC4EncryptionHeader(); - verifier = new BinaryRC4EncryptionVerifier(); - decryptor = new BinaryRC4Decryptor(this); - encryptor = new BinaryRC4Encryptor(this); - } - - public BinaryRC4EncryptionHeader getHeader() { - return header; - } - - public BinaryRC4EncryptionVerifier getVerifier() { - return verifier; - } - - public BinaryRC4Decryptor getDecryptor() { - return decryptor; - } - - public BinaryRC4Encryptor getEncryptor() { - return encryptor; - } - - public EncryptionInfo getEncryptionInfo() { - return info; + info.setHeader(new BinaryRC4EncryptionHeader()); + info.setVerifier(new BinaryRC4EncryptionVerifier()); + Decryptor dec = new BinaryRC4Decryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new BinaryRC4Encryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java index 86cf4ac18..654b64a3e 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java @@ -23,7 +23,7 @@ import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.LittleEndianInput; -public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord { +public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable { protected BinaryRC4EncryptionVerifier() { setSpinCount(-1); @@ -50,6 +50,7 @@ public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements E setHashAlgorithm(HashAlgorithm.md5); } + @Override protected void setSalt(byte salt[]) { if (salt == null || salt.length != 16) { throw new EncryptedDocumentException("invalid verifier salt"); @@ -58,14 +59,17 @@ public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements E super.setSalt(salt); } + @Override protected void setEncryptedVerifier(byte encryptedVerifier[]) { super.setEncryptedVerifier(encryptedVerifier); } + @Override protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { super.setEncryptedVerifierHash(encryptedVerifierHash); } + @Override public void write(LittleEndianByteArrayOutputStream bos) { byte salt[] = getSalt(); assert (salt.length == 16); @@ -78,4 +82,8 @@ public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements E bos.write(encryptedVerifierHash); } + @Override + public BinaryRC4EncryptionVerifier clone() throws CloneNotSupportedException { + return (BinaryRC4EncryptionVerifier)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java index 2cf2d9334..ef49c9dc7 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java @@ -34,24 +34,95 @@ import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.DataSpaceMapUtils; import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.util.LittleEndianByteArrayOutputStream; -public class BinaryRC4Encryptor extends Encryptor { +public class BinaryRC4Encryptor extends Encryptor implements Cloneable { + + protected BinaryRC4Encryptor() { + } + + @Override + public void confirmPassword(String password) { + Random r = new SecureRandom(); + byte salt[] = new byte[16]; + byte verifier[] = new byte[16]; + r.nextBytes(salt); + r.nextBytes(verifier); + confirmPassword(password, null, null, verifier, salt, null); + } + + @Override + public void confirmPassword(String password, byte keySpec[], + byte keySalt[], byte verifier[], byte verifierSalt[], + byte integritySalt[]) { + BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)getEncryptionInfo().getVerifier(); + ver.setSalt(verifierSalt); + SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver); + setSecretKey(skey); + try { + Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, getEncryptionInfo(), skey, Cipher.ENCRYPT_MODE); + byte encryptedVerifier[] = new byte[16]; + cipher.update(verifier, 0, 16, encryptedVerifier); + ver.setEncryptedVerifier(encryptedVerifier); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte calcVerifierHash[] = hashAlg.digest(verifier); + byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash); + ver.setEncryptedVerifierHash(encryptedVerifierHash); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("Password confirmation failed", e); + } + } + + @Override + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + OutputStream countStream = new BinaryRC4CipherOutputStream(dir); + return countStream; + } + + protected int getKeySizeInBytes() { + return getEncryptionInfo().getHeader().getKeySize() / 8; + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + DataSpaceMapUtils.addDefaultDataSpace(dir); + final EncryptionInfo info = getEncryptionInfo(); + final BinaryRC4EncryptionHeader header = (BinaryRC4EncryptionHeader)info.getHeader(); + final BinaryRC4EncryptionVerifier verifier = (BinaryRC4EncryptionVerifier)info.getVerifier(); + EncryptionRecord er = new EncryptionRecord() { + @Override + public void write(LittleEndianByteArrayOutputStream bos) { + bos.writeShort(info.getVersionMajor()); + bos.writeShort(info.getVersionMinor()); + header.write(bos); + verifier.write(bos); + } + }; + DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); + } + + @Override + public BinaryRC4Encryptor clone() throws CloneNotSupportedException { + return (BinaryRC4Encryptor)super.clone(); + } - private final BinaryRC4EncryptionInfoBuilder builder; - protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream { + @Override protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) throws GeneralSecurityException { - return BinaryRC4Decryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + return BinaryRC4Decryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); } + @Override protected void calculateChecksum(File file, int i) { } + @Override protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) throws IOException, GeneralSecurityException { BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir); @@ -62,66 +133,4 @@ public class BinaryRC4Encryptor extends Encryptor { super(dir, 512); } } - - protected BinaryRC4Encryptor(BinaryRC4EncryptionInfoBuilder builder) { - this.builder = builder; - } - - public void confirmPassword(String password) { - Random r = new SecureRandom(); - byte salt[] = new byte[16]; - byte verifier[] = new byte[16]; - r.nextBytes(salt); - r.nextBytes(verifier); - confirmPassword(password, null, null, verifier, salt, null); - } - - public void confirmPassword(String password, byte keySpec[], - byte keySalt[], byte verifier[], byte verifierSalt[], - byte integritySalt[]) { - BinaryRC4EncryptionVerifier ver = builder.getVerifier(); - ver.setSalt(verifierSalt); - SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver); - setSecretKey(skey); - try { - Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, builder, skey, Cipher.ENCRYPT_MODE); - byte encryptedVerifier[] = new byte[16]; - cipher.update(verifier, 0, 16, encryptedVerifier); - ver.setEncryptedVerifier(encryptedVerifier); - org.apache.poi.poifs.crypt.HashAlgorithm hashAlgo = ver - .getHashAlgorithm(); - MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); - byte calcVerifierHash[] = hashAlg.digest(verifier); - byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash); - ver.setEncryptedVerifierHash(encryptedVerifierHash); - } catch (GeneralSecurityException e) { - throw new EncryptedDocumentException("Password confirmation failed", e); - } - } - - public OutputStream getDataStream(DirectoryNode dir) - throws IOException, GeneralSecurityException { - OutputStream countStream = new BinaryRC4CipherOutputStream(dir); - return countStream; - } - - protected int getKeySizeInBytes() { - return builder.getHeader().getKeySize() / 8; - } - - protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { - DataSpaceMapUtils.addDefaultDataSpace(dir); - final EncryptionInfo info = builder.getEncryptionInfo(); - final BinaryRC4EncryptionHeader header = builder.getHeader(); - final BinaryRC4EncryptionVerifier verifier = builder.getVerifier(); - EncryptionRecord er = new EncryptionRecord() { - public void write(LittleEndianByteArrayOutputStream bos) { - bos.writeShort(info.getVersionMajor()); - bos.writeShort(info.getVersionMinor()); - header.write(bos); - verifier.write(bos); - } - }; - DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); - } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java index 09f82e5a6..07b791074 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java @@ -17,7 +17,6 @@ package org.apache.poi.poifs.crypt.cryptoapi; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -27,79 +26,34 @@ import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.SecretKey; -import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.EncryptionHeader; -import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.EncryptionVerifier; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.DocumentNode; -import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.BoundedInputStream; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.StringUtil; -public class CryptoAPIDecryptor extends Decryptor { +public class CryptoAPIDecryptor extends Decryptor implements Cloneable { private long _length; + private int _chunkSize = -1; - private class SeekableByteArrayInputStream extends ByteArrayInputStream { - Cipher cipher; - byte oneByte[] = { 0 }; - - public void seek(int newpos) { - if (newpos > count) { - throw new ArrayIndexOutOfBoundsException(newpos); - } - - this.pos = newpos; - mark = newpos; - } - - public void setBlock(int block) throws GeneralSecurityException { - cipher = initCipherForBlock(cipher, block); - } - - public synchronized int read() { - int ch = super.read(); - if (ch == -1) return -1; - oneByte[0] = (byte) ch; - try { - cipher.update(oneByte, 0, 1, oneByte); - } catch (ShortBufferException e) { - throw new EncryptedDocumentException(e); - } - return oneByte[0]; - } - - public synchronized int read(byte b[], int off, int len) { - int readLen = super.read(b, off, len); - if (readLen ==-1) return -1; - try { - cipher.update(b, off, readLen, b, off); - } catch (ShortBufferException e) { - throw new EncryptedDocumentException(e); - } - return readLen; - } - - public SeekableByteArrayInputStream(byte buf[]) - throws GeneralSecurityException { - super(buf); - cipher = initCipherForBlock(null, 0); - } - } - static class StreamDescriptorEntry { static BitField flagStream = BitFieldFactory.getInstance(1); @@ -111,16 +65,16 @@ public class CryptoAPIDecryptor extends Decryptor { String streamName; } - protected CryptoAPIDecryptor(CryptoAPIEncryptionInfoBuilder builder) { - super(builder); + protected CryptoAPIDecryptor() { _length = -1L; } + @Override public boolean verifyPassword(String password) { - EncryptionVerifier ver = builder.getVerifier(); + EncryptionVerifier ver = getEncryptionInfo().getVerifier(); SecretKey skey = generateSecretKey(password, ver); try { - Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE); + Cipher cipher = initCipherForBlock(null, 0, getEncryptionInfo(), skey, Cipher.DECRYPT_MODE); byte encryptedVerifier[] = ver.getEncryptedVerifier(); byte verifier[] = new byte[encryptedVerifier.length]; cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier); @@ -140,30 +94,25 @@ public class CryptoAPIDecryptor extends Decryptor { return false; } - /** - * Initializes a cipher object for a given block index for decryption - * - * @param cipher may be null, otherwise the given instance is reset to the new block index - * @param block the block index, e.g. the persist/slide id (hslf) - * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher - * @throws GeneralSecurityException - */ + @Override public Cipher initCipherForBlock(Cipher cipher, int block) throws GeneralSecurityException { - return initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.DECRYPT_MODE); + EncryptionInfo ei = getEncryptionInfo(); + SecretKey sk = getSecretKey(); + return initCipherForBlock(cipher, block, ei, sk, Cipher.DECRYPT_MODE); } protected static Cipher initCipherForBlock(Cipher cipher, int block, - EncryptionInfoBuilder builder, SecretKey skey, int encryptMode) + EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode) throws GeneralSecurityException { - EncryptionVerifier ver = builder.getVerifier(); + EncryptionVerifier ver = encryptionInfo.getVerifier(); HashAlgorithm hashAlgo = ver.getHashAlgorithm(); byte blockKey[] = new byte[4]; LittleEndian.putUInt(blockKey, 0, block); MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); hashAlg.update(skey.getEncoded()); byte encKey[] = hashAlg.digest(blockKey); - EncryptionHeader header = builder.getHeader(); + EncryptionHeader header = encryptionInfo.getHeader(); int keyBits = header.getKeySize(); encKey = CryptoFunctions.getBlock0(encKey, keyBits / 8); if (keyBits == 40) { @@ -190,6 +139,18 @@ public class CryptoAPIDecryptor extends Decryptor { return skey; } + @Override + public ChunkedCipherInputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + throw new IOException("not supported"); + } + + @Override + public ChunkedCipherInputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + throws IOException, GeneralSecurityException { + return new CryptoAPICipherInputStream(stream, size, initialPos); + } + /** * Decrypt the Document-/SummaryInformation and other optionally streams. * Opposed to other crypto modes, cryptoapi is record based and can't be used @@ -197,15 +158,17 @@ public class CryptoAPIDecryptor extends Decryptor { * * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream */ - public InputStream getDataStream(DirectoryNode dir) + public POIFSFileSystem getSummaryEntries(DirectoryNode root, String encryptedStream) throws IOException, GeneralSecurityException { - NPOIFSFileSystem fsOut = new NPOIFSFileSystem(); - DocumentNode es = (DocumentNode) dir.getEntry("EncryptedSummary"); - DocumentInputStream dis = dir.createDocumentInputStream(es); + POIFSFileSystem fsOut = new POIFSFileSystem(); + // HSLF: encryptedStream + // HSSF: encryption + DocumentNode es = (DocumentNode) root.getEntry(encryptedStream); + DocumentInputStream dis = root.createDocumentInputStream(es); ByteArrayOutputStream bos = new ByteArrayOutputStream(); IOUtils.copy(dis, bos); dis.close(); - SeekableByteArrayInputStream sbis = new SeekableByteArrayInputStream(bos.toByteArray()); + CryptoAPIDocumentInputStream sbis = new CryptoAPIDocumentInputStream(this, bos.toByteArray()); LittleEndianInputStream leis = new LittleEndianInputStream(sbis); int streamDescriptorArrayOffset = (int) leis.readUInt(); /* int streamDescriptorArraySize = (int) */ leis.readUInt(); @@ -239,21 +202,40 @@ public class CryptoAPIDecryptor extends Decryptor { leis.close(); sbis.close(); sbis = null; - bos.reset(); - fsOut.writeFilesystem(bos); - fsOut.close(); - _length = bos.size(); - ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); - return bis; + return fsOut; } /** * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)} */ + @Override public long getLength() { if (_length == -1L) { throw new IllegalStateException("Decryptor.getDataStream() was not called"); } return _length; } + + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + + @Override + public CryptoAPIDecryptor clone() throws CloneNotSupportedException { + return (CryptoAPIDecryptor)super.clone(); + } + + private class CryptoAPICipherInputStream extends ChunkedCipherInputStream { + + @Override + protected Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException { + return CryptoAPIDecryptor.this.initCipherForBlock(existing, block); + } + + public CryptoAPICipherInputStream(LittleEndianInput stream, long size, int initialPos) + throws GeneralSecurityException { + super(stream, size, _chunkSize, initialPos); + } + } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentInputStream.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentInputStream.java new file mode 100644 index 000000000..573664932 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentInputStream.java @@ -0,0 +1,86 @@ +/* ==================================================================== + 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.poifs.crypt.cryptoapi; + +import java.io.ByteArrayInputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; +import javax.crypto.ShortBufferException; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.util.Internal; + +/** + * A seekable InputStream, which is used to decrypt/extract the document entries + * within the encrypted stream + */ +@Internal +/* package */ class CryptoAPIDocumentInputStream extends ByteArrayInputStream { + private Cipher cipher; + private final CryptoAPIDecryptor decryptor; + private byte oneByte[] = { 0 }; + + public void seek(int newpos) { + if (newpos > count) { + throw new ArrayIndexOutOfBoundsException(newpos); + } + + this.pos = newpos; + mark = newpos; + } + + public void setBlock(int block) throws GeneralSecurityException { + cipher = decryptor.initCipherForBlock(cipher, block); + } + + @Override + public synchronized int read() { + int ch = super.read(); + if (ch == -1) { + return -1; + } + oneByte[0] = (byte) ch; + try { + cipher.update(oneByte, 0, 1, oneByte); + } catch (ShortBufferException e) { + throw new EncryptedDocumentException(e); + } + return oneByte[0]; + } + + @Override + public synchronized int read(byte b[], int off, int len) { + int readLen = super.read(b, off, len); + if (readLen ==-1) { + return -1; + } + try { + cipher.update(b, off, readLen, b, off); + } catch (ShortBufferException e) { + throw new EncryptedDocumentException(e); + } + return readLen; + } + + public CryptoAPIDocumentInputStream(CryptoAPIDecryptor decryptor, byte buf[]) + throws GeneralSecurityException { + super(buf); + this.decryptor = decryptor; + cipher = decryptor.initCipherForBlock(null, 0); + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentOutputStream.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentOutputStream.java new file mode 100644 index 000000000..6bf04871d --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDocumentOutputStream.java @@ -0,0 +1,74 @@ +/* ==================================================================== + 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.poifs.crypt.cryptoapi; + +import java.io.ByteArrayOutputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.util.Internal; + +/** + * An OutputStream for the document entries within the encrypted stream + */ +@Internal +/* package */ class CryptoAPIDocumentOutputStream extends ByteArrayOutputStream { + private Cipher cipher; + private CryptoAPIEncryptor encryptor; + private byte oneByte[] = { 0 }; + + public CryptoAPIDocumentOutputStream(CryptoAPIEncryptor encryptor) throws GeneralSecurityException { + this.encryptor = encryptor; + setBlock(0); + } + + public byte[] getBuf() { + return buf; + } + + public void setSize(int count) { + this.count = count; + } + + public void setBlock(int block) throws GeneralSecurityException { + cipher = encryptor.initCipherForBlock(cipher, block); + } + + @Override + public void write(int b) { + try { + oneByte[0] = (byte)b; + cipher.update(oneByte, 0, 1, oneByte, 0); + super.write(oneByte); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + } + + @Override + public void write(byte[] b, int off, int len) { + try { + cipher.update(b, off, len, b, off); + super.write(b, off, len); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + } + +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java index 151b6588a..b54dc2f0d 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java @@ -27,7 +27,7 @@ import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.standard.StandardEncryptionHeader; import org.apache.poi.util.LittleEndianInput; -public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader { +public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader implements Cloneable { public CryptoAPIEncryptionHeader(LittleEndianInput is) throws IOException { super(is); @@ -39,6 +39,7 @@ public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader { super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); } + @Override public void setKeySize(int keyBits) { // Microsoft Base Cryptographic Provider is limited up to 40 bits // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx @@ -59,4 +60,9 @@ public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader { setCspName(CipherProvider.rc4.cipherProviderName); } } + + @Override + public CryptoAPIEncryptionHeader clone() throws CloneNotSupportedException { + return (CryptoAPIEncryptionHeader)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java index 36df52876..fef4dde16 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java @@ -23,63 +23,52 @@ import org.apache.poi.poifs.crypt.*; import org.apache.poi.util.LittleEndianInput; public class CryptoAPIEncryptionInfoBuilder implements EncryptionInfoBuilder { - EncryptionInfo info; - CryptoAPIEncryptionHeader header; - CryptoAPIEncryptionVerifier verifier; - CryptoAPIDecryptor decryptor; - CryptoAPIEncryptor encryptor; - public CryptoAPIEncryptionInfoBuilder() { } /** * initialize the builder from a stream */ + @Override public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException { - this.info = info; /* int hSize = */ dis.readInt(); - header = new CryptoAPIEncryptionHeader(dis); - verifier = new CryptoAPIEncryptionVerifier(dis, header); - decryptor = new CryptoAPIDecryptor(this); - encryptor = new CryptoAPIEncryptor(this); + CryptoAPIEncryptionHeader header = new CryptoAPIEncryptionHeader(dis); + info.setHeader(header); + info.setVerifier(new CryptoAPIEncryptionVerifier(dis, header)); + CryptoAPIDecryptor dec = new CryptoAPIDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + CryptoAPIEncryptor enc = new CryptoAPIEncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } /** * initialize the builder from scratch */ + @Override public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { - this.info = info; - if (cipherAlgorithm == null) cipherAlgorithm = CipherAlgorithm.rc4; - if (hashAlgorithm == null) hashAlgorithm = HashAlgorithm.sha1; - if (keyBits == -1) keyBits = 0x28; + if (cipherAlgorithm == null) { + cipherAlgorithm = CipherAlgorithm.rc4; + } + if (hashAlgorithm == null) { + hashAlgorithm = HashAlgorithm.sha1; + } + if (keyBits == -1) { + keyBits = 0x28; + } assert(cipherAlgorithm == CipherAlgorithm.rc4 && hashAlgorithm == HashAlgorithm.sha1); - header = new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - verifier = new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - decryptor = new CryptoAPIDecryptor(this); - encryptor = new CryptoAPIEncryptor(this); - } - - public CryptoAPIEncryptionHeader getHeader() { - return header; - } - - public CryptoAPIEncryptionVerifier getVerifier() { - return verifier; - } - - public CryptoAPIDecryptor getDecryptor() { - return decryptor; - } - - public CryptoAPIEncryptor getEncryptor() { - return encryptor; - } - - public EncryptionInfo getEncryptionInfo() { - return info; + info.setHeader(new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + info.setVerifier(new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + CryptoAPIDecryptor dec = new CryptoAPIDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + CryptoAPIEncryptor enc = new CryptoAPIEncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java index 160d1f9f9..d2c87b7ab 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java @@ -23,7 +23,7 @@ import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.standard.StandardEncryptionVerifier; import org.apache.poi.util.LittleEndianInput; -public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier { +public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier implements Cloneable { protected CryptoAPIEncryptionVerifier(LittleEndianInput is, CryptoAPIEncryptionHeader header) { @@ -36,15 +36,23 @@ public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier { super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); } + @Override protected void setSalt(byte salt[]) { super.setSalt(salt); } + @Override protected void setEncryptedVerifier(byte encryptedVerifier[]) { super.setEncryptedVerifier(encryptedVerifier); } + @Override protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { super.setEncryptedVerifierHash(encryptedVerifierHash); } + + @Override + public CryptoAPIEncryptionVerifier clone() throws CloneNotSupportedException { + return (CryptoAPIEncryptionVerifier)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java index 47d4696cb..02d28761a 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java @@ -18,7 +18,7 @@ package org.apache.poi.poifs.crypt.cryptoapi; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; @@ -36,6 +36,7 @@ import org.apache.poi.hpsf.DocumentSummaryInformation; import org.apache.poi.hpsf.PropertySetFactory; import org.apache.poi.hpsf.SummaryInformation; import org.apache.poi.hpsf.WritingNotSupportedException; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.DataSpaceMapUtils; import org.apache.poi.poifs.crypt.EncryptionInfo; @@ -50,13 +51,14 @@ import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.StringUtil; -public class CryptoAPIEncryptor extends Encryptor { - private final CryptoAPIEncryptionInfoBuilder builder; - - protected CryptoAPIEncryptor(CryptoAPIEncryptionInfoBuilder builder) { - this.builder = builder; +public class CryptoAPIEncryptor extends Encryptor implements Cloneable { + + private int _chunkSize = 512; + + protected CryptoAPIEncryptor() { } + @Override public void confirmPassword(String password) { Random r = new SecureRandom(); byte salt[] = new byte[16]; @@ -66,11 +68,12 @@ public class CryptoAPIEncryptor extends Encryptor { confirmPassword(password, null, null, verifier, salt, null); } + @Override public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { assert(verifier != null && verifierSalt != null); - CryptoAPIEncryptionVerifier ver = builder.getVerifier(); + CryptoAPIEncryptionVerifier ver = (CryptoAPIEncryptionVerifier)getEncryptionInfo().getVerifier(); ver.setSalt(verifierSalt); SecretKey skey = CryptoAPIDecryptor.generateSecretKey(password, ver); setSecretKey(skey); @@ -99,8 +102,19 @@ public class CryptoAPIEncryptor extends Encryptor { */ public Cipher initCipherForBlock(Cipher cipher, int block) throws GeneralSecurityException { - return CryptoAPIDecryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + return CryptoAPIDecryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); } + + @Override + public ChunkedCipherOutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + throw new IOException("not supported"); + } + + public CryptoAPICipherOutputStream getDataStream(OutputStream stream) + throws IOException, GeneralSecurityException { + return new CryptoAPICipherOutputStream(stream); + } /** * Encrypt the Document-/SummaryInformation and other optionally streams. @@ -109,9 +123,9 @@ public class CryptoAPIEncryptor extends Encryptor { * * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream */ - public OutputStream getDataStream(DirectoryNode dir) + public OutputStream getSummaryEntries(DirectoryNode dir) throws IOException, GeneralSecurityException { - CipherByteArrayOutputStream bos = new CipherByteArrayOutputStream(); + CryptoAPIDocumentOutputStream bos = new CryptoAPIDocumentOutputStream(this); byte buf[] = new byte[8]; bos.write(buf, 0, 8); // skip header @@ -124,7 +138,9 @@ public class CryptoAPIEncryptor extends Encryptor { int block = 0; for (String entryName : entryNames) { - if (!dir.hasEntry(entryName)) continue; + if (!dir.hasEntry(entryName)) { + continue; + } StreamDescriptorEntry descEntry = new StreamDescriptorEntry(); descEntry.block = block; descEntry.streamOffset = bos.size(); @@ -193,15 +209,20 @@ public class CryptoAPIEncryptor extends Encryptor { } protected int getKeySizeInBytes() { - return builder.getHeader().getKeySize() / 8; + return getEncryptionInfo().getHeader().getKeySize() / 8; } + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { DataSpaceMapUtils.addDefaultDataSpace(dir); - final EncryptionInfo info = builder.getEncryptionInfo(); - final CryptoAPIEncryptionHeader header = builder.getHeader(); - final CryptoAPIEncryptionVerifier verifier = builder.getVerifier(); + final EncryptionInfo info = getEncryptionInfo(); + final CryptoAPIEncryptionHeader header = (CryptoAPIEncryptionHeader)getEncryptionInfo().getHeader(); + final CryptoAPIEncryptionVerifier verifier = (CryptoAPIEncryptionVerifier)getEncryptionInfo().getVerifier(); EncryptionRecord er = new EncryptionRecord() { + @Override public void write(LittleEndianByteArrayOutputStream bos) { bos.writeShort(info.getVersionMajor()); bos.writeShort(info.getVersionMinor()); @@ -212,44 +233,42 @@ public class CryptoAPIEncryptor extends Encryptor { DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); } - private class CipherByteArrayOutputStream extends ByteArrayOutputStream { - Cipher cipher; - byte oneByte[] = { 0 }; - - public CipherByteArrayOutputStream() throws GeneralSecurityException { - setBlock(0); - } - - public byte[] getBuf() { - return buf; - } - - public void setSize(int count) { - this.count = count; - } - - public void setBlock(int block) throws GeneralSecurityException { - cipher = initCipherForBlock(cipher, block); - } - - public void write(int b) { - try { - oneByte[0] = (byte)b; - cipher.update(oneByte, 0, 1, oneByte, 0); - super.write(oneByte); - } catch (Exception e) { - throw new EncryptedDocumentException(e); - } - } - - public void write(byte[] b, int off, int len) { - try { - cipher.update(b, off, len, b, off); - super.write(b, off, len); - } catch (Exception e) { - throw new EncryptedDocumentException(e); - } - } + @Override + public CryptoAPIEncryptor clone() throws CloneNotSupportedException { + return (CryptoAPIEncryptor)super.clone(); } + + protected class CryptoAPICipherOutputStream extends ChunkedCipherOutputStream { + + @Override + protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) + throws IOException, GeneralSecurityException { + flush(); + EncryptionInfo ei = getEncryptionInfo(); + SecretKey sk = getSecretKey(); + return CryptoAPIDecryptor.initCipherForBlock(cipher, block, ei, sk, Cipher.ENCRYPT_MODE); + } + + @Override + protected void calculateChecksum(File file, int i) { + } + + @Override + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + throw new RuntimeException("createEncryptionInfoEntry not supported"); + } + + public CryptoAPICipherOutputStream(OutputStream stream) + throws IOException, GeneralSecurityException { + super(stream, CryptoAPIEncryptor.this._chunkSize); + } + + @Override + public void flush() throws IOException { + writeChunk(false); + } + } + } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java index a6d6dbfa0..d06f9a373 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java @@ -34,7 +34,6 @@ import org.apache.poi.poifs.crypt.ChainingMode; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.EncryptionHeader; -import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; import org.apache.poi.poifs.crypt.EncryptionVerifier; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.filesystem.DirectoryNode; @@ -44,15 +43,15 @@ import org.apache.poi.util.LittleEndian; /** */ -public class StandardDecryptor extends Decryptor { +public class StandardDecryptor extends Decryptor implements Cloneable { private long _length = -1; - protected StandardDecryptor(EncryptionInfoBuilder builder) { - super(builder); + protected StandardDecryptor() { } + @Override public boolean verifyPassword(String password) { - EncryptionVerifier ver = builder.getVerifier(); + EncryptionVerifier ver = getEncryptionInfo().getVerifier(); SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes()); Cipher cipher = getCipher(skey); @@ -116,12 +115,13 @@ public class StandardDecryptor extends Decryptor { } private Cipher getCipher(SecretKey key) { - EncryptionHeader em = builder.getHeader(); + EncryptionHeader em = getEncryptionInfo().getHeader(); ChainingMode cm = em.getChainingMode(); assert(cm == ChainingMode.ecb); return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE); } + @Override @SuppressWarnings("resource") public InputStream getDataStream(DirectoryNode dir) throws IOException { DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY); @@ -134,7 +134,7 @@ public class StandardDecryptor extends Decryptor { // limit wrong calculated ole entries - (bug #57080) // standard encryption always uses aes encoding, so blockSize is always 16 // http://stackoverflow.com/questions/3283787/size-of-data-after-aes-encryption - int blockSize = builder.getHeader().getCipherAlgorithm().blockSize; + int blockSize = getEncryptionInfo().getHeader().getCipherAlgorithm().blockSize; long cipherLen = (_length/blockSize + 1) * blockSize; Cipher cipher = getCipher(getSecretKey()); @@ -145,8 +145,16 @@ public class StandardDecryptor extends Decryptor { /** * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)} */ + @Override public long getLength(){ - if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called"); + if(_length == -1) { + throw new IllegalStateException("Decryptor.getDataStream() was not called"); + } return _length; } + + @Override + public StandardDecryptor clone() throws CloneNotSupportedException { + return (StandardDecryptor)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java index 44d7bc595..30f35581f 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java @@ -22,6 +22,7 @@ import static org.apache.poi.poifs.crypt.EncryptionInfo.flagCryptoAPI; import java.io.IOException; import java.io.InputStream; +import org.apache.poi.hssf.record.RecordInputStream; import org.apache.poi.poifs.crypt.ChainingMode; import org.apache.poi.poifs.crypt.CipherAlgorithm; import org.apache.poi.poifs.crypt.CipherProvider; @@ -33,7 +34,7 @@ import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianOutput; import org.apache.poi.util.StringUtil; -public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord { +public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable { protected StandardEncryptionHeader(LittleEndianInput is) throws IOException { setFlags(is.readInt()); @@ -55,9 +56,17 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp // CSPName may not always be specified // In some cases, the salt value of the EncryptionVerifier is the next chunk of data - ((InputStream)is).mark(LittleEndianConsts.INT_SIZE+1); + if (is instanceof RecordInputStream) { + ((RecordInputStream)is).mark(LittleEndianConsts.INT_SIZE+1); + } else { + ((InputStream)is).mark(LittleEndianConsts.INT_SIZE+1); + } int checkForSalt = is.readInt(); - ((InputStream)is).reset(); + if (is instanceof RecordInputStream) { + ((RecordInputStream)is).reset(); + } else { + ((InputStream)is).reset(); + } if (checkForSalt == 16) { setCspName(""); @@ -65,7 +74,9 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp StringBuilder builder = new StringBuilder(); while (true) { char c = (char) is.readShort(); - if (c == 0) break; + if (c == 0) { + break; + } builder.append(c); } setCspName(builder.toString()); @@ -90,6 +101,7 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp /** * serializes the header */ + @Override public void write(LittleEndianByteArrayOutputStream bos) { int startIdx = bos.getWriteIndex(); LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE); @@ -102,10 +114,17 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp bos.writeInt(0); // reserved1 bos.writeInt(0); // reserved2 String cspName = getCspName(); - if (cspName == null) cspName = getCipherProvider().cipherProviderName; + if (cspName == null) { + cspName = getCipherProvider().cipherProviderName; + } bos.write(StringUtil.getToUnicodeLE(cspName)); bos.writeShort(0); int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE; sizeOutput.writeInt(headerSize); } + + @Override + public StandardEncryptionHeader clone() throws CloneNotSupportedException { + return (StandardEncryptionHeader)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java index d55c8e0d6..c2bffdd05 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java @@ -27,34 +27,29 @@ import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.util.LittleEndianInput; public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder { - - EncryptionInfo info; - StandardEncryptionHeader header; - StandardEncryptionVerifier verifier; - StandardDecryptor decryptor; - StandardEncryptor encryptor; /** * initialize the builder from a stream */ + @Override public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException { - this.info = info; - /* int hSize = */ dis.readInt(); - header = new StandardEncryptionHeader(dis); - verifier = new StandardEncryptionVerifier(dis, header); + StandardEncryptionHeader header = new StandardEncryptionHeader(dis); + info.setHeader(header); + info.setVerifier(new StandardEncryptionVerifier(dis, header)); if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) { - decryptor = new StandardDecryptor(this); + StandardDecryptor dec = new StandardDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); } } /** * initialize the builder from scratch */ + @Override public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { - this.info = info; - if (cipherAlgorithm == null) { cipherAlgorithm = CipherAlgorithm.aes128; } @@ -89,29 +84,13 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder { if (!found) { throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for Cipher "+cipherAlgorithm.toString()); } - header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - decryptor = new StandardDecryptor(this); - encryptor = new StandardEncryptor(this); - } - - public StandardEncryptionHeader getHeader() { - return header; - } - - public StandardEncryptionVerifier getVerifier() { - return verifier; - } - - public StandardDecryptor getDecryptor() { - return decryptor; - } - - public StandardEncryptor getEncryptor() { - return encryptor; - } - - public EncryptionInfo getEncryptionInfo() { - return info; + info.setHeader(new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + info.setVerifier(new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + StandardDecryptor dec = new StandardDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + StandardEncryptor enc = new StandardEncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java index 021d82f9e..f00efecfb 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java @@ -27,7 +27,7 @@ import org.apache.poi.util.LittleEndianInput; /** * Used when checking if a key is valid for a document */ -public class StandardEncryptionVerifier extends EncryptionVerifier implements EncryptionRecord { +public class StandardEncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable { private static final int SPIN_COUNT = 50000; private final int verifierHashSize; @@ -68,6 +68,7 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En } // make method visible for this package + @Override protected void setSalt(byte salt[]) { if (salt == null || salt.length != 16) { throw new EncryptedDocumentException("invalid verifier salt"); @@ -76,15 +77,18 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En } // make method visible for this package + @Override protected void setEncryptedVerifier(byte encryptedVerifier[]) { super.setEncryptedVerifier(encryptedVerifier); } // make method visible for this package + @Override protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { super.setEncryptedVerifierHash(encryptedVerifierHash); } + @Override public void write(LittleEndianByteArrayOutputStream bos) { // see [MS-OFFCRYPTO] - 2.3.4.9 byte salt[] = getSalt(); @@ -115,4 +119,9 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En protected int getVerifierHashSize() { return verifierHashSize; } + + @Override + public StandardEncryptionVerifier clone() throws CloneNotSupportedException { + return (StandardEncryptionVerifier)super.clone(); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java index fded6c606..5372c2af8 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java @@ -53,15 +53,13 @@ import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.poi.util.TempFile; -public class StandardEncryptor extends Encryptor { +public class StandardEncryptor extends Encryptor implements Cloneable { private static final POILogger logger = POILogFactory.getLogger(StandardEncryptor.class); - private final StandardEncryptionInfoBuilder builder; - - protected StandardEncryptor(StandardEncryptionInfoBuilder builder) { - this.builder = builder; + protected StandardEncryptor() { } + @Override public void confirmPassword(String password) { // see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier Random r = new SecureRandom(); @@ -79,8 +77,9 @@ public class StandardEncryptor extends Encryptor { * * see [MS-OFFCRYPTO] - 2.3.4.7 ECMA-376 Document Encryption Key Generation */ + @Override public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { - StandardEncryptionVerifier ver = builder.getVerifier(); + StandardEncryptionVerifier ver = (StandardEncryptionVerifier)getEncryptionInfo().getVerifier(); ver.setSalt(verifierSalt); SecretKey secretKey = generateSecretKey(password, ver, getKeySizeInBytes()); @@ -111,10 +110,11 @@ public class StandardEncryptor extends Encryptor { } private Cipher getCipher(SecretKey key, String padding) { - EncryptionVerifier ver = builder.getVerifier(); + EncryptionVerifier ver = getEncryptionInfo().getVerifier(); return CryptoFunctions.getCipher(key, ver.getCipherAlgorithm(), ver.getChainingMode(), null, Cipher.ENCRYPT_MODE, padding); } + @Override public OutputStream getDataStream(final DirectoryNode dir) throws IOException, GeneralSecurityException { createEncryptionInfoEntry(dir); @@ -163,6 +163,7 @@ public class StandardEncryptor extends Encryptor { countBytes++; } + @Override public void close() throws IOException { // the CipherOutputStream adds the padding bytes on close() super.close(); @@ -175,6 +176,7 @@ public class StandardEncryptor extends Encryptor { // TODO: any properties??? } + @Override public void processPOIFSWriterEvent(POIFSWriterEvent event) { try { LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream()); @@ -200,15 +202,16 @@ public class StandardEncryptor extends Encryptor { } protected int getKeySizeInBytes() { - return builder.getHeader().getKeySize()/8; + return getEncryptionInfo().getHeader().getKeySize()/8; } protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { - final EncryptionInfo info = builder.getEncryptionInfo(); - final StandardEncryptionHeader header = builder.getHeader(); - final StandardEncryptionVerifier verifier = builder.getVerifier(); + final EncryptionInfo info = getEncryptionInfo(); + final StandardEncryptionHeader header = (StandardEncryptionHeader)info.getHeader(); + final StandardEncryptionVerifier verifier = (StandardEncryptionVerifier)info.getVerifier(); EncryptionRecord er = new EncryptionRecord(){ + @Override public void write(LittleEndianByteArrayOutputStream bos) { bos.writeShort(info.getVersionMajor()); bos.writeShort(info.getVersionMinor()); @@ -222,4 +225,9 @@ public class StandardEncryptor extends Encryptor { // TODO: any properties??? } + + @Override + public StandardEncryptor clone() throws CloneNotSupportedException { + return (StandardEncryptor)super.clone(); + } } diff --git a/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java b/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java index 54388e151..4594343bf 100644 --- a/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java +++ b/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java @@ -17,103 +17,91 @@ package org.apache.poi.util; +import java.io.ByteArrayInputStream; + /** * Adapts a plain byte array to {@link LittleEndianInput} - * - * @author Josh Micich */ -public final class LittleEndianByteArrayInputStream implements LittleEndianInput { - private final byte[] _buf; - private final int _endIndex; - private int _readIndex; - +public final class LittleEndianByteArrayInputStream extends ByteArrayInputStream implements LittleEndianInput { public LittleEndianByteArrayInputStream(byte[] buf, int startOffset, int maxReadLen) { // NOSONAR - _buf = buf; - _readIndex = startOffset; - _endIndex = startOffset + maxReadLen; + super(buf, startOffset, maxReadLen); } + public LittleEndianByteArrayInputStream(byte[] buf, int startOffset) { - this(buf, startOffset, buf.length - startOffset); + super(buf, startOffset, buf.length - startOffset); } + public LittleEndianByteArrayInputStream(byte[] buf) { - this(buf, 0, buf.length); + super(buf); } - public int available() { - return _endIndex - _readIndex; - } private void checkPosition(int i) { - if (i > _endIndex - _readIndex) { + if (i > count - pos) { throw new RuntimeException("Buffer overrun"); } } public int getReadIndex() { - return _readIndex; + return pos; } - public byte readByte() { + + @Override + public byte readByte() { checkPosition(1); - return _buf[_readIndex++]; + return (byte)read(); } - public int readInt() { - checkPosition(4); - int i = _readIndex; - - int b0 = _buf[i++] & 0xFF; - int b1 = _buf[i++] & 0xFF; - int b2 = _buf[i++] & 0xFF; - int b3 = _buf[i++] & 0xFF; - _readIndex = i; - return (b3 << 24) + (b2 << 16) + (b1 << 8) + (b0 << 0); + @Override + public int readInt() { + final int size = LittleEndianConsts.INT_SIZE; + checkPosition(size); + int le = LittleEndian.getInt(buf, pos); + super.skip(size); + return le; } - public long readLong() { - checkPosition(8); - int i = _readIndex; - int b0 = _buf[i++] & 0xFF; - int b1 = _buf[i++] & 0xFF; - int b2 = _buf[i++] & 0xFF; - int b3 = _buf[i++] & 0xFF; - int b4 = _buf[i++] & 0xFF; - int b5 = _buf[i++] & 0xFF; - int b6 = _buf[i++] & 0xFF; - int b7 = _buf[i++] & 0xFF; - _readIndex = i; - return (((long)b7 << 56) + - ((long)b6 << 48) + - ((long)b5 << 40) + - ((long)b4 << 32) + - ((long)b3 << 24) + - (b2 << 16) + - (b1 << 8) + - (b0 << 0)); + @Override + public long readLong() { + final int size = LittleEndianConsts.LONG_SIZE; + checkPosition(size); + long le = LittleEndian.getLong(buf, pos); + super.skip(size); + return le; } - public short readShort() { + + @Override + public short readShort() { return (short)readUShort(); } - public int readUByte() { - checkPosition(1); - return _buf[_readIndex++] & 0xFF; - } - public int readUShort() { - checkPosition(2); - int i = _readIndex; - int b0 = _buf[i++] & 0xFF; - int b1 = _buf[i++] & 0xFF; - _readIndex = i; - return (b1 << 8) + (b0 << 0); + @Override + public int readUByte() { + return readByte() & 0xFF; } - public void readFully(byte[] buf, int off, int len) { + + @Override + public int readUShort() { + final int size = LittleEndianConsts.SHORT_SIZE; + checkPosition(size); + int le = LittleEndian.getUShort(buf, pos); + super.skip(size); + return le; + } + + @Override + public double readDouble() { + return Double.longBitsToDouble(readLong()); + } + + @Override + public void readFully(byte[] buffer, int off, int len) { checkPosition(len); - System.arraycopy(_buf, _readIndex, buf, off, len); - _readIndex+=len; + read(buffer, off, len); } - public void readFully(byte[] buf) { - readFully(buf, 0, buf.length); - } - public double readDouble() { - return Double.longBitsToDouble(readLong()); + + @Override + public void readFully(byte[] buffer) { + checkPosition(buffer.length); + read(buffer, 0, buffer.length); } } diff --git a/src/java/org/apache/poi/util/LittleEndianByteArrayOutputStream.java b/src/java/org/apache/poi/util/LittleEndianByteArrayOutputStream.java index 081309cc2..fe9949f1a 100644 --- a/src/java/org/apache/poi/util/LittleEndianByteArrayOutputStream.java +++ b/src/java/org/apache/poi/util/LittleEndianByteArrayOutputStream.java @@ -17,28 +17,26 @@ package org.apache.poi.util; +import java.io.OutputStream; /** - * Adapts a plain byte array to {@link LittleEndianOutput} - * - * - * @author Josh Micich + * Adapts a plain byte array to {@link LittleEndianOutput} */ -public final class LittleEndianByteArrayOutputStream implements LittleEndianOutput, DelayableLittleEndianOutput { +public final class LittleEndianByteArrayOutputStream extends OutputStream implements LittleEndianOutput, DelayableLittleEndianOutput { private final byte[] _buf; private final int _endIndex; private int _writeIndex; public LittleEndianByteArrayOutputStream(byte[] buf, int startOffset, int maxWriteLen) { // NOSONAR if (startOffset < 0 || startOffset > buf.length) { - throw new IllegalArgumentException("Specified startOffset (" + startOffset + throw new IllegalArgumentException("Specified startOffset (" + startOffset + ") is out of allowable range (0.." + buf.length + ")"); } _buf = buf; _writeIndex = startOffset; _endIndex = startOffset + maxWriteLen; if (_endIndex < startOffset || _endIndex > buf.length) { - throw new IllegalArgumentException("calculated end index (" + _endIndex + throw new IllegalArgumentException("calculated end index (" + _endIndex + ") is out of allowable range (" + _writeIndex + ".." + buf.length + ")"); } } @@ -52,16 +50,19 @@ public final class LittleEndianByteArrayOutputStream implements LittleEndianOutp } } - public void writeByte(int v) { + @Override + public void writeByte(int v) { checkPosition(1); _buf[_writeIndex++] = (byte)v; } - public void writeDouble(double v) { + @Override + public void writeDouble(double v) { writeLong(Double.doubleToLongBits(v)); } - public void writeInt(int v) { + @Override + public void writeInt(int v) { checkPosition(4); int i = _writeIndex; _buf[i++] = (byte)((v >>> 0) & 0xFF); @@ -71,33 +72,47 @@ public final class LittleEndianByteArrayOutputStream implements LittleEndianOutp _writeIndex = i; } - public void writeLong(long v) { + @Override + public void writeLong(long v) { writeInt((int)(v >> 0)); writeInt((int)(v >> 32)); } - public void writeShort(int v) { + @Override + public void writeShort(int v) { checkPosition(2); int i = _writeIndex; _buf[i++] = (byte)((v >>> 0) & 0xFF); _buf[i++] = (byte)((v >>> 8) & 0xFF); _writeIndex = i; } - public void write(byte[] b) { + + @Override + public void write(int b) { + writeByte(b); + } + + @Override + public void write(byte[] b) { int len = b.length; checkPosition(len); System.arraycopy(b, 0, _buf, _writeIndex, len); _writeIndex += len; } - public void write(byte[] b, int offset, int len) { + + @Override + public void write(byte[] b, int offset, int len) { checkPosition(len); System.arraycopy(b, offset, _buf, _writeIndex, len); _writeIndex += len; } + public int getWriteIndex() { return _writeIndex; } - public LittleEndianOutput createDelayedOutput(int size) { + + @Override + public LittleEndianOutput createDelayedOutput(int size) { checkPosition(size); LittleEndianOutput result = new LittleEndianByteArrayOutputStream(_buf, _writeIndex, size); _writeIndex += size; diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java index f8c9bb551..480137ef8 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java @@ -45,7 +45,7 @@ import org.apache.poi.poifs.crypt.CipherAlgorithm; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.EncryptionHeader; -import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.EncryptionVerifier; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry; @@ -56,14 +56,14 @@ import org.apache.poi.util.LittleEndian; /** * Decryptor implementation for Agile Encryption */ -public class AgileDecryptor extends Decryptor { +public class AgileDecryptor extends Decryptor implements Cloneable { private long _length = -1; - protected static final byte[] kVerifierInputBlock; - protected static final byte[] kHashedVerifierBlock; - protected static final byte[] kCryptoKeyBlock; - protected static final byte[] kIntegrityKeyBlock; - protected static final byte[] kIntegrityValueBlock; + /* package */ static final byte[] kVerifierInputBlock; + /* package */ static final byte[] kHashedVerifierBlock; + /* package */ static final byte[] kCryptoKeyBlock; + /* package */ static final byte[] kIntegrityKeyBlock; + /* package */ static final byte[] kIntegrityValueBlock; static { kVerifierInputBlock = @@ -83,16 +83,16 @@ public class AgileDecryptor extends Decryptor { (byte)0xb2, (byte)0x2c, (byte)0x84, (byte)0x33 }; } - protected AgileDecryptor(AgileEncryptionInfoBuilder builder) { - super(builder); + protected AgileDecryptor() { } /** * set decryption password */ + @Override public boolean verifyPassword(String password) throws GeneralSecurityException { - AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier(); - AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader(); + AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); + AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); int blockSize = header.getBlockSize(); @@ -113,7 +113,7 @@ public class AgileDecryptor extends Decryptor { * blockSize bytes. * 4. Use base64 to encode the result of step 3. */ - byte verfierInputEnc[] = hashInput(builder, pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE); + byte verfierInputEnc[] = hashInput(getEncryptionInfo(), pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE); setVerifier(verfierInputEnc); MessageDigest hashMD = getMessageDigest(hashAlgo); byte[] verifierHash = hashMD.digest(verfierInputEnc); @@ -130,7 +130,7 @@ public class AgileDecryptor extends Decryptor { * blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes. * 4. Use base64 to encode the result of step 3. */ - byte verifierHashDec[] = hashInput(builder, pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE); + byte verifierHashDec[] = hashInput(getEncryptionInfo(), pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE); verifierHashDec = getBlock0(verifierHashDec, hashAlgo.hashSize); /** @@ -146,7 +146,7 @@ public class AgileDecryptor extends Decryptor { * blockSize bytes. * 4. Use base64 to encode the result of step 3. */ - byte keyspec[] = hashInput(builder, pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE); + byte keyspec[] = hashInput(getEncryptionInfo(), pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE); keyspec = getBlock0(keyspec, keySize); SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId); @@ -204,8 +204,8 @@ public class AgileDecryptor extends Decryptor { * @throws GeneralSecurityException */ public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException { - AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier(); - AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader(); + AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); + AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); int blockSize = header.getBlockSize(); @@ -217,7 +217,9 @@ public class AgileDecryptor extends Decryptor { break; } } - if (ace == null) return false; + if (ace == null) { + return false; + } Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); @@ -255,9 +257,9 @@ public class AgileDecryptor extends Decryptor { return fillSize; } - protected static byte[] hashInput(EncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) { - EncryptionVerifier ver = builder.getVerifier(); - AgileDecryptor dec = (AgileDecryptor)builder.getDecryptor(); + protected static byte[] hashInput(EncryptionInfo encryptionInfo, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) { + EncryptionVerifier ver = encryptionInfo.getVerifier(); + AgileDecryptor dec = (AgileDecryptor)encryptionInfo.getDecryptor(); int keySize = dec.getKeySizeInBytes(); int blockSize = dec.getBlockSizeInBytes(); HashAlgorithm hashAlgo = ver.getHashAlgorithm(); @@ -278,6 +280,7 @@ public class AgileDecryptor extends Decryptor { } } + @Override @SuppressWarnings("resource") public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY); @@ -285,17 +288,20 @@ public class AgileDecryptor extends Decryptor { return new AgileCipherInputStream(dis, _length); } + @Override public long getLength(){ - if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called"); + if(_length == -1) { + throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called"); + } return _length; } - protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfoBuilder builder, SecretKey skey, int encryptionMode) + protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfo encryptionInfo, SecretKey skey, int encryptionMode) throws GeneralSecurityException { - EncryptionHeader header = builder.getHeader(); - if (existing == null || lastChunk) { - String padding = (lastChunk ? "PKCS5Padding" : "NoPadding"); + EncryptionHeader header = encryptionInfo.getHeader(); + String padding = (lastChunk ? "PKCS5Padding" : "NoPadding"); + if (existing == null || !existing.getAlgorithm().endsWith(padding)) { existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding); } @@ -339,9 +345,15 @@ public class AgileDecryptor extends Decryptor { // TODO: calculate integrity hmac while reading the stream // for a post-validation of the data + @Override protected Cipher initCipherForBlock(Cipher cipher, int block) throws GeneralSecurityException { - return AgileDecryptor.initCipherForBlock(cipher, block, false, builder, getSecretKey(), Cipher.DECRYPT_MODE); + return AgileDecryptor.initCipherForBlock(cipher, block, false, getEncryptionInfo(), getSecretKey(), Cipher.DECRYPT_MODE); } } + + @Override + public AgileDecryptor clone() throws CloneNotSupportedException { + return (AgileDecryptor)super.clone(); + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java index a5fb14428..88bccabf6 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java @@ -27,7 +27,7 @@ import com.microsoft.schemas.office.x2006.encryption.CTKeyData; import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; import com.microsoft.schemas.office.x2006.encryption.STCipherChaining; -public class AgileEncryptionHeader extends EncryptionHeader { +public class AgileEncryptionHeader extends EncryptionHeader implements Cloneable { private byte encryptedHmacKey[], encryptedHmacValue[]; public AgileEncryptionHeader(String descriptor) { @@ -99,6 +99,7 @@ public class AgileEncryptionHeader extends EncryptionHeader { } // make method visible for this package + @Override protected void setKeySalt(byte salt[]) { if (salt == null || salt.length != getBlockSize()) { throw new EncryptedDocumentException("invalid verifier salt"); @@ -121,4 +122,13 @@ public class AgileEncryptionHeader extends EncryptionHeader { protected void setEncryptedHmacValue(byte[] encryptedHmacValue) { this.encryptedHmacValue = (encryptedHmacValue == null) ? null : encryptedHmacValue.clone(); } + + @Override + public AgileEncryptionHeader clone() throws CloneNotSupportedException { + AgileEncryptionHeader other = (AgileEncryptionHeader)super.clone(); + other.encryptedHmacKey = (encryptedHmacKey == null) ? null : encryptedHmacKey.clone(); + other.encryptedHmacValue = (encryptedHmacValue == null) ? null : encryptedHmacValue.clone(); + return other; + } + } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java index ecf7fe8e6..acfa01581 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java @@ -35,30 +35,24 @@ import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder { - EncryptionInfo info; - AgileEncryptionHeader header; - AgileEncryptionVerifier verifier; - AgileDecryptor decryptor; - AgileEncryptor encryptor; - @Override - public void initialize(EncryptionInfo ei, LittleEndianInput dis) throws IOException { - this.info = ei; - + public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException { EncryptionDocument ed = parseDescriptor((InputStream)dis); - header = new AgileEncryptionHeader(ed); - verifier = new AgileEncryptionVerifier(ed); - if (ei.getVersionMajor() == EncryptionMode.agile.versionMajor - && ei.getVersionMinor() == EncryptionMode.agile.versionMinor) { - decryptor = new AgileDecryptor(this); - encryptor = new AgileEncryptor(this); + info.setHeader(new AgileEncryptionHeader(ed)); + info.setVerifier(new AgileEncryptionVerifier(ed)); + if (info.getVersionMajor() == EncryptionMode.agile.versionMajor + && info.getVersionMinor() == EncryptionMode.agile.versionMinor) { + AgileDecryptor dec = new AgileDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + AgileEncryptor enc = new AgileEncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } } @Override - public void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { - this.info = ei; - + public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { if (cipherAlgorithm == null) { cipherAlgorithm = CipherAlgorithm.aes128; } @@ -87,30 +81,14 @@ public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder { if (!found) { throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for Cipher "+cipherAlgorithm.toString()); } - header = new AgileEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - verifier = new AgileEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); - decryptor = new AgileDecryptor(this); - encryptor = new AgileEncryptor(this); - } - - public AgileEncryptionHeader getHeader() { - return header; - } - - public AgileEncryptionVerifier getVerifier() { - return verifier; - } - - public AgileDecryptor getDecryptor() { - return decryptor; - } - - public AgileEncryptor getEncryptor() { - return encryptor; - } - - protected EncryptionInfo getInfo() { - return info; + info.setHeader(new AgileEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + info.setVerifier(new AgileEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode)); + AgileDecryptor dec = new AgileDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + AgileEncryptor enc = new AgileEncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); } protected static EncryptionDocument parseDescriptor(String descriptor) { diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java index e2910431a..53d4cd6ed 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java @@ -39,7 +39,7 @@ import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEnc /** * Used when checking if a key is valid for a document */ -public class AgileEncryptionVerifier extends EncryptionVerifier { +public class AgileEncryptionVerifier extends EncryptionVerifier implements Cloneable { public static class AgileCertificateEntry { X509Certificate x509; @@ -87,8 +87,9 @@ public class AgileEncryptionVerifier extends EncryptionVerifier { setEncryptedVerifierHash(keyData.getEncryptedVerifierHashValue()); int saltSize = keyData.getSaltSize(); - if (saltSize != getSalt().length) + if (saltSize != getSalt().length) { throw new EncryptedDocumentException("Invalid salt size"); + } switch (keyData.getCipherChaining().intValue()) { case STCipherChaining.INT_CHAINING_MODE_CBC: @@ -101,7 +102,9 @@ public class AgileEncryptionVerifier extends EncryptionVerifier { throw new EncryptedDocumentException("Unsupported chaining mode - "+keyData.getCipherChaining().toString()); } - if (!encList.hasNext()) return; + if (!encList.hasNext()) { + return; + } try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); @@ -125,6 +128,7 @@ public class AgileEncryptionVerifier extends EncryptionVerifier { setSpinCount(100000); // TODO: use parameter } + @Override protected void setSalt(byte salt[]) { if (salt == null || salt.length != getCipherAlgorithm().blockSize) { throw new EncryptedDocumentException("invalid verifier salt"); @@ -133,16 +137,19 @@ public class AgileEncryptionVerifier extends EncryptionVerifier { } // make method visible for this package + @Override protected void setEncryptedVerifier(byte encryptedVerifier[]) { super.setEncryptedVerifier(encryptedVerifier); } // make method visible for this package + @Override protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { super.setEncryptedVerifierHash(encryptedVerifierHash); } // make method visible for this package + @Override protected void setEncryptedKey(byte[] encryptedKey) { super.setEncryptedKey(encryptedKey); } @@ -156,4 +163,12 @@ public class AgileEncryptionVerifier extends EncryptionVerifier { public List getCertificates() { return certList; } + + @Override + public AgileEncryptionVerifier clone() throws CloneNotSupportedException { + AgileEncryptionVerifier other = (AgileEncryptionVerifier)super.clone(); + // TODO: deep copy of certList + other.certList = new ArrayList(certList); + return other; + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java index c9601f1ba..5ff45ad81 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java @@ -60,6 +60,7 @@ import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianConsts; import org.apache.xmlbeans.XmlOptions; import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity; @@ -74,21 +75,20 @@ import com.microsoft.schemas.office.x2006.encryption.STHashAlgorithm; import com.microsoft.schemas.office.x2006.keyEncryptor.certificate.CTCertificateKeyEncryptor; import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEncryptor; -public class AgileEncryptor extends Encryptor { - private final AgileEncryptionInfoBuilder builder; +public class AgileEncryptor extends Encryptor implements Cloneable { private byte integritySalt[]; private byte pwHash[]; - protected AgileEncryptor(AgileEncryptionInfoBuilder builder) { - this.builder = builder; + protected AgileEncryptor() { } + @Override public void confirmPassword(String password) { // see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier Random r = new SecureRandom(); - int blockSize = builder.getHeader().getBlockSize(); - int keySize = builder.getHeader().getKeySize()/8; - int hashSize = builder.getHeader().getHashAlgorithmEx().hashSize; + int blockSize = getEncryptionInfo().getHeader().getBlockSize(); + int keySize = getEncryptionInfo().getHeader().getKeySize()/8; + int hashSize = getEncryptionInfo().getHeader().getHashAlgorithmEx().hashSize; byte[] newVerifierSalt = new byte[blockSize] , newVerifier = new byte[blockSize] @@ -104,10 +104,11 @@ public class AgileEncryptor extends Encryptor { confirmPassword(password, newKeySpec, newKeySalt, newVerifierSalt, newVerifier, newIntegritySalt); } - public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { - AgileEncryptionVerifier ver = builder.getVerifier(); + @Override + public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { + AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); ver.setSalt(verifierSalt); - AgileEncryptionHeader header = builder.getHeader(); + AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); header.setKeySalt(keySalt); HashAlgorithm hashAlgo = ver.getHashAlgorithm(); @@ -128,7 +129,7 @@ public class AgileEncryptor extends Encryptor { * blockSize bytes. * 4. Use base64 to encode the result of step 3. */ - byte encryptedVerifier[] = hashInput(builder, pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE); + byte encryptedVerifier[] = hashInput(getEncryptionInfo(), pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE); ver.setEncryptedVerifier(encryptedVerifier); @@ -146,7 +147,7 @@ public class AgileEncryptor extends Encryptor { */ MessageDigest hashMD = getMessageDigest(hashAlgo); byte[] hashedVerifier = hashMD.digest(verifier); - byte encryptedVerifierHash[] = hashInput(builder, pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE); + byte encryptedVerifierHash[] = hashInput(getEncryptionInfo(), pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE); ver.setEncryptedVerifierHash(encryptedVerifierHash); /** @@ -162,7 +163,7 @@ public class AgileEncryptor extends Encryptor { * blockSize bytes. * 4. Use base64 to encode the result of step 3. */ - byte encryptedKey[] = hashInput(builder, pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE); + byte encryptedKey[] = hashInput(getEncryptionInfo(), pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE); ver.setEncryptedKey(encryptedKey); SecretKey secretKey = new SecretKeySpec(keySpec, ver.getCipherAlgorithm().jceId); @@ -214,6 +215,7 @@ public class AgileEncryptor extends Encryptor { } } + @Override public OutputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { // TODO: initialize headers @@ -234,14 +236,14 @@ public class AgileEncryptor extends Encryptor { // as the integrity hmac needs to contain the StreamSize, // it's not possible to calculate it on-the-fly while buffering // TODO: add stream size parameter to getDataStream() - AgileEncryptionVerifier ver = builder.getVerifier(); + AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); HashAlgorithm hashAlgo = ver.getHashAlgorithm(); Mac integrityMD = CryptoFunctions.getMac(hashAlgo); integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId)); byte buf[] = new byte[1024]; LittleEndian.putLong(buf, 0, oleStreamSize); - integrityMD.update(buf, 0, LittleEndian.LONG_SIZE); + integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE); InputStream fis = new FileInputStream(tmpFile); try { @@ -255,7 +257,7 @@ public class AgileEncryptor extends Encryptor { byte hmacValue[] = integrityMD.doFinal(); - AgileEncryptionHeader header = builder.getHeader(); + AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); int blockSize = header.getBlockSize(); byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize); Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE); @@ -271,8 +273,8 @@ public class AgileEncryptor extends Encryptor { CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE; protected EncryptionDocument createEncryptionDocument() { - AgileEncryptionVerifier ver = builder.getVerifier(); - AgileEncryptionHeader header = builder.getHeader(); + AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); + AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); EncryptionDocument ed = EncryptionDocument.Factory.newInstance(); CTEncryption edRoot = ed.addNewEncryption(); @@ -379,9 +381,10 @@ public class AgileEncryptor extends Encryptor { throws IOException, GeneralSecurityException { DataSpaceMapUtils.addDefaultDataSpace(dir); - final EncryptionInfo info = builder.getInfo(); + final EncryptionInfo info = getEncryptionInfo(); EncryptionRecord er = new EncryptionRecord(){ + @Override public void write(LittleEndianByteArrayOutputStream bos) { // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004 @@ -422,7 +425,7 @@ public class AgileEncryptor extends Encryptor { @Override protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk) throws GeneralSecurityException { - return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); } @Override @@ -439,4 +442,11 @@ public class AgileEncryptor extends Encryptor { } } + @Override + public AgileEncryptor clone() throws CloneNotSupportedException { + AgileEncryptor other = (AgileEncryptor)super.clone(); + other.integritySalt = (integritySalt == null) ? null : integritySalt.clone(); + other.pwHash = (pwHash == null) ? null : pwHash.clone(); + return other; + } } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java index 0d441d68d..2cefa95cf 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java @@ -16,8 +16,7 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; @@ -94,6 +93,7 @@ public class TestAgileEncryptionParameters { os.close(); bos.reset(); fsEnc.writeFilesystem(bos); + fsEnc.close(); POIFSFileSystem fsDec = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); EncryptionInfo infoDec = new EncryptionInfo(fsDec); @@ -103,6 +103,7 @@ public class TestAgileEncryptionParameters { InputStream is = dec.getDataStream(fsDec); byte actualData[] = IOUtils.toByteArray(is); is.close(); - assertThat("Failed roundtrip - "+ca+"-"+ha+"-"+cm, testData, equalTo(actualData)); + fsDec.close(); + assertArrayEquals("Failed roundtrip - "+ca+"-"+ha+"-"+cm, testData, actualData); } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java index 3d77d92d2..ece68eef7 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java @@ -17,6 +17,8 @@ package org.apache.poi.hslf.usermodel; +import java.io.Closeable; +import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; import java.util.ArrayList; @@ -25,9 +27,6 @@ import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; - import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException; import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException; import org.apache.poi.hslf.record.DocumentEncryptionAtom; @@ -36,26 +35,45 @@ import org.apache.poi.hslf.record.PositionDependentRecord; import org.apache.poi.hslf.record.Record; import org.apache.poi.hslf.record.UserEditAtom; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor; import org.apache.poi.util.BitField; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianByteArrayInputStream; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; /** * This class provides helper functions for encrypted PowerPoint documents. */ @Internal -public class HSLFSlideShowEncrypted { +public class HSLFSlideShowEncrypted implements Closeable { DocumentEncryptionAtom dea; CryptoAPIEncryptor enc = null; CryptoAPIDecryptor dec = null; - Cipher cipher = null; - CipherOutputStream cyos = null; +// Cipher cipher = null; + ChunkedCipherOutputStream cyos = null; private static final BitField fieldRecInst = new BitField(0xFFF0); + + private static final int BLIB_STORE_ENTRY_PARTS[] = { + 1, // btWin32 + 1, // btMacOS + 16, // rgbUid + 2, // tag + 4, // size + 4, // cRef + 4, // foDelay + 1, // unused1 + 1, // cbName (@ index 33) + 1, // unused2 + 1, // unused3 + }; protected HSLFSlideShowEncrypted(DocumentEncryptionAtom dea) { this.dea = dea; @@ -67,7 +85,9 @@ public class HSLFSlideShowEncrypted { UserEditAtom userEditAtomWithEncryption = null; for (Map.Entry me : recordMap.descendingMap().entrySet()) { Record r = me.getValue(); - if (!(r instanceof UserEditAtom)) continue; + if (!(r instanceof UserEditAtom)) { + continue; + } UserEditAtom uea = (UserEditAtom)r; if (uea.getEncryptSessionPersistIdRef() != -1) { userEditAtomWithEncryption = uea; @@ -83,7 +103,7 @@ public class HSLFSlideShowEncrypted { Record r = recordMap.get(userEditAtomWithEncryption.getPersistPointersOffset()); assert(r instanceof PersistPtrHolder); PersistPtrHolder ptr = (PersistPtrHolder)r; - + Integer encOffset = ptr.getSlideLocationsLookup().get(userEditAtomWithEncryption.getEncryptSessionPersistIdRef()); if (encOffset == null) { // encryption info doesn't exist anymore @@ -91,7 +111,7 @@ public class HSLFSlideShowEncrypted { dea = null; return; } - + r = recordMap.get(encOffset); if (r == null) { r = Record.buildRecordAtOffset(docstream, encOffset); @@ -100,7 +120,7 @@ public class HSLFSlideShowEncrypted { assert(r instanceof DocumentEncryptionAtom); this.dea = (DocumentEncryptionAtom)r; decryptInit(); - + String pass = Biff8EncryptionKey.getCurrentUserPassword(); if(!dec.verifyPassword(pass != null ? pass : Decryptor.DEFAULT_PASSWORD)) { throw new EncryptedPowerPointFileException("PowerPoint file is encrypted. The correct password needs to be set via Biff8EncryptionKey.setCurrentUserPassword()"); @@ -110,119 +130,144 @@ public class HSLFSlideShowEncrypted { public DocumentEncryptionAtom getDocumentEncryptionAtom() { return dea; } - - protected void setPersistId(int persistId) { - if (enc != null && dec != null) { - throw new EncryptedPowerPointFileException("Use instance either for en- or decryption"); - } - - try { - if (enc != null) cipher = enc.initCipherForBlock(cipher, persistId); - if (dec != null) cipher = dec.initCipherForBlock(cipher, persistId); - } catch (GeneralSecurityException e) { - throw new EncryptedPowerPointFileException(e); - } - } - + protected void decryptInit() { - if (dec != null) return; + if (dec != null) { + return; + } EncryptionInfo ei = dea.getEncryptionInfo(); dec = (CryptoAPIDecryptor)ei.getDecryptor(); } - + protected void encryptInit() { - if (enc != null) return; + if (enc != null) { + return; + } EncryptionInfo ei = dea.getEncryptionInfo(); enc = (CryptoAPIEncryptor)ei.getEncryptor(); } - - + + protected OutputStream encryptRecord(OutputStream plainStream, int persistId, Record record) { - boolean isPlain = (dea == null + boolean isPlain = (dea == null || record instanceof UserEditAtom || record instanceof PersistPtrHolder || record instanceof DocumentEncryptionAtom ); - if (isPlain) return plainStream; - encryptInit(); - setPersistId(persistId); - - if (cyos == null) { - cyos = new CipherOutputStream(plainStream, cipher); + try { + if (isPlain) { + if (cyos != null) { + // write cached data to stream + cyos.flush(); + } + return plainStream; + } + + encryptInit(); + + if (cyos == null) { + enc.setChunkSize(-1); + cyos = enc.getDataStream(plainStream); + } + cyos.initCipherForBlock(persistId, false); + } catch (Exception e) { + throw new EncryptedPowerPointFileException(e); } return cyos; } + private static void readFully(ChunkedCipherInputStream ccis, byte[] docstream, int offset, int len) throws IOException { + if (IOUtils.readFully(ccis, docstream, offset, len) == -1) { + throw new EncryptedPowerPointFileException("unexpected EOF"); + } + } + protected void decryptRecord(byte[] docstream, int persistId, int offset) { - if (dea == null) return; + if (dea == null) { + return; + } decryptInit(); - setPersistId(persistId); - + dec.setChunkSize(-1); + LittleEndianByteArrayInputStream lei = new LittleEndianByteArrayInputStream(docstream, offset); + ChunkedCipherInputStream ccis = null; try { + ccis = dec.getDataStream(lei, docstream.length-offset, 0); + ccis.initCipherForBlock(persistId); + // decrypt header and read length to be decrypted - cipher.update(docstream, offset, 8, docstream, offset); + readFully(ccis, docstream, offset, 8); // decrypt the rest of the record int rlen = (int)LittleEndian.getUInt(docstream, offset+4); - cipher.update(docstream, offset+8, rlen, docstream, offset+8); - } catch (GeneralSecurityException e) { - throw new CorruptPowerPointFileException(e); - } - } + readFully(ccis, docstream, offset+8, rlen); + + } catch (Exception e) { + throw new EncryptedPowerPointFileException(e); + } finally { + try { + if (ccis != null) { + ccis.close(); + } + lei.close(); + } catch (IOException e) { + throw new EncryptedPowerPointFileException(e); + } + } + } + + private void decryptPicBytes(byte[] pictstream, int offset, int len) + throws IOException, GeneralSecurityException { + // when reading the picture elements, each time a segment is read, the cipher needs + // to be reset (usually done when calling Cipher.doFinal) + LittleEndianByteArrayInputStream lei = new LittleEndianByteArrayInputStream(pictstream, offset); + ChunkedCipherInputStream ccis = dec.getDataStream(lei, len, 0); + readFully(ccis, pictstream, offset, len); + ccis.close(); + lei.close(); + } protected void decryptPicture(byte[] pictstream, int offset) { - if (dea == null) return; - + if (dea == null) { + return; + } + decryptInit(); - setPersistId(0); - + try { // decrypt header and read length to be decrypted - cipher.doFinal(pictstream, offset, 8, pictstream, offset); + decryptPicBytes(pictstream, offset, 8); int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset)); int recType = LittleEndian.getUShort(pictstream, offset+2); int rlen = (int)LittleEndian.getUInt(pictstream, offset+4); offset += 8; - int endOffset = offset + rlen; + int endOffset = offset + rlen; if (recType == 0xF007) { // TOOD: get a real example file ... to actual test the FBSE entry // not sure where the foDelay block is - + // File BLIP Store Entry (FBSE) - cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32 - offset++; - cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS - offset++; - cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid - offset += 16; - cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag - offset += 2; - cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size - offset += 4; - cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef - offset += 4; - cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay - offset += 4; - cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1 - cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName - cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2 - cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3 - int cbName = LittleEndian.getUShort(pictstream, offset+1); - offset += 4; + for (int part : BLIB_STORE_ENTRY_PARTS) { + decryptPicBytes(pictstream, offset, part); + } + offset += 36; + + int cbName = LittleEndian.getUShort(pictstream, offset-3); if (cbName > 0) { - cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData + // read nameData + decryptPicBytes(pictstream, offset, cbName); offset += cbName; } + if (offset == endOffset) { return; // no embedded blip } // fall through, read embedded blip now // update header data - cipher.doFinal(pictstream, offset, 8, pictstream, offset); + decryptPicBytes(pictstream, offset, 8); recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset)); recType = LittleEndian.getUShort(pictstream, offset+2); // rlen = (int)LittleEndian.getUInt(pictstream, offset+4); @@ -231,70 +276,73 @@ public class HSLFSlideShowEncrypted { int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 || recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1; - + + // rgbUid 1/2 for (int i=0; i 0) { - cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData + ccos.write(pictstream, offset, cbName); + ccos.flush(); offset += cbName; } + if (offset == endOffset) { return; // no embedded blip } @@ -303,32 +351,45 @@ public class HSLFSlideShowEncrypted { // update header data recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset)); recType = LittleEndian.getUShort(pictstream, offset+2); - // rlen = (int) LittleEndian.getUInt(pictstream, offset+4); - cipher.doFinal(pictstream, offset, 8, pictstream, offset); + ccos.write(pictstream, offset, 8); + ccos.flush(); offset += 8; } - + int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 || recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1; - + for (int i=0; i slideLocations = new TreeMap(); @@ -386,7 +447,7 @@ public class HSLFSlideShowEncrypted { uea = (UserEditAtom)pdr; continue; } - + if (pdr instanceof PersistPtrHolder) { if (pph != null) { duplicatedCount++; @@ -394,16 +455,18 @@ public class HSLFSlideShowEncrypted { pph = (PersistPtrHolder)pdr; for (Map.Entry me : pph.getSlideLocationsLookup().entrySet()) { Integer oldOffset = slideLocations.put(me.getKey(), me.getValue()); - if (oldOffset != null) obsoleteOffsets.add(oldOffset); + if (oldOffset != null) { + obsoleteOffsets.add(oldOffset); + } } continue; } - + recordMap.put(pdr.getLastOnDiskOffset(), r); } - + assert(uea != null && pph != null && uea.getPersistPointersOffset() == pph.getLastOnDiskOffset()); - + recordMap.put(pph.getLastOnDiskOffset(), pph); recordMap.put(uea.getLastOnDiskOffset(), uea); @@ -416,15 +479,15 @@ public class HSLFSlideShowEncrypted { for (Map.Entry me : slideLocations.entrySet()) { pph.addSlideLookup(me.getKey(), me.getValue()); } - + for (Integer oldOffset : obsoleteOffsets) { recordMap.remove(oldOffset); } - + return recordMap.values().toArray(new Record[recordMap.size()]); } - - + + protected static Record[] removeEncryptionRecord(Record records[]) { int deaSlideId = -1; int deaOffset = -1; @@ -444,23 +507,27 @@ public class HSLFSlideShowEncrypted { } recordList.add(r); } - + assert(ptr != null); - if (deaSlideId == -1 && deaOffset == -1) return records; - + if (deaSlideId == -1 && deaOffset == -1) { + return records; + } + TreeMap tm = new TreeMap(ptr.getSlideLocationsLookup()); ptr.clear(); int maxSlideId = -1; for (Map.Entry me : tm.entrySet()) { - if (me.getKey() == deaSlideId || me.getValue() == deaOffset) continue; + if (me.getKey() == deaSlideId || me.getValue() == deaOffset) { + continue; + } ptr.addSlideLookup(me.getKey(), me.getValue()); maxSlideId = Math.max(me.getKey(), maxSlideId); } - + uea.setMaxPersistWritten(maxSlideId); records = recordList.toArray(new Record[recordList.size()]); - + return records; } @@ -470,9 +537,13 @@ public class HSLFSlideShowEncrypted { int ueaIdx = -1, ptrIdx = -1, deaIdx = -1, idx = -1; for (Record r : records) { idx++; - if (r instanceof UserEditAtom) ueaIdx = idx; - else if (r instanceof PersistPtrHolder) ptrIdx = idx; - else if (r instanceof DocumentEncryptionAtom) deaIdx = idx; + if (r instanceof UserEditAtom) { + ueaIdx = idx; + } else if (r instanceof PersistPtrHolder) { + ptrIdx = idx; + } else if (r instanceof DocumentEncryptionAtom) { + deaIdx = idx; + } } assert(ueaIdx != -1 && ptrIdx != -1 && ptrIdx < ueaIdx); if (deaIdx != -1) { @@ -488,13 +559,22 @@ public class HSLFSlideShowEncrypted { ptr.addSlideLookup(nextSlideId, ptr.getLastOnDiskOffset()-1); uea.setEncryptSessionPersistIdRef(nextSlideId); uea.setMaxPersistWritten(nextSlideId); - + Record newRecords[] = new Record[records.length+1]; - if (ptrIdx > 0) System.arraycopy(records, 0, newRecords, 0, ptrIdx); - if (ptrIdx < records.length-1) System.arraycopy(records, ptrIdx, newRecords, ptrIdx+1, records.length-ptrIdx); + if (ptrIdx > 0) { + System.arraycopy(records, 0, newRecords, 0, ptrIdx); + } + if (ptrIdx < records.length-1) { + System.arraycopy(records, ptrIdx, newRecords, ptrIdx+1, records.length-ptrIdx); + } newRecords[ptrIdx] = dea; return newRecords; } } + public void close() throws IOException { + if (cyos != null) { + cyos.close(); + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowImpl.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowImpl.java index 4e4c28d2b..9f4fada4d 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowImpl.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowImpl.java @@ -54,6 +54,7 @@ import org.apache.poi.poifs.filesystem.EntryUtils; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.sl.usermodel.PictureData.PictureType; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -205,14 +206,12 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { // Grab the document stream int len = docProps.getSize(); - _docstream = new byte[len]; - InputStream is = directory.createDocumentInputStream("PowerPoint Document"); - int readLen = is.read(_docstream); - is.close(); - - if (len != readLen) { - throw new IOException("Document input stream ended prematurely - expected "+len+" bytes - received "+readLen+" bytes"); - } + InputStream is = directory.createDocumentInputStream("PowerPoint Document"); + try { + _docstream = IOUtils.toByteArray(is, len); + } finally { + is.close(); + } } /** @@ -364,17 +363,10 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { HSLFSlideShowEncrypted decryptData = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom()); DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures"); - int len = entry.getSize(); - byte[] pictstream = new byte[len]; - DocumentInputStream is = directory.createDocumentInputStream(entry); - int readLen = is.read(pictstream); + DocumentInputStream is = directory.createDocumentInputStream(entry); + byte[] pictstream = IOUtils.toByteArray(is, entry.getSize()); is.close(); - if (len != readLen) { - throw new IOException("Picture stream ended prematurely - expected "+len+" bytes - received "+readLen+" bytes"); - } - - int pos = 0; // An empty picture record (length 0) will take up 8 bytes while (pos <= (pictstream.length-8)) { @@ -512,7 +504,7 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { } HSLFSlideShowEncrypted encData = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom()); - + for (Record record : _records) { assert(record instanceof PositionDependentRecord); // We've already figured out their new location, and @@ -533,6 +525,8 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { record.writeOut(encData.encryptRecord(os, persistId, record)); } } + + encData.close(); // Update and write out the Current User atom int oldLastUserEditAtomPos = (int)currentUser.getCurrentEditOffset(); @@ -733,7 +727,7 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { if (dea != null) { CryptoAPIEncryptor enc = (CryptoAPIEncryptor)dea.getEncryptionInfo().getEncryptor(); try { - enc.getDataStream(outFS.getRoot()); // ignore OutputStream + enc.getSummaryEntries(outFS.getRoot()); // ignore OutputStream } catch (IOException e) { throw e; } catch (GeneralSecurityException e) { diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java index b7e6ea947..62db5cb85 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java @@ -25,6 +25,7 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; import java.security.MessageDigest; import java.util.List; @@ -44,6 +45,7 @@ import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; @@ -176,7 +178,7 @@ public class TestDocumentEncryption { DocumentEncryptionAtom dea = hss.getDocumentEncryptionAtom(); - POIFSFileSystem fs2 = new POIFSFileSystem(dea.getEncryptionInfo().getDecryptor().getDataStream(fs)); + POIFSFileSystem fs2 = ((CryptoAPIDecryptor)dea.getEncryptionInfo().getDecryptor()).getSummaryEntries(fs.getRoot(), "EncryptedSummary"); PropertySet ps = PropertySetFactory.create(fs2.getRoot(), SummaryInformation.DEFAULT_STREAM_NAME); assertTrue(ps.isSummaryInformation()); assertEquals("RC4 CryptoAPI Encryption", ps.getProperties()[1].getValue()); From 0bfefdfc04a10e3c8668ddd6376a4730dbe57d21 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 8 Aug 2016 00:10:44 +0000 Subject: [PATCH 3/9] HSSF CryptoAPI decryption support git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755461 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hssf/record/FilePassRecord.java | 288 +++++------------- .../hssf/record/RecordFactoryInputStream.java | 39 +-- .../poi/hssf/record/RecordInputStream.java | 5 +- .../record/crypto/Biff8DecryptingStream.java | 209 +++++++++---- .../record/crypto/Biff8EncryptionKey.java | 27 +- .../poi/hssf/record/crypto/Biff8RC4.java | 195 ------------ .../poi/hssf/record/crypto/Biff8RC4Key.java | 155 ---------- .../poi/hssf/record/crypto/Biff8XOR.java | 153 ---------- .../poi/hssf/record/crypto/Biff8XORKey.java | 44 --- .../poifs/crypt/ChunkedCipherInputStream.java | 123 ++++++-- .../crypt/ChunkedCipherOutputStream.java | 28 +- .../org/apache/poi/poifs/crypt/Decryptor.java | 3 +- .../poi/poifs/crypt/EncryptionInfo.java | 49 +-- .../poi/poifs/crypt/EncryptionMode.java | 4 +- .../crypt/binaryrc4/BinaryRC4Decryptor.java | 7 +- .../crypt/cryptoapi/CryptoAPIDecryptor.java | 5 +- .../poi/poifs/crypt/xor/XORDecryptor.java | 174 +++++++++++ .../crypt/xor/XOREncryptionHeader.java} | 67 ++-- .../crypt/xor/XOREncryptionInfoBuilder.java | 62 ++++ .../crypt/xor/XOREncryptionVerifier.java | 61 ++++ .../poi/poifs/crypt/xor/XOREncryptor.java | 99 ++++++ .../hslf/record/DocumentEncryptionAtom.java | 4 +- .../poi/hssf/record/AllRecordTests.java | 4 +- .../record/crypto/TestBiff8EncryptionKey.java | 102 ------- .../poi/hssf/usermodel/TestCryptoAPI.java | 62 ++++ .../crypt/AllEncryptionTests.java} | 11 +- .../crypt}/TestBiff8DecryptingStream.java | 25 +- .../poi/poifs/crypt/TestCipherAlgorithm.java | 41 ++- .../crypt}/TestXorEncryption.java | 3 +- .../poifs/crypt/binaryrc4/TestBinaryRC4.java | 106 +++++++ 30 files changed, 1051 insertions(+), 1104 deletions(-) delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java rename src/java/org/apache/poi/{hssf/record/crypto/Biff8Cipher.java => poifs/crypt/xor/XOREncryptionHeader.java} (60%) create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java delete mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java create mode 100644 src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java rename src/testcases/org/apache/poi/{hssf/record/crypto/AllHSSFEncryptionTests.java => poifs/crypt/AllEncryptionTests.java} (83%) rename src/testcases/org/apache/poi/{hssf/record/crypto => poifs/crypt}/TestBiff8DecryptingStream.java (94%) rename src/testcases/org/apache/poi/{hssf/record/crypto => poifs/crypt}/TestXorEncryption.java (94%) create mode 100644 src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index 7c8ae948f..9f43b94ef 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -17,8 +17,19 @@ package org.apache.poi.hssf.record; +import java.io.IOException; + import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionHeader; +import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionVerifier; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier; +import org.apache.poi.poifs.crypt.xor.XOREncryptionHeader; +import org.apache.poi.poifs.crypt.xor.XOREncryptionVerifier; import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.LittleEndianOutput; /** @@ -31,228 +42,82 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { private static final int ENCRYPTION_XOR = 0; private static final int ENCRYPTION_OTHER = 1; - private int _encryptionType; - private KeyData _keyData; - - private static interface KeyData extends Cloneable { - void read(RecordInputStream in); - void serialize(LittleEndianOutput out); - int getDataSize(); - void appendToString(StringBuffer buffer); - KeyData clone(); // NOSONAR - } - - public static final class Rc4KeyData implements KeyData, Cloneable { - private static final int ENCRYPTION_OTHER_RC4 = 1; - private static final int ENCRYPTION_OTHER_CAPI_2 = 2; - private static final int ENCRYPTION_OTHER_CAPI_3 = 3; - private static final int ENCRYPTION_OTHER_CAPI_4 = 4; - - private byte[] _salt; - private byte[] _encryptedVerifier; - private byte[] _encryptedVerifierHash; - private int _encryptionInfo; - private int _minorVersionNo; - - public void read(RecordInputStream in) { - _encryptionInfo = in.readUShort(); - switch (_encryptionInfo) { - case ENCRYPTION_OTHER_RC4: - // handled below - break; - case ENCRYPTION_OTHER_CAPI_2: - case ENCRYPTION_OTHER_CAPI_3: - case ENCRYPTION_OTHER_CAPI_4: - throw new EncryptedDocumentException( - "HSSF does not currently support CryptoAPI encryption"); - default: - throw new RecordFormatException("Unknown encryption info " + _encryptionInfo); - } - _minorVersionNo = in.readUShort(); - if (_minorVersionNo!=1) { - throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo); - } - _salt = FilePassRecord.read(in, 16); - _encryptedVerifier = FilePassRecord.read(in, 16); - _encryptedVerifierHash = FilePassRecord.read(in, 16); - } - - public void serialize(LittleEndianOutput out) { - out.writeShort(_encryptionInfo); - out.writeShort(_minorVersionNo); - out.write(_salt); - out.write(_encryptedVerifier); - out.write(_encryptedVerifierHash); - } - - public int getDataSize() { - return 54; - } - - public byte[] getSalt() { - return _salt.clone(); - } - - public void setSalt(byte[] salt) { - this._salt = salt.clone(); - } - - public byte[] getEncryptedVerifier() { - return _encryptedVerifier.clone(); - } - - public void setEncryptedVerifier(byte[] encryptedVerifier) { - this._encryptedVerifier = encryptedVerifier.clone(); - } - - public byte[] getEncryptedVerifierHash() { - return _encryptedVerifierHash.clone(); - } - - public void setEncryptedVerifierHash(byte[] encryptedVerifierHash) { - this._encryptedVerifierHash = encryptedVerifierHash.clone(); - } - - public void appendToString(StringBuffer buffer) { - buffer.append(" .rc4.info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n"); - buffer.append(" .rc4.ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n"); - buffer.append(" .rc4.salt = ").append(HexDump.toHex(_salt)).append("\n"); - buffer.append(" .rc4.verifier = ").append(HexDump.toHex(_encryptedVerifier)).append("\n"); - buffer.append(" .rc4.verifierHash = ").append(HexDump.toHex(_encryptedVerifierHash)).append("\n"); - } - - @Override - public Rc4KeyData clone() { - Rc4KeyData other = new Rc4KeyData(); - other._salt = this._salt.clone(); - other._encryptedVerifier = this._encryptedVerifier.clone(); - other._encryptedVerifierHash = this._encryptedVerifierHash.clone(); - other._encryptionInfo = this._encryptionInfo; - other._minorVersionNo = this._minorVersionNo; - return other; - } - } - - public static final class XorKeyData implements KeyData, Cloneable { - /** - * key (2 bytes): An unsigned integer that specifies the obfuscation key. - * See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR - * array where it describes the generation of 16-bit XorKey value. - */ - private int _key; - - /** - * verificationBytes (2 bytes): An unsigned integer that specifies - * the password verification identifier. - */ - private int _verifier; - - public void read(RecordInputStream in) { - _key = in.readUShort(); - _verifier = in.readUShort(); - } - - public void serialize(LittleEndianOutput out) { - out.writeShort(_key); - out.writeShort(_verifier); - } - - public int getDataSize() { - // TODO: Check! - return 6; - } - - public int getKey() { - return _key; - } - - public int getVerifier() { - return _verifier; - } - - public void setKey(int key) { - this._key = key; - } - - public void setVerifier(int verifier) { - this._verifier = verifier; - } - - public void appendToString(StringBuffer buffer) { - buffer.append(" .xor.key = ").append(HexDump.intToHex(_key)).append("\n"); - buffer.append(" .xor.verifier = ").append(HexDump.intToHex(_verifier)).append("\n"); - } - - @Override - public XorKeyData clone() { - XorKeyData other = new XorKeyData(); - other._key = this._key; - other._verifier = this._verifier; - return other; - } - } - + private int encryptionType; + private EncryptionInfo encryptionInfo; + private int dataLength; private FilePassRecord(FilePassRecord other) { - _encryptionType = other._encryptionType; - _keyData = other._keyData.clone(); + dataLength = other.dataLength; + encryptionType = other.encryptionType; + try { + encryptionInfo = other.encryptionInfo.clone(); + } catch (CloneNotSupportedException e) { + throw new EncryptedDocumentException(e); + } } public FilePassRecord(RecordInputStream in) { - _encryptionType = in.readUShort(); - - switch (_encryptionType) { - case ENCRYPTION_XOR: - _keyData = new XorKeyData(); - break; - case ENCRYPTION_OTHER: - _keyData = new Rc4KeyData(); - break; - default: - throw new RecordFormatException("Unknown encryption type " + _encryptionType); - } - - _keyData.read(in); - } - - private static byte[] read(RecordInputStream in, int size) { - byte[] result = new byte[size]; - in.readFully(result); - return result; + dataLength = in.remaining(); + encryptionType = in.readUShort(); + + EncryptionMode preferredMode; + switch (encryptionType) { + case ENCRYPTION_XOR: + preferredMode = EncryptionMode.xor; + break; + case ENCRYPTION_OTHER: + preferredMode = EncryptionMode.cryptoAPI; + break; + default: + throw new EncryptedDocumentException("invalid encryption type"); + } + + try { + encryptionInfo = new EncryptionInfo(in, preferredMode); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } } public void serialize(LittleEndianOutput out) { - out.writeShort(_encryptionType); - assert(_keyData != null); - _keyData.serialize(out); + out.writeShort(encryptionType); + + byte data[] = new byte[1024]; + LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0); + + switch (encryptionInfo.getEncryptionMode()) { + case xor: + ((XOREncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((XOREncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + case binaryRC4: + out.writeShort(encryptionInfo.getVersionMajor()); + out.writeShort(encryptionInfo.getVersionMinor()); + ((BinaryRC4EncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((BinaryRC4EncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + case cryptoAPI: + out.writeShort(encryptionInfo.getVersionMajor()); + out.writeShort(encryptionInfo.getVersionMinor()); + ((CryptoAPIEncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((CryptoAPIEncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + default: + throw new RuntimeException("not supported"); + } + + out.write(data, 0, bos.getWriteIndex()); } protected int getDataSize() { - assert(_keyData != null); - return _keyData.getDataSize(); + return dataLength; } - public Rc4KeyData getRc4KeyData() { - return (_keyData instanceof Rc4KeyData) - ? (Rc4KeyData) _keyData - : null; - } - - public XorKeyData getXorKeyData() { - return (_keyData instanceof XorKeyData) - ? (XorKeyData) _keyData - : null; - } - - private Rc4KeyData checkRc4() { - Rc4KeyData rc4 = getRc4KeyData(); - if (rc4 == null) { - throw new RecordFormatException("file pass record doesn't contain a rc4 key."); - } - return rc4; + public EncryptionInfo getEncryptionInfo() { + return encryptionInfo; } - public short getSid() { + public short getSid() { return sid; } @@ -265,8 +130,13 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { StringBuffer buffer = new StringBuffer(); buffer.append("[FILEPASS]\n"); - buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n"); - _keyData.appendToString(buffer); + buffer.append(" .type = ").append(HexDump.shortToHex(encryptionType)).append("\n"); + String prefix = " ."+encryptionInfo.getEncryptionMode(); + buffer.append(prefix+".info = ").append(HexDump.shortToHex(encryptionInfo.getVersionMajor())).append("\n"); + buffer.append(prefix+".ver = ").append(HexDump.shortToHex(encryptionInfo.getVersionMinor())).append("\n"); + buffer.append(prefix+".salt = ").append(HexDump.toHex(encryptionInfo.getVerifier().getSalt())).append("\n"); + buffer.append(prefix+".verifier = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifier())).append("\n"); + buffer.append(prefix+".verifierHash = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifierHash())).append("\n"); buffer.append("[/FILEPASS]\n"); return buffer.toString(); } diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index c7480147b..6338f9a69 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -17,18 +17,16 @@ package org.apache.poi.hssf.record; import java.io.InputStream; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; import org.apache.poi.hssf.eventusermodel.HSSFListener; -import org.apache.poi.hssf.record.FilePassRecord.Rc4KeyData; -import org.apache.poi.hssf.record.FilePassRecord.XorKeyData; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; -import org.apache.poi.hssf.record.crypto.Biff8RC4Key; -import org.apache.poi.hssf.record.crypto.Biff8XORKey; import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; /** * A stream based way to get at complete records, with @@ -114,31 +112,18 @@ public final class RecordFactoryInputStream { userPassword = Decryptor.DEFAULT_PASSWORD; } - Biff8EncryptionKey key; - if (fpr.getRc4KeyData() != null) { - Rc4KeyData rc4 = fpr.getRc4KeyData(); - Biff8RC4Key rc4key = Biff8RC4Key.create(userPassword, rc4.getSalt()); - key = rc4key; - if (!rc4key.validate(rc4.getEncryptedVerifier(), rc4.getEncryptedVerifierHash())) { - throw new EncryptedDocumentException( - (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") - + " password is invalid for salt/verifier/verifierHash"); - } - } else if (fpr.getXorKeyData() != null) { - XorKeyData xor = fpr.getXorKeyData(); - Biff8XORKey xorKey = Biff8XORKey.create(userPassword, xor.getKey()); - key = xorKey; - - if (!xorKey.validate(userPassword, xor.getVerifier())) { + EncryptionInfo info = fpr.getEncryptionInfo(); + try { + if (!info.getDecryptor().verifyPassword(userPassword)) { throw new EncryptedDocumentException( - (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") - + " password is invalid for key/verifier"); - } - } else { - throw new EncryptedDocumentException("Crypto API not yet supported."); - } + (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") + + " password is invalid for salt/verifier/verifierHash"); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } - return new RecordInputStream(original, key, _initialRecordsSize); + return new RecordInputStream(original, info, _initialRecordsSize); } public boolean hasEncryption() { diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index 8991eb770..929f0f2bf 100644 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -25,6 +25,7 @@ import java.util.Locale; import org.apache.poi.hssf.dev.BiffViewer; import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInput; @@ -33,8 +34,6 @@ import org.apache.poi.util.LittleEndianInputStream; /** * Title: Record Input Stream

* Description: Wraps a stream and provides helper methods for the construction of records.

- * - * @author Jason Height (jheight @ apache dot org) */ public final class RecordInputStream implements LittleEndianInput { /** Maximum size of a single record (minus the 4 byte header) without a continue*/ @@ -122,7 +121,7 @@ public final class RecordInputStream implements LittleEndianInput { this (in, null, 0); } - public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException { + public RecordInputStream(InputStream in, EncryptionInfo key, int initialOffset) throws RecordFormatException { if (key == null) { _dataInput = getLEI(in); _bhi = new SimpleHeaderInput(in); diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java index 564174b60..ef574beea 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java @@ -17,103 +17,202 @@ package org.apache.poi.hssf.record.crypto; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; -import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.hssf.record.BOFRecord; import org.apache.poi.hssf.record.BiffHeaderInput; +import org.apache.poi.hssf.record.FilePassRecord; +import org.apache.poi.hssf.record.InterfaceHdrRecord; +import org.apache.poi.hssf.record.RecordFormatException; +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInput; -import org.apache.poi.util.LittleEndianInputStream; -/** - * - * @author Josh Micich - */ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput { - private final LittleEndianInput _le; - private final Biff8Cipher _cipher; + private static final int RC4_REKEYING_INTERVAL = 1024; - public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) { - if (key instanceof Biff8RC4Key) { - _cipher = new Biff8RC4(initialOffset, (Biff8RC4Key)key); - } else if (key instanceof Biff8XORKey) { - _cipher = new Biff8XOR(initialOffset, (Biff8XORKey)key); - } else { - throw new EncryptedDocumentException("Crypto API not supported yet."); - } + private final EncryptionInfo info; + private ChunkedCipherInputStream ccis; + private final byte buffer[] = new byte[LittleEndianConsts.LONG_SIZE]; + private boolean shouldSkipEncryptionOnCurrentRecord = false; - if (in instanceof LittleEndianInput) { - // accessing directly is an optimisation - _le = (LittleEndianInput) in; - } else { - // less optimal, but should work OK just the same. Often occurs in junit tests. - _le = new LittleEndianInputStream(in); - } + public Biff8DecryptingStream(InputStream in, int initialOffset, EncryptionInfo info) throws RecordFormatException { + try { + byte initialBuf[] = new byte[initialOffset]; + InputStream stream; + if (initialOffset == 0) { + stream = in; + } else { + stream = new PushbackInputStream(in, initialOffset); + ((PushbackInputStream)stream).unread(initialBuf); + } + + this.info = info; + Decryptor dec = this.info.getDecryptor(); + dec.setChunkSize(RC4_REKEYING_INTERVAL); + ccis = (ChunkedCipherInputStream)dec.getDataStream(stream, Integer.MAX_VALUE, 0); + + if (initialOffset > 0) { + ccis.readFully(initialBuf); + } + } catch (Exception e) { + throw new RecordFormatException(e); + } } - public int available() { - return _le.available(); + @Override + public int available() { + return ccis.available(); } /** * Reads an unsigned short value without decrypting */ - public int readRecordSID() { - int sid = _le.readUShort(); - _cipher.skipTwoBytes(); - _cipher.startRecord(sid); + @Override + public int readRecordSID() { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + int sid = LittleEndian.getUShort(buffer, 0); + shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(sid); return sid; } /** * Reads an unsigned short value without decrypting */ - public int readDataSize() { - int dataSize = _le.readUShort(); - _cipher.skipTwoBytes(); - _cipher.setNextRecordSize(dataSize); + @Override + public int readDataSize() { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + int dataSize = LittleEndian.getUShort(buffer, 0); + ccis.setNextRecordSize(dataSize); return dataSize; } - public double readDouble() { - long valueLongBits = readLong(); + @Override + public double readDouble() { + long valueLongBits = readLong(); double result = Double.longBitsToDouble(valueLongBits); if (Double.isNaN(result)) { - throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN + // (Because Excel typically doesn't write NaN + throw new RuntimeException("Did not expect to read NaN"); } return result; } - public void readFully(byte[] buf) { - readFully(buf, 0, buf.length); + @Override + public void readFully(byte[] buf) { + readFully(buf, 0, buf.length); } - public void readFully(byte[] buf, int off, int len) { - _le.readFully(buf, off, len); - _cipher.xor(buf, off, len); + @Override + public void readFully(byte[] buf, int off, int len) { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buf, off, buf.length); + } else { + ccis.readFully(buf, off, len); + } } - - public int readUByte() { - return readByte() & 0xFF; + @Override + public int readUByte() { + return readByte() & 0xFF; } - public byte readByte() { - return (byte) _cipher.xorByte(_le.readUByte()); + + @Override + public byte readByte() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.BYTE_SIZE); + return buffer[0]; + } else { + return ccis.readByte(); + } } - - public int readUShort() { - return readShort() & 0xFFFF; + @Override + public int readUShort() { + return readShort() & 0xFFFF; } - public short readShort() { - return (short) _cipher.xorShort(_le.readUShort()); + + @Override + public short readShort() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + return LittleEndian.getShort(buffer); + } else { + return ccis.readShort(); + } } - public int readInt() { - return _cipher.xorInt(_le.readInt()); + @Override + public int readInt() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.INT_SIZE); + return LittleEndian.getInt(buffer); + } else { + return ccis.readInt(); + } } - public long readLong() { - return _cipher.xorLong(_le.readLong()); + @Override + public long readLong() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.LONG_SIZE); + return LittleEndian.getLong(buffer); + } else { + return ccis.readLong(); + } } + + /** + * @return the absolute position in the stream + */ + public long getPosition() { + return ccis.getPos(); + } + + /** + * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted. + * + * @return true if record type specified by sid is never encrypted + */ + private static boolean isNeverEncryptedRecord(int sid) { + switch (sid) { + case BOFRecord.sid: + // sheet BOFs for sure + // TODO - find out about chart BOFs + + case InterfaceHdrRecord.sid: + // don't know why this record doesn't seem to get encrypted + + case FilePassRecord.sid: + // this only really counts when writing because FILEPASS is read early + + // UsrExcl(0x0194) + // FileLock + // RRDInfo(0x0196) + // RRDHead(0x0138) + + return true; + + default: + return false; + } + } + + private void readPlain(byte b[], int off, int len) { + try { + int readBytes = ccis.readPlain(b, off, len); + if (readBytes < len) { + throw new RecordFormatException("buffer underrun"); + } + } catch (IOException e) { + throw new RecordFormatException(e); + } + } + } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java index 3a28b81af..382ae2f13 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java @@ -16,34 +16,9 @@ ==================================================================== */ package org.apache.poi.hssf.record.crypto; -import javax.crypto.SecretKey; - -import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.poifs.crypt.Decryptor; - -public abstract class Biff8EncryptionKey { - protected SecretKey _secretKey; - - /** - * Create using the default password and a specified docId - * @param salt 16 bytes - */ - public static Biff8EncryptionKey create(byte[] salt) { - return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt); - } - - public static Biff8EncryptionKey create(String password, byte[] salt) { - return Biff8RC4Key.create(password, salt); - } - - /** - * @return true if the keyDigest is compatible with the specified saltData and saltHash - */ - public boolean validate(byte[] saltData, byte[] saltHash) { - throw new EncryptedDocumentException("validate is not supported (in super-class)."); - } +public final class Biff8EncryptionKey { /** * Stores the BIFF8 encryption/decryption password for the current thread. This has been done * using a {@link ThreadLocal} in order to avoid further overloading the various public APIs diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java deleted file mode 100644 index 9d0275fec..000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java +++ /dev/null @@ -1,195 +0,0 @@ -/* ==================================================================== - 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.record.crypto; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import javax.crypto.Cipher; -import javax.crypto.ShortBufferException; - -import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.hssf.record.BOFRecord; -import org.apache.poi.hssf.record.FilePassRecord; -import org.apache.poi.hssf.record.InterfaceHdrRecord; - -/** - * Used for both encrypting and decrypting BIFF8 streams. The internal - * {@link Cipher} instance is renewed (re-keyed) every 1024 bytes. - */ -final class Biff8RC4 implements Biff8Cipher { - - private static final int RC4_REKEYING_INTERVAL = 1024; - - private Cipher _rc4; - - /** - * This field is used to keep track of when to change the {@link Cipher} - * instance. The change occurs every 1024 bytes. Every byte passed over is - * counted. - */ - private int _streamPos; - private int _nextRC4BlockStart; - private int _currentKeyIndex; - private boolean _shouldSkipEncryptionOnCurrentRecord; - private final Biff8RC4Key _key; - private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); - - public Biff8RC4(int initialOffset, Biff8RC4Key key) { - if (initialOffset >= RC4_REKEYING_INTERVAL) { - throw new RuntimeException("initialOffset (" + initialOffset + ")>" - + RC4_REKEYING_INTERVAL + " not supported yet"); - } - _key = key; - _rc4 = _key.getCipher(); - _streamPos = 0; - rekeyForNextBlock(); - _streamPos = initialOffset; - _shouldSkipEncryptionOnCurrentRecord = false; - - encryptBytes(new byte[initialOffset], 0, initialOffset); - } - - - private void rekeyForNextBlock() { - _currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL; - _key.initCipherForBlock(_rc4, _currentKeyIndex); - _nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL; - } - - private void encryptBytes(byte data[], int offset, final int bytesToRead) { - if (bytesToRead == 0) return; - - if (_shouldSkipEncryptionOnCurrentRecord) { - // even when encryption is skipped, we need to update the cipher - byte dataCpy[] = new byte[bytesToRead]; - System.arraycopy(data, offset, dataCpy, 0, bytesToRead); - data = dataCpy; - offset = 0; - } - - try { - _rc4.update(data, offset, bytesToRead, data, offset); - } catch (ShortBufferException e) { - throw new EncryptedDocumentException("input buffer too small", e); - } - } - - public void startRecord(int currentSid) { - _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid); - } - - /** - * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted. - * - * @return true if record type specified by sid is never encrypted - */ - private static boolean isNeverEncryptedRecord(int sid) { - switch (sid) { - case BOFRecord.sid: - // sheet BOFs for sure - // TODO - find out about chart BOFs - - case InterfaceHdrRecord.sid: - // don't know why this record doesn't seem to get encrypted - - case FilePassRecord.sid: - // this only really counts when writing because FILEPASS is read early - - // UsrExcl(0x0194) - // FileLock - // RRDInfo(0x0196) - // RRDHead(0x0138) - - return true; - } - return false; - } - - /** - * Used when BIFF header fields (sid, size) are being read. The internal - * {@link Cipher} instance must step even when unencrypted bytes are read - */ - public void skipTwoBytes() { - xor(_buffer.array(), 0, 2); - } - - public void xor(byte[] buf, int pOffset, int pLen) { - int nLeftInBlock; - nLeftInBlock = _nextRC4BlockStart - _streamPos; - if (pLen <= nLeftInBlock) { - // simple case - this read does not cross key blocks - encryptBytes(buf, pOffset, pLen); - _streamPos += pLen; - return; - } - - int offset = pOffset; - int len = pLen; - - // start by using the rest of the current block - if (len > nLeftInBlock) { - if (nLeftInBlock > 0) { - encryptBytes(buf, offset, nLeftInBlock); - _streamPos += nLeftInBlock; - offset += nLeftInBlock; - len -= nLeftInBlock; - } - rekeyForNextBlock(); - } - // all full blocks following - while (len > RC4_REKEYING_INTERVAL) { - encryptBytes(buf, offset, RC4_REKEYING_INTERVAL); - _streamPos += RC4_REKEYING_INTERVAL; - offset += RC4_REKEYING_INTERVAL; - len -= RC4_REKEYING_INTERVAL; - rekeyForNextBlock(); - } - // finish with incomplete block - encryptBytes(buf, offset, len); - _streamPos += len; - } - - public int xorByte(int rawVal) { - _buffer.put(0, (byte)rawVal); - xor(_buffer.array(), 0, 1); - return _buffer.get(0); - } - - public int xorShort(int rawVal) { - _buffer.putShort(0, (short)rawVal); - xor(_buffer.array(), 0, 2); - return _buffer.getShort(0); - } - - public int xorInt(int rawVal) { - _buffer.putInt(0, rawVal); - xor(_buffer.array(), 0, 4); - return _buffer.getInt(0); - } - - public long xorLong(long rawVal) { - _buffer.putLong(0, rawVal); - xor(_buffer.array(), 0, 8); - return _buffer.getLong(0); - } - - public void setNextRecordSize(int recordSize) { - /* no-op */ - } -} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java deleted file mode 100644 index e75f297b0..000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java +++ /dev/null @@ -1,155 +0,0 @@ -/* ==================================================================== - 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.record.crypto; - -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.poifs.crypt.CipherAlgorithm; -import org.apache.poi.poifs.crypt.CryptoFunctions; -import org.apache.poi.poifs.crypt.HashAlgorithm; -import org.apache.poi.util.HexDump; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianConsts; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; - -public class Biff8RC4Key extends Biff8EncryptionKey { - // these two constants coincidentally have the same value - public static final int KEY_DIGEST_LENGTH = 5; - private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5; - - private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class); - - Biff8RC4Key(byte[] keyDigest) { - if (keyDigest.length != KEY_DIGEST_LENGTH) { - throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest)); - } - - CipherAlgorithm ca = CipherAlgorithm.rc4; - _secretKey = new SecretKeySpec(keyDigest.clone(), ca.jceId); - } - - /** - * Create using the default password and a specified docId - * @param salt 16 bytes - */ - public static Biff8RC4Key create(String password, byte[] salt) { - return new Biff8RC4Key(createKeyDigest(password, salt)); - } - - /** - * @return true if the keyDigest is compatible with the specified saltData and saltHash - */ - public boolean validate(byte[] verifier, byte[] verifierHash) { - check16Bytes(verifier, "verifier"); - check16Bytes(verifierHash, "verifierHash"); - - // validation uses the RC4 for block zero - Cipher rc4 = getCipher(); - initCipherForBlock(rc4, 0); - - byte[] verifierPrime = verifier.clone(); - byte[] verifierHashPrime = verifierHash.clone(); - - try { - rc4.update(verifierPrime, 0, verifierPrime.length, verifierPrime); - rc4.update(verifierHashPrime, 0, verifierHashPrime.length, verifierHashPrime); - } catch (ShortBufferException e) { - throw new EncryptedDocumentException("buffer too short", e); - } - - MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5); - md5.update(verifierPrime); - byte[] finalVerifierResult = md5.digest(); - - if (log.check(POILogger.DEBUG)) { - byte[] verifierHashThatWouldWork = xor(verifierHash, xor(verifierHashPrime, finalVerifierResult)); - log.log(POILogger.DEBUG, "valid verifierHash value", HexDump.toHex(verifierHashThatWouldWork)); - } - - return Arrays.equals(verifierHashPrime, finalVerifierResult); - } - - Cipher getCipher() { - CipherAlgorithm ca = CipherAlgorithm.rc4; - Cipher rc4 = CryptoFunctions.getCipher(_secretKey, ca, null, null, Cipher.ENCRYPT_MODE); - return rc4; - } - - static byte[] createKeyDigest(String password, byte[] docIdData) { - check16Bytes(docIdData, "docId"); - int nChars = Math.min(password.length(), 16); - byte[] passwordData = new byte[nChars*2]; - for (int i=0; itrue if record type specified by sid is never encrypted - */ - private static boolean isNeverEncryptedRecord(int sid) { - switch (sid) { - case BOFRecord.sid: - // sheet BOFs for sure - // TODO - find out about chart BOFs - - case InterfaceHdrRecord.sid: - // don't know why this record doesn't seem to get encrypted - - case FilePassRecord.sid: - // this only really counts when writing because FILEPASS is read early - - // UsrExcl(0x0194) - // FileLock - // RRDInfo(0x0196) - // RRDHead(0x0138) - - return true; - } - return false; - } - - /** - * Used when BIFF header fields (sid, size) are being read. The internal - * {@link Cipher} instance must step even when unencrypted bytes are read - */ - public void skipTwoBytes() { - _dataLength += 2; - } - - /** - * Decrypts a xor obfuscated byte array. - * The data is decrypted in-place - * - * @see 2.3.7.3 Binary Document XOR Data Transformation Method 1 - */ - public void xor(byte[] buf, int pOffset, int pLen) { - if (_shouldSkipEncryptionOnCurrentRecord) { - _dataLength += pLen; - return; - } - - // The following is taken from the Libre Office implementation - // It seems that the encrypt and decrypt method is mixed up - // in the MS-OFFCRYPTO docs - - byte xorArray[] = _key._secretKey.getEncoded(); - - for (int i=0; i>> (8 - shift))); - } - - public int xorByte(int rawVal) { - _buffer.put(0, (byte)rawVal); - xor(_buffer.array(), 0, 1); - return _buffer.get(0); - } - - public int xorShort(int rawVal) { - _buffer.putShort(0, (short)rawVal); - xor(_buffer.array(), 0, 2); - return _buffer.getShort(0); - } - - public int xorInt(int rawVal) { - _buffer.putInt(0, rawVal); - xor(_buffer.array(), 0, 4); - return _buffer.getInt(0); - } - - public long xorLong(long rawVal) { - _buffer.putLong(0, rawVal); - xor(_buffer.array(), 0, 8); - return _buffer.getLong(0); - } -} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java deleted file mode 100644 index 7f2903dd5..000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java +++ /dev/null @@ -1,44 +0,0 @@ -/* ==================================================================== - 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.record.crypto; - -import javax.crypto.spec.SecretKeySpec; - -import org.apache.poi.poifs.crypt.CryptoFunctions; - - -public class Biff8XORKey extends Biff8EncryptionKey { - final int _xorKey; - - public Biff8XORKey(String password, int xorKey) { - _xorKey = xorKey; - byte xorArray[] = CryptoFunctions.createXorArray1(password); - _secretKey = new SecretKeySpec(xorArray, "XOR"); - } - - public static Biff8XORKey create(String password, int xorKey) { - return new Biff8XORKey(password, xorKey); - } - - public boolean validate(String password, int verifier) { - int keyComp = CryptoFunctions.createXorKey1(password); - int verifierComp = CryptoFunctions.createXorVerifier1(password); - - return (_xorKey == keyComp && verifierComp == verifier); - } -} diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java index 255494d6a..7b5632dea 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java @@ -21,56 +21,56 @@ import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; @Internal public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { private final int _chunkSize; private final int _chunkBits; - + private final long _size; - private final byte[] _chunk; + private final byte[] _chunk, _plain; private final Cipher _cipher; private int _lastIndex; private long _pos; private boolean _chunkIsValid = false; - public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize) + public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize) throws GeneralSecurityException { this(stream, size, chunkSize, 0); } - public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize, int initialPos) + public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize, int initialPos) throws GeneralSecurityException { - super((InputStream)stream); + super(stream); _size = size; _pos = initialPos; this._chunkSize = chunkSize; - if (chunkSize == -1) { - _chunk = new byte[4096]; - } else { - _chunk = new byte[chunkSize]; - } + int cs = chunkSize == -1 ? 4096 : chunkSize; + _chunk = new byte[cs]; + _plain = new byte[cs]; _chunkBits = Integer.bitCount(_chunk.length-1); _lastIndex = (int)(_pos >> _chunkBits); _cipher = initCipherForBlock(null, _lastIndex); } - + public final Cipher initCipherForBlock(int block) throws IOException, GeneralSecurityException { if (_chunkSize != -1) { throw new GeneralSecurityException("the cipher block can only be set for streaming encryption, e.g. CryptoAPI..."); } - + _chunkIsValid = false; return initCipherForBlock(_cipher, block); } - + protected abstract Cipher initCipherForBlock(Cipher existing, int block) throws GeneralSecurityException; @@ -88,8 +88,12 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { @Override public int read(byte[] b, int off, int len) throws IOException { + return read(b, off, len, false); + } + + private int read(byte[] b, int off, int len, boolean readPlain) throws IOException { int total = 0; - + if (available() <= 0) { return -1; } @@ -110,7 +114,9 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { return total; } count = Math.min(avail, Math.min(count, len)); - System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count); + + System.arraycopy(readPlain ? _plain : _chunk, (int)(_pos & chunkMask), b, off, count); + off += count; len -= count; _pos += count; @@ -139,7 +145,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { public int available() { return remainingBytes(); } - + /** * Helper method for forbidden available call - we know the size beforehand, so it's ok ... * @@ -148,7 +154,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { private int remainingBytes() { return (int)(_size - _pos); } - + @Override public boolean markSupported() { return false; @@ -158,21 +164,21 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { public synchronized void mark(int readlimit) { throw new UnsupportedOperationException(); } - + @Override public synchronized void reset() throws IOException { throw new UnsupportedOperationException(); } - private int getChunkMask() { + protected int getChunkMask() { return _chunk.length-1; } - + private void nextChunk() throws GeneralSecurityException, IOException { if (_chunkSize != -1) { int index = (int)(_pos >> _chunkBits); initCipherForBlock(_cipher, index); - + if (_lastIndex != index) { super.skip((index - _lastIndex) << _chunkBits); } @@ -183,18 +189,81 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { final int todo = (int)Math.min(_size, _chunk.length); int readBytes = 0, totalBytes = 0; do { - readBytes = super.read(_chunk, totalBytes, todo-totalBytes); + readBytes = super.read(_plain, totalBytes, todo-totalBytes); totalBytes += Math.max(0, readBytes); } while (readBytes != -1 && totalBytes < todo); - if (readBytes == -1 && _pos+totalBytes < _size) { + if (readBytes == -1 && _pos+totalBytes < _size && _size < Integer.MAX_VALUE) { throw new EOFException("buffer underrun"); } - if (_chunkSize == -1) { - _cipher.update(_chunk, 0, totalBytes, _chunk); + System.arraycopy(_plain, 0, _chunk, 0, totalBytes); + + invokeCipher(totalBytes, _chunkSize > -1); + } + + /** + * Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher + * and uses it's own implementation + * + * @return + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws ShortBufferException + */ + protected int invokeCipher(int totalBytes, boolean doFinal) throws GeneralSecurityException { + if (doFinal) { + return _cipher.doFinal(_chunk, 0, totalBytes, _chunk); } else { - _cipher.doFinal(_chunk, 0, totalBytes, _chunk); + return _cipher.update(_chunk, 0, totalBytes, _chunk); } } + + /** + * Used when BIFF header fields (sid, size) are being read. The internal + * {@link Cipher} instance must step even when unencrypted bytes are read + */ + public int readPlain(byte b[], int off, int len) throws IOException { + if (len <= 0) { + return len; + } + + int readBytes, total = 0; + do { + readBytes = read(b, off, len, true); + total += Math.max(0, readBytes); + } while (readBytes > -1 && total < len); + + return total; + } + + /** + * Some ciphers (actually just XOR) are based on the record size, + * which needs to be set before encryption + * + * @param recordSize the size of the next record + */ + public void setNextRecordSize(int recordSize) { + } + + /** + * @return the chunk bytes + */ + protected byte[] getChunk() { + return _chunk; + } + + /** + * @return the plain bytes + */ + protected byte[] getPlain() { + return _plain; + } + + /** + * @return the absolute position in the stream + */ + public long getPos() { + return _pos; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java index 573cbdeb6..08ca4c3be 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java @@ -26,7 +26,10 @@ import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.filesystem.DirectoryNode; @@ -153,19 +156,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { int ciLen; try { + boolean doFinal = true; if (_chunkSize == STREAMING) { if (continued) { - ciLen = _cipher.update(_chunk, 0, posInChunk, _chunk); - } else { - ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + doFinal = false; } - // reset stream (not only) in case we were interrupted by plain stream parts _pos = 0; } else { _cipher = initCipherForBlock(_cipher, index, lastChunk); - ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); } + ciLen = invokeCipher(posInChunk, doFinal); } catch (GeneralSecurityException e) { throw new IOException("can't re-/initialize cipher", e); } @@ -173,6 +174,23 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { out.write(_chunk, 0, ciLen); } + /** + * Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher + * and uses it's own implementation + * + * @return + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws ShortBufferException + */ + protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException { + if (doFinal) { + return _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + } else { + return _cipher.update(_chunk, 0, posInChunk, _chunk); + } + } + @Override public void close() throws IOException { try { diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index 41621853e..203002955 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -29,7 +29,6 @@ import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; -import org.apache.poi.util.LittleEndianInput; public abstract class Decryptor implements Cloneable { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; @@ -66,7 +65,7 @@ public abstract class Decryptor implements Cloneable { * @param initialPos initial/current byte position within the stream * @return decrypted stream */ - public InputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + public InputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { throw new RuntimeException("this decryptor doesn't support reading from a stream"); } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java index 34b83cb8c..20115f1b4 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -20,6 +20,7 @@ import static org.apache.poi.poifs.crypt.EncryptionMode.agile; import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4; import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI; import static org.apache.poi.poifs.crypt.EncryptionMode.standard; +import static org.apache.poi.poifs.crypt.EncryptionMode.xor; import java.io.IOException; @@ -35,6 +36,7 @@ import org.apache.poi.util.LittleEndianInput; /** */ public class EncryptionInfo implements Cloneable { + private final EncryptionMode encryptionMode; private final int versionMajor; private final int versionMinor; private final int encryptionFlags; @@ -75,49 +77,55 @@ public class EncryptionInfo implements Cloneable { public EncryptionInfo(POIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(OPOIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(NPOIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(DirectoryNode dir) throws IOException { - this(dir.createDocumentInputStream("EncryptionInfo"), false); + this(dir.createDocumentInputStream("EncryptionInfo"), null); } - public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException { - final EncryptionMode encryptionMode; - versionMajor = dis.readUShort(); - versionMinor = dis.readUShort(); + public EncryptionInfo(LittleEndianInput dis, EncryptionMode preferredEncryptionMode) throws IOException { + if (preferredEncryptionMode == xor) { + versionMajor = xor.versionMajor; + versionMinor = xor.versionMinor; + } else { + versionMajor = dis.readUShort(); + versionMinor = dis.readUShort(); + } - if ( versionMajor == binaryRC4.versionMajor + if ( versionMajor == xor.versionMajor + && versionMinor == xor.versionMinor) { + encryptionMode = xor; + encryptionFlags = -1; + } else if ( versionMajor == binaryRC4.versionMajor && versionMinor == binaryRC4.versionMinor) { encryptionMode = binaryRC4; encryptionFlags = -1; - } else if (!isCryptoAPI - && versionMajor == agile.versionMajor + } else if ( + 2 <= versionMajor && versionMajor <= 4 + && versionMinor == 2) { + encryptionMode = (preferredEncryptionMode == cryptoAPI) ? cryptoAPI : standard; + encryptionFlags = dis.readInt(); + } else if ( + versionMajor == agile.versionMajor && versionMinor == agile.versionMinor){ encryptionMode = agile; encryptionFlags = dis.readInt(); - } else if (!isCryptoAPI - && 2 <= versionMajor && versionMajor <= 4 - && versionMinor == standard.versionMinor) { - encryptionMode = standard; - encryptionFlags = dis.readInt(); - } else if (isCryptoAPI - && 2 <= versionMajor && versionMajor <= 4 - && versionMinor == cryptoAPI.versionMinor) { - encryptionMode = cryptoAPI; - encryptionFlags = dis.readInt(); } else { encryptionFlags = dis.readInt(); throw new EncryptedDocumentException( @@ -170,6 +178,7 @@ public class EncryptionInfo implements Cloneable { , int blockSize , ChainingMode chainingMode ) { + this.encryptionMode = encryptionMode; versionMajor = encryptionMode.versionMajor; versionMinor = encryptionMode.versionMinor; encryptionFlags = encryptionMode.encryptionFlags; @@ -236,6 +245,10 @@ public class EncryptionInfo implements Cloneable { this.encryptor = encryptor; } + public EncryptionMode getEncryptionMode() { + return encryptionMode; + } + @Override public EncryptionInfo clone() throws CloneNotSupportedException { EncryptionInfo other = (EncryptionInfo)super.clone(); diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java index 86f4b8508..50064b5a6 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java @@ -33,7 +33,9 @@ public enum EncryptionMode { /* @see 2.3.4.5 \EncryptionInfo Stream (Standard Encryption) */ standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24), /* @see 2.3.4.10 \EncryptionInfo Stream (Agile Encryption) */ - agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40) + agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40), + /* @see XOR Obfuscation */ + xor("org.apache.poi.poifs.crypt.xor.XOREncryptionInfoBuilder", 0, 0, 0) ; public final String builder; diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java index 1fdf3a982..b6d8eda00 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java @@ -29,11 +29,9 @@ import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.crypt.*; -import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.StringUtil; public class BinaryRC4Decryptor extends Decryptor implements Cloneable { @@ -53,7 +51,7 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable { super(stream, size, _chunkSize); } - public BinaryRC4CipherInputStream(LittleEndianInput stream) + public BinaryRC4CipherInputStream(InputStream stream) throws GeneralSecurityException { super(stream, Integer.MAX_VALUE, _chunkSize); } @@ -140,7 +138,8 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable { return new BinaryRC4CipherInputStream(dis, _length); } - public InputStream getDataStream(LittleEndianInput stream) + @Override + public InputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { return new BinaryRC4CipherInputStream(stream); } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java index 07b791074..451708c6e 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java @@ -45,7 +45,6 @@ import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.BoundedInputStream; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.StringUtil; @@ -146,7 +145,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable { } @Override - public ChunkedCipherInputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + public ChunkedCipherInputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { return new CryptoAPICipherInputStream(stream, size, initialPos); } @@ -233,7 +232,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable { return CryptoAPIDecryptor.this.initCipherForBlock(existing, block); } - public CryptoAPICipherInputStream(LittleEndianInput stream, long size, int initialPos) + public CryptoAPICipherInputStream(InputStream stream, long size, int initialPos) throws GeneralSecurityException { super(stream, size, _chunkSize, initialPos); } diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java new file mode 100644 index 000000000..cb50b4782 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java @@ -0,0 +1,174 @@ +/* ==================================================================== + 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.poifs.crypt.xor; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.util.LittleEndian; + +public class XORDecryptor extends Decryptor implements Cloneable { + private long _length = -1L; + private int _chunkSize = 512; + + private class XORCipherInputStream extends ChunkedCipherInputStream { + private final int _initialOffset; + private int _recordStart = 0; + private int _recordEnd = 0; + + @Override + protected Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException { + return XORDecryptor.this.initCipherForBlock(existing, block); + } + + public XORCipherInputStream(InputStream stream, int initialPos) + throws GeneralSecurityException { + super(stream, Integer.MAX_VALUE, _chunkSize); + _initialOffset = initialPos; + } + + @Override + protected int invokeCipher(int totalBytes, boolean doFinal) { + final int pos = (int)getPos(); + final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded(); + final byte chunk[] = getChunk(); + final byte plain[] = getPlain(); + final int posInChunk = pos & getChunkMask(); + + /* + * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 + * + * The initial value for XorArrayIndex is as follows: + * XorArrayIndex = (FileOffset + Data.Length) % 16 + * + * The FileOffset variable in this context is the stream offset into the Workbook stream at + * the time we are about to write each of the bytes of the record data. + * This (the value) is then incremented after each byte is written. + */ + final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart); + + for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) { + // The following is taken from the Libre Office implementation + // It seems that the encrypt and decrypt method is mixed up + // in the MS-OFFCRYPTO docs + byte value = plain[posInChunk+i]; + value = rotateLeft(value, 3); + value ^= xorArray[(xorArrayIndex+i) & 0x0F]; + chunk[posInChunk+i] = value; + } + + // the other bytes will be encoded, when setNextRecordSize is called the next time + return totalBytes; + } + + private byte rotateLeft(byte bits, int shift) { + return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); + } + + + /** + * Decrypts a xor obfuscated byte array. + * The data is decrypted in-place + * + * @see 2.3.7.3 Binary Document XOR Data Transformation Method 1 + */ + @Override + public void setNextRecordSize(int recordSize) { + _recordStart = (int)getPos(); + _recordEnd = _recordStart+recordSize; + int pos = (int)getPos(); + byte chunk[] = getChunk(); + int chunkMask = getChunkMask(); + int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask)); + invokeCipher(nextBytes, true); + } + } + + protected XORDecryptor() { + } + + @Override + public boolean verifyPassword(String password) { + XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier(); + int keyVer = LittleEndian.getUShort(ver.getEncryptedKey()); + int verifierVer = LittleEndian.getUShort(ver.getEncryptedVerifier()); + int keyComp = CryptoFunctions.createXorKey1(password); + int verifierComp = CryptoFunctions.createXorVerifier1(password); + if (keyVer == keyComp && verifierVer == verifierComp) { + byte xorArray[] = CryptoFunctions.createXorArray1(password); + setSecretKey(new SecretKeySpec(xorArray, "XOR")); + return true; + } else { + return false; + } + } + + @Override + public Cipher initCipherForBlock(Cipher cipher, int block) + throws GeneralSecurityException { + return null; + } + + protected static Cipher initCipherForBlock(Cipher cipher, int block, + EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode) + throws GeneralSecurityException { + return null; + } + + @Override + public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + throw new RuntimeException("not supported"); + } + + @Override + public InputStream getDataStream(InputStream stream, int size, int initialPos) + throws IOException, GeneralSecurityException { + return new XORCipherInputStream(stream, initialPos); + } + + + @Override + public long getLength() { + if (_length == -1L) { + throw new IllegalStateException("Decryptor.getDataStream() was not called"); + } + + return _length; + } + + @Override + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + + @Override + public XORDecryptor clone() throws CloneNotSupportedException { + return (XORDecryptor)super.clone(); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java similarity index 60% rename from src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java rename to src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java index 8ac742e0d..cc5068f6b 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java @@ -1,30 +1,37 @@ -/* ==================================================================== - 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.record.crypto; - - -public interface Biff8Cipher { - void startRecord(int currentSid); - void setNextRecordSize(int recordSize); - void skipTwoBytes(); - void xor(byte[] buf, int pOffset, int pLen); - int xorByte(int rawVal); - int xorShort(int rawVal); - int xorInt(int rawVal); - long xorLong(long rawVal); -} +/* ==================================================================== + 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.poifs.crypt.xor; + +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class XOREncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable { + + protected XOREncryptionHeader() { + } + + @Override + public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) { + } + + @Override + public XOREncryptionHeader clone() throws CloneNotSupportedException { + return (XOREncryptionHeader)super.clone(); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java new file mode 100644 index 000000000..9fcaf8b35 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java @@ -0,0 +1,62 @@ +/* ==================================================================== + 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.poifs.crypt.xor; + +import java.io.IOException; + +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.util.LittleEndianInput; + +public class XOREncryptionInfoBuilder implements EncryptionInfoBuilder { + + public XOREncryptionInfoBuilder() { + } + + @Override + public void initialize(EncryptionInfo info, LittleEndianInput dis) + throws IOException { + info.setHeader(new XOREncryptionHeader()); + info.setVerifier(new XOREncryptionVerifier(dis)); + Decryptor dec = new XORDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new XOREncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); + } + + @Override + public void initialize(EncryptionInfo info, + CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, + int keyBits, int blockSize, ChainingMode chainingMode) { + info.setHeader(new XOREncryptionHeader()); + info.setVerifier(new XOREncryptionVerifier()); + Decryptor dec = new XORDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new XOREncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java new file mode 100644 index 000000000..1dcfb941c --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java @@ -0,0 +1,61 @@ +/* ==================================================================== + 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.poifs.crypt.xor; + +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianInput; + +public class XOREncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable { + + protected XOREncryptionVerifier() { + setEncryptedKey(new byte[2]); + setEncryptedVerifier(new byte[2]); + } + + protected XOREncryptionVerifier(LittleEndianInput is) { + /** + * key (2 bytes): An unsigned integer that specifies the obfuscation key. + * See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR + * array where it describes the generation of 16-bit XorKey value. + */ + byte key[] = new byte[2]; + is.readFully(key); + setEncryptedKey(key); + + /** + * verificationBytes (2 bytes): An unsigned integer that specifies + * the password verification identifier. + */ + byte verifier[] = new byte[2]; + is.readFully(verifier); + setEncryptedVerifier(verifier); + } + + @Override + public void write(LittleEndianByteArrayOutputStream bos) { + bos.write(getEncryptedKey()); + bos.write(getEncryptedVerifier()); + } + + @Override + public XOREncryptionVerifier clone() throws CloneNotSupportedException { + return (XOREncryptionVerifier)super.clone(); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java new file mode 100644 index 000000000..054b5e0e4 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java @@ -0,0 +1,99 @@ +/* ==================================================================== + 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.poifs.crypt.xor; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.DataSpaceMapUtils; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class XOREncryptor extends Encryptor implements Cloneable { + + protected XOREncryptor() { + } + + @Override + public void confirmPassword(String password) { + } + + @Override + public void confirmPassword(String password, byte keySpec[], + byte keySalt[], byte verifier[], byte verifierSalt[], + byte integritySalt[]) { + } + + @Override + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + OutputStream countStream = new XORCipherOutputStream(dir); + return countStream; + } + + protected int getKeySizeInBytes() { + return -1; + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + } + + @Override + public XOREncryptor clone() throws CloneNotSupportedException { + return (XOREncryptor)super.clone(); + } + + protected class XORCipherOutputStream extends ChunkedCipherOutputStream { + + @Override + protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) + throws GeneralSecurityException { + return XORDecryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); + } + + @Override + protected void calculateChecksum(File file, int i) { + } + + @Override + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + XOREncryptor.this.createEncryptionInfoEntry(dir); + } + + public XORCipherOutputStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + super(dir, 512); + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java index c21f89dd1..57f0f31ed 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java @@ -53,7 +53,8 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom { ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8); LittleEndianInputStream leis = new LittleEndianInputStream(bis); - ei = new EncryptionInfo(leis, true); + ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI); + leis.close(); } public DocumentEncryptionAtom() { @@ -121,6 +122,7 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom { LittleEndian.putInt(_header, 4, bos.getWriteIndex()); out.write(_header); out.write(data, 0, bos.getWriteIndex()); + bos.close(); } @Override diff --git a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java index b7598fd12..e32816756 100644 --- a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java +++ b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java @@ -21,8 +21,8 @@ import org.apache.poi.hssf.record.aggregates.AllRecordAggregateTests; import org.apache.poi.hssf.record.cf.TestCellRange; import org.apache.poi.hssf.record.chart.AllChartRecordTests; import org.apache.poi.hssf.record.common.TestUnicodeString; -import org.apache.poi.hssf.record.crypto.AllHSSFEncryptionTests; import org.apache.poi.hssf.record.pivot.AllPivotRecordTests; +import org.apache.poi.poifs.crypt.AllEncryptionTests; import org.apache.poi.ss.formula.constant.TestConstantValueParser; import org.apache.poi.ss.formula.ptg.AllFormulaTests; import org.junit.runner.RunWith; @@ -34,7 +34,7 @@ import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ AllChartRecordTests.class, - AllHSSFEncryptionTests.class, + AllEncryptionTests.class, AllFormulaTests.class, AllPivotRecordTests.class, AllRecordAggregateTests.class, diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java deleted file mode 100644 index 294cb09e7..000000000 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java +++ /dev/null @@ -1,102 +0,0 @@ -/* ==================================================================== - 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.record.crypto; - -import java.util.Arrays; - -import junit.framework.ComparisonFailure; -import junit.framework.TestCase; - -import org.apache.poi.util.HexDump; -import org.apache.poi.util.HexRead; - -/** - * Tests for {@link Biff8EncryptionKey} - * - * @author Josh Micich - */ -public final class TestBiff8EncryptionKey extends TestCase { - - private static byte[] fromHex(String hexString) { - return HexRead.readFromString(hexString); - } - public void testCreateKeyDigest() { - byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A"); - byte[] keyDigest = Biff8RC4Key.createKeyDigest("MoneyForNothing", docIdData); - byte[] expResult = fromHex("C2 D9 56 B2 6B"); - if (!Arrays.equals(expResult, keyDigest)) { - throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest)); - } - } - - - public void testValidateWithDefaultPassword() { - - String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D' - String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A"; - String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; - - String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D"; - String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C' - String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16"; - - confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA); - confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB); - confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA); - confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB); - } - - public void testValidateWithSuppliedPassword() { - - String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; - String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; - String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret' - String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret' - - confirmValid(true, docId, saltData, saltHashA, "5ecret"); - confirmValid(false, docId, saltData, saltHashA, "Secret"); - confirmValid(true, docId, saltData, saltHashB, "Secret"); - confirmValid(false, docId, saltData, saltHashB, "secret"); - } - - - private static void confirmValid(boolean expectedResult, - String docIdHex, String saltDataHex, String saltHashHex) { - confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null); - } - private static void confirmValid(boolean expectedResult, - String docIdHex, String saltDataHex, String saltHashHex, String password) { - byte[] docId = fromHex(docIdHex); - byte[] saltData = fromHex(saltDataHex); - byte[] saltHash = fromHex(saltHashHex); - - - Biff8EncryptionKey key; - if (password == null) { - key = Biff8EncryptionKey.create(docId); - } else { - key = Biff8EncryptionKey.create(password, docId); - } - boolean actResult = key.validate(saltData, saltHash); - if (expectedResult) { - assertTrue("validate failed", actResult); - } else { - assertFalse("validate succeeded unexpectedly", actResult); - } - } -} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java new file mode 100644 index 000000000..e7618073b --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java @@ -0,0 +1,62 @@ +/* ==================================================================== + 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.apache.poi.hssf.HSSFITestDataProvider; +import org.apache.poi.hssf.extractor.ExcelExtractor; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.junit.AfterClass; +import org.junit.Test; + +public class TestCryptoAPI { + final HSSFITestDataProvider ssTests = HSSFITestDataProvider.instance; + + @AfterClass + public static void resetPW() { + Biff8EncryptionKey.setCurrentUserPassword(null); + } + + @Test + public void bug59857() throws IOException { + Biff8EncryptionKey.setCurrentUserPassword("abc"); + HSSFWorkbook wb1 = ssTests.openSampleWorkbook("xor-encryption-abc.xls"); + String textExpected = "Sheet1\n1\n2\n3\n"; + String textActual = new ExcelExtractor(wb1).getText(); + assertEquals(textExpected, textActual); + wb1.close(); + + Biff8EncryptionKey.setCurrentUserPassword("password"); + HSSFWorkbook wb2 = ssTests.openSampleWorkbook("password.xls"); + textExpected = "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed."; + textActual = new ExcelExtractor(wb2).getText(); + assertTrue(textActual.contains(textExpected)); + wb2.close(); + + Biff8EncryptionKey.setCurrentUserPassword("freedom"); + HSSFWorkbook wb3 = ssTests.openSampleWorkbook("35897-type4.xls"); + textExpected = "Sheet1\nhello there!\n"; + textActual = new ExcelExtractor(wb3).getText(); + assertEquals(textExpected, textActual); + wb3.close(); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java b/src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java similarity index 83% rename from src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java rename to src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java index c72700878..8bd67db91 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java +++ b/src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java @@ -15,20 +15,19 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import org.junit.runner.RunWith; import org.junit.runners.Suite; /** - * Collects all tests for package org.apache.poi.hssf.record.crypto. - * - * @author Josh Micich + * Collects all tests for package org.apache.poi.poifs.crypt. */ @RunWith(Suite.class) @Suite.SuiteClasses({ TestBiff8DecryptingStream.class, - TestBiff8EncryptionKey.class + TestCipherAlgorithm.class, + TestXorEncryption.class }) -public final class AllHSSFEncryptionTests { +public final class AllEncryptionTests { } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java b/src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java similarity index 94% rename from src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java rename to src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java index 26eb16b2f..43ee42941 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java @@ -15,7 +15,7 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -23,17 +23,18 @@ import static org.junit.Assert.assertFalse; import java.io.InputStream; import java.util.Arrays; -import junit.framework.AssertionFailedError; -import junit.framework.ComparisonFailure; +import javax.crypto.spec.SecretKeySpec; +import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.util.HexDump; import org.apache.poi.util.HexRead; import org.junit.Test; +import junit.framework.AssertionFailedError; +import junit.framework.ComparisonFailure; + /** * Tests for {@link Biff8DecryptingStream} - * - * @author Josh Micich */ public final class TestBiff8DecryptingStream { @@ -49,12 +50,10 @@ public final class TestBiff8DecryptingStream { public MockStream(int initialValue) { _initialValue = initialValue; } + public int read() { return (_initialValue+_position++) & 0xFF; } - public int getPosition() { - return _position; - } } private static final class StreamTester { @@ -70,7 +69,11 @@ public final class TestBiff8DecryptingStream { public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) { _ms = ms; byte[] keyDigest = HexRead.readFromString(keyDigestHex); - _bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest)); + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + Decryptor dec = ei.getDecryptor(); + dec.setSecretKey(new SecretKeySpec(keyDigest, "RC4")); + + _bds = new Biff8DecryptingStream(_ms, 0, ei); assertEquals(expectedFirstInt, _bds.readInt()); _errorsOccurred = false; } @@ -84,11 +87,11 @@ public final class TestBiff8DecryptingStream { * Also confirms that read position of the underlying stream is aligned. */ public void rollForward(int fromPosition, int toPosition) { - assertEquals(fromPosition, _ms.getPosition()); + assertEquals(fromPosition, _bds.getPosition()); for (int i = fromPosition; i < toPosition; i++) { _bds.readByte(); } - assertEquals(toPosition, _ms.getPosition()); + assertEquals(toPosition, _bds.getPosition()); } public void confirmByte(int expVal) { diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java b/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java index 1e0fc14d8..68d6ab290 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java @@ -17,14 +17,14 @@ package org.apache.poi.poifs.crypt; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.apache.poi.EncryptedDocumentException; import org.junit.Test; public class TestCipherAlgorithm { @Test - public void test() { + public void validInputs() { assertEquals(128, CipherAlgorithm.aes128.defaultKeySize); for(CipherAlgorithm alg : CipherAlgorithm.values()) { @@ -33,27 +33,20 @@ public class TestCipherAlgorithm { assertEquals(CipherAlgorithm.aes128, CipherAlgorithm.fromEcmaId(0x660E)); assertEquals(CipherAlgorithm.aes192, CipherAlgorithm.fromXmlId("AES", 192)); - - try { - CipherAlgorithm.fromEcmaId(0); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } - - try { - CipherAlgorithm.fromXmlId("AES", 1); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } - - try { - CipherAlgorithm.fromXmlId("RC1", 0x40); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } } - + + @Test(expected=EncryptedDocumentException.class) + public void invalidEcmaId() { + CipherAlgorithm.fromEcmaId(0); + } + + @Test(expected=EncryptedDocumentException.class) + public void invalidXmlId1() { + CipherAlgorithm.fromXmlId("AES", 1); + } + + @Test(expected=EncryptedDocumentException.class) + public void invalidXmlId2() { + CipherAlgorithm.fromXmlId("RC1", 0x40); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java b/src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java similarity index 94% rename from src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java rename to src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java index e79f2fcc6..cae6426f6 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java @@ -15,13 +15,14 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.poifs.crypt.CryptoFunctions; diff --git a/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java b/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java new file mode 100644 index 000000000..b1155c3f5 --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java @@ -0,0 +1,106 @@ +/* ==================================================================== + 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.poifs.crypt.binaryrc4; + +import static org.apache.poi.util.HexRead.readFromString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.security.GeneralSecurityException; + +import javax.crypto.SecretKey; + +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.junit.Test; + +public class TestBinaryRC4 { + @Test + public void createKeyDigest() throws GeneralSecurityException { + byte[] docIdData = readFromString("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A"); + byte[] expResult = readFromString("C2 D9 56 B2 6B"); + + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier(); + ver.setSalt(docIdData); + SecretKey sk = BinaryRC4Decryptor.generateSecretKey("MoneyForNothing", ver); + + assertArrayEquals("keyDigest mismatch", expResult, sk.getEncoded()); + } + + @Test + public void testValidateWithDefaultPassword() throws GeneralSecurityException { + + String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D' + String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A"; + String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + + String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D"; + String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C' + String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16"; + + confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB); + confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB); + } + + @Test + public void testValidateWithSuppliedPassword() throws GeneralSecurityException { + + String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; + String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret' + String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret' + + confirmValid(true, docId, saltData, saltHashA, "5ecret"); + confirmValid(false, docId, saltData, saltHashA, "Secret"); + confirmValid(true, docId, saltData, saltHashB, "Secret"); + confirmValid(false, docId, saltData, saltHashB, "secret"); + } + + + private static void confirmValid(boolean expectedResult, + String docIdHex, String saltDataHex, String saltHashHex) throws GeneralSecurityException { + confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null); + } + + private static void confirmValid(boolean expectedResult, String docIdHex, + String saltDataHex, String saltHashHex, String password) throws GeneralSecurityException { + byte[] docId = readFromString(docIdHex); + byte[] saltData = readFromString(saltDataHex); + byte[] saltHash = readFromString(saltHashHex); + + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier(); + ver.setSalt(docId); + ver.setEncryptedVerifier(saltData); + ver.setEncryptedVerifierHash(saltHash); + + String pass = password == null ? Decryptor.DEFAULT_PASSWORD : password; + boolean actResult = ei.getDecryptor().verifyPassword(pass); + if (expectedResult) { + assertTrue("validate failed", actResult); + } else { + assertFalse("validate succeeded unexpectedly", actResult); + } + } + +} From 680683cf7726c1661130a08f47c8a0a2c438a927 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 8 Aug 2016 01:14:36 +0000 Subject: [PATCH 4/9] merge down trunk git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755463 13f79535-47bb-0310-9956-ffa450edef68 --- build.xml | 77 +- maven/poi.pom | 5 + sonar/main/pom.xml | 5 + .../aggregates/RowRecordsAggregate.java | 786 +++++++++--------- .../hssf/usermodel/HSSFFormulaEvaluator.java | 553 +++++------- .../poi/hssf/usermodel/HSSFWorkbook.java | 24 +- .../poi/poifs/crypt/CryptoFunctions.java | 26 +- .../poi/ss/formula/BaseFormulaEvaluator.java | 194 +++++ .../apache/poi/ss/formula/LazyRefEval.java | 19 +- .../poi/ss/formula/functions/Subtotal.java | 30 +- .../apache/poi/ss/usermodel/CellStyle.java | 2 +- .../org/apache/poi/ss/usermodel/Workbook.java | 22 +- .../org/apache/poi/util/CommonsLogger.java | 12 +- src/java/org/apache/poi/util/NullLogger.java | 10 +- .../org/apache/poi/util/SystemOutLogger.java | 9 +- .../java/org/apache/poi/POIXMLDocument.java | 8 +- .../apache/poi/openxml4j/opc/OPCPackage.java | 8 +- .../opc/internal/ContentTypeManager.java | 7 +- .../poi/xslf/usermodel/XSLFGroupShape.java | 6 +- .../poi/xssf/extractor/XSSFExportToXml.java | 47 +- .../apache/poi/xssf/streaming/SXSSFCell.java | 13 +- .../poi/xssf/streaming/SXSSFWorkbook.java | 43 +- .../usermodel/BaseXSSFEvaluationWorkbook.java | 2 +- .../usermodel/BaseXSSFFormulaEvaluator.java | 116 +-- .../xssf/usermodel/XSSFFormulaEvaluator.java | 6 +- .../apache/poi/xssf/usermodel/XSSFName.java | 11 +- .../apache/poi/xssf/usermodel/XSSFSheet.java | 4 +- .../poi/xssf/usermodel/XSSFWorkbook.java | 180 ++-- .../usermodel/helpers/XSSFFormulaUtils.java | 4 +- .../usermodel/helpers/XSSFPasswordHelper.java | 136 +++ .../usermodel/helpers/XSSFPaswordHelper.java | 190 ++--- .../usermodel/helpers/XSSFRowShifter.java | 4 +- .../opc/internal/TestContentTypeManager.java | 24 +- .../poi/xssf/usermodel/TestXSSFBugs.java | 26 +- .../usermodel/TestXSSFFormulaEvaluation.java | 37 +- .../poi/xssf/usermodel/TestXSSFName.java | 42 +- .../poi/xssf/usermodel/TestXSSFSheet.java | 25 + .../poi/xssf/usermodel/TestXSSFWorkbook.java | 40 + .../src/org/apache/poi/hwpf/HWPFDocument.java | 38 +- .../poi/hwpf/usermodel/TestHWPFWrite.java | 81 ++ .../poi/sl/usermodel/BaseTestSlideShow.java | 22 +- .../ss/formula/functions/TestIndirect.java | 9 +- .../ss/formula/functions/TestSubtotal.java | 97 ++- .../ss/formula/functions/TestWeekdayFunc.java | 66 ++ .../ss/usermodel/BaseTestBugzillaIssues.java | 79 +- .../poi/ss/usermodel/BaseTestNamedRange.java | 20 +- .../ss/usermodel/BaseTestSheetShiftRows.java | 8 +- .../apache/poi/ss/util/BaseTestCellUtil.java | 29 +- .../org/apache/poi/util/DummyPOILogger.java | 4 + .../org/apache/poi/util/TestPOILogger.java | 10 +- test-data/spreadsheet/59736.xlsx | Bin 0 -> 8752 bytes 51 files changed, 1967 insertions(+), 1249 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPasswordHelper.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestWeekdayFunc.java create mode 100644 test-data/spreadsheet/59736.xlsx diff --git a/build.xml b/build.xml index 0285acae3..2eddf722f 100644 --- a/build.xml +++ b/build.xml @@ -17,7 +17,6 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> - @@ -169,6 +169,9 @@ under the License. + + @@ -192,8 +195,8 @@ under the License. value="${repository.m2}/maven2/org/apache/xmlbeans/xmlbeans/2.6.0/xmlbeans-2.6.0.jar"/> - - + + @@ -285,6 +288,7 @@ under the License. + @@ -303,6 +307,8 @@ under the License. + + @@ -310,6 +316,7 @@ under the License. + @@ -498,9 +505,7 @@ under the License. - - - - + @@ -531,12 +536,14 @@ under the License. + + @@ -587,6 +594,7 @@ under the License. + @@ -605,6 +613,7 @@ under the License. + @@ -1161,6 +1170,8 @@ under the License. + + @@ -1242,6 +1253,8 @@ under the License. + + @@ -1258,6 +1271,16 @@ under the License. + + + + + + + + + + @@ -1288,6 +1311,8 @@ under the License. + + @@ -1335,6 +1360,8 @@ under the License. and on Windows with jdk-1.5.22 --> + + @@ -1373,6 +1400,8 @@ under the License. + + @@ -1396,6 +1425,8 @@ under the License. + + @@ -1439,6 +1470,8 @@ under the License. + + @@ -1485,6 +1518,8 @@ under the License. + + @@ -1503,7 +1538,20 @@ under the License. - + + + + + + + + + + + + + @@ -1522,6 +1570,8 @@ under the License. + + @@ -1529,6 +1579,8 @@ under the License. + + @@ -1557,6 +1609,8 @@ under the License. + + @@ -1868,6 +1922,7 @@ under the License. + @@ -2083,7 +2138,7 @@ under the License. - + @@ -2103,11 +2158,13 @@ under the License. output="xml:withMessages" outputFile="build/findbugs.xml" effort="max" + failOnError="true" excludeFilter="src/resources/devtools/findbugs-filters.xml"> + @@ -2117,9 +2174,11 @@ under the License. + + diff --git a/maven/poi.pom b/maven/poi.pom index 7c1b4cf84..efd24cfe0 100644 --- a/maven/poi.pom +++ b/maven/poi.pom @@ -91,6 +91,11 @@ test 4.12 + + org.apache.commons + commons-collections4 + 4.1 + diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index 72c3eebfc..ffc9499d9 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -110,6 +110,11 @@ + + org.apache.commons + commons-collections4 + 4.1 + commons-codec commons-codec diff --git a/src/java/org/apache/poi/hssf/record/aggregates/RowRecordsAggregate.java b/src/java/org/apache/poi/hssf/record/aggregates/RowRecordsAggregate.java index 0319af64e..5551639b4 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/RowRecordsAggregate.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/RowRecordsAggregate.java @@ -34,184 +34,184 @@ import org.apache.poi.ss.formula.FormulaShifter; * @author Jason Height (jheight at chariot dot net dot au) */ public final class RowRecordsAggregate extends RecordAggregate { - private int _firstrow = -1; - private int _lastrow = -1; - private final Map _rowRecords; - private final ValueRecordsAggregate _valuesAgg; - private final List _unknownRecords; - private final SharedValueManager _sharedValueManager; + private int _firstrow = -1; + private int _lastrow = -1; + private final Map _rowRecords; + private final ValueRecordsAggregate _valuesAgg; + private final List _unknownRecords; + private final SharedValueManager _sharedValueManager; - // Cache values to speed up performance of + // Cache values to speed up performance of // getStartRowNumberForBlock / getEndRowNumberForBlock, see Bugzilla 47405 private RowRecord[] _rowRecordValues = null; - /** Creates a new instance of ValueRecordsAggregate */ - public RowRecordsAggregate() { - this(SharedValueManager.createEmpty()); - } - private RowRecordsAggregate(SharedValueManager svm) { - if (svm == null) { - throw new IllegalArgumentException("SharedValueManager must be provided."); - } - _rowRecords = new TreeMap(); - _valuesAgg = new ValueRecordsAggregate(); - _unknownRecords = new ArrayList(); - _sharedValueManager = svm; - } + /** Creates a new instance of ValueRecordsAggregate */ + public RowRecordsAggregate() { + this(SharedValueManager.createEmpty()); + } + private RowRecordsAggregate(SharedValueManager svm) { + if (svm == null) { + throw new IllegalArgumentException("SharedValueManager must be provided."); + } + _rowRecords = new TreeMap(); + _valuesAgg = new ValueRecordsAggregate(); + _unknownRecords = new ArrayList(); + _sharedValueManager = svm; + } - /** - * @param rs record stream with all {@link SharedFormulaRecord} - * {@link ArrayRecord}, {@link TableRecord} {@link MergeCellsRecord} Records removed - * @param svm an initialised {@link SharedValueManager} (from the shared formula, array - * and table records of the current sheet). Never null. - */ - public RowRecordsAggregate(RecordStream rs, SharedValueManager svm) { - this(svm); - while(rs.hasNext()) { - Record rec = rs.getNext(); - switch (rec.getSid()) { - case RowRecord.sid: - insertRow((RowRecord) rec); - continue; + /** + * @param rs record stream with all {@link SharedFormulaRecord} + * {@link ArrayRecord}, {@link TableRecord} {@link MergeCellsRecord} Records removed + * @param svm an initialised {@link SharedValueManager} (from the shared formula, array + * and table records of the current sheet). Never null. + */ + public RowRecordsAggregate(RecordStream rs, SharedValueManager svm) { + this(svm); + while(rs.hasNext()) { + Record rec = rs.getNext(); + switch (rec.getSid()) { + case RowRecord.sid: + insertRow((RowRecord) rec); + continue; case DConRefRecord.sid: addUnknownRecord(rec); continue; case DBCellRecord.sid: - // end of 'Row Block'. Should only occur after cell records - // ignore DBCELL records because POI generates them upon re-serialization - continue; - } - if (rec instanceof UnknownRecord) { - // might need to keep track of where exactly these belong - addUnknownRecord(rec); - while (rs.peekNextSid() == ContinueRecord.sid) { - addUnknownRecord(rs.getNext()); - } - continue; - } - if (rec instanceof MulBlankRecord) { - _valuesAgg.addMultipleBlanks((MulBlankRecord) rec); - continue; - } - if (!(rec instanceof CellValueRecordInterface)) { - throw new RuntimeException("Unexpected record type (" + rec.getClass().getName() + ")"); - } - _valuesAgg.construct((CellValueRecordInterface)rec, rs, svm); - } - } - /** - * Handles UnknownRecords which appear within the row/cell records - */ - private void addUnknownRecord(Record rec) { - // ony a few distinct record IDs are encountered by the existing POI test cases: - // 0x1065 // many - // 0x01C2 // several - // 0x0034 // few - // No documentation could be found for these + // end of 'Row Block'. Should only occur after cell records + // ignore DBCELL records because POI generates them upon re-serialization + continue; + } + if (rec instanceof UnknownRecord) { + // might need to keep track of where exactly these belong + addUnknownRecord(rec); + while (rs.peekNextSid() == ContinueRecord.sid) { + addUnknownRecord(rs.getNext()); + } + continue; + } + if (rec instanceof MulBlankRecord) { + _valuesAgg.addMultipleBlanks((MulBlankRecord) rec); + continue; + } + if (!(rec instanceof CellValueRecordInterface)) { + throw new RuntimeException("Unexpected record type (" + rec.getClass().getName() + ")"); + } + _valuesAgg.construct((CellValueRecordInterface)rec, rs, svm); + } + } + /** + * Handles UnknownRecords which appear within the row/cell records + */ + private void addUnknownRecord(Record rec) { + // ony a few distinct record IDs are encountered by the existing POI test cases: + // 0x1065 // many + // 0x01C2 // several + // 0x0034 // few + // No documentation could be found for these - // keep the unknown records for re-serialization - _unknownRecords.add(rec); - } - public void insertRow(RowRecord row) { - // Integer integer = Integer.valueOf(row.getRowNumber()); - _rowRecords.put(Integer.valueOf(row.getRowNumber()), row); - // Clear the cached values - _rowRecordValues = null; - if ((row.getRowNumber() < _firstrow) || (_firstrow == -1)) { - _firstrow = row.getRowNumber(); - } - if ((row.getRowNumber() > _lastrow) || (_lastrow == -1)) { - _lastrow = row.getRowNumber(); - } - } + // keep the unknown records for re-serialization + _unknownRecords.add(rec); + } + public void insertRow(RowRecord row) { + // Integer integer = Integer.valueOf(row.getRowNumber()); + _rowRecords.put(Integer.valueOf(row.getRowNumber()), row); + // Clear the cached values + _rowRecordValues = null; + if ((row.getRowNumber() < _firstrow) || (_firstrow == -1)) { + _firstrow = row.getRowNumber(); + } + if ((row.getRowNumber() > _lastrow) || (_lastrow == -1)) { + _lastrow = row.getRowNumber(); + } + } - public void removeRow(RowRecord row) { - int rowIndex = row.getRowNumber(); - _valuesAgg.removeAllCellsValuesForRow(rowIndex); - Integer key = Integer.valueOf(rowIndex); - RowRecord rr = _rowRecords.remove(key); - if (rr == null) { - throw new RuntimeException("Invalid row index (" + key.intValue() + ")"); - } - if (row != rr) { - _rowRecords.put(key, rr); - throw new RuntimeException("Attempt to remove row that does not belong to this sheet"); - } - - // Clear the cached values - _rowRecordValues = null; - } + public void removeRow(RowRecord row) { + int rowIndex = row.getRowNumber(); + _valuesAgg.removeAllCellsValuesForRow(rowIndex); + Integer key = Integer.valueOf(rowIndex); + RowRecord rr = _rowRecords.remove(key); + if (rr == null) { + throw new RuntimeException("Invalid row index (" + key.intValue() + ")"); + } + if (row != rr) { + _rowRecords.put(key, rr); + throw new RuntimeException("Attempt to remove row that does not belong to this sheet"); + } + + // Clear the cached values + _rowRecordValues = null; + } - public RowRecord getRow(int rowIndex) { + public RowRecord getRow(int rowIndex) { int maxrow = SpreadsheetVersion.EXCEL97.getLastRowIndex(); if (rowIndex < 0 || rowIndex > maxrow) { - throw new IllegalArgumentException("The row number must be between 0 and " + maxrow + ", but had: " + rowIndex); - } - return _rowRecords.get(Integer.valueOf(rowIndex)); - } + throw new IllegalArgumentException("The row number must be between 0 and " + maxrow + ", but had: " + rowIndex); + } + return _rowRecords.get(Integer.valueOf(rowIndex)); + } - public int getPhysicalNumberOfRows() - { - return _rowRecords.size(); - } + public int getPhysicalNumberOfRows() + { + return _rowRecords.size(); + } - public int getFirstRowNum() - { - return _firstrow; - } + public int getFirstRowNum() + { + return _firstrow; + } - public int getLastRowNum() - { - return _lastrow; - } + public int getLastRowNum() + { + return _lastrow; + } - /** Returns the number of row blocks. - *

The row blocks are goupings of rows that contain the DBCell record - * after them - */ - public int getRowBlockCount() { - int size = _rowRecords.size()/DBCellRecord.BLOCK_SIZE; - if ((_rowRecords.size() % DBCellRecord.BLOCK_SIZE) != 0) - size++; - return size; - } + /** Returns the number of row blocks. + *

The row blocks are goupings of rows that contain the DBCell record + * after them + */ + public int getRowBlockCount() { + int size = _rowRecords.size()/DBCellRecord.BLOCK_SIZE; + if ((_rowRecords.size() % DBCellRecord.BLOCK_SIZE) != 0) + size++; + return size; + } - private int getRowBlockSize(int block) { - return RowRecord.ENCODED_SIZE * getRowCountForBlock(block); - } + private int getRowBlockSize(int block) { + return RowRecord.ENCODED_SIZE * getRowCountForBlock(block); + } - /** Returns the number of physical rows within a block*/ - public int getRowCountForBlock(int block) { - int startIndex = block * DBCellRecord.BLOCK_SIZE; - int endIndex = startIndex + DBCellRecord.BLOCK_SIZE - 1; - if (endIndex >= _rowRecords.size()) - endIndex = _rowRecords.size()-1; + /** Returns the number of physical rows within a block*/ + public int getRowCountForBlock(int block) { + int startIndex = block * DBCellRecord.BLOCK_SIZE; + int endIndex = startIndex + DBCellRecord.BLOCK_SIZE - 1; + if (endIndex >= _rowRecords.size()) + endIndex = _rowRecords.size()-1; - return endIndex-startIndex+1; - } + return endIndex-startIndex+1; + } - /** Returns the physical row number of the first row in a block*/ - private int getStartRowNumberForBlock(int block) { - int startIndex = block * DBCellRecord.BLOCK_SIZE; + /** Returns the physical row number of the first row in a block*/ + private int getStartRowNumberForBlock(int block) { + int startIndex = block * DBCellRecord.BLOCK_SIZE; - if(_rowRecordValues == null){ + if (_rowRecordValues == null) { _rowRecordValues = _rowRecords.values().toArray(new RowRecord[_rowRecords.size()]); } try { return _rowRecordValues[startIndex].getRowNumber(); } catch(ArrayIndexOutOfBoundsException e) { - throw new RuntimeException("Did not find start row for block " + block); - } - } + throw new RuntimeException("Did not find start row for block " + block); + } + } - /** Returns the physical row number of the end row in a block*/ - private int getEndRowNumberForBlock(int block) { - int endIndex = ((block + 1)*DBCellRecord.BLOCK_SIZE)-1; - if (endIndex >= _rowRecords.size()) - endIndex = _rowRecords.size()-1; + /** Returns the physical row number of the end row in a block*/ + private int getEndRowNumberForBlock(int block) { + int endIndex = ((block + 1)*DBCellRecord.BLOCK_SIZE)-1; + if (endIndex >= _rowRecords.size()) + endIndex = _rowRecords.size()-1; - if(_rowRecordValues == null){ + if (_rowRecordValues == null){ _rowRecordValues = _rowRecords.values().toArray(new RowRecord[_rowRecords.size()]); } @@ -219,287 +219,287 @@ public final class RowRecordsAggregate extends RecordAggregate { return _rowRecordValues[endIndex].getRowNumber(); } catch(ArrayIndexOutOfBoundsException e) { throw new RuntimeException("Did not find end row for block " + block); - } - } + } + } - private int visitRowRecordsForBlock(int blockIndex, RecordVisitor rv) { - final int startIndex = blockIndex*DBCellRecord.BLOCK_SIZE; - final int endIndex = startIndex + DBCellRecord.BLOCK_SIZE; + private int visitRowRecordsForBlock(int blockIndex, RecordVisitor rv) { + final int startIndex = blockIndex*DBCellRecord.BLOCK_SIZE; + final int endIndex = startIndex + DBCellRecord.BLOCK_SIZE; - Iterator rowIterator = _rowRecords.values().iterator(); + Iterator rowIterator = _rowRecords.values().iterator(); - //Given that we basically iterate through the rows in order, - //For a performance improvement, it would be better to return an instance of - //an iterator and use that instance throughout, rather than recreating one and - //having to move it to the right position. - int i=0; - for (;i getIterator() { - return _rowRecords.values().iterator(); - } + public Iterator getIterator() { + return _rowRecords.values().iterator(); + } - public int findStartOfRowOutlineGroup(int row) { - // Find the start of the group. - RowRecord rowRecord = this.getRow( row ); - int level = rowRecord.getOutlineLevel(); - int currentRow = row; - while (currentRow >= 0 && this.getRow( currentRow ) != null) { - rowRecord = this.getRow( currentRow ); - if (rowRecord.getOutlineLevel() < level) { - return currentRow + 1; - } - currentRow--; - } + public int findStartOfRowOutlineGroup(int row) { + // Find the start of the group. + RowRecord rowRecord = this.getRow( row ); + int level = rowRecord.getOutlineLevel(); + int currentRow = row; + while (currentRow >= 0 && this.getRow( currentRow ) != null) { + rowRecord = this.getRow( currentRow ); + if (rowRecord.getOutlineLevel() < level) { + return currentRow + 1; + } + currentRow--; + } - return currentRow + 1; - } + return currentRow + 1; + } - public int findEndOfRowOutlineGroup(int row) { - int level = getRow( row ).getOutlineLevel(); - int currentRow; - for (currentRow = row; currentRow < getLastRowNum(); currentRow++) { - if (getRow(currentRow) == null || getRow(currentRow).getOutlineLevel() < level) { - break; - } - } + public int findEndOfRowOutlineGroup(int row) { + int level = getRow( row ).getOutlineLevel(); + int currentRow; + for (currentRow = row; currentRow < getLastRowNum(); currentRow++) { + if (getRow(currentRow) == null || getRow(currentRow).getOutlineLevel() < level) { + break; + } + } - return currentRow-1; - } + return currentRow-1; + } - /** - * Hide all rows at or below the current outline level - * @return index of the next row after the last row that gets hidden - */ - private int writeHidden(RowRecord pRowRecord, int row) { - int rowIx = row; - RowRecord rowRecord = pRowRecord; - int level = rowRecord.getOutlineLevel(); - while (rowRecord != null && getRow(rowIx).getOutlineLevel() >= level) { - rowRecord.setZeroHeight(true); - rowIx++; - rowRecord = getRow(rowIx); - } - return rowIx; - } + /** + * Hide all rows at or below the current outline level + * @return index of the next row after the last row that gets hidden + */ + private int writeHidden(RowRecord pRowRecord, int row) { + int rowIx = row; + RowRecord rowRecord = pRowRecord; + int level = rowRecord.getOutlineLevel(); + while (rowRecord != null && getRow(rowIx).getOutlineLevel() >= level) { + rowRecord.setZeroHeight(true); + rowIx++; + rowRecord = getRow(rowIx); + } + return rowIx; + } - public void collapseRow(int rowNumber) { + public void collapseRow(int rowNumber) { - // Find the start of the group. - int startRow = findStartOfRowOutlineGroup(rowNumber); - RowRecord rowRecord = getRow(startRow); + // Find the start of the group. + int startRow = findStartOfRowOutlineGroup(rowNumber); + RowRecord rowRecord = getRow(startRow); - // Hide all the columns until the end of the group - int nextRowIx = writeHidden(rowRecord, startRow); + // Hide all the columns until the end of the group + int nextRowIx = writeHidden(rowRecord, startRow); - RowRecord row = getRow(nextRowIx); - if (row == null) { - row = createRow(nextRowIx); - insertRow(row); - } - // Write collapse field - row.setColapsed(true); - } + RowRecord row = getRow(nextRowIx); + if (row == null) { + row = createRow(nextRowIx); + insertRow(row); + } + // Write collapse field + row.setColapsed(true); + } - /** - * Create a row record. - * - * @param rowNumber row number - * @return RowRecord created for the passed in row number - * @see org.apache.poi.hssf.record.RowRecord - */ - public static RowRecord createRow(int rowNumber) { - return new RowRecord(rowNumber); - } + /** + * Create a row record. + * + * @param rowNumber row number + * @return RowRecord created for the passed in row number + * @see org.apache.poi.hssf.record.RowRecord + */ + public static RowRecord createRow(int rowNumber) { + return new RowRecord(rowNumber); + } - public boolean isRowGroupCollapsed(int row) { - int collapseRow = findEndOfRowOutlineGroup(row) + 1; + public boolean isRowGroupCollapsed(int row) { + int collapseRow = findEndOfRowOutlineGroup(row) + 1; - return getRow(collapseRow) != null && getRow(collapseRow).getColapsed(); - } + return getRow(collapseRow) != null && getRow(collapseRow).getColapsed(); + } - public void expandRow(int rowNumber) { - if (rowNumber == -1) - return; + public void expandRow(int rowNumber) { + if (rowNumber == -1) + return; - // If it is already expanded do nothing. - if (!isRowGroupCollapsed(rowNumber)) { - return; - } + // If it is already expanded do nothing. + if (!isRowGroupCollapsed(rowNumber)) { + return; + } - // Find the start of the group. - int startIdx = findStartOfRowOutlineGroup(rowNumber); - RowRecord row = getRow(startIdx); + // Find the start of the group. + int startIdx = findStartOfRowOutlineGroup(rowNumber); + RowRecord row = getRow(startIdx); - // Find the end of the group. - int endIdx = findEndOfRowOutlineGroup(rowNumber); + // Find the end of the group. + int endIdx = findEndOfRowOutlineGroup(rowNumber); - // expand: - // collapsed bit must be unset - // hidden bit gets unset _if_ surrounding groups are expanded you can determine - // this by looking at the hidden bit of the enclosing group. You will have - // to look at the start and the end of the current group to determine which - // is the enclosing group - // hidden bit only is altered for this outline level. ie. don't un-collapse contained groups - if (!isRowGroupHiddenByParent(rowNumber)) { - for (int i = startIdx; i <= endIdx; i++) { - RowRecord otherRow = getRow(i); - if (row.getOutlineLevel() == otherRow.getOutlineLevel() || !isRowGroupCollapsed(i)) { - otherRow.setZeroHeight(false); - } - } - } + // expand: + // collapsed bit must be unset + // hidden bit gets unset _if_ surrounding groups are expanded you can determine + // this by looking at the hidden bit of the enclosing group. You will have + // to look at the start and the end of the current group to determine which + // is the enclosing group + // hidden bit only is altered for this outline level. ie. don't un-collapse contained groups + if (!isRowGroupHiddenByParent(rowNumber)) { + for (int i = startIdx; i <= endIdx; i++) { + RowRecord otherRow = getRow(i); + if (row.getOutlineLevel() == otherRow.getOutlineLevel() || !isRowGroupCollapsed(i)) { + otherRow.setZeroHeight(false); + } + } + } - // Write collapse field - getRow(endIdx + 1).setColapsed(false); - } + // Write collapse field + getRow(endIdx + 1).setColapsed(false); + } - public boolean isRowGroupHiddenByParent(int row) { - // Look out outline details of end - int endLevel; - boolean endHidden; - int endOfOutlineGroupIdx = findEndOfRowOutlineGroup(row); - if (getRow(endOfOutlineGroupIdx + 1) == null) { - endLevel = 0; - endHidden = false; - } else { - endLevel = getRow(endOfOutlineGroupIdx + 1).getOutlineLevel(); - endHidden = getRow(endOfOutlineGroupIdx + 1).getZeroHeight(); - } + public boolean isRowGroupHiddenByParent(int row) { + // Look out outline details of end + int endLevel; + boolean endHidden; + int endOfOutlineGroupIdx = findEndOfRowOutlineGroup(row); + if (getRow(endOfOutlineGroupIdx + 1) == null) { + endLevel = 0; + endHidden = false; + } else { + endLevel = getRow(endOfOutlineGroupIdx + 1).getOutlineLevel(); + endHidden = getRow(endOfOutlineGroupIdx + 1).getZeroHeight(); + } - // Look out outline details of start - int startLevel; - boolean startHidden; - int startOfOutlineGroupIdx = findStartOfRowOutlineGroup( row ); - if (startOfOutlineGroupIdx - 1 < 0 || getRow(startOfOutlineGroupIdx - 1) == null) { - startLevel = 0; - startHidden = false; - } else { - startLevel = getRow(startOfOutlineGroupIdx - 1).getOutlineLevel(); - startHidden = getRow(startOfOutlineGroupIdx - 1).getZeroHeight(); - } + // Look out outline details of start + int startLevel; + boolean startHidden; + int startOfOutlineGroupIdx = findStartOfRowOutlineGroup( row ); + if (startOfOutlineGroupIdx - 1 < 0 || getRow(startOfOutlineGroupIdx - 1) == null) { + startLevel = 0; + startHidden = false; + } else { + startLevel = getRow(startOfOutlineGroupIdx - 1).getOutlineLevel(); + startHidden = getRow(startOfOutlineGroupIdx - 1).getZeroHeight(); + } - if (endLevel > startLevel) { - return endHidden; - } + if (endLevel > startLevel) { + return endHidden; + } - return startHidden; - } - - /** - * Returns an iterator for the cell values - */ - public Iterator getCellValueIterator() { - return _valuesAgg.iterator(); - } + return startHidden; + } + + /** + * Returns an iterator for the cell values + */ + public Iterator getCellValueIterator() { + return _valuesAgg.iterator(); + } - public IndexRecord createIndexRecord(int indexRecordOffset, int sizeOfInitialSheetRecords) { - IndexRecord result = new IndexRecord(); - result.setFirstRow(_firstrow); - result.setLastRowAdd1(_lastrow + 1); - // Calculate the size of the records from the end of the BOF - // and up to the RowRecordsAggregate... + public IndexRecord createIndexRecord(int indexRecordOffset, int sizeOfInitialSheetRecords) { + IndexRecord result = new IndexRecord(); + result.setFirstRow(_firstrow); + result.setLastRowAdd1(_lastrow + 1); + // Calculate the size of the records from the end of the BOF + // and up to the RowRecordsAggregate... - // Add the references to the DBCells in the IndexRecord (one for each block) - // Note: The offsets are relative to the Workbook BOF. Assume that this is - // 0 for now..... + // Add the references to the DBCells in the IndexRecord (one for each block) + // Note: The offsets are relative to the Workbook BOF. Assume that this is + // 0 for now..... - int blockCount = getRowBlockCount(); - // Calculate the size of this IndexRecord - int indexRecSize = IndexRecord.getRecordSizeForBlockCount(blockCount); + int blockCount = getRowBlockCount(); + // Calculate the size of this IndexRecord + int indexRecSize = IndexRecord.getRecordSizeForBlockCount(blockCount); - int currentOffset = indexRecordOffset + indexRecSize + sizeOfInitialSheetRecords; + int currentOffset = indexRecordOffset + indexRecSize + sizeOfInitialSheetRecords; - for (int block = 0; block < blockCount; block++) { - // each row-block has a DBCELL record. - // The offset of each DBCELL record needs to be updated in the INDEX record + for (int block = 0; block < blockCount; block++) { + // each row-block has a DBCELL record. + // The offset of each DBCELL record needs to be updated in the INDEX record - // account for row records in this row-block - currentOffset += getRowBlockSize(block); - // account for cell value records after those - currentOffset += _valuesAgg.getRowCellBlockSize( - getStartRowNumberForBlock(block), getEndRowNumberForBlock(block)); + // account for row records in this row-block + currentOffset += getRowBlockSize(block); + // account for cell value records after those + currentOffset += _valuesAgg.getRowCellBlockSize( + getStartRowNumberForBlock(block), getEndRowNumberForBlock(block)); - // currentOffset is now the location of the DBCELL record for this row-block - result.addDbcell(currentOffset); - // Add space required to write the DBCELL record (whose reference was just added). - currentOffset += (8 + (getRowCountForBlock(block) * 2)); - } - return result; - } - public void insertCell(CellValueRecordInterface cvRec) { - _valuesAgg.insertCell(cvRec); - } - public void removeCell(CellValueRecordInterface cvRec) { - if (cvRec instanceof FormulaRecordAggregate) { - ((FormulaRecordAggregate)cvRec).notifyFormulaChanging(); - } - _valuesAgg.removeCell(cvRec); - } - public FormulaRecordAggregate createFormula(int row, int col) { - FormulaRecord fr = new FormulaRecord(); - fr.setRow(row); - fr.setColumn((short) col); - return new FormulaRecordAggregate(fr, null, _sharedValueManager); - } - public void updateFormulasAfterRowShift(FormulaShifter formulaShifter, int currentExternSheetIndex) { - _valuesAgg.updateFormulasAfterRowShift(formulaShifter, currentExternSheetIndex); - } - public DimensionsRecord createDimensions() { - DimensionsRecord result = new DimensionsRecord(); - result.setFirstRow(_firstrow); - result.setLastRow(_lastrow); - result.setFirstCol((short) _valuesAgg.getFirstCellNum()); - result.setLastCol((short) _valuesAgg.getLastCellNum()); - return result; - } + // currentOffset is now the location of the DBCELL record for this row-block + result.addDbcell(currentOffset); + // Add space required to write the DBCELL record (whose reference was just added). + currentOffset += (8 + (getRowCountForBlock(block) * 2)); + } + return result; + } + public void insertCell(CellValueRecordInterface cvRec) { + _valuesAgg.insertCell(cvRec); + } + public void removeCell(CellValueRecordInterface cvRec) { + if (cvRec instanceof FormulaRecordAggregate) { + ((FormulaRecordAggregate)cvRec).notifyFormulaChanging(); + } + _valuesAgg.removeCell(cvRec); + } + public FormulaRecordAggregate createFormula(int row, int col) { + FormulaRecord fr = new FormulaRecord(); + fr.setRow(row); + fr.setColumn((short) col); + return new FormulaRecordAggregate(fr, null, _sharedValueManager); + } + public void updateFormulasAfterRowShift(FormulaShifter formulaShifter, int currentExternSheetIndex) { + _valuesAgg.updateFormulasAfterRowShift(formulaShifter, currentExternSheetIndex); + } + public DimensionsRecord createDimensions() { + DimensionsRecord result = new DimensionsRecord(); + result.setFirstRow(_firstrow); + result.setLastRow(_lastrow); + result.setFirstCol((short) _valuesAgg.getFirstCellNum()); + result.setLastCol((short) _valuesAgg.getLastCellNum()); + return result; + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index a6f39a5f0..8d7d781f9 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -19,10 +19,10 @@ package org.apache.poi.hssf.usermodel; import java.util.Map; +import org.apache.poi.ss.formula.BaseFormulaEvaluator; import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment; import org.apache.poi.ss.formula.IStabilityClassifier; import org.apache.poi.ss.formula.WorkbookEvaluator; -import org.apache.poi.ss.formula.WorkbookEvaluatorProvider; import org.apache.poi.ss.formula.eval.BoolEval; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.NumericValueEval; @@ -33,8 +33,6 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.FormulaEvaluator; -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.Internal; @@ -45,362 +43,251 @@ import org.apache.poi.util.Internal; * cell values. Be sure to call {@link #clearAllCachedResultValues()} if any workbook cells are changed between * calls to evaluate~ methods on this class. */ -public class HSSFFormulaEvaluator implements FormulaEvaluator, WorkbookEvaluatorProvider { +public class HSSFFormulaEvaluator extends BaseFormulaEvaluator { + private final HSSFWorkbook _book; - private final WorkbookEvaluator _bookEvaluator; - private final HSSFWorkbook _book; - - public HSSFFormulaEvaluator(HSSFWorkbook workbook) { - this(workbook, null); - } - /** - * @param workbook The workbook to perform the formula evaluations in - * @param stabilityClassifier used to optimise caching performance. Pass null - * for the (conservative) assumption that any cell may have its definition changed after - * evaluation begins. - */ - public HSSFFormulaEvaluator(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier) { - this(workbook, stabilityClassifier, null); - } - - /** - * @param workbook The workbook to perform the formula evaluations in + public HSSFFormulaEvaluator(HSSFWorkbook workbook) { + this(workbook, null); + } + /** + * @param workbook The workbook to perform the formula evaluations in * @param stabilityClassifier used to optimise caching performance. Pass null * for the (conservative) assumption that any cell may have its definition changed after * evaluation begins. - * @param udfFinder pass null for default (AnalysisToolPak only) - */ - private HSSFFormulaEvaluator(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { - _book = workbook; - _bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook), stabilityClassifier, udfFinder); - } + */ + public HSSFFormulaEvaluator(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier) { + this(workbook, stabilityClassifier, null); + } - /** - * @param workbook The workbook to perform the formula evaluations in - * @param stabilityClassifier used to optimise caching performance. Pass null - * for the (conservative) assumption that any cell may have its definition changed after - * evaluation begins. - * @param udfFinder pass null for default (AnalysisToolPak only) - */ - public static HSSFFormulaEvaluator create(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { - return new HSSFFormulaEvaluator(workbook, stabilityClassifier, udfFinder); - } + /** + * @param workbook The workbook to perform the formula evaluations in + * @param stabilityClassifier used to optimise caching performance. Pass null + * for the (conservative) assumption that any cell may have its definition changed after + * evaluation begins. + * @param udfFinder pass null for default (AnalysisToolPak only) + */ + private HSSFFormulaEvaluator(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { + super(new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook), stabilityClassifier, udfFinder)); + _book = workbook; + } + + /** + * @param workbook The workbook to perform the formula evaluations in + * @param stabilityClassifier used to optimise caching performance. Pass null + * for the (conservative) assumption that any cell may have its definition changed after + * evaluation begins. + * @param udfFinder pass null for default (AnalysisToolPak only) + */ + public static HSSFFormulaEvaluator create(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { + return new HSSFFormulaEvaluator(workbook, stabilityClassifier, udfFinder); + } - /** - * Coordinates several formula evaluators together so that formulas that involve external - * references can be evaluated. - * @param workbookNames the simple file names used to identify the workbooks in formulas - * with external links (for example "MyData.xls" as used in a formula "[MyData.xls]Sheet1!A1") - * @param evaluators all evaluators for the full set of workbooks required by the formulas. - */ - public static void setupEnvironment(String[] workbookNames, HSSFFormulaEvaluator[] evaluators) { - WorkbookEvaluator[] wbEvals = new WorkbookEvaluator[evaluators.length]; - for (int i = 0; i < wbEvals.length; i++) { - wbEvals[i] = evaluators[i]._bookEvaluator; - } - CollaboratingWorkbooksEnvironment.setup(workbookNames, wbEvals); - } + /** + * Coordinates several formula evaluators together so that formulas that involve external + * references can be evaluated. + * @param workbookNames the simple file names used to identify the workbooks in formulas + * with external links (for example "MyData.xls" as used in a formula "[MyData.xls]Sheet1!A1") + * @param evaluators all evaluators for the full set of workbooks required by the formulas. + */ + public static void setupEnvironment(String[] workbookNames, HSSFFormulaEvaluator[] evaluators) { + BaseFormulaEvaluator.setupEnvironment(workbookNames, evaluators); + } - @Override + @Override public void setupReferencedWorkbooks(Map evaluators) { CollaboratingWorkbooksEnvironment.setupFormulaEvaluator(evaluators); } - - @Override - public WorkbookEvaluator _getWorkbookEvaluator() { - return _bookEvaluator; + + /** + * Should be called to tell the cell value cache that the specified (value or formula) cell + * has changed. + * Failure to call this method after changing cell values will cause incorrect behaviour + * of the evaluate~ methods of this class + */ + public void notifyUpdateCell(HSSFCell cell) { + _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell(cell)); } - - /** - * Should be called whenever there are major changes (e.g. moving sheets) to input cells - * in the evaluated workbook. If performance is not critical, a single call to this method - * may be used instead of many specific calls to the notify~ methods. - * - * Failure to call this method after changing cell values will cause incorrect behaviour - * of the evaluate~ methods of this class - */ - @Override - public void clearAllCachedResultValues() { - _bookEvaluator.clearAllCachedResultValues(); - } - /** - * Should be called to tell the cell value cache that the specified (value or formula) cell - * has changed. - * Failure to call this method after changing cell values will cause incorrect behaviour - * of the evaluate~ methods of this class - */ - public void notifyUpdateCell(HSSFCell cell) { - _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell(cell)); - } @Override public void notifyUpdateCell(Cell cell) { _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell((HSSFCell)cell)); } - /** - * Should be called to tell the cell value cache that the specified cell has just been - * deleted. - * Failure to call this method after changing cell values will cause incorrect behaviour - * of the evaluate~ methods of this class - */ - public void notifyDeleteCell(HSSFCell cell) { - _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell(cell)); - } - @Override + /** + * Should be called to tell the cell value cache that the specified cell has just been + * deleted. + * Failure to call this method after changing cell values will cause incorrect behaviour + * of the evaluate~ methods of this class + */ + public void notifyDeleteCell(HSSFCell cell) { + _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell(cell)); + } + @Override public void notifyDeleteCell(Cell cell) { - _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell((HSSFCell)cell)); - } + _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell((HSSFCell)cell)); + } - /** - * Should be called to tell the cell value cache that the specified (value or formula) cell - * has changed. - * Failure to call this method after changing cell values will cause incorrect behaviour - * of the evaluate~ methods of this class - */ - @Override + /** + * Should be called to tell the cell value cache that the specified (value or formula) cell + * has changed. + * Failure to call this method after changing cell values will cause incorrect behaviour + * of the evaluate~ methods of this class + */ + @Override public void notifySetFormula(Cell cell) { - _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell((HSSFCell)cell)); - } + _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell((HSSFCell)cell)); + } - /** - * If cell contains a formula, the formula is evaluated and returned, - * else the CellValue simply copies the appropriate cell value from - * the cell and also its cell type. This method should be preferred over - * evaluateInCell() when the call should not modify the contents of the - * original cell. - * - * @param cell may be null signifying that the cell is not present (or blank) - * @return null if the supplied cell is null or blank - */ - @Override - public CellValue evaluate(Cell cell) { - if (cell == null) { - return null; - } + /** + * If cell contains formula, it evaluates the formula, and saves the result of the formula. The + * cell remains as a formula cell. If the cell does not contain formula, rather than throwing an + * exception, this method returns {@link CellType#_NONE} and leaves the cell unchanged. + * + * Note that the type of the formula result is returned, so you know what kind of + * cached formula result is also stored with the formula. + *

+     * CellType evaluatedCellType = evaluator.evaluateFormulaCell(cell);
+     * 
+ * Be aware that your cell will hold both the formula, and the result. If you want the cell + * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} + * @param cell The cell to evaluate + * @return {@link CellType#_NONE} for non-formula cells, or the type of the formula result + * @since POI 3.15 beta 3 + * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. + */ + @Internal + @Override + public CellType evaluateFormulaCellEnum(Cell cell) { + if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { + return CellType._NONE; + } + CellValue cv = evaluateFormulaCellValue(cell); + // cell remains a formula cell, but the cached value is changed + setCellValue(cell, cv); + return cv.getCellType(); + } - switch (cell.getCellTypeEnum()) { - case BOOLEAN: - return CellValue.valueOf(cell.getBooleanCellValue()); - case ERROR: - return CellValue.getError(cell.getErrorCellValue()); - case FORMULA: - return evaluateFormulaCellValue(cell); - case NUMERIC: - return new CellValue(cell.getNumericCellValue()); - case STRING: - return new CellValue(cell.getRichStringCellValue().getString()); - case BLANK: - return null; - default: - throw new IllegalStateException("Bad cell type (" + cell.getCellTypeEnum() + ")"); - } - - } + /** + * If cell contains formula, it evaluates the formula, and + * puts the formula result back into the cell, in place + * of the old formula. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + * Note that the same instance of HSSFCell is returned to + * allow chained calls like: + *
+     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
+     * 
+ * Be aware that your cell value will be changed to hold the + * result of the formula. If you simply want the formula + * value computed for you, use {@link #evaluateFormulaCellEnum(Cell)}} + */ + @Override + public HSSFCell evaluateInCell(Cell cell) { + if (cell == null) { + return null; + } + HSSFCell result = (HSSFCell) cell; + if (cell.getCellTypeEnum() == CellType.FORMULA) { + CellValue cv = evaluateFormulaCellValue(cell); + setCellValue(cell, cv); + setCellType(cell, cv); // cell will no longer be a formula cell + } + return result; + } + private static void setCellValue(Cell cell, CellValue cv) { + CellType cellType = cv.getCellType(); + switch (cellType) { + case BOOLEAN: + cell.setCellValue(cv.getBooleanValue()); + break; + case ERROR: + cell.setCellErrorValue(cv.getErrorValue()); + break; + case NUMERIC: + cell.setCellValue(cv.getNumberValue()); + break; + case STRING: + cell.setCellValue(new HSSFRichTextString(cv.getStringValue())); + break; + case BLANK: + // never happens - blanks eventually get translated to zero + case FORMULA: + // this will never happen, we have already evaluated the formula + default: + throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); + } + } - /** - * If cell contains formula, it evaluates the formula, and saves the result of the formula. The - * cell remains as a formula cell. If the cell does not contain formula, this method returns -1 - * and leaves the cell unchanged. - * - * Note that the type of the formula result is returned, so you know what kind of - * cached formula result is also stored with the formula. - *
-	 * int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
-	 * 
- * Be aware that your cell will hold both the formula, and the result. If you want the cell - * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} - * @param cell The cell to evaluate - * @return -1 for non-formula cells, or the type of the formula result - */ - @Override - public int evaluateFormulaCell(Cell cell) { - return evaluateFormulaCellEnum(cell).getCode(); - } - - /** - * If cell contains formula, it evaluates the formula, and saves the result of the formula. The - * cell remains as a formula cell. If the cell does not contain formula, rather than throwing an - * exception, this method returns {@link CellType#_NONE} and leaves the cell unchanged. - * - * Note that the type of the formula result is returned, so you know what kind of - * cached formula result is also stored with the formula. - *
-	 * CellType evaluatedCellType = evaluator.evaluateFormulaCell(cell);
-	 * 
- * Be aware that your cell will hold both the formula, and the result. If you want the cell - * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} - * @param cell The cell to evaluate - * @return {@link CellType#_NONE} for non-formula cells, or the type of the formula result - * @since POI 3.15 beta 3 - * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. - */ - @Internal - @Override - public CellType evaluateFormulaCellEnum(Cell cell) { - if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { - return CellType._NONE; - } - CellValue cv = evaluateFormulaCellValue(cell); - // cell remains a formula cell, but the cached value is changed - setCellValue(cell, cv); - return cv.getCellType(); - } + /** + * Loops over all cells in all sheets of the supplied + * workbook. + * For cells that contain formulas, their formulas are + * evaluated, and the results are saved. These cells + * remain as formula cells. + * For cells that do not contain formulas, no changes + * are made. + * This is a helpful wrapper around looping over all + * cells, and calling evaluateFormulaCell on each one. + */ + public static void evaluateAllFormulaCells(HSSFWorkbook wb) { + evaluateAllFormulaCells(wb, new HSSFFormulaEvaluator(wb)); + } - /** - * If cell contains formula, it evaluates the formula, and - * puts the formula result back into the cell, in place - * of the old formula. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the same instance of HSSFCell is returned to - * allow chained calls like: - *
-	 * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
-	 * 
- * Be aware that your cell value will be changed to hold the - * result of the formula. If you simply want the formula - * value computed for you, use {@link #evaluateFormulaCellEnum(Cell)}} - */ - @Override - public HSSFCell evaluateInCell(Cell cell) { - if (cell == null) { - return null; - } - HSSFCell result = (HSSFCell) cell; - if (cell.getCellTypeEnum() == CellType.FORMULA) { - CellValue cv = evaluateFormulaCellValue(cell); - setCellValue(cell, cv); - setCellType(cell, cv); // cell will no longer be a formula cell - } - return result; - } - private static void setCellType(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); - switch (cellType) { - case BOOLEAN: - case ERROR: - case NUMERIC: - case STRING: - cell.setCellType(cellType); - return; - case BLANK: - // never happens - blanks eventually get translated to zero - case FORMULA: - // this will never happen, we have already evaluated the formula - default: - throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); - } - - } + /** + * Loops over all cells in all sheets of the supplied + * workbook. + * For cells that contain formulas, their formulas are + * evaluated, and the results are saved. These cells + * remain as formula cells. + * For cells that do not contain formulas, no changes + * are made. + * This is a helpful wrapper around looping over all + * cells, and calling evaluateFormulaCell on each one. + */ + public static void evaluateAllFormulaCells(Workbook wb) { + BaseFormulaEvaluator.evaluateAllFormulaCells(wb); + } - private static void setCellValue(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); - switch (cellType) { - case BOOLEAN: - cell.setCellValue(cv.getBooleanValue()); - break; - case ERROR: - cell.setCellErrorValue(cv.getErrorValue()); - break; - case NUMERIC: - cell.setCellValue(cv.getNumberValue()); - break; - case STRING: - cell.setCellValue(new HSSFRichTextString(cv.getStringValue())); - break; - case BLANK: - // never happens - blanks eventually get translated to zero - case FORMULA: - // this will never happen, we have already evaluated the formula - default: - throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); - } - } + /** + * Loops over all cells in all sheets of the supplied + * workbook. + * For cells that contain formulas, their formulas are + * evaluated, and the results are saved. These cells + * remain as formula cells. + * For cells that do not contain formulas, no changes + * are made. + * This is a helpful wrapper around looping over all + * cells, and calling evaluateFormulaCell on each one. + */ + @Override + public void evaluateAll() { + evaluateAllFormulaCells(_book, this); + } - /** - * Loops over all cells in all sheets of the supplied - * workbook. - * For cells that contain formulas, their formulas are - * evaluated, and the results are saved. These cells - * remain as formula cells. - * For cells that do not contain formulas, no changes - * are made. - * This is a helpful wrapper around looping over all - * cells, and calling evaluateFormulaCell on each one. - */ - public static void evaluateAllFormulaCells(HSSFWorkbook wb) { - evaluateAllFormulaCells(wb, new HSSFFormulaEvaluator(wb)); - } - - /** - * Loops over all cells in all sheets of the supplied - * workbook. - * For cells that contain formulas, their formulas are - * evaluated, and the results are saved. These cells - * remain as formula cells. - * For cells that do not contain formulas, no changes - * are made. - * This is a helpful wrapper around looping over all - * cells, and calling evaluateFormulaCell on each one. - */ - public static void evaluateAllFormulaCells(Workbook wb) { - FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); - evaluateAllFormulaCells(wb, evaluator); - } - private static void evaluateAllFormulaCells(Workbook wb, FormulaEvaluator evaluator) { - for(int i=0; iindexes. * - * @param indexes + * @param indexes Array of sheets to select, the index is 0-based. */ public void setSelectedTabs(int[] indexes) { Collection list = new ArrayList(indexes.length); @@ -563,7 +564,7 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss * the 'active' sheet (which is the sheet with focus). * Unselects sheets that are not in indexes. * - * @param indexes + * @param indexes Collection of sheets to select, the index is 0-based. */ public void setSelectedTabs(Collection indexes) { @@ -893,8 +894,7 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss */ @Override public Iterator sheetIterator() { - Iterator result = new SheetIterator(); - return result; + return new SheetIterator(); } /** @@ -1280,9 +1280,9 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss /** * Closes the underlying {@link NPOIFSFileSystem} from which - * the Workbook was read, if any. Has no effect on Workbooks - * opened from an InputStream, or newly created ones. - *

Once {@link #close()} has been called, no further + * the Workbook was read, if any. + * + *

Once this has been called, no further * operations, updates or reads should be performed on the * Workbook. */ @@ -1531,6 +1531,11 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss return names.get(nameIndex); } + @Override + public List getAllNames() { + return Collections.unmodifiableList(names); + } + public NameRecord getNameRecord(int nameIndex) { return getWorkbook().getNameRecord(nameIndex); } @@ -1702,8 +1707,9 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss * * @param name the name to remove. */ - void removeName(HSSFName name) { - int index = getNameIndex(name); + @Override + public void removeName(Name name) { + int index = getNameIndex((HSSFName) name); removeName(index); } diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index f681f3ad1..69f8d6768 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -374,20 +374,22 @@ public class CryptoFunctions { // SET Verifier TO 0x0000 short verifier = 0; - // FOR EACH PasswordByte IN PasswordArray IN REVERSE ORDER - for (int i = arrByteChars.length-1; i >= 0; i--) { - // SET Verifier TO Intermediate3 BITWISE XOR PasswordByte + if (!"".equals(password)) { + // FOR EACH PasswordByte IN PasswordArray IN REVERSE ORDER + for (int i = arrByteChars.length-1; i >= 0; i--) { + // SET Verifier TO Intermediate3 BITWISE XOR PasswordByte + verifier = rotateLeftBase15Bit(verifier); + verifier ^= arrByteChars[i]; + } + + // as we haven't prepended the password length into the input array + // we need to do it now separately ... verifier = rotateLeftBase15Bit(verifier); - verifier ^= arrByteChars[i]; + verifier ^= arrByteChars.length; + + // RETURN Verifier BITWISE XOR 0xCE4B + verifier ^= 0xCE4B; // (0x8000 | ('N' << 8) | 'K') } - - // as we haven't prepended the password length into the input array - // we need to do it now separately ... - verifier = rotateLeftBase15Bit(verifier); - verifier ^= arrByteChars.length; - - // RETURN Verifier BITWISE XOR 0xCE4B - verifier ^= 0xCE4B; // (0x8000 | ('N' << 8) | 'K') return verifier & 0xFFFF; } diff --git a/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java b/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java new file mode 100644 index 000000000..8746ba7fa --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java @@ -0,0 +1,194 @@ +/* ==================================================================== + 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; + +import java.util.Map; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.CellValue; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Common functionality across file formats for evaluating formula cells.

+ */ +public abstract class BaseFormulaEvaluator implements FormulaEvaluator, WorkbookEvaluatorProvider { + protected final WorkbookEvaluator _bookEvaluator; + + protected BaseFormulaEvaluator(WorkbookEvaluator bookEvaluator) { + this._bookEvaluator = bookEvaluator; + } + + /** + * Coordinates several formula evaluators together so that formulas that involve external + * references can be evaluated. + * @param workbookNames the simple file names used to identify the workbooks in formulas + * with external links (for example "MyData.xls" as used in a formula "[MyData.xls]Sheet1!A1") + * @param evaluators all evaluators for the full set of workbooks required by the formulas. + */ + public static void setupEnvironment(String[] workbookNames, BaseFormulaEvaluator[] evaluators) { + WorkbookEvaluator[] wbEvals = new WorkbookEvaluator[evaluators.length]; + for (int i = 0; i < wbEvals.length; i++) { + wbEvals[i] = evaluators[i]._bookEvaluator; + } + CollaboratingWorkbooksEnvironment.setup(workbookNames, wbEvals); + } + + @Override + public void setupReferencedWorkbooks(Map evaluators) { + CollaboratingWorkbooksEnvironment.setupFormulaEvaluator(evaluators); + } + + @Override + public WorkbookEvaluator _getWorkbookEvaluator() { + return _bookEvaluator; + } + + /** + * Should be called whenever there are major changes (e.g. moving sheets) to input cells + * in the evaluated workbook. If performance is not critical, a single call to this method + * may be used instead of many specific calls to the notify~ methods. + * + * Failure to call this method after changing cell values will cause incorrect behaviour + * of the evaluate~ methods of this class + */ + @Override + public void clearAllCachedResultValues() { + _bookEvaluator.clearAllCachedResultValues(); + } + + /** + * If cell contains a formula, the formula is evaluated and returned, + * else the CellValue simply copies the appropriate cell value from + * the cell and also its cell type. This method should be preferred over + * evaluateInCell() when the call should not modify the contents of the + * original cell. + * + * @param cell may be null signifying that the cell is not present (or blank) + * @return null if the supplied cell is null or blank + */ + @Override + public CellValue evaluate(Cell cell) { + if (cell == null) { + return null; + } + + switch (cell.getCellTypeEnum()) { + case BOOLEAN: + return CellValue.valueOf(cell.getBooleanCellValue()); + case ERROR: + return CellValue.getError(cell.getErrorCellValue()); + case FORMULA: + return evaluateFormulaCellValue(cell); + case NUMERIC: + return new CellValue(cell.getNumericCellValue()); + case STRING: + return new CellValue(cell.getRichStringCellValue().getString()); + case BLANK: + return null; + default: + throw new IllegalStateException("Bad cell type (" + cell.getCellTypeEnum() + ")"); + } + } + + protected abstract CellValue evaluateFormulaCellValue(Cell cell); + + /** + * If cell contains formula, it evaluates the formula, and saves the result of the formula. The + * cell remains as a formula cell. If the cell does not contain formula, this method returns -1 + * and leaves the cell unchanged. + * + * Note that the type of the formula result is returned, so you know what kind of + * cached formula result is also stored with the formula. + *

+     * int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
+     * 
+ * Be aware that your cell will hold both the formula, and the result. If you want the cell + * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} + * @param cell The cell to evaluate + * @return -1 for non-formula cells, or the type of the formula result + */ + @Override + public int evaluateFormulaCell(Cell cell) { + return evaluateFormulaCellEnum(cell).getCode(); + } + + protected static void setCellType(Cell cell, CellValue cv) { + CellType cellType = cv.getCellType(); + switch (cellType) { + case BOOLEAN: + case ERROR: + case NUMERIC: + case STRING: + cell.setCellType(cellType); + return; + case BLANK: + // never happens - blanks eventually get translated to zero + throw new IllegalArgumentException("This should never happen. Blanks eventually get translated to zero."); + case FORMULA: + // this will never happen, we have already evaluated the formula + throw new IllegalArgumentException("This should never happen. Formulas should have already been evaluated."); + default: + throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); + } + } + + /** + * Loops over all cells in all sheets of the supplied + * workbook. + * For cells that contain formulas, their formulas are + * evaluated, and the results are saved. These cells + * remain as formula cells. + * For cells that do not contain formulas, no changes + * are made. + * This is a helpful wrapper around looping over all + * cells, and calling evaluateFormulaCell on each one. + */ + public static void evaluateAllFormulaCells(Workbook wb) { + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + evaluateAllFormulaCells(wb, evaluator); + } + protected static void evaluateAllFormulaCells(Workbook wb, FormulaEvaluator evaluator) { + for(int i=0; i * @@ -61,7 +67,6 @@ import org.apache.poi.ss.formula.eval.ValueEval; public class Subtotal implements Function { private static Function findFunction(int functionCode) throws EvaluationException { - Function func; switch (functionCode) { case 1: return subtotalInstance(AggregateFunction.AVERAGE); case 2: return Count.subtotalInstance(); @@ -87,7 +92,7 @@ public class Subtotal implements Function { return ErrorEval.VALUE_INVALID; } - Function innerFunc; + final Function innerFunc; try { ValueEval ve = OperandResolver.getSingleValue(args[0], srcRowIndex, srcColumnIndex); int functionCode = OperandResolver.coerceValueToInt(ve); @@ -96,9 +101,24 @@ public class Subtotal implements Function { return e.getErrorEval(); } - ValueEval[] innerArgs = new ValueEval[nInnerArgs]; - System.arraycopy(args, 1, innerArgs, 0, nInnerArgs); + // ignore the first arg, this is the function-type, we check for the length above + final List list = new ArrayList(Arrays.asList(args).subList(1, args.length)); - return innerFunc.evaluate(innerArgs, srcRowIndex, srcColumnIndex); + Iterator it = list.iterator(); + + // See https://support.office.com/en-us/article/SUBTOTAL-function-7b027003-f060-4ade-9040-e478765b9939 + // "If there are other subtotals within ref1, ref2,... (or nested subtotals), these nested subtotals are ignored to avoid double counting." + // For array references it is handled in other evaluation steps, but we need to handle this here for references to subtotal-functions + while(it.hasNext()) { + ValueEval eval = it.next(); + if(eval instanceof LazyRefEval) { + LazyRefEval lazyRefEval = (LazyRefEval) eval; + if(lazyRefEval.isSubTotal()) { + it.remove(); + } + } + } + + return innerFunc.evaluate(list.toArray(new ValueEval[list.size()]), srcRowIndex, srcColumnIndex); } } diff --git a/src/java/org/apache/poi/ss/usermodel/CellStyle.java b/src/java/org/apache/poi/ss/usermodel/CellStyle.java index c82ffb98e..b6c971979 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellStyle.java +++ b/src/java/org/apache/poi/ss/usermodel/CellStyle.java @@ -81,7 +81,7 @@ public interface CellStyle { /** * vertically justified vertical alignment - * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#TOP} instead. + * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#JUSTIFY} instead. */ static final short VERTICAL_JUSTIFY = 0x3; //VerticalAlignment.JUSTIFY.getCode(); diff --git a/src/java/org/apache/poi/ss/usermodel/Workbook.java b/src/java/org/apache/poi/ss/usermodel/Workbook.java index 1fcc29a3e..e52615fb1 100644 --- a/src/java/org/apache/poi/ss/usermodel/Workbook.java +++ b/src/java/org/apache/poi/ss/usermodel/Workbook.java @@ -341,9 +341,11 @@ public interface Workbook extends Closeable, Iterable { /** * Close the underlying input resource (File or Stream), - * from which the Workbook was read. After closing, the - * Workbook should no longer be used. - *

This will have no effect newly created Workbooks. + * from which the Workbook was read. + * + *

Once this has been called, no further + * operations, updates or reads should be performed on the + * Workbook. */ @Override void close() throws IOException; @@ -367,6 +369,13 @@ public interface Workbook extends Closeable, Iterable { */ List getNames(String name); + /** + * Returns all defined names. + * + * @return a list of the defined names. An empty list is returned if none is found. + */ + List getAllNames(); + /** * @param nameIndex position of the named range (0-based) * @return the defined name at the specified index @@ -405,6 +414,13 @@ public interface Workbook extends Closeable, Iterable { */ void removeName(String name); + /** + * Remove a defined name + * + * @param name the name of the defined name + */ + void removeName(Name name); + /** * Adds the linking required to allow formulas referencing * the specified external workbook to be added to this one. diff --git a/src/java/org/apache/poi/util/CommonsLogger.java b/src/java/org/apache/poi/util/CommonsLogger.java index 16a48f28f..1cbb9628c 100644 --- a/src/java/org/apache/poi/util/CommonsLogger.java +++ b/src/java/org/apache/poi/util/CommonsLogger.java @@ -27,19 +27,13 @@ import org.apache.commons.logging.LogFactory; * developers to write log calls, while simultaneously making those * calls as cheap as possible by performing lazy evaluation of the log * message.

- * - * @author Marc Johnson (mjohnson at apache dot org) - * @author Glen Stampoultzis (glens at apache.org) - * @author Nicola Ken Barozzi (nicolaken at apache.org) */ - public class CommonsLogger extends POILogger { - private static LogFactory _creator = LogFactory.getFactory(); private Log log = null; - + @Override public void initialize(final String cat) { this.log = _creator.getInstance(cat); @@ -51,6 +45,7 @@ public class CommonsLogger extends POILogger * @param level One of DEBUG, INFO, WARN, ERROR, FATAL * @param obj1 The object to log. */ + @Override public void log(final int level, final Object obj1) { if(level==FATAL) @@ -104,6 +99,7 @@ public class CommonsLogger extends POILogger * @param obj1 The object to log. This is converted to a string. * @param exception An exception to be logged */ + @Override public void log(final int level, final Object obj1, final Throwable exception) { @@ -175,7 +171,7 @@ public class CommonsLogger extends POILogger * * @param level One of DEBUG, INFO, WARN, ERROR, FATAL */ - + @Override public boolean check(final int level) { if(level==FATAL) diff --git a/src/java/org/apache/poi/util/NullLogger.java b/src/java/org/apache/poi/util/NullLogger.java index 24643c8fd..fe0979cfe 100644 --- a/src/java/org/apache/poi/util/NullLogger.java +++ b/src/java/org/apache/poi/util/NullLogger.java @@ -22,14 +22,10 @@ package org.apache.poi.util; * developers to write log calls, while simultaneously making those * calls as cheap as possible by performing lazy evaluation of the log * message.

- * - * @author Marc Johnson (mjohnson at apache dot org) - * @author Glen Stampoultzis (glens at apache.org) - * @author Nicola Ken Barozzi (nicolaken at apache.org) */ public class NullLogger extends POILogger { @Override - public void initialize(final String cat){ + public void initialize(final String cat) { // do nothing } @@ -41,8 +37,7 @@ public class NullLogger extends POILogger { */ @Override - public void log(final int level, final Object obj1) - { + public void log(final int level, final Object obj1) { // do nothing } @@ -53,6 +48,7 @@ public class NullLogger extends POILogger { * @param obj1 The object to log. This is converted to a string. * @param exception An exception to be logged */ + @Override public void log(int level, Object obj1, final Throwable exception) { // do nothing } diff --git a/src/java/org/apache/poi/util/SystemOutLogger.java b/src/java/org/apache/poi/util/SystemOutLogger.java index e665ba632..36d96ca03 100644 --- a/src/java/org/apache/poi/util/SystemOutLogger.java +++ b/src/java/org/apache/poi/util/SystemOutLogger.java @@ -24,15 +24,12 @@ package org.apache.poi.util; * developers to write log calls, while simultaneously making those * calls as cheap as possible by performing lazy evaluation of the log * message. - * - * @author Marc Johnson (mjohnson at apache dot org) - * @author Glen Stampoultzis (glens at apache.org) - * @author Nicola Ken Barozzi (nicolaken at apache.org) */ public class SystemOutLogger extends POILogger { private String _cat; + @Override public void initialize(final String cat) { this._cat=cat; @@ -44,7 +41,7 @@ public class SystemOutLogger extends POILogger * @param level One of DEBUG, INFO, WARN, ERROR, FATAL * @param obj1 The object to log. */ - + @Override public void log(final int level, final Object obj1) { log(level, obj1, null); @@ -57,6 +54,7 @@ public class SystemOutLogger extends POILogger * @param obj1 The object to log. This is converted to a string. * @param exception An exception to be logged */ + @Override @SuppressForbidden("uses printStackTrace") public void log(final int level, final Object obj1, final Throwable exception) { @@ -78,6 +76,7 @@ public class SystemOutLogger extends POILogger * @see #ERROR * @see #FATAL */ + @Override public boolean check(final int level) { int currentLevel; diff --git a/src/ooxml/java/org/apache/poi/POIXMLDocument.java b/src/ooxml/java/org/apache/poi/POIXMLDocument.java index 55b1a4d18..4ec3d442e 100644 --- a/src/ooxml/java/org/apache/poi/POIXMLDocument.java +++ b/src/ooxml/java/org/apache/poi/POIXMLDocument.java @@ -193,8 +193,12 @@ public abstract class POIXMLDocument extends POIXMLDocumentPart implements Close /** * Closes the underlying {@link OPCPackage} from which this * document was read, if there is one - * - * @throws IOException for writable packages, if an IO exception occur during the saving process. + * + *

Once this has been called, no further + * operations, updates or reads should be performed on the + * document. + * + * @throws IOException for writable packages, if an IO exception occur during the saving process. */ @Override public void close() throws IOException { diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java index a438c061f..e029d7dec 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java @@ -382,8 +382,7 @@ public abstract class OPCPackage implements RelationshipSource, Closeable { } // Creates a new package - OPCPackage pkg = null; - pkg = new ZipPackage(); + OPCPackage pkg = new ZipPackage(); pkg.originalPackagePath = file.getAbsolutePath(); configurePackage(pkg); @@ -391,8 +390,7 @@ public abstract class OPCPackage implements RelationshipSource, Closeable { } public static OPCPackage create(OutputStream output) { - OPCPackage pkg = null; - pkg = new ZipPackage(); + OPCPackage pkg = new ZipPackage(); pkg.originalPackagePath = null; pkg.output = output; @@ -542,7 +540,7 @@ public abstract class OPCPackage implements RelationshipSource, Closeable { // Create the thumbnail part name String contentType = ContentTypes .getContentTypeFromFileExtension(filename); - PackagePartName thumbnailPartName = null; + PackagePartName thumbnailPartName; try { thumbnailPartName = PackagingURIHelper.createPartName("/docProps/" + filename); diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java index 537dd15e9..750f9cd71 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java @@ -29,10 +29,7 @@ import java.util.TreeMap; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.InvalidOperationException; import org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException; -import org.apache.poi.openxml4j.opc.OPCPackage; -import org.apache.poi.openxml4j.opc.PackagePart; -import org.apache.poi.openxml4j.opc.PackagePartName; -import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.openxml4j.opc.*; import org.apache.poi.util.DocumentHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -54,7 +51,7 @@ public abstract class ContentTypeManager { /** * Content type namespace */ - public static final String TYPES_NAMESPACE_URI = "http://schemas.openxmlformats.org/package/2006/content-types"; + public static final String TYPES_NAMESPACE_URI = PackageNamespaces.CONTENT_TYPES; /* Xml elements in content type part */ diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java index 3652b89b1..08022e976 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java @@ -304,13 +304,13 @@ implements XSLFShapeContainer, GroupShape { @Override public boolean getFlipHorizontal(){ CTGroupTransform2D xfrm = getXfrm(); - return (xfrm == null || !xfrm.isSetFlipH()) ? false : xfrm.getFlipH(); + return !(xfrm == null || !xfrm.isSetFlipH()) && xfrm.getFlipH(); } @Override public boolean getFlipVertical(){ CTGroupTransform2D xfrm = getXfrm(); - return (xfrm == null || !xfrm.isSetFlipV()) ? false : xfrm.getFlipV(); + return !(xfrm == null || !xfrm.isSetFlipV()) && xfrm.getFlipV(); } @Override @@ -333,7 +333,7 @@ implements XSLFShapeContainer, GroupShape { // recursively update each shape for(XSLFShape shape : gr.getShapes()) { - XSLFShape newShape = null; + XSLFShape newShape; if (shape instanceof XSLFTextBox) { newShape = createTextBox(); } else if (shape instanceof XSLFAutoShape) { diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java index abba81430..2277d600a 100644 --- a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java @@ -41,7 +41,6 @@ import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; -import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.util.DocumentHelper; @@ -55,7 +54,6 @@ import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFTable; import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -117,8 +115,7 @@ public class XSSFExportToXml implements Comparator{ * @param validate if true, validates the XML againts the XML Schema * @throws SAXException * @throws ParserConfigurationException - * @throws TransformerException - * @throws InvalidFormatException + * @throws TransformerException */ public void exportToXML(OutputStream os, String encoding, boolean validate) throws SAXException, ParserConfigurationException, TransformerException{ List singleXMLCells = map.getRelatedSingleXMLCell(); @@ -128,10 +125,10 @@ public class XSSFExportToXml implements Comparator{ Document doc = DocumentHelper.createDocument(); - Element root = null; + final Element root; if (isNamespaceDeclared()) { - root=doc.createElementNS(getNamespace(),rootElement); + root = doc.createElementNS(getNamespace(),rootElement); } else { root = doc.createElementNS("", rootElement); } @@ -152,7 +149,6 @@ public class XSSFExportToXml implements Comparator{ tableMappings.put(commonXPath, table); } - Collections.sort(xpaths,this); for(String xpath : xpaths) { @@ -167,8 +163,7 @@ public class XSSFExportToXml implements Comparator{ XSSFCell cell = simpleXmlCell.getReferencedCell(); if (cell!=null) { Node currentNode = getNodeByXPath(xpath,doc.getFirstChild(),doc,false); - STXmlDataType.Enum dataType = simpleXmlCell.getXmlDataType(); - mapCellOnNode(cell,currentNode,dataType); + mapCellOnNode(cell,currentNode); //remove nodes which are empty in order to keep the output xml valid if("".equals(currentNode.getTextContent()) && currentNode.getParentNode() != null) { @@ -202,22 +197,15 @@ public class XSSFExportToXml implements Comparator{ XSSFXmlColumnPr pointer = tableColumns.get(j-startColumnIndex); String localXPath = pointer.getLocalXPath(); Node currentNode = getNodeByXPath(localXPath,tableRootNode,doc,false); - STXmlDataType.Enum dataType = pointer.getXmlDataType(); - - mapCellOnNode(cell,currentNode,dataType); + mapCellOnNode(cell,currentNode); } - } - } - - - } - } else { + } /*else { // TODO: implement filtering management in xpath - } + }*/ } boolean isValid = true; @@ -225,8 +213,6 @@ public class XSSFExportToXml implements Comparator{ isValid =isValid(doc); } - - if (isValid) { ///////////////// @@ -275,7 +261,7 @@ public class XSSFExportToXml implements Comparator{ } - private void mapCellOnNode(XSSFCell cell, Node node, STXmlDataType.Enum outputDataType) { + private void mapCellOnNode(XSSFCell cell, Node node) { String value =""; switch (cell.getCellTypeEnum()) { @@ -349,11 +335,7 @@ public class XSSFExportToXml implements Comparator{ } currentNode = selectedNode; } else { - - - Node attribute = createAttribute(doc, currentNode, axisName); - - currentNode = attribute; + currentNode = createAttribute(doc, currentNode, axisName); } } return currentNode; @@ -421,12 +403,11 @@ public class XSSFExportToXml implements Comparator{ for(int i =1;i { }if ( leftIndex > rightIndex) { return 1; } - } else { + } /*else { // NOTE: the xpath doesn't match correctly in the schema - } + }*/ } } @@ -483,7 +464,7 @@ public class XSSFExportToXml implements Comparator{ // Note: we expect that all the complex types are defined at root level Node complexTypeNode = null; if (!"".equals(complexTypeName)) { - complexTypeNode = getComplexTypeNodeFromSchemaChildren(xmlSchema, complexTypeNode, complexTypeName); + complexTypeNode = getComplexTypeNodeFromSchemaChildren(xmlSchema, null, complexTypeName); } return complexTypeNode; diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java index 6c6574443..6174af54b 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java @@ -338,7 +338,11 @@ public class SXSSFCell implements Cell { } if(_value.getType()==CellType.FORMULA) - ((StringFormulaValue)_value).setPreEvaluatedValue(value); + if(_value instanceof NumericFormulaValue) { + ((NumericFormulaValue) _value).setPreEvaluatedValue(Double.parseDouble(value)); + } else { + ((StringFormulaValue) _value).setPreEvaluatedValue(value); + } else ((PlainStringValue)_value).setValue(value); } else { @@ -956,6 +960,7 @@ public class SXSSFCell implements Cell { } /*package*/ void setFormulaType(CellType type) { + Value prevValue = _value; switch(type) { case NUMERIC: @@ -983,7 +988,13 @@ public class SXSSFCell implements Cell { throw new IllegalArgumentException("Illegal type " + type); } } + + // if we had a Formula before, we should copy over the _value of the formula + if(prevValue instanceof FormulaValue) { + ((FormulaValue)_value)._value = ((FormulaValue)prevValue)._value; + } } + //TODO: implement this correctly @NotImplemented /*package*/ CellType computeTypeFromFormula(String formula) diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java index 6993579cc..331ad9a0f 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java @@ -893,8 +893,11 @@ public class SXSSFWorkbook implements Workbook { /** * Closes the underlying {@link XSSFWorkbook} and {@link OPCPackage} - * on which this Workbook is based, if any. Has no effect on Workbooks - * created from scratch. + * on which this Workbook is based, if any. + * + *

Once this has been called, no further + * operations, updates or reads should be performed on the + * Workbook. */ @Override public void close() throws IOException { @@ -1003,12 +1006,25 @@ public class SXSSFWorkbook implements Workbook { return _wb.getNames(name); } + /** + * Returns all defined names + * + * @return all defined names + */ + @Override + public List getAllNames() + { + return _wb.getAllNames(); + } + /** * @param nameIndex position of the named range (0-based) * @return the defined name at the specified index * @throws IllegalArgumentException if the supplied index is invalid + * @deprecated 3.16. New projects should avoid accessing named ranges by index. */ @Override + @Deprecated public Name getNameAt(int nameIndex) { return _wb.getNameAt(nameIndex); @@ -1033,8 +1049,12 @@ public class SXSSFWorkbook implements Workbook { * * @param name the name of the defined name * @return zero based index of the defined name. -1 if not found. + * + * @deprecated 3.16. New projects should avoid accessing named ranges by index. + * Use {@link #getName(String)} instead. */ @Override + @Deprecated public int getNameIndex(String name) { return _wb.getNameIndex(name); @@ -1044,8 +1064,11 @@ public class SXSSFWorkbook implements Workbook { * Remove the defined name at the specified index * * @param index named range index (0 based) + * + * @deprecated 3.16. New projects should use {@link #removeName(Name)}. */ @Override + @Deprecated public void removeName(int index) { _wb.removeName(index); @@ -1054,10 +1077,24 @@ public class SXSSFWorkbook implements Workbook { /** * Remove a defined name by name * - * @param name the name of the defined name + * @param name the name of the defined name + * + * @deprecated 3.16. New projects should use {@link #removeName(Name)}. */ @Override + @Deprecated public void removeName(String name) + { + _wb.removeName(name); + } + + /** + * Remove the given defined name + * + * @param name the name to remove + */ + @Override + public void removeName(Name name) { _wb.removeName(name); } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFEvaluationWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFEvaluationWorkbook.java index f3c6bc267..9c5e6ffb1 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFEvaluationWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFEvaluationWorkbook.java @@ -232,7 +232,7 @@ public abstract class BaseXSSFEvaluationWorkbook implements FormulaRenderingWork // Otherwise, try it as a named range if (sheet == null) { - if (_uBook.getNameIndex(name) > -1) { + if (!_uBook.getNames(name).isEmpty()) { return new NameXPxg(null, name); } return null; diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java index 5fe8660b1..c6c030b95 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java @@ -17,12 +17,9 @@ package org.apache.poi.xssf.usermodel; -import java.util.Map; - -import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment; +import org.apache.poi.ss.formula.BaseFormulaEvaluator; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.WorkbookEvaluator; -import org.apache.poi.ss.formula.WorkbookEvaluatorProvider; import org.apache.poi.ss.formula.eval.BoolEval; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.NumberEval; @@ -31,28 +28,16 @@ import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; -import org.apache.poi.ss.usermodel.FormulaEvaluator; import org.apache.poi.util.Internal; /** * Internal POI use only - parent of XSSF and SXSSF formula evaluators */ -public abstract class BaseXSSFFormulaEvaluator implements FormulaEvaluator, WorkbookEvaluatorProvider { - private WorkbookEvaluator _bookEvaluator; - +public abstract class BaseXSSFFormulaEvaluator extends BaseFormulaEvaluator { protected BaseXSSFFormulaEvaluator(WorkbookEvaluator bookEvaluator) { - _bookEvaluator = bookEvaluator; + super(bookEvaluator); } - /** - * Should be called whenever there are major changes (e.g. moving sheets) to input cells - * in the evaluated workbook. - * Failure to call this method after changing cell values will cause incorrect behaviour - * of the evaluate~ methods of this class - */ - public void clearAllCachedResultValues() { - _bookEvaluator.clearAllCachedResultValues(); - } public void notifySetFormula(Cell cell) { _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell)); } @@ -63,60 +48,6 @@ public abstract class BaseXSSFFormulaEvaluator implements FormulaEvaluator, Work _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell)); } - /** - * If cell contains a formula, the formula is evaluated and returned, - * else the CellValue simply copies the appropriate cell value from - * the cell and also its cell type. This method should be preferred over - * evaluateInCell() when the call should not modify the contents of the - * original cell. - * @param cell - */ - public CellValue evaluate(Cell cell) { - if (cell == null) { - return null; - } - - switch (cell.getCellTypeEnum()) { - case BOOLEAN: - return CellValue.valueOf(cell.getBooleanCellValue()); - case ERROR: - return CellValue.getError(cell.getErrorCellValue()); - case FORMULA: - return evaluateFormulaCellValue(cell); - case NUMERIC: - return new CellValue(cell.getNumericCellValue()); - case STRING: - return new CellValue(cell.getRichStringCellValue().getString()); - case BLANK: - return null; - default: - throw new IllegalStateException("Bad cell type (" + cell.getCellTypeEnum() + ")"); - } - } - - - /** - * If cell contains formula, it evaluates the formula, - * and saves the result of the formula. The cell - * remains as a formula cell. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the type of the formula result is returned, - * so you know what kind of value is also stored with - * the formula. - *

-     * int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
-     * 
- * Be aware that your cell will hold both the formula, - * and the result. If you want the cell replaced with - * the result of the formula, use {@link #evaluate(org.apache.poi.ss.usermodel.Cell)} } - * @param cell The cell to evaluate - * @return The type of the formula result (the cell's type remains as CellType.FORMULA however) - */ - public int evaluateFormulaCell(Cell cell) { - return evaluateFormulaCellEnum(cell).getCode(); - } - /** * If cell contains formula, it evaluates the formula, * and saves the result of the formula. The cell @@ -164,27 +95,6 @@ public abstract class BaseXSSFFormulaEvaluator implements FormulaEvaluator, Work setCellValue(cell, cv); } } - private static void setCellType(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); - switch (cellType) { - case BOOLEAN: - case ERROR: - case NUMERIC: - case STRING: - cell.setCellType(cellType); - return; - case BLANK: - // never happens - blanks eventually get translated to zero - throw new IllegalArgumentException("This should never happen. Blanks eventually get translated to zero."); - case FORMULA: - // this will never happen, we have already evaluated the formula - throw new IllegalArgumentException("This should never happen. Formulas should have already been evaluated."); - default: - throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); - - } - - } private static void setCellValue(Cell cell, CellValue cv) { CellType cellType = cv.getCellType(); @@ -218,7 +128,7 @@ public abstract class BaseXSSFFormulaEvaluator implements FormulaEvaluator, Work /** * Returns a CellValue wrapper around the supplied ValueEval instance. */ - private CellValue evaluateFormulaCellValue(Cell cell) { + protected CellValue evaluateFormulaCellValue(Cell cell) { EvaluationCell evalCell = toEvaluationCell(cell); ValueEval eval = _bookEvaluator.evaluate(evalCell); if (eval instanceof NumberEval) { @@ -238,22 +148,4 @@ public abstract class BaseXSSFFormulaEvaluator implements FormulaEvaluator, Work } throw new RuntimeException("Unexpected eval class (" + eval.getClass().getName() + ")"); } - - public void setupReferencedWorkbooks(Map evaluators) { - CollaboratingWorkbooksEnvironment.setupFormulaEvaluator(evaluators); - } - - public WorkbookEvaluator _getWorkbookEvaluator() { - return _bookEvaluator; - } - - /** {@inheritDoc} */ - public void setIgnoreMissingWorkbooks(boolean ignore){ - _bookEvaluator.setIgnoreMissingWorkbooks(ignore); - } - - /** {@inheritDoc} */ - public void setDebugEvaluationOutputForNextEval(boolean value){ - _bookEvaluator.setDebugEvaluationOutputForNextEval(value); - } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java index ec9ecd462..fc456b7e5 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java @@ -17,7 +17,7 @@ package org.apache.poi.xssf.usermodel; -import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.ss.formula.BaseFormulaEvaluator; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.IStabilityClassifier; import org.apache.poi.ss.formula.WorkbookEvaluator; @@ -88,7 +88,7 @@ public final class XSSFFormulaEvaluator extends BaseXSSFFormulaEvaluator { * cells, and calling evaluateFormulaCell on each one. */ public static void evaluateAllFormulaCells(XSSFWorkbook wb) { - HSSFFormulaEvaluator.evaluateAllFormulaCells(wb); + BaseFormulaEvaluator.evaluateAllFormulaCells(wb); } /** * Loops over all cells in all sheets of the supplied @@ -102,7 +102,7 @@ public final class XSSFFormulaEvaluator extends BaseXSSFFormulaEvaluator { * cells, and calling evaluateFormulaCell on each one. */ public void evaluateAll() { - HSSFFormulaEvaluator.evaluateAllFormulaCells(_book); + evaluateAllFormulaCells(_book, this); } /** diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFName.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFName.java index 462bd6517..071062620 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFName.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFName.java @@ -167,19 +167,18 @@ public final class XSSFName implements Name { public void setNameName(String name) { validateName(name); + String oldName = getNameName(); int sheetIndex = getSheetIndex(); - int numberOfNames = _workbook.getNumberOfNames(); //Check to ensure no other names have the same case-insensitive name at the same scope - for (int i = 0; i < numberOfNames; i++) { - XSSFName nm = _workbook.getNameAt(i); - if ((nm != this) - && name.equalsIgnoreCase(nm.getNameName()) - && (sheetIndex == nm.getSheetIndex())) { + for (XSSFName foundName : _workbook.getNames(name)) { + if (foundName.getSheetIndex() == sheetIndex && foundName != this) { String msg = "The "+(sheetIndex == -1 ? "workbook" : "sheet")+" already contains this name: " + name; throw new IllegalArgumentException(msg); } } _ctName.setName(name); + //Need to update the name -> named ranges map + _workbook.updateName(this, oldName); } public String getRefersToFormula() { diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index 2271e0b4d..5f1da12ec 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -18,8 +18,8 @@ package org.apache.poi.xssf.usermodel; import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; -import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.setPassword; -import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.validatePassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPasswordHelper.setPassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPasswordHelper.validatePassword; import java.io.IOException; import java.io.InputStream; diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java index 1e9b3d6ce..afddcd9c4 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java @@ -18,8 +18,8 @@ package org.apache.poi.xssf.usermodel; import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; -import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.setPassword; -import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.validatePassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPasswordHelper.setPassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPasswordHelper.validatePassword; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -29,16 +29,20 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.regex.Pattern; import javax.xml.namespace.QName; +import org.apache.commons.collections4.ListValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.poi.POIXMLDocument; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.POIXMLException; @@ -59,6 +63,7 @@ import org.apache.poi.ss.formula.SheetNameFormatter; import org.apache.poi.ss.formula.udf.AggregatingUDFFinder; import org.apache.poi.ss.formula.udf.IndexedUDFFinder; import org.apache.poi.ss.formula.udf.UDFFinder; +import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; import org.apache.poi.ss.usermodel.Sheet; @@ -140,6 +145,11 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { */ private List sheets; + /** + * this holds the XSSFName objects attached to this workbook, keyed by lower-case name + */ + private ListValuedMap namedRangesByName; + /** * this holds the XSSFName objects attached to this workbook */ @@ -442,6 +452,7 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { stylesSource.setWorkbook(this); namedRanges = new ArrayList(); + namedRangesByName = new ArrayListValuedHashMap(); sheets = new ArrayList(); pivotTables = new ArrayList(); } @@ -733,8 +744,13 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { public XSSFName createName() { CTDefinedName ctName = CTDefinedName.Factory.newInstance(); ctName.setName(""); + return createAndStoreName(ctName); + } + + private XSSFName createAndStoreName(CTDefinedName ctName) { XSSFName name = new XSSFName(ctName, this); namedRanges.add(name); + namedRangesByName.put(ctName.getName().toLowerCase(Locale.ENGLISH), name); return name; } @@ -938,28 +954,47 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { return stylesSource.getFontAt(idx); } + /** + * Get the first named range with the given name. + * + * Note: names of named ranges are not unique as they are scoped by sheet. + * {@link #getNames(String name)} returns all named ranges with the given name. + * + * @param name named range name + * @return XSSFName with the given name. null is returned no named range could be found. + */ @Override public XSSFName getName(String name) { - int nameIndex = getNameIndex(name); - if (nameIndex < 0) { + Collection list = getNames(name); + if (list.isEmpty()) { return null; } - return namedRanges.get(nameIndex); + return list.iterator().next(); } + /** + * Get the named ranges with the given name. + * Note:Excel named ranges are case-insensitive and + * this method performs a case-insensitive search. + * + * @param name named range name + * @return list of XSSFNames with the given name. An empty list if no named ranges could be found + */ @Override public List getNames(String name) { - List names = new ArrayList(); - for(XSSFName nr : namedRanges) { - if(nr.getNameName().equals(name)) { - names.add(nr); - } - } - - return names; + return Collections.unmodifiableList(namedRangesByName.get(name.toLowerCase(Locale.ENGLISH))); } + /** + * Get the named range at the given index. + * + * @param nameIndex the index of the named range + * @return the XSSFName at the given index + * + * @deprecated 3.16. New projects should avoid accessing named ranges by index. + */ @Override + @Deprecated public XSSFName getNameAt(int nameIndex) { int nNames = namedRanges.size(); if (nNames < 1) { @@ -973,21 +1008,30 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { } /** - * Gets the named range index by his name - * Note:Excel named ranges are case-insensitive and - * this method performs a case-insensitive search. + * Get a list of all the named ranges in the workbook. * - * @param name named range name - * @return named range index + * @return list of XSSFNames in the workbook */ @Override + public List getAllNames() { + return Collections.unmodifiableList(namedRanges); + } + + /** + * Gets the named range index by name. + * + * @param name named range name + * @return named range index. -1 is returned if no named ranges could be found. + * + * @deprecated 3.16. New projects should avoid accessing named ranges by index. + * Use {@link #getName(String)} instead. + */ + @Override + @Deprecated public int getNameIndex(String name) { - int i = 0; - for(XSSFName nr : namedRanges) { - if(nr.getNameName().equals(name)) { - return i; - } - i++; + XSSFName nm = getName(name); + if (nm != null) { + return namedRanges.indexOf(nm); } return -1; } @@ -1258,22 +1302,40 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { return getPackagePart().getContentType().equals(XSSFRelation.MACROS_WORKBOOK.getContentType()); } + /** + * Remove the named range at the given index. + * + * @param nameIndex the index of the named range name to remove + * + * @deprecated 3.16. New projects should use {@link #removeName(Name)}. + */ @Override + @Deprecated public void removeName(int nameIndex) { - namedRanges.remove(nameIndex); + removeName(getNameAt(nameIndex)); } + /** + * Remove the first named range found with the given name. + * + * Note: names of named ranges are not unique (name + sheet + * index is unique), so {@link #removeName(Name)} should + * be used if possible. + * + * @param name the named range name to remove + * + * @throws IllegalArgumentException if no named range could be found + * + * @deprecated 3.16. New projects should use {@link #removeName(Name)}. + */ @Override + @Deprecated public void removeName(String name) { - int idx = 0; - for (XSSFName nm : namedRanges) { - if(nm.getNameName().equalsIgnoreCase(name)) { - removeName(idx); - return; - } - idx++; + List names = namedRangesByName.get(name.toLowerCase(Locale.ENGLISH)); + if (names.isEmpty()) { + throw new IllegalArgumentException("Named range was not found: " + name); } - throw new IllegalArgumentException("Named range was not found: " + name); + removeName(names.get(0)); } @@ -1282,13 +1344,24 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { * (name + sheet index is unique), this method is more accurate. * * @param name the name to remove. + * + * @throws IllegalArgumentException if the named range is not a part of this XSSFWorkbook */ - void removeName(XSSFName name) { - if (!namedRanges.remove(name)) { + @Override + public void removeName(Name name) { + if (!namedRangesByName.removeMapping(name.getNameName().toLowerCase(Locale.ENGLISH), name) + || !namedRanges.remove(name)) { throw new IllegalArgumentException("Name was not found: " + name); } } + void updateName(XSSFName name, String oldName) { + if (!namedRangesByName.removeMapping(oldName.toLowerCase(Locale.ENGLISH), name)) { + throw new IllegalArgumentException("Name was not found: " + name); + } + namedRangesByName.put(name.getNameName().toLowerCase(Locale.ENGLISH), name); + } + /** * Delete the printarea for the sheet specified @@ -1297,13 +1370,9 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { */ @Override public void removePrintArea(int sheetIndex) { - int cont = 0; - for (XSSFName name : namedRanges) { - if (name.getNameName().equals(XSSFName.BUILTIN_PRINT_AREA) && name.getSheetIndex() == sheetIndex) { - namedRanges.remove(cont); - break; - } - cont++; + XSSFName name = getBuiltInName(XSSFName.BUILTIN_PRINT_AREA, sheetIndex); + if (name != null) { + removeName(name); } } @@ -1369,17 +1438,20 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { } //adjust indices of names ranges - for (Iterator it = namedRanges.iterator(); it.hasNext();) { - XSSFName nm = it.next(); + List toRemove = new ArrayList(); + for (XSSFName nm : namedRanges) { CTDefinedName ct = nm.getCTName(); if(!ct.isSetLocalSheetId()) continue; if (ct.getLocalSheetId() == index) { - it.remove(); + toRemove.add(nm); } else if (ct.getLocalSheetId() > index){ // Bump down by one, so still points at the same sheet ct.setLocalSheetId(ct.getLocalSheetId()-1); } } + for (XSSFName nm : toRemove) { + removeName(nm); + } } /** @@ -1514,8 +1586,8 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { } XSSFName getBuiltInName(String builtInCode, int sheetNumber) { - for (XSSFName name : namedRanges) { - if (name.getNameName().equalsIgnoreCase(builtInCode) && name.getSheetIndex() == sheetNumber) { + for (XSSFName name : namedRangesByName.get(builtInCode.toLowerCase(Locale.ENGLISH))) { + if (name.getSheetIndex() == sheetNumber) { return name; } } @@ -1537,15 +1609,12 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { nameRecord.setName(builtInName); nameRecord.setLocalSheetId(sheetNumber); - XSSFName name = new XSSFName(nameRecord, this); - for (XSSFName nr : namedRanges) { - if (nr.equals(name)) - throw new POIXMLException("Builtin (" + builtInName - + ") already exists for sheet (" + sheetNumber + ")"); + if (getBuiltInName(builtInName, sheetNumber) != null) { + throw new POIXMLException("Builtin (" + builtInName + + ") already exists for sheet (" + sheetNumber + ")"); } - namedRanges.add(name); - return name; + return createAndStoreName(nameRecord); } /** @@ -1665,10 +1734,11 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook { } private void reprocessNamedRanges() { + namedRangesByName = new ArrayListValuedHashMap(); namedRanges = new ArrayList(); if(workbook.isSetDefinedNames()) { for(CTDefinedName ctName : workbook.getDefinedNames().getDefinedNameArray()) { - namedRanges.add(new XSSFName(ctName, this)); + createAndStoreName(ctName); } } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFFormulaUtils.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFFormulaUtils.java index 1d146c09c..ef0c5ea63 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFFormulaUtils.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFFormulaUtils.java @@ -65,9 +65,7 @@ public final class XSSFFormulaUtils { */ public void updateSheetName(final int sheetIndex, final String oldName, final String newName) { // update named ranges - final int numberOfNames = _wb.getNumberOfNames(); - for (int i = 0; i < numberOfNames; i++) { - XSSFName nm = _wb.getNameAt(i); + for (XSSFName nm : _wb.getAllNames()) { if (nm.getSheetIndex() == -1 || nm.getSheetIndex() == sheetIndex) { updateName(nm, oldName, newName); } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPasswordHelper.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPasswordHelper.java new file mode 100644 index 000000000..46e47f688 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPasswordHelper.java @@ -0,0 +1,136 @@ +/* + * ==================================================================== + * 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.xssf.usermodel.helpers; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Locale; + +import javax.xml.bind.DatatypeConverter; +import javax.xml.namespace.QName; + +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.util.Internal; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlObject; + +@Internal(since="3.15 beta 3") +public final class XSSFPasswordHelper { + private XSSFPasswordHelper() { + // no instances of this static class + } + + /** + * Sets the XORed or hashed password + * + * @param xobj the xmlbeans object which contains the password attributes + * @param password the password, if null, the password attributes will be removed + * @param hashAlgo the hash algorithm, if null the password will be XORed + * @param prefix the prefix of the password attributes, may be null + */ + public static void setPassword(XmlObject xobj, String password, HashAlgorithm hashAlgo, String prefix) { + XmlCursor cur = xobj.newCursor(); + + if (password == null) { + cur.removeAttribute(getAttrName(prefix, "password")); + cur.removeAttribute(getAttrName(prefix, "algorithmName")); + cur.removeAttribute(getAttrName(prefix, "hashValue")); + cur.removeAttribute(getAttrName(prefix, "saltValue")); + cur.removeAttribute(getAttrName(prefix, "spinCount")); + return; + } + + cur.toFirstContentToken(); + if (hashAlgo == null) { + int hash = CryptoFunctions.createXorVerifier1(password); + cur.insertAttributeWithValue(getAttrName(prefix, "password"), + String.format(Locale.ROOT, "%04X", hash).toUpperCase(Locale.ROOT)); + } else { + SecureRandom random = new SecureRandom(); + byte salt[] = random.generateSeed(16); + + // Iterations specifies the number of times the hashing function shall be iteratively run (using each + // iteration's result as the input for the next iteration). + int spinCount = 100000; + + // Implementation Notes List: + // --> In this third stage, the reversed byte order legacy hash from the second stage shall + // be converted to Unicode hex string representation + byte hash[] = CryptoFunctions.hashPassword(password, hashAlgo, salt, spinCount, false); + + cur.insertAttributeWithValue(getAttrName(prefix, "algorithmName"), hashAlgo.jceId); + cur.insertAttributeWithValue(getAttrName(prefix, "hashValue"), DatatypeConverter.printBase64Binary(hash)); + cur.insertAttributeWithValue(getAttrName(prefix, "saltValue"), DatatypeConverter.printBase64Binary(salt)); + cur.insertAttributeWithValue(getAttrName(prefix, "spinCount"), ""+spinCount); + } + cur.dispose(); + } + + /** + * Validates the password, i.e. + * calculates the hash of the given password and compares it against the stored hash + * + * @param xobj the xmlbeans object which contains the password attributes + * @param password the password, if null the method will always return false, + * even if there's no password set + * @param prefix the prefix of the password attributes, may be null + * + * @return true, if the hashes match + */ + public static boolean validatePassword(XmlObject xobj, String password, String prefix) { + // TODO: is "velvetSweatshop" the default password? + if (password == null) return false; + + XmlCursor cur = xobj.newCursor(); + String xorHashVal = cur.getAttributeText(getAttrName(prefix, "password")); + String algoName = cur.getAttributeText(getAttrName(prefix, "algorithmName")); + String hashVal = cur.getAttributeText(getAttrName(prefix, "hashValue")); + String saltVal = cur.getAttributeText(getAttrName(prefix, "saltValue")); + String spinCount = cur.getAttributeText(getAttrName(prefix, "spinCount")); + cur.dispose(); + + if (xorHashVal != null) { + int hash1 = Integer.parseInt(xorHashVal, 16); + int hash2 = CryptoFunctions.createXorVerifier1(password); + return hash1 == hash2; + } else { + if (hashVal == null || algoName == null || saltVal == null || spinCount == null) { + return false; + } + + byte hash1[] = DatatypeConverter.parseBase64Binary(hashVal); + HashAlgorithm hashAlgo = HashAlgorithm.fromString(algoName); + byte salt[] = DatatypeConverter.parseBase64Binary(saltVal); + int spinCnt = Integer.parseInt(spinCount); + byte hash2[] = CryptoFunctions.hashPassword(password, hashAlgo, salt, spinCnt, false); + return Arrays.equals(hash1, hash2); + } + } + + + private static QName getAttrName(String prefix, String name) { + if (prefix == null || "".equals(prefix)) { + return new QName(name); + } else { + return new QName(prefix+Character.toUpperCase(name.charAt(0))+name.substring(1)); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPaswordHelper.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPaswordHelper.java index c91049948..4e3c90819 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPaswordHelper.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPaswordHelper.java @@ -1,130 +1,60 @@ -/* - * ==================================================================== - * 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.xssf.usermodel.helpers; - -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Locale; - -import javax.xml.bind.DatatypeConverter; -import javax.xml.namespace.QName; - -import org.apache.poi.poifs.crypt.CryptoFunctions; -import org.apache.poi.poifs.crypt.HashAlgorithm; -import org.apache.xmlbeans.XmlCursor; -import org.apache.xmlbeans.XmlObject; - -public class XSSFPaswordHelper { - /** - * Sets the XORed or hashed password - * - * @param xobj the xmlbeans object which contains the password attributes - * @param password the password, if null, the password attributes will be removed - * @param hashAlgo the hash algorithm, if null the password will be XORed - * @param prefix the prefix of the password attributes, may be null - */ - public static void setPassword(XmlObject xobj, String password, HashAlgorithm hashAlgo, String prefix) { - XmlCursor cur = xobj.newCursor(); - - if (password == null) { - cur.removeAttribute(getAttrName(prefix, "password")); - cur.removeAttribute(getAttrName(prefix, "algorithmName")); - cur.removeAttribute(getAttrName(prefix, "hashValue")); - cur.removeAttribute(getAttrName(prefix, "saltValue")); - cur.removeAttribute(getAttrName(prefix, "spinCount")); - return; - } - - cur.toFirstContentToken(); - if (hashAlgo == null) { - int hash = CryptoFunctions.createXorVerifier1(password); - cur.insertAttributeWithValue(getAttrName(prefix, "password"), - Integer.toHexString(hash).toUpperCase(Locale.ROOT)); - } else { - SecureRandom random = new SecureRandom(); - byte salt[] = random.generateSeed(16); - - // Iterations specifies the number of times the hashing function shall be iteratively run (using each - // iteration's result as the input for the next iteration). - int spinCount = 100000; - - // Implementation Notes List: - // --> In this third stage, the reversed byte order legacy hash from the second stage shall - // be converted to Unicode hex string representation - byte hash[] = CryptoFunctions.hashPassword(password, hashAlgo, salt, spinCount, false); - - cur.insertAttributeWithValue(getAttrName(prefix, "algorithmName"), hashAlgo.jceId); - cur.insertAttributeWithValue(getAttrName(prefix, "hashValue"), DatatypeConverter.printBase64Binary(hash)); - cur.insertAttributeWithValue(getAttrName(prefix, "saltValue"), DatatypeConverter.printBase64Binary(salt)); - cur.insertAttributeWithValue(getAttrName(prefix, "spinCount"), ""+spinCount); - } - cur.dispose(); - } - - /** - * Validates the password, i.e. - * calculates the hash of the given password and compares it against the stored hash - * - * @param xobj the xmlbeans object which contains the password attributes - * @param password the password, if null the method will always return false, - * even if there's no password set - * @param prefix the prefix of the password attributes, may be null - * - * @return true, if the hashes match - */ - public static boolean validatePassword(XmlObject xobj, String password, String prefix) { - // TODO: is "velvetSweatshop" the default password? - if (password == null) return false; - - XmlCursor cur = xobj.newCursor(); - String xorHashVal = cur.getAttributeText(getAttrName(prefix, "password")); - String algoName = cur.getAttributeText(getAttrName(prefix, "algorithmName")); - String hashVal = cur.getAttributeText(getAttrName(prefix, "hashValue")); - String saltVal = cur.getAttributeText(getAttrName(prefix, "saltValue")); - String spinCount = cur.getAttributeText(getAttrName(prefix, "spinCount")); - cur.dispose(); - - if (xorHashVal != null) { - int hash1 = Integer.parseInt(xorHashVal, 16); - int hash2 = CryptoFunctions.createXorVerifier1(password); - return hash1 == hash2; - } else { - if (hashVal == null || algoName == null || saltVal == null || spinCount == null) { - return false; - } - - byte hash1[] = DatatypeConverter.parseBase64Binary(hashVal); - HashAlgorithm hashAlgo = HashAlgorithm.fromString(algoName); - byte salt[] = DatatypeConverter.parseBase64Binary(saltVal); - int spinCnt = Integer.parseInt(spinCount); - byte hash2[] = CryptoFunctions.hashPassword(password, hashAlgo, salt, spinCnt, false); - return Arrays.equals(hash1, hash2); - } - } - - - private static QName getAttrName(String prefix, String name) { - if (prefix == null || "".equals(prefix)) { - return new QName(name); - } else { - return new QName(prefix+Character.toUpperCase(name.charAt(0))+name.substring(1)); - } - } -} +/* + * ==================================================================== + * 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.xssf.usermodel.helpers; + +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; +import org.apache.xmlbeans.XmlObject; + +/** + * @deprecated POI 3.15 beta 3. Use {@link XSSFPasswordHelper} instead. + */ +@Internal(since="3.15 beta 3") +@Deprecated +@Removal(version="3.17") +public class XSSFPaswordHelper { + /** + * Sets the XORed or hashed password + * + * @param xobj the xmlbeans object which contains the password attributes + * @param password the password, if null, the password attributes will be removed + * @param hashAlgo the hash algorithm, if null the password will be XORed + * @param prefix the prefix of the password attributes, may be null + */ + public static void setPassword(XmlObject xobj, String password, HashAlgorithm hashAlgo, String prefix) { + XSSFPasswordHelper.setPassword(xobj, password, hashAlgo, prefix); + } + + /** + * Validates the password, i.e. + * calculates the hash of the given password and compares it against the stored hash + * + * @param xobj the xmlbeans object which contains the password attributes + * @param password the password, if null the method will always return false, + * even if there's no password set + * @param prefix the prefix of the password attributes, may be null + * + * @return true, if the hashes match + */ + public static boolean validatePassword(XmlObject xobj, String password, String prefix) { + return XSSFPasswordHelper.validatePassword(xobj, password, prefix); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java index 63104dd9e..d11ed1fa8 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java @@ -83,9 +83,7 @@ public final class XSSFRowShifter extends RowShifter { public void updateNamedRanges(FormulaShifter shifter) { Workbook wb = sheet.getWorkbook(); XSSFEvaluationWorkbook fpb = XSSFEvaluationWorkbook.create((XSSFWorkbook) wb); - final int numberOfNames = wb.getNumberOfNames(); - for (int i = 0; i < numberOfNames; i++) { - Name name = wb.getNameAt(i); + for (Name name : wb.getAllNames()) { String formula = name.getRefersToFormula(); int sheetIndex = name.getSheetIndex(); final int rowIndex = -1; //don't care, named ranges are not allowed to include structured references diff --git a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/internal/TestContentTypeManager.java b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/internal/TestContentTypeManager.java index 9acf0d10b..936d0f9e8 100644 --- a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/internal/TestContentTypeManager.java +++ b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/internal/TestContentTypeManager.java @@ -18,6 +18,7 @@ package org.apache.poi.openxml4j.opc.internal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -44,16 +45,21 @@ public final class TestContentTypeManager { // Retrieves core properties part OPCPackage p = OPCPackage.open(filepath, PackageAccess.READ); - PackageRelationshipCollection rels = p.getRelationshipsByType(PackageRelationshipTypes.CORE_PROPERTIES); - PackageRelationship corePropertiesRelationship = rels.getRelationship(0); - PackagePart coreDocument = p.getPart(corePropertiesRelationship); - - assertEquals("application/vnd.openxmlformats-package.core-properties+xml", coreDocument.getContentType()); + try { + PackageRelationshipCollection rels = p.getRelationshipsByType(PackageRelationshipTypes.CORE_PROPERTIES); + PackageRelationship corePropertiesRelationship = rels.getRelationship(0); + PackagePart coreDocument = p.getPart(corePropertiesRelationship); - // TODO - finish writing this test - assumeTrue("finish writing this test", false); - - ContentTypeManager ctm = new ZipContentTypeManager(coreDocument.getInputStream(), p); + assertEquals("application/vnd.openxmlformats-package.core-properties+xml", coreDocument.getContentType()); + + // TODO - finish writing this test + assumeTrue("finish writing this test", false); + + ContentTypeManager ctm = new ZipContentTypeManager(coreDocument.getInputStream(), p); + assertNotNull(ctm); + } finally { + p.close(); + } } /** diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java index 3280a9121..2be21e830 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java @@ -115,25 +115,25 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { assertFalse(wb.isMacroEnabled()); assertEquals(3, wb.getNumberOfNames()); - assertEquals(0, wb.getNameAt(0).getCTName().getLocalSheetId()); - assertFalse(wb.getNameAt(0).getCTName().isSetLocalSheetId()); - assertEquals("SheetA!$A$1", wb.getNameAt(0).getRefersToFormula()); - assertEquals("SheetA", wb.getNameAt(0).getSheetName()); + assertEquals(0, wb.getName("SheetAA1").getCTName().getLocalSheetId()); + assertFalse(wb.getName("SheetAA1").getCTName().isSetLocalSheetId()); + assertEquals("SheetA!$A$1", wb.getName("SheetAA1").getRefersToFormula()); + assertEquals("SheetA", wb.getName("SheetAA1").getSheetName()); - assertEquals(0, wb.getNameAt(1).getCTName().getLocalSheetId()); - assertFalse(wb.getNameAt(1).getCTName().isSetLocalSheetId()); - assertEquals("SheetB!$A$1", wb.getNameAt(1).getRefersToFormula()); - assertEquals("SheetB", wb.getNameAt(1).getSheetName()); + assertEquals(0, wb.getName("SheetBA1").getCTName().getLocalSheetId()); + assertFalse(wb.getName("SheetBA1").getCTName().isSetLocalSheetId()); + assertEquals("SheetB!$A$1", wb.getName("SheetBA1").getRefersToFormula()); + assertEquals("SheetB", wb.getName("SheetBA1").getSheetName()); - assertEquals(0, wb.getNameAt(2).getCTName().getLocalSheetId()); - assertFalse(wb.getNameAt(2).getCTName().isSetLocalSheetId()); - assertEquals("SheetC!$A$1", wb.getNameAt(2).getRefersToFormula()); - assertEquals("SheetC", wb.getNameAt(2).getSheetName()); + assertEquals(0, wb.getName("SheetCA1").getCTName().getLocalSheetId()); + assertFalse(wb.getName("SheetCA1").getCTName().isSetLocalSheetId()); + assertEquals("SheetC!$A$1", wb.getName("SheetCA1").getRefersToFormula()); + assertEquals("SheetC", wb.getName("SheetCA1").getSheetName()); // Save and re-load, still there XSSFWorkbook nwb = XSSFTestDataSamples.writeOutAndReadBack(wb); assertEquals(3, nwb.getNumberOfNames()); - assertEquals("SheetA!$A$1", nwb.getNameAt(0).getRefersToFormula()); + assertEquals("SheetA!$A$1", nwb.getName("SheetAA1").getRefersToFormula()); nwb.close(); wb.close(); diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java index 566944d18..6dcaef960 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java @@ -154,7 +154,9 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { evaluator.evaluate(cXSL_cell); fail("Without a fix for #56752, shouldn't be able to evaluate a " + "reference to a non-provided linked workbook"); - } catch(Exception e) {} + } catch(Exception e) { + // expected here + } // Setup the environment Map evaluators = new HashMap(); @@ -171,8 +173,19 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { evaluator.evaluate(c); } } + // And evaluate the other way too + evaluator.evaluateAll(); - // Evaluate and check results + // Static evaluator won't work, as no references passed in + try { + XSSFFormulaEvaluator.evaluateAllFormulaCells(wb); + fail("Static method lacks references, shouldn't work"); + } catch(Exception e) { + // expected here + } + + + // Evaluate specific cells and check results assertEquals("\"Hello!\"", evaluator.evaluate(cXSLX_cell).formatAsString()); assertEquals("\"Test A1\"", evaluator.evaluate(cXSLX_sNR).formatAsString()); assertEquals("142.0", evaluator.evaluate(cXSLX_gNR).formatAsString()); @@ -196,7 +209,9 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { try { cXSLX_nw_cell.setCellFormula("[alt.xlsx]Sheet1!$A$1"); fail("New workbook not linked, shouldn't be able to add"); - } catch (Exception e) {} + } catch (Exception e) { + // expected here + } // Link and re-try Workbook alt = new XSSFWorkbook(); @@ -651,4 +666,20 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { private Cell getCell(Sheet sheet, int rowNo, int column) { return sheet.getRow(rowNo).getCell(column); } + + @Test + public void test59736() { + Workbook wb = XSSFTestDataSamples.openSampleWorkbook("59736.xlsx"); + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + Cell cell = wb.getSheetAt(0).getRow(0).getCell(0); + assertEquals(1, cell.getNumericCellValue(), 0.001); + + cell = wb.getSheetAt(0).getRow(1).getCell(0); + CellValue value = evaluator.evaluate(cell); + assertEquals(1, value.getNumberValue(), 0.001); + + cell = wb.getSheetAt(0).getRow(2).getCell(0); + value = evaluator.evaluate(cell); + assertEquals(1, value.getNumberValue(), 0.001); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFName.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFName.java index 3e0f0b165..a188a11ed 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFName.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFName.java @@ -53,9 +53,8 @@ public final class TestXSSFName extends BaseTestNamedRange { //sheet.createFreezePane(0, 3); } assertEquals(1, wb.getNumberOfNames()); - XSSFName nr1 = wb.getNameAt(0); + XSSFName nr1 = wb.getName(XSSFName.BUILTIN_PRINT_TITLE); - assertEquals(XSSFName.BUILTIN_PRINT_TITLE, nr1.getNameName()); assertEquals("'First Sheet'!$A:$A,'First Sheet'!$1:$4", nr1.getRefersToFormula()); //remove the columns part @@ -77,9 +76,8 @@ public final class TestXSSFName extends BaseTestNamedRange { wb.close(); assertEquals(1, nwb.getNumberOfNames()); - nr1 = nwb.getNameAt(0); + nr1 = nwb.getName(XSSFName.BUILTIN_PRINT_TITLE); - assertEquals(XSSFName.BUILTIN_PRINT_TITLE, nr1.getNameName()); assertEquals("'First Sheet'!$A:$A,'First Sheet'!$1:$4", nr1.getRefersToFormula()); // check that setting RR&C on a second sheet causes a new Print_Titles built-in @@ -89,7 +87,7 @@ public final class TestXSSFName extends BaseTestNamedRange { sheet2.setRepeatingColumns(CellRangeAddress.valueOf("B:C")); assertEquals(2, nwb.getNumberOfNames()); - XSSFName nr2 = nwb.getNameAt(1); + XSSFName nr2 = nwb.getNames(XSSFName.BUILTIN_PRINT_TITLE).get(1); assertEquals(XSSFName.BUILTIN_PRINT_TITLE, nr2.getNameName()); assertEquals("SecondSheet!$B:$C,SecondSheet!$1:$1", nr2.getRefersToFormula()); @@ -98,4 +96,38 @@ public final class TestXSSFName extends BaseTestNamedRange { sheet2.setRepeatingColumns(null); nwb.close(); } + + @Test + public void testSetNameName() throws Exception { + // Test that renaming named ranges doesn't break our new named range map + XSSFWorkbook wb = new XSSFWorkbook(); + wb.createSheet("First Sheet"); + + // Two named ranges called "name1", one scoped to sheet1 and one globally + XSSFName nameSheet1 = wb.createName(); + nameSheet1.setNameName("name1"); + nameSheet1.setRefersToFormula("'First Sheet'!$A$1"); + nameSheet1.setSheetIndex(0); + + XSSFName nameGlobal = wb.createName(); + nameGlobal.setNameName("name1"); + nameGlobal.setRefersToFormula("'First Sheet'!$B$1"); + + // Rename sheet-scoped name to "name2", check everything is updated properly + // and that the other name is unaffected + nameSheet1.setNameName("name2"); + assertEquals(1, wb.getNames("name1").size()); + assertEquals(1, wb.getNames("name2").size()); + assertEquals(nameGlobal, wb.getName("name1")); + assertEquals(nameSheet1, wb.getName("name2")); + + // Rename the other name to "name" and check everything again + nameGlobal.setNameName("name2"); + assertEquals(0, wb.getNames("name1").size()); + assertEquals(2, wb.getNames("name2").size()); + assertTrue(wb.getNames("name2").contains(nameGlobal)); + assertTrue(wb.getNames("name2").contains(nameSheet1)); + + wb.close(); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java index a9b0d1f0b..689e999bc 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java @@ -80,6 +80,7 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXf; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCalcMode; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STPane; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.STUnsignedShortHex; public final class TestXSSFSheet extends BaseTestXSheet { @@ -1099,6 +1100,30 @@ public final class TestXSSFSheet extends BaseTestXSheet { wb.close(); } + @Test + public void protectSheet_emptyPassword() throws IOException { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.createSheet(); + CTSheetProtection pr = sheet.getCTWorksheet().getSheetProtection(); + assertNull("CTSheetProtection should be null by default", pr); + String password = ""; + sheet.protectSheet(password); + pr = sheet.getCTWorksheet().getSheetProtection(); + assertNotNull("CTSheetProtection should be not null", pr); + assertTrue("sheet protection should be on", pr.isSetSheet()); + assertTrue("object protection should be on", pr.isSetObjects()); + assertTrue("scenario protection should be on", pr.isSetScenarios()); + int hashVal = CryptoFunctions.createXorVerifier1(password); + STUnsignedShortHex xpassword = pr.xgetPassword(); + int actualVal = Integer.parseInt(xpassword.getStringValue(),16); + assertEquals("well known value for top secret hash should match", hashVal, actualVal); + + sheet.protectSheet(null); + assertNull("protectSheet(null) should unset CTSheetProtection", sheet.getCTWorksheet().getSheetProtection()); + + wb.close(); + } + @Test public void protectSheet_lowlevel_2013() throws IOException { String password = "test"; diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java index c3420c780..7f832c4d9 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java @@ -1140,4 +1140,44 @@ public final class TestXSSFWorkbook extends BaseTestXWorkbook { wb.close(); } + + @Test + public void testRemoveSheet() throws IOException { + // Test removing a sheet maintains the named ranges correctly + XSSFWorkbook wb = new XSSFWorkbook(); + wb.createSheet("Sheet1"); + wb.createSheet("Sheet2"); + + XSSFName sheet1Name = wb.createName(); + sheet1Name.setNameName("name1"); + sheet1Name.setSheetIndex(0); + sheet1Name.setRefersToFormula("Sheet1!$A$1"); + + XSSFName sheet2Name = wb.createName(); + sheet2Name.setNameName("name1"); + sheet2Name.setSheetIndex(1); + sheet2Name.setRefersToFormula("Sheet2!$A$1"); + + assertTrue(wb.getAllNames().contains(sheet1Name)); + assertTrue(wb.getAllNames().contains(sheet2Name)); + + assertEquals(2, wb.getNames("name1").size()); + assertEquals(sheet1Name, wb.getNames("name1").get(0)); + assertEquals(sheet2Name, wb.getNames("name1").get(1)); + + // Remove sheet1, we should only have sheet2Name now + wb.removeSheetAt(0); + + assertFalse(wb.getAllNames().contains(sheet1Name)); + assertTrue(wb.getAllNames().contains(sheet2Name)); + assertEquals(1, wb.getNames("name1").size()); + assertEquals(sheet2Name, wb.getNames("name1").get(0)); + + // Check by index as well for sanity + assertEquals(1, wb.getNumberOfNames()); + assertEquals(0, wb.getNameIndex("name1")); + assertEquals(sheet2Name, wb.getNameAt(0)); + + wb.close(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java b/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java index f9d53dcbd..fe22c03ef 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java @@ -571,20 +571,39 @@ public final class HWPFDocument extends HWPFDocumentCore { return _fields; } + /** + * Warning - not currently implemented for HWPF! + */ @Override public void write() throws IOException { + // TODO Implement throw new IllegalStateException("Coming soon!"); } + + /** + * Writes out the word file that is represented by an instance of this class. + * + * If the {@link File} exists, it will be replaced, otherwise a new one + * will be created + * + * @param newFile The File to write to. + * @throws IOException If there is an unexpected IOException from writing + * to the File. + * + * @since 3.15 beta 3 + */ @Override public void write(File newFile) throws IOException { - throw new IllegalStateException("Coming soon!"); + NPOIFSFileSystem pfs = POIFSFileSystem.create(newFile); + write(pfs, true); + pfs.writeFilesystem(); } /** * Writes out the word file that is represented by an instance of this class. * - * If {@code stream} is a {@link java.io.FileOutputStream} on a networked drive - * or has a high cost/latency associated with each written byte, + * For better performance when writing to files, use {@link #write(File)}. + * If {@code stream} has a high cost/latency associated with each written byte, * consider wrapping the OutputStream in a {@link java.io.BufferedOutputStream} * to improve write performance. * @@ -592,9 +611,12 @@ public final class HWPFDocument extends HWPFDocumentCore { * @throws IOException If there is an unexpected IOException from the passed * in OutputStream. */ - public void write(OutputStream out) - throws IOException - { + public void write(OutputStream out) throws IOException { + NPOIFSFileSystem pfs = new NPOIFSFileSystem(); + write(pfs, true); + pfs.writeFilesystem( out ); + } + private void write(NPOIFSFileSystem pfs, boolean copyOtherEntries) throws IOException { // initialize our streams for writing. HWPFFileSystem docSys = new HWPFFileSystem(); HWPFOutputStream wordDocumentStream = docSys.getStream(STREAM_WORD_DOCUMENT); @@ -891,7 +913,8 @@ public final class HWPFDocument extends HWPFDocumentCore { } // create new document preserving order of entries - NPOIFSFileSystem pfs = new NPOIFSFileSystem(); + // TODO Check "copyOtherEntries" and tweak behaviour based on that + // TODO That's needed for in-place write boolean docWritten = false; boolean dataWritten = false; boolean objectPoolWritten = false; @@ -967,7 +990,6 @@ public final class HWPFDocument extends HWPFDocumentCore { if ( !objectPoolWritten ) _objectPool.writeTo( pfs.getRoot() ); - pfs.writeFilesystem( out ); this.directory = pfs.getRoot(); /* diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java new file mode 100644 index 000000000..95d7919e3 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java @@ -0,0 +1,81 @@ +/* ==================================================================== + 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.hwpf.usermodel; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; + +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.hwpf.HWPFTestCase; +import org.apache.poi.hwpf.HWPFTestDataSamples; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.TempFile; + +/** + * Test various write situations + */ +public final class TestHWPFWrite extends HWPFTestCase { + /** + * Write to a stream + */ + public void testWriteStream() throws Exception { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("SampleDoc.doc"); + + Range r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doc.write(baos); + doc.close(); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + + doc = new HWPFDocument(bais); + r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + doc.close(); + } + + /** + * Write to a new file + */ + public void testWriteNewFile() throws Exception { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("SampleDoc.doc"); + + Range r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + + File file = TempFile.createTempFile("TestDocument", ".doc"); + doc.write(file); + doc.close(); + + // Check reading from File and Stream + doc = new HWPFDocument(new FileInputStream(file)); + r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + doc.close(); + + doc = new HWPFDocument(new POIFSFileSystem(file)); + r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + doc.close(); + } + + // TODO In-place write positive and negative checks +} diff --git a/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java b/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java index 404e0da07..e02260844 100644 --- a/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java +++ b/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java @@ -50,14 +50,20 @@ public abstract class BaseTestSlideShow { @Test public void addPicture_Stream() throws IOException { SlideShow show = createSlideShow(); - InputStream stream = slTests.openResourceAsStream("clock.jpg"); - - assertEquals(0, show.getPictureData().size()); - PictureData picture = show.addPicture(stream, PictureType.JPEG); - assertEquals(1, show.getPictureData().size()); - assertSame(picture, show.getPictureData().get(0)); - - show.close(); + try { + InputStream stream = slTests.openResourceAsStream("clock.jpg"); + try { + assertEquals(0, show.getPictureData().size()); + PictureData picture = show.addPicture(stream, PictureType.JPEG); + assertEquals(1, show.getPictureData().size()); + assertSame(picture, show.getPictureData().get(0)); + + } finally { + stream.close(); + } + } finally { + show.close(); + } } @Test diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestIndirect.java b/src/testcases/org/apache/poi/ss/formula/functions/TestIndirect.java index 8e93152c8..ea5ac9c7a 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestIndirect.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestIndirect.java @@ -27,6 +27,7 @@ import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; @@ -105,6 +106,7 @@ public final class TestIndirect { // non-error cases confirm(feA, c, "INDIRECT(\"C2\")", 23); + confirm(feA, c, "INDIRECT(\"C2\", TRUE)", 23); confirm(feA, c, "INDIRECT(\"$C2\")", 23); confirm(feA, c, "INDIRECT(\"C$2\")", 23); confirm(feA, c, "SUM(INDIRECT(\"Sheet2!B1:C3\"))", 351); // area ref @@ -149,7 +151,7 @@ public final class TestIndirect { // confirm(feA, c, "INDIRECT(\"Sheet1!A65537\")", ErrorEval.REF_INVALID); // bad row // } confirm(feA, c, "INDIRECT(\"Sheet1!A 1\")", ErrorEval.REF_INVALID); // space in cell ref - + wbA.close(); } @@ -203,4 +205,9 @@ public final class TestIndirect { + "' but got '" + cv.formatAsString() + "'."); } } + + @Test + public void testInvalidInput() { + assertEquals(ErrorEval.VALUE_INVALID, Indirect.instance.evaluate(new ValueEval[] {}, null)); + } } diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestSubtotal.java b/src/testcases/org/apache/poi/ss/formula/functions/TestSubtotal.java index 7b63eaf79..f2750b591 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestSubtotal.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestSubtotal.java @@ -20,9 +20,8 @@ package org.apache.poi.ss.formula.functions; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.ss.formula.eval.AreaEval; -import org.apache.poi.ss.formula.eval.NumberEval; -import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.FormulaParseException; +import org.apache.poi.ss.formula.eval.*; import junit.framework.TestCase; import org.apache.poi.ss.usermodel.*; @@ -75,7 +74,6 @@ public final class TestSubtotal extends TestCase { } public void testAvg(){ - Workbook wb = new HSSFWorkbook(); FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); @@ -95,16 +93,18 @@ public final class TestSubtotal extends TestCase { a6.setCellFormula("SUBTOTAL(1,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(1,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(1,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(2.0, a3.getNumericCellValue()); assertEquals(8.0, a6.getNumericCellValue()); assertEquals(3.0, a7.getNumericCellValue()); + assertEquals(3.0, a8.getNumericCellValue()); } public void testSum(){ - Workbook wb = new HSSFWorkbook(); FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); @@ -124,12 +124,15 @@ public final class TestSubtotal extends TestCase { a6.setCellFormula("SUBTOTAL(9,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(9,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(9,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(4.0, a3.getNumericCellValue()); assertEquals(26.0, a6.getNumericCellValue()); assertEquals(12.0, a7.getNumericCellValue()); + assertEquals(12.0, a8.getNumericCellValue()); } public void testCount(){ @@ -147,18 +150,21 @@ public final class TestSubtotal extends TestCase { a3.setCellFormula("SUBTOTAL(2,B2:B3)"); Cell a4 = sh.createRow(4).createCell(1); a4.setCellValue("POI"); // A4 is string and not counted - Cell a5 = sh.createRow(5).createCell(1); // A5 is blank and not counted + /*Cell a5 =*/ sh.createRow(5).createCell(1); // A5 is blank and not counted Cell a6 = sh.createRow(6).createCell(1); a6.setCellFormula("SUBTOTAL(2,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(2,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(2,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(2.0, a3.getNumericCellValue()); assertEquals(6.0, a6.getNumericCellValue()); assertEquals(2.0, a7.getNumericCellValue()); + assertEquals(2.0, a8.getNumericCellValue()); } public void testCounta(){ @@ -176,18 +182,21 @@ public final class TestSubtotal extends TestCase { a3.setCellFormula("SUBTOTAL(3,B2:B3)"); Cell a4 = sh.createRow(4).createCell(1); a4.setCellValue("POI"); // A4 is string and not counted - Cell a5 = sh.createRow(5).createCell(1); // A5 is blank and not counted + /*Cell a5 =*/ sh.createRow(5).createCell(1); // A5 is blank and not counted Cell a6 = sh.createRow(6).createCell(1); a6.setCellFormula("SUBTOTAL(3,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(3,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(3,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(2.0, a3.getNumericCellValue()); assertEquals(8.0, a6.getNumericCellValue()); assertEquals(3.0, a7.getNumericCellValue()); + assertEquals(3.0, a8.getNumericCellValue()); } public void testMax(){ @@ -211,12 +220,15 @@ public final class TestSubtotal extends TestCase { a6.setCellFormula("SUBTOTAL(4,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(4,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(4,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(3.0, a3.getNumericCellValue()); assertEquals(16.0, a6.getNumericCellValue()); assertEquals(7.0, a7.getNumericCellValue()); + assertEquals(7.0, a8.getNumericCellValue()); } public void testMin(){ @@ -240,12 +252,15 @@ public final class TestSubtotal extends TestCase { a6.setCellFormula("SUBTOTAL(5,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(5,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(5,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(1.0, a3.getNumericCellValue()); assertEquals(4.0, a6.getNumericCellValue()); assertEquals(1.0, a7.getNumericCellValue()); + assertEquals(1.0, a8.getNumericCellValue()); } public void testStdev(){ @@ -269,12 +284,15 @@ public final class TestSubtotal extends TestCase { a6.setCellFormula("SUBTOTAL(7,B2:B6)*2 + 2"); Cell a7 = sh.createRow(7).createCell(1); a7.setCellFormula("SUBTOTAL(7,B2:B7)"); + Cell a8 = sh.createRow(8).createCell(1); + a8.setCellFormula("SUBTOTAL(7,B2,B3,B4,B5,B6,B7,B8)"); fe.evaluateAll(); assertEquals(1.41421, a3.getNumericCellValue(), 0.0001); assertEquals(7.65685, a6.getNumericCellValue(), 0.0001); assertEquals(2.82842, a7.getNumericCellValue(), 0.0001); + assertEquals(2.82842, a8.getNumericCellValue(), 0.0001); } public void test50209(){ @@ -328,4 +346,69 @@ public final class TestSubtotal extends TestCase { confirmExpectedResult(evaluator, "SUBTOTAL(COUNT;B2:B8,C2:C8)", cellC2, 3.0); confirmExpectedResult(evaluator, "SUBTOTAL(COUNTA;B2:B8,C2:C8)", cellC3, 5.0); } + + public void testUnimplemented(){ + Workbook wb = new HSSFWorkbook(); + + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + Sheet sh = wb.createSheet(); + Cell a3 = sh.createRow(3).createCell(1); + a3.setCellFormula("SUBTOTAL(8,B2:B3)"); + + try { + fe.evaluateAll(); + fail("Should catch an NotImplementedFunctionException here, adjust these tests if it was actually implemented"); + } catch (NotImplementedException e) { + // expected here + } + + a3.setCellFormula("SUBTOTAL(10,B2:B3)"); + + try { + fe.evaluateAll(); + fail("Should catch an NotImplementedFunctionException here, adjust these tests if it was actually implemented"); + } catch (NotImplementedException e) { + // expected here + } + + a3.setCellFormula("SUBTOTAL(11,B2:B3)"); + + try { + fe.evaluateAll(); + fail("Should catch an NotImplementedFunctionException here, adjust these tests if it was actually implemented"); + } catch (NotImplementedException e) { + // expected here + } + + a3.setCellFormula("SUBTOTAL(107,B2:B3)"); + + try { + fe.evaluateAll(); + fail("Should catch an NotImplementedFunctionException here, adjust these tests if it was actually implemented"); + } catch (NotImplementedException e) { + // expected here + } + + a3.setCellFormula("SUBTOTAL(0,B2:B3)"); + fe.evaluateAll(); + assertEquals(FormulaError.VALUE.getCode(), a3.getErrorCellValue()); + + try { + a3.setCellFormula("SUBTOTAL(9)"); + fail("Should catch an exception here"); + } catch (FormulaParseException e) { + // expected here + } + + try { + a3.setCellFormula("SUBTOTAL()"); + fail("Should catch an exception here"); + } catch (FormulaParseException e) { + // expected here + } + + Subtotal subtotal = new Subtotal(); + assertEquals(ErrorEval.VALUE_INVALID, subtotal.evaluate(new ValueEval[] {}, 0, 0)); + } } diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestWeekdayFunc.java b/src/testcases/org/apache/poi/ss/formula/functions/TestWeekdayFunc.java new file mode 100644 index 000000000..c6c797c18 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestWeekdayFunc.java @@ -0,0 +1,66 @@ +/* ==================================================================== + 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.eval.*; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class TestWeekdayFunc { + @Test + public void testEvaluate() throws Exception { + assertEquals(2.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(2.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(1.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(1.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(2.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(0.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(3.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(1.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(11.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(7.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(12.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(6.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(13.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(5.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(14.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(4.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(15.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(3.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(16.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(2.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(17.0)}, 0, 0)).getNumberValue(), 0.001); + + assertEquals(3.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(3.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(1.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(2.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(2.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(1.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(3.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(2.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(11.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(1.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(12.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(7.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(13.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(6.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(14.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(5.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(15.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(4.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(16.0)}, 0, 0)).getNumberValue(), 0.001); + assertEquals(3.0, ((NumberEval)WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(39448.0), new NumberEval(17.0)}, 0, 0)).getNumberValue(), 0.001); + } + + @Test + public void testEvaluateInvalid() throws Exception { + assertEquals(ErrorEval.VALUE_INVALID, WeekdayFunc.instance.evaluate(new ValueEval[]{}, 0, 0)); + assertEquals(ErrorEval.VALUE_INVALID, WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(1.0), new NumberEval(1.0)}, 0, 0)); + + assertEquals(ErrorEval.NUM_ERROR, WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(-1.0)}, 0, 0)); + assertEquals(ErrorEval.VALUE_INVALID, WeekdayFunc.instance.evaluate(new ValueEval[]{new StringEval("")}, 0, 0)); + assertEquals(ErrorEval.VALUE_INVALID, WeekdayFunc.instance.evaluate(new ValueEval[]{new StringEval("1"), new StringEval("")}, 0, 0)); + assertEquals(ErrorEval.NUM_ERROR, WeekdayFunc.instance.evaluate(new ValueEval[]{new StringEval("2"), BlankEval.instance}, 0, 0)); + assertEquals(ErrorEval.NUM_ERROR, WeekdayFunc.instance.evaluate(new ValueEval[]{new StringEval("3"), MissingArgEval.instance}, 0, 0)); + assertEquals(ErrorEval.NUM_ERROR, WeekdayFunc.instance.evaluate(new ValueEval[]{new NumberEval(1.0), new NumberEval(18.0)}, 0, 0)); + } +} \ No newline at end of file diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java index a832635c1..bdd09bfd6 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java @@ -33,9 +33,12 @@ import java.awt.font.FontRenderContext; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.geom.Rectangle2D; +import java.io.FileInputStream; import java.io.IOException; import java.text.AttributedString; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.junit.Assert.*; @@ -1606,4 +1609,78 @@ public abstract class BaseTestBugzillaIssues { assertNull("Sheet0 after write", wb2.getPrintArea(0)); // CURRENTLY FAILS with "Sheet0!$A$1:$C$6" assertEquals("Sheet1 after write", "Sheet1!$A$1:$A$1", wb2.getPrintArea(1)); } -} + + + @Test + public void test55384() throws Exception { + Workbook wb = _testDataProvider.createWorkbook(); + try { + Sheet sh = wb.createSheet(); + for (int rownum = 0; rownum < 10; rownum++) { + org.apache.poi.ss.usermodel.Row row = sh.createRow(rownum); + for (int cellnum = 0; cellnum < 3; cellnum++) { + Cell cell = row.createCell(cellnum); + cell.setCellValue(rownum + cellnum); + } + } + Row row = sh.createRow(10); + // setting no precalculated value works just fine. + Cell cell1 = row.createCell(0); + cell1.setCellFormula("SUM(A1:A10)"); + + // but setting a precalculated STRING value fails totally in SXSSF + Cell cell2 = row.createCell(1); + cell2.setCellFormula("SUM(B1:B10)"); + cell2.setCellValue("55"); + + // setting a precalculated int value works as expected + Cell cell3 = row.createCell(2); + cell3.setCellFormula("SUM(C1:C10)"); + cell3.setCellValue(65); + + assertEquals(CellType.FORMULA, cell1.getCellTypeEnum()); + assertEquals(CellType.FORMULA, cell2.getCellTypeEnum()); + assertEquals(CellType.FORMULA, cell3.getCellTypeEnum()); + + assertEquals("SUM(A1:A10)", cell1.getCellFormula()); + assertEquals("SUM(B1:B10)", cell2.getCellFormula()); + assertEquals("SUM(C1:C10)", cell3.getCellFormula()); + + /*String name = wb.getClass().getCanonicalName(); + String ext = (wb instanceof HSSFWorkbook) ? ".xls" : ".xlsx"; + OutputStream output = new FileOutputStream("/tmp" + name + ext); + try { + wb.write(output); + } finally { + output.close(); + }*/ + + Workbook wbBack = _testDataProvider.writeOutAndReadBack(wb); + checkFormulaPreevaluatedString(wbBack); + wbBack.close(); + } finally { + wb.close(); + } + } + + private void checkFormulaPreevaluatedString(Workbook readFile) { + Sheet sheet = readFile.getSheetAt(0); + Row row = sheet.getRow(sheet.getLastRowNum()); + assertEquals(10, row.getRowNum()); + + for (Cell cell : row) { + String cellValue = null; + switch (cell.getCellTypeEnum()) { + case STRING: + cellValue = cell.getRichStringCellValue().getString(); + break; + case FORMULA: + cellValue = cell.getCellFormula(); + break; + } + assertNotNull(cellValue); + cellValue = cellValue.isEmpty() ? null : cellValue; + assertNotNull(cellValue); + } + } +} \ No newline at end of file diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestNamedRange.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestNamedRange.java index 75fb77e82..92e362df5 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestNamedRange.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestNamedRange.java @@ -201,11 +201,7 @@ public abstract class BaseTestNamedRange { assertEquals("The sheet already contains this name: aaa", e.getMessage()); } - int cnt = 0; - for (int i = 0; i < wb.getNumberOfNames(); i++) { - if("aaa".equals(wb.getNameAt(i).getNameName())) cnt++; - } - assertEquals(3, cnt); + assertEquals(3, wb.getNames("aaa").size()); wb.close(); } @@ -250,11 +246,11 @@ public abstract class BaseTestNamedRange { // Write the workbook to a file // Read the Excel file and verify its content Workbook wb2 = _testDataProvider.writeOutAndReadBack(wb1); - Name nm1 = wb2.getNameAt(wb2.getNameIndex("RangeTest1")); + Name nm1 = wb2.getName("RangeTest1"); assertTrue("Name is "+nm1.getNameName(),"RangeTest1".equals(nm1.getNameName())); assertTrue("Reference is "+nm1.getRefersToFormula(),(wb2.getSheetName(0)+"!$A$1:$L$41").equals(nm1.getRefersToFormula())); - Name nm2 = wb2.getNameAt(wb2.getNameIndex("RangeTest2")); + Name nm2 = wb2.getName("RangeTest2"); assertTrue("Name is "+nm2.getNameName(),"RangeTest2".equals(nm2.getNameName())); assertTrue("Reference is "+nm2.getRefersToFormula(),(wb2.getSheetName(1)+"!$A$1:$O$21").equals(nm2.getRefersToFormula())); @@ -466,11 +462,11 @@ public abstract class BaseTestNamedRange { wb1.getNameAt(0); Workbook wb2 = _testDataProvider.writeOutAndReadBack(wb1); - Name nm =wb2.getNameAt(wb2.getNameIndex("RangeTest")); + Name nm =wb2.getName("RangeTest"); assertTrue("Name is "+nm.getNameName(),"RangeTest".equals(nm.getNameName())); assertTrue("Reference is "+nm.getRefersToFormula(),(wb2.getSheetName(0)+"!$D$4:$E$8").equals(nm.getRefersToFormula())); - nm = wb2.getNameAt(wb2.getNameIndex("AnotherTest")); + nm = wb2.getName("AnotherTest"); assertTrue("Name is "+nm.getNameName(),"AnotherTest".equals(nm.getNameName())); assertTrue("Reference is "+nm.getRefersToFormula(),newNamedRange2.getRefersToFormula().equals(nm.getRefersToFormula())); @@ -499,8 +495,7 @@ public abstract class BaseTestNamedRange { namedCell.setRefersToFormula(reference); // retrieve the newly created named range - int namedCellIdx = wb.getNameIndex(cellName); - Name aNamedCell = wb.getNameAt(namedCellIdx); + Name aNamedCell = wb.getName(cellName); assertNotNull(aNamedCell); // retrieve the cell at the named range and test its contents @@ -540,8 +535,7 @@ public abstract class BaseTestNamedRange { namedCell.setRefersToFormula(reference); // retrieve the newly created named range - int namedCellIdx = wb.getNameIndex(cname); - Name aNamedCell = wb.getNameAt(namedCellIdx); + Name aNamedCell = wb.getName(cname); assertNotNull(aNamedCell); // retrieve the cell at the named range and test its contents diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java index 3f8febb11..efae0d80c 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java @@ -261,17 +261,17 @@ public abstract class BaseTestSheetShiftRows { name4.setSheetIndex(1); sheet1.shiftRows(0, 1, 2); //shift down the top row on Sheet1. - name1 = wb.getNameAt(0); + name1 = wb.getName("name1"); assertEquals("Sheet1!$A$3+Sheet1!$B$3", name1.getRefersToFormula()); - name2 = wb.getNameAt(1); + name2 = wb.getName("name2"); assertEquals("Sheet1!$A$3", name2.getRefersToFormula()); //name3 and name4 refer to Sheet2 and should not be affected - name3 = wb.getNameAt(2); + name3 = wb.getName("name3"); assertEquals("Sheet2!$A$1", name3.getRefersToFormula()); - name4 = wb.getNameAt(3); + name4 = wb.getName("name4"); assertEquals("A1", name4.getRefersToFormula()); wb.close(); diff --git a/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java b/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java index ed0c1da7e..a63ce5a60 100644 --- a/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java +++ b/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java @@ -78,14 +78,16 @@ public class BaseTestCellUtil { @Test(expected=RuntimeException.class) public void setCellStylePropertyWithInvalidValue() throws IOException { Workbook wb = _testDataProvider.createWorkbook(); - Sheet s = wb.createSheet(); - Row r = s.createRow(0); - Cell c = r.createCell(0); + try { + Sheet s = wb.createSheet(); + Row r = s.createRow(0); + Cell c = r.createCell(0); - // An invalid BorderStyle constant - CellUtil.setCellStyleProperty(c, CellUtil.BORDER_BOTTOM, 42); - - wb.close(); + // An invalid BorderStyle constant + CellUtil.setCellStyleProperty(c, CellUtil.BORDER_BOTTOM, 42); + } finally { + wb.close(); + } } @Test() @@ -352,10 +354,8 @@ public class BaseTestCellUtil { CellUtil.setFont(A1, font2); fail("setFont not allowed if font belongs to a different workbook"); } catch (final IllegalArgumentException e) { - if (e.getMessage().startsWith("Font does not belong to this workbook")) { - // expected - } - else { + // one specific message is expected + if (!e.getMessage().startsWith("Font does not belong to this workbook")) { throw e; } } finally { @@ -371,7 +371,7 @@ public class BaseTestCellUtil { */ // bug 55555 @Test - public void setFillForegroundColorBeforeFillBackgroundColor() { + public void setFillForegroundColorBeforeFillBackgroundColor() throws IOException { Workbook wb1 = _testDataProvider.createWorkbook(); Cell A1 = wb1.createSheet().createRow(0).createCell(0); Map properties = new HashMap(); @@ -386,13 +386,14 @@ public class BaseTestCellUtil { assertEquals("fill pattern", CellStyle.BRICKS, style.getFillPattern()); assertEquals("fill foreground color", IndexedColors.BLUE, IndexedColors.fromInt(style.getFillForegroundColor())); assertEquals("fill background color", IndexedColors.RED, IndexedColors.fromInt(style.getFillBackgroundColor())); + wb1.close(); } /** * bug 55555 * @since POI 3.15 beta 3 */ @Test - public void setFillForegroundColorBeforeFillBackgroundColorEnum() { + public void setFillForegroundColorBeforeFillBackgroundColorEnum() throws IOException { Workbook wb1 = _testDataProvider.createWorkbook(); Cell A1 = wb1.createSheet().createRow(0).createCell(0); Map properties = new HashMap(); @@ -407,5 +408,7 @@ public class BaseTestCellUtil { assertEquals("fill pattern", FillPatternType.BRICKS, style.getFillPatternEnum()); assertEquals("fill foreground color", IndexedColors.BLUE, IndexedColors.fromInt(style.getFillForegroundColor())); assertEquals("fill background color", IndexedColors.RED, IndexedColors.fromInt(style.getFillBackgroundColor())); + + wb1.close(); } } diff --git a/src/testcases/org/apache/poi/util/DummyPOILogger.java b/src/testcases/org/apache/poi/util/DummyPOILogger.java index 976c7d918..c8566fe66 100644 --- a/src/testcases/org/apache/poi/util/DummyPOILogger.java +++ b/src/testcases/org/apache/poi/util/DummyPOILogger.java @@ -30,16 +30,20 @@ public class DummyPOILogger extends POILogger { logged = new ArrayList(); } + @Override public boolean check(int level) { return true; } + @Override public void initialize(String cat) {} + @Override public void log(int level, Object obj1) { logged.add(level + " - " + obj1); } + @Override public void log(int level, Object obj1, Throwable exception) { logged.add(level + " - " + obj1 + " - " + exception); } diff --git a/src/testcases/org/apache/poi/util/TestPOILogger.java b/src/testcases/org/apache/poi/util/TestPOILogger.java index a53de44eb..3914b7643 100644 --- a/src/testcases/org/apache/poi/util/TestPOILogger.java +++ b/src/testcases/org/apache/poi/util/TestPOILogger.java @@ -26,10 +26,6 @@ import org.junit.Test; /** * Tests the log class. - * - * @author Glen Stampoultzis (glens at apache.org) - * @author Marc Johnson (mjohnson at apache dot org) - * @author Nicola Ken Barozzi (nicolaken at apache.org) */ public final class TestPOILogger extends POILogger { private String lastLog = ""; @@ -61,20 +57,26 @@ public final class TestPOILogger extends POILogger { POILogFactory._loggerClassName = oldLCN; } } + + // ---------- POI Logger methods implemented for testing ---------- + @Override public void initialize(String cat) { } + @Override public void log(int level, Object obj1) { lastLog = (obj1 == null) ? "" : obj1.toString(); lastEx = null; } + @Override public void log(int level, Object obj1, Throwable exception) { lastLog = (obj1 == null) ? "" : obj1.toString(); lastEx = exception; } + @Override public boolean check(int level) { return true; } diff --git a/test-data/spreadsheet/59736.xlsx b/test-data/spreadsheet/59736.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f29d54c502aa92dc8b17300df3f4dd455a23320 GIT binary patch literal 8752 zcmeHMbyQSq*B=F%Ms1cOpK#32P~qy?1j86+jY(f7Nb z_i}yLdjI`+$)qL2X402lxOfF2O1+7bjt0st~l0RUnE z2C|`?vy+FllZUDH6BlcDV{RWu2f88@WVQkTGGhKe$A9q(3_w5+k@*P1VJue>pemb& zXHk6NVZbd7(J%PryPR_)j@b`nPEUr?5`g;j@wP)su?(ZyWmgg5{{^c2r!e|1A_$W#RU3N6VqP*PQ(^)tV8EO6ySM)do zHMg-27_-06T8>q{0H2Oz5>UgZKoN7_uH_^kI6pY4tnjHdv zRZtlWR>jnp(*%g61p2JbJ1Gn&mh!r3j%&5?UljO7KRgn^9b)uM2BcF?H$>kRJ7Geh ze=H&xQ&}WN@B-TyPyD5?-FXi31a$31Rs1^y4zI6)0QJ8hv`&YY@d)8|Y6t*gBM@rp zX6@k4!~Ns_4>JFY;rORdk4+wa(8-I_15;|1TRGoyQo<-KZBbKdq1UE)&$LHd7gx+A zeR3QM3|6t68$DhhmNHz}TLWv;Qd#gk=xk<_sS&X7n|kb1k*|wiV8LJuCh4V!r!s#u zzu;i%Qv%7hm^v?i6B9$x47p&m5Tt%cg1x`vR+_}C(Qj@ynsU3``ZHuzBD|eOolATI z`WOS<5B;U&nfUiN4T2zNAD*=Ui}VMv(s{&UAH7!PY_MMp&y{D%epC**b&xYo*murg z3Cv@eHF|h>T`%Iwpc*xz^yNT&G1^^vdpn3b-|od}+mQHDS?E`g)u}nO8(-TL@!@|* zi9mnFZ6pMK27mwn1poud$ARZxK=F2Vv$t?|w*Rq{{Q(*z#CC?5``>-Era&JfoF*I= z(iSor;F|1Y9eYpWHI6U!Jdj*53PdfYw^!eKsfMvr-LO zBZkv-e8sUyzRdm+3liSc$b?*-w8$XSh&#?WT0>A0;qW_aW|OKNY~YDkX+62Uf~jkC zd1;{qBGa`y<3$%+O{cxg%a-@(#mf?s6JUbqfvzDF_`h`^O?~69rU<8SLD1}v83+gZ zv+KlaLVrN4jd34eV~^8#E{?AvHxy8VEoz+ksoRK7Ec;15${U)(D;G%~HF^p>i9cezEUfV#|7yr3!k8<9<$yrjwikZh66y z*$O#n)BNk?rTYw5ry3VBsV}3)Me>e6EJVkJ4h5KQsk%+8aH*w_dW28X3{P+k!DMYY z!m1MQfLJfK$hvt!X)ZeN&=XOo`E5&3FUuXdhMn;>xJN}9N4XBev8hc9 z_-IG=^EXzI%{s>Bc z+QP%m+R>Wl=bi5dly~$WI?vrDX~&=Trt@%iWUZ$}_l%yd$*urVb~#1RfHTwdw2Gw1 zlmjSdm!BbZHk+p-we0dbaEu_gc;vGfMpOXLZXGMBydnRnI)+9$IT0bx@YMf$ahcKc zXf`CWP>z9V)phQbaiwo$mn7}Dh@GEz_@yc(O)^%m<$#ac>_UzbYEDO5b}Qjf9`#Vfx5y;bowTM1-2i99Nh#z`r=FgN!XQ zT)bH@1~XyerCtm?fcp$EA{~A!E-8!EDmWl=i97y&u*D$2naC)kM(&*v#aL_NAu33F z?wl8zNQmuQmG|^|>$}$B*7uakx9qEgK$LmwH#Zq~J z2jBL|R(h%u3%8Z zF+2pqU71-KUEkg6B+#-&#S8Mn+|0wG{!$@oW8H}78t45;dY=twl0WZlMICDK+zxY{ z(tHJQB4SI;t%BHM{y&T?plX;48 zjQd7VH7hA$45)~Rx&2+UPj z?ojL$sh49uAf2dpzKkqbOwdK%03|XnGm6O6g!OaOA~XlY%cp~;(yaA@k>c8T#)f38 zeidHn(9q^M-PNCLdm>+B5_XQCwm~f(bAoBr{f+dfQnvlo$bzA1;iJ`W-BYUycf#2# zD2e9mvxmeCW_%v9SptP@SDp*DHH6~YeU6Z68=|_W97v!l>+>lS2Zzdtu3A3?g0N)pFN%P5xUa!m^Z_a_#P$7C4pWO zW^dpi#gJG=QcZo!k;WS7a#)N<63%``iR73s&w05`4y3z3nZW>`66b8kCwcjO&UTBhn=9}x-F^h|KcrP1LmFm2j z1?l^Ml&VnNbFGvatM47z6>5CKn2%k~&3E6GRBNFV(NEx|72@Iwq=o}xgjhMQ4S)l_ z8dwnR;#)Z4%7(fyGx@)rX(g3+|!3QYc^K$8azI! zY{)hDnx}Dm@OvI_ecBG^m1J4X!Po=@M#}>-I7i$yyDOJMn zb41X*A|QMaD_iAl5|v7)nU|Jzcf-*Z?`3TJP}7HTXbvr|;x&&=+Z5{3JD#2X*xrCw zja&10CQn6lWts6EPrH^WH<~d$rkTO8M@sz%Xsrw|3}UXdbVXr)r6R94;oh5%9VP4w zDVp;{-5FWp+3=>1-VTRy@}dZ8O^$d5Rgx>>=jcS5tN7NXUngH9*8cB#!hc{Dfq$&> z|814J@6XET5%wBJWN-flD?g(?NQ$ZRI4?=}V$vyITE6t`Qi*l!edXuULVTDg7SynK z_Y}ROe6yC_v)v|TNU*l_D*aen)TLPOde6-KTU{&sAys>~EO|a=v(SoCu=`nUQ+uFd z6#nN=M*2WeiQqEt?vG?M(B=vl09ht@UvyV?L#wBCL)x8oh6X1P{CZs6Y};VX551K} z7~D1I4w#NBa6*N?ShIz$ke9v!nNAU3IgfJaZ~e zFQG~l>870h>*drAcPUoq#*7(D_wXrAk!?p}hOU~vjqjO{9J&*=t?IgnacYRsK@UDM zaiv3U&14LFW1?Rhf{?3dWP&i%mowOIFIBys$HS(D>%_`Sag#?Hp)^I{7mx&}i$G~p zdf47Sz*0DVl{c~18x-JK5*`lIKDEK8m8mzDo}t7s`#@ijU=K$|f7UU&v}WZu2;||n z#Yc&V;tm*&FlAJ(xjGnPAnu&ym$^P?=$obiVfqueF>qZ))Jkh=iA%OE`p-b}O)4t; zS3G;No_>jV#(lS0yw@}}ru~lE_XjnHtzpBT>&ASF3U0q`DeVG%?IE0m9?KOqkb79m zRr~JUYZZAW+D(|{_!`$eL<&dDxO<89xAZwOpafAuQ0EQ`0D${1`gHer;$ZFm1Dz!h zP|O@Jq7)f+gc6#z$&q=DFRytp z`$?wK8bmGS0-bNNG-o=Pa4)SbL`Tw`+l*Z!^Adg%k}fVhyEO&^D0=9B%W2bTVp2se z5#+9OBVBFQzoc!04tzo zltjd$m81yJd%bH4gm~}N%fC#@^cYqME(8S-a3t$wLSq~u+wg#~f z_MOqmY9SGG(Q3`^TA?n-*$;X;Lmi-6XG07o+b36A9r1J!^*mu%kcE;_?cw|g|7<*d z{^WUhP+jw53fOCU$G_b&w0c4ALR~H{gK843@I9U?N0v8aAtPT&iiG+yrDG>%!k1nhm` z=F{nxutqO3-v!D>5g{)mAO?0>q`e{y{9&D7lq7v#LX=;4koS9iPycbgMKLhX;JY-m zvHcfk3Ve&`Md)NE`!)K#lyWZ9r<>{(k5dX(RPSx{1~so+vo3}iO|du>i2GcTHJ|JY z`XfuUWvm?JR0RrZeLMYpB=uX))HliN$U^+njJOC87ovt`Y3^VtXJ-y}`r*Lh@fuhi zyae*ss4#-#MW+fn22cz}o-{teSRE!)b~0{JgK+Sf0I#!|&Ds9#5MOoDe7(+kunggx z;FCO|&z0$eNDz|F6wD~)&KQ&)^R}%D%;TL{TOCS0!A8wA#v(n%QKb~prL50d@z1rh zZ6VAlnO%gbLmY7>@VXTW~~`=kGxp^%5HBJ_3kf z1pgEM8Hkq7Zr1-G5nn*#(>Vc^ej&vd#{@8uSWDaAA zR$k{P&v$iS9YA|ma?V$CRKKliOhQ@7IVf}rn4&cR(LzS$FQs|rbbKO^x67Amok&PF zP)NBx&SmF-J1Bq;pO2d504RNu--K==@8*u9SbjXFdlAm~ayVc@z1-;$j(Vp1{S2fn3av`~STxG5h7$RBR@sKG zEQ5yq^99T=CuOD!_Ve!N6hJDUFCj^VJqk~9CB&T`m8&f?VJ~y4Ej#kF9-u1Qz~&F6 z)>;ZW!^9&_S@{?bAYCz-`^aqW{zszyJcw2Nm-8VZa{>NcPyCOy{$u=2hg@CtuL}O! zvin2u$5@2eTK?3)yD9kBF2*I&Y^Sih~s z-c)ci$NHtffbjpG{Un$FKhZ|L@As literal 0 HcmV?d00001 From 5a486ec7c8e9d8f16408c5fc1ad19c93d587501a Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 8 Aug 2016 21:00:10 +0000 Subject: [PATCH 5/9] forgot cryptoapi encryption flags git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755541 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hssf/record/FilePassRecord.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index 9f43b94ef..344096e59 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -17,6 +17,7 @@ package org.apache.poi.hssf.record; +import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.poi.EncryptedDocumentException; @@ -31,6 +32,7 @@ import org.apache.poi.poifs.crypt.xor.XOREncryptionVerifier; import org.apache.poi.util.HexDump; import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.LittleEndianOutput; +import org.apache.poi.util.LittleEndianOutputStream; /** * Title: File Pass Record (0x002F)

@@ -42,12 +44,10 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { private static final int ENCRYPTION_XOR = 0; private static final int ENCRYPTION_OTHER = 1; - private int encryptionType; + private final int encryptionType; private EncryptionInfo encryptionInfo; - private int dataLength; private FilePassRecord(FilePassRecord other) { - dataLength = other.dataLength; encryptionType = other.encryptionType; try { encryptionInfo = other.encryptionInfo.clone(); @@ -57,7 +57,6 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { } public FilePassRecord(RecordInputStream in) { - dataLength = in.remaining(); encryptionType = in.readUShort(); EncryptionMode preferredMode; @@ -79,7 +78,8 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { } } - public void serialize(LittleEndianOutput out) { + @Override + public void serialize(LittleEndianOutput out) { out.writeShort(encryptionType); byte data[] = new byte[1024]; @@ -99,6 +99,7 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { case cryptoAPI: out.writeShort(encryptionInfo.getVersionMajor()); out.writeShort(encryptionInfo.getVersionMinor()); + out.writeInt(encryptionInfo.getEncryptionFlags()); ((CryptoAPIEncryptionHeader)encryptionInfo.getHeader()).write(bos); ((CryptoAPIEncryptionVerifier)encryptionInfo.getVerifier()).write(bos); break; @@ -109,14 +110,20 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { out.write(data, 0, bos.getWriteIndex()); } - protected int getDataSize() { - return dataLength; + @Override + @SuppressWarnings("resource") + protected int getDataSize() { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + LittleEndianOutputStream leos = new LittleEndianOutputStream(bos); + serialize(leos); + return bos.size(); } public EncryptionInfo getEncryptionInfo() { return encryptionInfo; } + @Override public short getSid() { return sid; } @@ -126,7 +133,8 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { return new FilePassRecord(this); } - public String toString() { + @Override + public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("[FILEPASS]\n"); From 952154615616326dbff2d1e5843bcd8309e76fbf Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 19 Aug 2016 20:23:16 +0000 Subject: [PATCH 6/9] add encryption support git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1756964 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hssf/model/InternalWorkbook.java | 59 ++++++- .../poi/hssf/record/BoundSheetRecord.java | 6 +- .../poi/hssf/record/FilePassRecord.java | 5 + .../hssf/record/RecordFactoryInputStream.java | 20 +-- .../poi/hssf/record/RecordInputStream.java | 19 ++- .../record/cont/ContinuableRecordInput.java | 18 ++- .../record/crypto/Biff8DecryptingStream.java | 17 +- .../poi/hssf/usermodel/HSSFWorkbook.java | 72 ++++++++- .../poifs/crypt/ChunkedCipherInputStream.java | 29 ++-- .../crypt/ChunkedCipherOutputStream.java | 79 +++++++-- .../poi/poifs/crypt/CryptoFunctions.java | 4 +- .../org/apache/poi/poifs/crypt/Encryptor.java | 18 ++- .../crypt/binaryrc4/BinaryRC4Encryptor.java | 32 +++- .../crypt/cryptoapi/CryptoAPIEncryptor.java | 5 +- .../poi/poifs/crypt/xor/XORDecryptor.java | 150 +++++++++--------- .../poifs/crypt/xor/XOREncryptionHeader.java | 2 +- .../crypt/xor/XOREncryptionVerifier.java | 10 ++ .../poi/poifs/crypt/xor/XOREncryptor.java | 117 ++++++++++++-- .../poifs/filesystem/DocumentInputStream.java | 5 + .../LittleEndianByteArrayInputStream.java | 5 + .../apache/poi/util/LittleEndianInput.java | 15 +- .../poi/util/LittleEndianInputStream.java | 35 ++-- .../usermodel/HSLFSlideShowEncrypted.java | 4 +- .../record/TestRecordFactoryInputStream.java | 5 +- .../poi/hssf/usermodel/TestCryptoAPI.java | 48 +++--- 25 files changed, 588 insertions(+), 191 deletions(-) diff --git a/src/java/org/apache/poi/hssf/model/InternalWorkbook.java b/src/java/org/apache/poi/hssf/model/InternalWorkbook.java index 84c6073c9..3a5fc1ef5 100644 --- a/src/java/org/apache/poi/hssf/model/InternalWorkbook.java +++ b/src/java/org/apache/poi/hssf/model/InternalWorkbook.java @@ -18,6 +18,7 @@ package org.apache.poi.hssf.model; import java.security.AccessControlException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; @@ -25,6 +26,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import javax.crypto.SecretKey; + +import org.apache.poi.EncryptedDocumentException; import org.apache.poi.ddf.EscherBSERecord; import org.apache.poi.ddf.EscherBoolProperty; import org.apache.poi.ddf.EscherContainerRecord; @@ -52,6 +56,7 @@ import org.apache.poi.hssf.record.EscherAggregate; import org.apache.poi.hssf.record.ExtSSTRecord; import org.apache.poi.hssf.record.ExtendedFormatRecord; import org.apache.poi.hssf.record.ExternSheetRecord; +import org.apache.poi.hssf.record.FilePassRecord; import org.apache.poi.hssf.record.FileSharingRecord; import org.apache.poi.hssf.record.FnGroupCountRecord; import org.apache.poi.hssf.record.FontRecord; @@ -82,8 +87,13 @@ import org.apache.poi.hssf.record.WindowProtectRecord; import org.apache.poi.hssf.record.WriteAccessRecord; import org.apache.poi.hssf.record.WriteProtectRecord; import org.apache.poi.hssf.record.common.UnicodeString; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.hssf.util.HSSFColor; import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.apache.poi.poifs.crypt.Encryptor; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalName; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheetRange; @@ -1082,10 +1092,8 @@ public final class InternalWorkbook { SSTRecord sst = null; int sstPos = 0; boolean wroteBoundSheets = false; - for ( int k = 0; k < records.size(); k++ ) - { + for ( Record record : records ) { - Record record = records.get( k ); int len = 0; if (record instanceof SSTRecord) { @@ -1124,6 +1132,8 @@ public final class InternalWorkbook { * Include in it ant code that modifies the workbook record stream and affects its size. */ public void preSerialize(){ + updateEncryptionRecord(); + // Ensure we have enough tab IDs // Can be a few short if new sheets were added if(records.getTabpos() > 0) { @@ -1134,6 +1144,49 @@ public final class InternalWorkbook { } } + private void updateEncryptionRecord() { + FilePassRecord fpr = null; + int fprPos = -1; + for (Record r : records.getRecords()) { + fprPos++; + if (r instanceof FilePassRecord) { + fpr = (FilePassRecord)r; + break; + } + } + + String password = Biff8EncryptionKey.getCurrentUserPassword(); + if (password == null) { + if (fpr != null) { + // need to remove password data + records.remove(fprPos); + } + return; + } else { + // create password record + if (fpr == null) { + fpr = new FilePassRecord(EncryptionMode.binaryRC4); + records.add(1, fpr); + } + + // check if the password has been changed + EncryptionInfo ei = fpr.getEncryptionInfo(); + byte encVer[] = ei.getVerifier().getEncryptedVerifier(); + try { + Decryptor dec = ei.getDecryptor(); + Encryptor enc = ei.getEncryptor(); + if (encVer == null || !dec.verifyPassword(password)) { + enc.confirmPassword(password); + } else { + SecretKey sk = dec.getSecretKey(); + ei.getEncryptor().setSecretKey(sk); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("can't validate/update encryption setting", e); + } + } + } + public int getSize() { int retval = 0; diff --git a/src/java/org/apache/poi/hssf/record/BoundSheetRecord.java b/src/java/org/apache/poi/hssf/record/BoundSheetRecord.java index 5aa756d32..61b92831c 100644 --- a/src/java/org/apache/poi/hssf/record/BoundSheetRecord.java +++ b/src/java/org/apache/poi/hssf/record/BoundSheetRecord.java @@ -24,6 +24,8 @@ import java.util.List; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianOutput; import org.apache.poi.util.StringUtil; import org.apache.poi.ss.util.WorkbookUtil; @@ -60,7 +62,9 @@ public final class BoundSheetRecord extends StandardRecord { * @param in the record stream to read from */ public BoundSheetRecord(RecordInputStream in) { - field_1_position_of_BOF = in.readInt(); + byte buf[] = new byte[LittleEndianConsts.INT_SIZE]; + in.readPlain(buf, 0, buf.length); + field_1_position_of_BOF = LittleEndian.getInt(buf); field_2_option_flags = in.readUShort(); int field_3_sheetname_length = in.readUByte(); field_4_isMultibyteUnicode = in.readByte(); diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index 344096e59..acb5c8da4 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -56,6 +56,11 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { } } + public FilePassRecord(EncryptionMode encryptionMode) { + encryptionType = (encryptionMode == EncryptionMode.xor) ? ENCRYPTION_XOR : ENCRYPTION_OTHER; + encryptionInfo = new EncryptionInfo(encryptionMode); + } + public FilePassRecord(RecordInputStream in) { encryptionType = in.readUShort(); diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index 6338f9a69..2c5ba94c0 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -78,20 +78,16 @@ public final class RecordFactoryInputStream { outputRecs.add(rec); } - // If it's a FILEPASS, track it specifically but - // don't include it in the main stream + // If it's a FILEPASS, track it specifically if (rec instanceof FilePassRecord) { fpr = (FilePassRecord) rec; - outputRecs.remove(outputRecs.size()-1); - // TODO - add fpr not added to outputRecs - rec = outputRecs.get(0); - } else { - // workbook not encrypted (typical case) - if (rec instanceof EOFRecord) { - // A workbook stream is never empty, so crash instead - // of trying to keep track of nesting level - throw new IllegalStateException("Nothing between BOF and EOF"); - } + } + + // workbook not encrypted (typical case) + if (rec instanceof EOFRecord) { + // A workbook stream is never empty, so crash instead + // of trying to keep track of nesting level + throw new IllegalStateException("Nothing between BOF and EOF"); } } } else { diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index 929f0f2bf..a3d84863e 100644 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -309,13 +309,22 @@ public final class RecordInputStream implements LittleEndianInput { } return result; } + + public void readPlain(byte[] buf, int off, int len) { + readFully(buf, 0, buf.length, true); + } + @Override public void readFully(byte[] buf) { - readFully(buf, 0, buf.length); + readFully(buf, 0, buf.length, false); } - @Override + @Override public void readFully(byte[] buf, int off, int len) { + readFully(buf, off, len, false); + } + + protected void readFully(byte[] buf, int off, int len, boolean isPlain) { int origLen = len; if (buf == null) { throw new NullPointerException(); @@ -335,7 +344,11 @@ public final class RecordInputStream implements LittleEndianInput { } } checkRecordPosition(nextChunk); - _dataInput.readFully(buf, off, nextChunk); + if (isPlain) { + _dataInput.readPlain(buf, off, nextChunk); + } else { + _dataInput.readFully(buf, off, nextChunk); + } _currentDataOffset+=nextChunk; off += nextChunk; len -= nextChunk; diff --git a/src/java/org/apache/poi/hssf/record/cont/ContinuableRecordInput.java b/src/java/org/apache/poi/hssf/record/cont/ContinuableRecordInput.java index 739aeacb8..ecd8d4b59 100644 --- a/src/java/org/apache/poi/hssf/record/cont/ContinuableRecordInput.java +++ b/src/java/org/apache/poi/hssf/record/cont/ContinuableRecordInput.java @@ -54,28 +54,34 @@ public class ContinuableRecordInput implements LittleEndianInput { public ContinuableRecordInput(RecordInputStream in){ _in = in; } + @Override public int available(){ return _in.available(); } + @Override public byte readByte(){ return _in.readByte(); } + @Override public int readUByte(){ return _in.readUByte(); } + @Override public short readShort(){ return _in.readShort(); } + @Override public int readUShort(){ int ch1 = readUByte(); int ch2 = readUByte(); return (ch2 << 8) + (ch1 << 0); } + @Override public int readInt(){ int ch1 = _in.readUByte(); int ch2 = _in.readUByte(); @@ -84,6 +90,7 @@ public class ContinuableRecordInput implements LittleEndianInput { return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0); } + @Override public long readLong(){ int b0 = _in.readUByte(); int b1 = _in.readUByte(); @@ -103,14 +110,23 @@ public class ContinuableRecordInput implements LittleEndianInput { (b0 << 0)); } + @Override public double readDouble(){ return _in.readDouble(); } + + @Override public void readFully(byte[] buf){ _in.readFully(buf); } + + @Override public void readFully(byte[] buf, int off, int len){ _in.readFully(buf, off, len); } - + + @Override + public void readPlain(byte[] buf, int off, int len) { + readFully(buf, off, len); + } } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java index ef574beea..cef3102ff 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java @@ -17,7 +17,6 @@ package org.apache.poi.hssf.record.crypto; -import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; @@ -35,7 +34,7 @@ import org.apache.poi.util.LittleEndianInput; public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput { - private static final int RC4_REKEYING_INTERVAL = 1024; + public static final int RC4_REKEYING_INTERVAL = 1024; private final EncryptionInfo info; private ChunkedCipherInputStream ccis; @@ -180,7 +179,7 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia * * @return true if record type specified by sid is never encrypted */ - private static boolean isNeverEncryptedRecord(int sid) { + public static boolean isNeverEncryptedRecord(int sid) { switch (sid) { case BOFRecord.sid: // sheet BOFs for sure @@ -204,15 +203,9 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia } } - private void readPlain(byte b[], int off, int len) { - try { - int readBytes = ccis.readPlain(b, off, len); - if (readBytes < len) { - throw new RecordFormatException("buffer underrun"); - } - } catch (IOException e) { - throw new RecordFormatException(e); - } + @Override + public void readPlain(byte b[], int off, int len) { + ccis.readPlain(b, off, len); } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index ba015a9f1..2800904af 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -23,6 +23,7 @@ import static org.apache.poi.hssf.model.InternalWorkbook.WORKBOOK_DIR_ENTRY_NAME import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -61,8 +62,10 @@ import org.apache.poi.hssf.model.InternalWorkbook; import org.apache.poi.hssf.model.RecordStream; import org.apache.poi.hssf.record.AbstractEscherHolderRecord; import org.apache.poi.hssf.record.BackupRecord; +import org.apache.poi.hssf.record.BoundSheetRecord; import org.apache.poi.hssf.record.DrawingGroupRecord; import org.apache.poi.hssf.record.ExtendedFormatRecord; +import org.apache.poi.hssf.record.FilePassRecord; import org.apache.poi.hssf.record.FontRecord; import org.apache.poi.hssf.record.LabelRecord; import org.apache.poi.hssf.record.LabelSSTRecord; @@ -74,8 +77,11 @@ import org.apache.poi.hssf.record.SSTRecord; import org.apache.poi.hssf.record.UnknownRecord; import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor; import org.apache.poi.hssf.record.common.UnicodeString; +import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.Encryptor; import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentNode; @@ -99,8 +105,11 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.WorkbookUtil; import org.apache.poi.util.Configurator; import org.apache.poi.util.HexDump; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianByteArrayInputStream; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -1443,7 +1452,7 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss if (log.check( POILogger.DEBUG )) { log.log(DEBUG, "HSSFWorkbook.getBytes()"); } - + HSSFSheet[] sheets = getSheets(); int nSheets = sheets.length; @@ -1485,9 +1494,70 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss } pos += serializedSize; } + + encryptBytes(retval); + return retval; } + @SuppressWarnings("resource") + protected void encryptBytes(byte buf[]) { + int initialOffset = 0; + FilePassRecord fpr = null; + for (Record r : workbook.getRecords()) { + initialOffset += r.getRecordSize(); + if (r instanceof FilePassRecord) { + fpr = (FilePassRecord)r; + break; + } + } + if (fpr == null) { + return; + } + + LittleEndianByteArrayInputStream plain = new LittleEndianByteArrayInputStream(buf, 0); + LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0); + Encryptor enc = fpr.getEncryptionInfo().getEncryptor(); + enc.setChunkSize(Biff8DecryptingStream.RC4_REKEYING_INTERVAL); + byte tmp[] = new byte[1024]; + try { + ChunkedCipherOutputStream os = enc.getDataStream(leos, initialOffset); + int totalBytes = 0; + while (totalBytes < buf.length) { + plain.read(tmp, 0, 4); + final int sid = LittleEndian.getUShort(tmp, 0); + final int len = LittleEndian.getUShort(tmp, 2); + boolean isPlain = Biff8DecryptingStream.isNeverEncryptedRecord(sid); + os.setNextRecordSize(len, isPlain); + os.writePlain(tmp, 0, 4); + if (sid == BoundSheetRecord.sid) { + // special case for the field_1_position_of_BOF (=lbPlyPos) field of + // the BoundSheet8 record which must be unencrypted + byte bsrBuf[] = new byte[len]; + plain.readFully(bsrBuf); + os.writePlain(bsrBuf, 0, 4); + os.write(bsrBuf, 4, len-4); + } else { + int todo = len; + while (todo > 0) { + int nextLen = Math.min(todo, tmp.length); + plain.readFully(tmp, 0, nextLen); + if (isPlain) { + os.writePlain(tmp, 0, nextLen); + } else { + os.write(tmp, 0, nextLen); + } + todo -= nextLen; + } + } + totalBytes += 4 + len; + } + os.close(); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + } + /*package*/ InternalWorkbook getWorkbook() { return workbook; } diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java index 7b5632dea..e59ceb369 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java @@ -222,24 +222,33 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { /** * Used when BIFF header fields (sid, size) are being read. The internal * {@link Cipher} instance must step even when unencrypted bytes are read + * */ - public int readPlain(byte b[], int off, int len) throws IOException { + @Override + public void readPlain(byte b[], int off, int len) { if (len <= 0) { - return len; + return; } - int readBytes, total = 0; - do { - readBytes = read(b, off, len, true); - total += Math.max(0, readBytes); - } while (readBytes > -1 && total < len); - - return total; + try { + int readBytes, total = 0; + do { + readBytes = read(b, off, len, true); + total += Math.max(0, readBytes); + } while (readBytes > -1 && total < len); + + if (total < len) { + throw new EOFException("buffer underrun"); + } + } catch (IOException e) { + // need to wrap checked exception, because of LittleEndianInput interface :( + throw new RuntimeException(e); + } } /** * Some ciphers (actually just XOR) are based on the record size, - * which needs to be set before encryption + * which needs to be set before decryption * * @param recordSize the size of the next record */ diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java index 08ca4c3be..b734dc3fb 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java @@ -25,6 +25,7 @@ import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; +import java.util.BitSet; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -48,14 +49,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { private static final POILogger LOG = POILogFactory.getLogger(ChunkedCipherOutputStream.class); private static final int STREAMING = -1; - protected final int _chunkSize; - protected final int _chunkBits; + private final int _chunkSize; + private final int _chunkBits; private final byte[] _chunk; + private final BitSet _plainByteFlags; private final File _fileOut; private final DirectoryNode _dir; private long _pos = 0; + private long _totalPos = 0; + private long _written = 0; private Cipher _cipher; public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException { @@ -63,6 +67,7 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { this._chunkSize = chunkSize; int cs = chunkSize == STREAMING ? 4096 : chunkSize; _chunk = new byte[cs]; + _plainByteFlags = new BitSet(cs); _chunkBits = Integer.bitCount(cs-1); _fileOut = TempFile.createTempFile("encrypted_package", "crypt"); _fileOut.deleteOnExit(); @@ -76,6 +81,7 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { this._chunkSize = chunkSize; int cs = chunkSize == STREAMING ? 4096 : chunkSize; _chunk = new byte[cs]; + _plainByteFlags = new BitSet(cs); _chunkBits = Integer.bitCount(cs-1); _fileOut = null; _dir = null; @@ -106,8 +112,15 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { } @Override - public void write(byte[] b, int off, int len) - throws IOException { + public void write(byte[] b, int off, int len) throws IOException { + write(b, off, len, false); + } + + public void writePlain(byte[] b, int off, int len) throws IOException { + write(b, off, len, true); + } + + protected void write(byte[] b, int off, int len, boolean writePlain) throws IOException { if (len == 0) { return; } @@ -121,7 +134,11 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { int posInChunk = (int)(_pos & chunkMask); int nextLen = Math.min(_chunk.length-posInChunk, len); System.arraycopy(b, off, _chunk, posInChunk, nextLen); + if (writePlain) { + _plainByteFlags.set(posInChunk, posInChunk+nextLen); + } _pos += nextLen; + _totalPos += nextLen; off += nextLen; len -= nextLen; if ((_pos & chunkMask) == 0) { @@ -130,12 +147,12 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { } } - private int getChunkMask() { + protected int getChunkMask() { return _chunk.length-1; } protected void writeChunk(boolean continued) throws IOException { - if (_pos == 0) { + if (_pos == 0 || _totalPos == _written) { return; } @@ -157,14 +174,18 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { int ciLen; try { boolean doFinal = true; + long oldPos = _pos; + // reset stream (not only) in case we were interrupted by plain stream parts + // this also needs to be set to prevent an endless loop + _pos = 0; if (_chunkSize == STREAMING) { if (continued) { doFinal = false; } - // reset stream (not only) in case we were interrupted by plain stream parts - _pos = 0; } else { _cipher = initCipherForBlock(_cipher, index, lastChunk); + // restore pos - only streaming chunks will be reset + _pos = oldPos; } ciLen = invokeCipher(posInChunk, doFinal); } catch (GeneralSecurityException e) { @@ -172,6 +193,8 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { } out.write(_chunk, 0, ciLen); + _plainByteFlags.clear(); + _written += ciLen; } /** @@ -184,11 +207,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { * @throws ShortBufferException */ protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException { - if (doFinal) { - return _cipher.doFinal(_chunk, 0, posInChunk, _chunk); - } else { - return _cipher.update(_chunk, 0, posInChunk, _chunk); + byte plain[] = (_plainByteFlags.isEmpty()) ? null : _chunk.clone(); + + int ciLen = (doFinal) + ? _cipher.doFinal(_chunk, 0, posInChunk, _chunk) + : _cipher.update(_chunk, 0, posInChunk, _chunk); + + for (int i = _plainByteFlags.nextSetBit(0); i >= 0 && i < posInChunk; i = _plainByteFlags.nextSetBit(i+1)) { + _chunk[i] = plain[i]; } + + return ciLen; } @Override @@ -208,7 +237,33 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { throw new IOException(e); } } + + protected byte[] getChunk() { + return _chunk; + } + protected BitSet getPlainByteFlags() { + return _plainByteFlags; + } + + protected long getPos() { + return _pos; + } + + protected long getTotalPos() { + return _totalPos; + } + + /** + * Some ciphers (actually just XOR) are based on the record size, + * which needs to be set before encryption + * + * @param recordSize the size of the next record + * @param isPlain {@code true} if the record is unencrypted + */ + public void setNextRecordSize(int recordSize, boolean isPlain) { + } + private class EncryptedPackageWriter implements POIFSWriterListener { @Override public void processPOIFSWriterEvent(POIFSWriterEvent event) { diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index 69f8d6768..b35d2c59c 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -498,7 +498,9 @@ public class CryptoFunctions { * @return the byte array for xor obfuscation */ public static byte[] createXorArray1(String password) { - if (password.length() > 15) password = password.substring(0, 15); + if (password.length() > 15) { + password = password.substring(0, 15); + } byte passBytes[] = password.getBytes(Charset.forName("ASCII")); // this code is based on the libre office implementation. diff --git a/src/java/org/apache/poi/poifs/crypt/Encryptor.java b/src/java/org/apache/poi/poifs/crypt/Encryptor.java index d92fc3097..546c96628 100644 --- a/src/java/org/apache/poi/poifs/crypt/Encryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Encryptor.java @@ -61,11 +61,16 @@ public abstract class Encryptor implements Cloneable { return getDataStream(fs.getRoot()); } + public ChunkedCipherOutputStream getDataStream(OutputStream stream, int initialOffset) + throws IOException, GeneralSecurityException { + throw new RuntimeException("this decryptor doesn't support writing directly to a stream"); + } + public SecretKey getSecretKey() { return secretKey; } - protected void setSecretKey(SecretKey secretKey) { + public void setSecretKey(SecretKey secretKey) { this.secretKey = secretKey; } @@ -77,6 +82,17 @@ public abstract class Encryptor implements Cloneable { this.encryptionInfo = encryptionInfo; } + /** + * Sets the chunk size of the data stream. + * Needs to be set before the data stream is requested. + * When not set, the implementation uses method specific default values + * + * @param chunkSize the chunk size, i.e. the block size with the same encryption key + */ + public void setChunkSize(int chunkSize) { + throw new RuntimeException("this decryptor doesn't support changing the chunk size"); + } + @Override public Encryptor clone() throws CloneNotSupportedException { Encryptor other = (Encryptor)super.clone(); diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java index ef49c9dc7..9545cbab0 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java @@ -41,6 +41,8 @@ import org.apache.poi.util.LittleEndianByteArrayOutputStream; public class BinaryRC4Encryptor extends Encryptor implements Cloneable { + private int _chunkSize = 512; + protected BinaryRC4Encryptor() { } @@ -84,6 +86,12 @@ public class BinaryRC4Encryptor extends Encryptor implements Cloneable { return countStream; } + @Override + public BinaryRC4CipherOutputStream getDataStream(OutputStream stream, int initialOffset) + throws IOException, GeneralSecurityException { + return new BinaryRC4CipherOutputStream(stream); + } + protected int getKeySizeInBytes() { return getEncryptionInfo().getHeader().getKeySize() / 8; } @@ -105,6 +113,11 @@ public class BinaryRC4Encryptor extends Encryptor implements Cloneable { DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); } + @Override + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + @Override public BinaryRC4Encryptor clone() throws CloneNotSupportedException { return (BinaryRC4Encryptor)super.clone(); @@ -112,12 +125,22 @@ public class BinaryRC4Encryptor extends Encryptor implements Cloneable { protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream { + public BinaryRC4CipherOutputStream(OutputStream stream) + throws IOException, GeneralSecurityException { + super(stream, BinaryRC4Encryptor.this._chunkSize); + } + + public BinaryRC4CipherOutputStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + super(dir, BinaryRC4Encryptor.this._chunkSize); + } + @Override protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) throws GeneralSecurityException { return BinaryRC4Decryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); } - + @Override protected void calculateChecksum(File file, int i) { } @@ -128,9 +151,10 @@ public class BinaryRC4Encryptor extends Encryptor implements Cloneable { BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir); } - public BinaryRC4CipherOutputStream(DirectoryNode dir) - throws IOException, GeneralSecurityException { - super(dir, 512); + @Override + public void flush() throws IOException { + writeChunk(false); + super.flush(); } } } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java index 02d28761a..e15558405 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java @@ -111,7 +111,8 @@ public class CryptoAPIEncryptor extends Encryptor implements Cloneable { throw new IOException("not supported"); } - public CryptoAPICipherOutputStream getDataStream(OutputStream stream) + @Override + public CryptoAPICipherOutputStream getDataStream(OutputStream stream, int initialOffset) throws IOException, GeneralSecurityException { return new CryptoAPICipherOutputStream(stream); } @@ -212,6 +213,7 @@ public class CryptoAPIEncryptor extends Encryptor implements Cloneable { return getEncryptionInfo().getHeader().getKeySize() / 8; } + @Override public void setChunkSize(int chunkSize) { _chunkSize = chunkSize; } @@ -268,6 +270,7 @@ public class CryptoAPIEncryptor extends Encryptor implements Cloneable { @Override public void flush() throws IOException { writeChunk(false); + super.flush(); } } diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java index cb50b4782..01725f48b 100644 --- a/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java @@ -36,80 +36,6 @@ public class XORDecryptor extends Decryptor implements Cloneable { private long _length = -1L; private int _chunkSize = 512; - private class XORCipherInputStream extends ChunkedCipherInputStream { - private final int _initialOffset; - private int _recordStart = 0; - private int _recordEnd = 0; - - @Override - protected Cipher initCipherForBlock(Cipher existing, int block) - throws GeneralSecurityException { - return XORDecryptor.this.initCipherForBlock(existing, block); - } - - public XORCipherInputStream(InputStream stream, int initialPos) - throws GeneralSecurityException { - super(stream, Integer.MAX_VALUE, _chunkSize); - _initialOffset = initialPos; - } - - @Override - protected int invokeCipher(int totalBytes, boolean doFinal) { - final int pos = (int)getPos(); - final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded(); - final byte chunk[] = getChunk(); - final byte plain[] = getPlain(); - final int posInChunk = pos & getChunkMask(); - - /* - * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 - * - * The initial value for XorArrayIndex is as follows: - * XorArrayIndex = (FileOffset + Data.Length) % 16 - * - * The FileOffset variable in this context is the stream offset into the Workbook stream at - * the time we are about to write each of the bytes of the record data. - * This (the value) is then incremented after each byte is written. - */ - final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart); - - for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) { - // The following is taken from the Libre Office implementation - // It seems that the encrypt and decrypt method is mixed up - // in the MS-OFFCRYPTO docs - byte value = plain[posInChunk+i]; - value = rotateLeft(value, 3); - value ^= xorArray[(xorArrayIndex+i) & 0x0F]; - chunk[posInChunk+i] = value; - } - - // the other bytes will be encoded, when setNextRecordSize is called the next time - return totalBytes; - } - - private byte rotateLeft(byte bits, int shift) { - return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); - } - - - /** - * Decrypts a xor obfuscated byte array. - * The data is decrypted in-place - * - * @see 2.3.7.3 Binary Document XOR Data Transformation Method 1 - */ - @Override - public void setNextRecordSize(int recordSize) { - _recordStart = (int)getPos(); - _recordEnd = _recordStart+recordSize; - int pos = (int)getPos(); - byte chunk[] = getChunk(); - int chunkMask = getChunkMask(); - int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask)); - invokeCipher(nextBytes, true); - } - } - protected XORDecryptor() { } @@ -166,9 +92,83 @@ public class XORDecryptor extends Decryptor implements Cloneable { public void setChunkSize(int chunkSize) { _chunkSize = chunkSize; } - + @Override public XORDecryptor clone() throws CloneNotSupportedException { return (XORDecryptor)super.clone(); } + + private class XORCipherInputStream extends ChunkedCipherInputStream { + private final int _initialOffset; + private int _recordStart = 0; + private int _recordEnd = 0; + + public XORCipherInputStream(InputStream stream, int initialPos) + throws GeneralSecurityException { + super(stream, Integer.MAX_VALUE, _chunkSize); + _initialOffset = initialPos; + } + + @Override + protected Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException { + return XORDecryptor.this.initCipherForBlock(existing, block); + } + + @Override + protected int invokeCipher(int totalBytes, boolean doFinal) { + final int pos = (int)getPos(); + final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded(); + final byte chunk[] = getChunk(); + final byte plain[] = getPlain(); + final int posInChunk = pos & getChunkMask(); + + /* + * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 + * + * The initial value for XorArrayIndex is as follows: + * XorArrayIndex = (FileOffset + Data.Length) % 16 + * + * The FileOffset variable in this context is the stream offset into the Workbook stream at + * the time we are about to write each of the bytes of the record data. + * This (the value) is then incremented after each byte is written. + */ + final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart); + + for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) { + // The following is taken from the Libre Office implementation + // It seems that the encrypt and decrypt method is mixed up + // in the MS-OFFCRYPTO docs + byte value = plain[posInChunk+i]; + value = rotateLeft(value, 3); + value ^= xorArray[(xorArrayIndex+i) & 0x0F]; + chunk[posInChunk+i] = value; + } + + // the other bytes will be encoded, when setNextRecordSize is called the next time + return totalBytes; + } + + private byte rotateLeft(byte bits, int shift) { + return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); + } + + + /** + * Decrypts a xor obfuscated byte array. + * The data is decrypted in-place + * + * @see 2.3.7.3 Binary Document XOR Data Transformation Method 1 + */ + @Override + public void setNextRecordSize(int recordSize) { + final int pos = (int)getPos(); + final byte chunk[] = getChunk(); + final int chunkMask = getChunkMask(); + _recordStart = pos; + _recordEnd = _recordStart+recordSize; + int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask)); + invokeCipher(nextBytes, true); + } + } } diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java index cc5068f6b..873c1abde 100644 --- a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java @@ -27,7 +27,7 @@ public class XOREncryptionHeader extends EncryptionHeader implements EncryptionR } @Override - public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) { + public void write(LittleEndianByteArrayOutputStream leos) { } @Override diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java index 1dcfb941c..388e3d872 100644 --- a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java @@ -58,4 +58,14 @@ public class XOREncryptionVerifier extends EncryptionVerifier implements Encrypt public XOREncryptionVerifier clone() throws CloneNotSupportedException { return (XOREncryptionVerifier)super.clone(); } + + @Override + protected void setEncryptedVerifier(byte[] encryptedVerifier) { + super.setEncryptedVerifier(encryptedVerifier); + } + + @Override + protected void setEncryptedKey(byte[] encryptedKey) { + super.setEncryptedKey(encryptedKey); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java index 054b5e0e4..4a0b48801 100644 --- a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java @@ -21,37 +21,41 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Random; +import java.util.BitSet; import javax.crypto.Cipher; -import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; -import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; import org.apache.poi.poifs.crypt.CryptoFunctions; -import org.apache.poi.poifs.crypt.DataSpaceMapUtils; -import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.Encryptor; -import org.apache.poi.poifs.crypt.HashAlgorithm; -import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.poifs.filesystem.DirectoryNode; -import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndian; public class XOREncryptor extends Encryptor implements Cloneable { - protected XOREncryptor() { } @Override public void confirmPassword(String password) { + int keyComp = CryptoFunctions.createXorKey1(password); + int verifierComp = CryptoFunctions.createXorVerifier1(password); + byte xorArray[] = CryptoFunctions.createXorArray1(password); + + byte shortBuf[] = new byte[2]; + XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier(); + LittleEndian.putUShort(shortBuf, 0, keyComp); + ver.setEncryptedKey(shortBuf); + LittleEndian.putUShort(shortBuf, 0, verifierComp); + ver.setEncryptedVerifier(shortBuf); + setSecretKey(new SecretKeySpec(xorArray, "XOR")); } @Override public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { + confirmPassword(password); } @Override @@ -61,10 +65,21 @@ public class XOREncryptor extends Encryptor implements Cloneable { return countStream; } + @Override + public XORCipherOutputStream getDataStream(OutputStream stream, int initialOffset) + throws IOException, GeneralSecurityException { + return new XORCipherOutputStream(stream, initialOffset); + } + protected int getKeySizeInBytes() { return -1; } + @Override + public void setChunkSize(int chunkSize) { + // chunkSize is irrelevant + } + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { } @@ -73,7 +88,21 @@ public class XOREncryptor extends Encryptor implements Cloneable { return (XOREncryptor)super.clone(); } - protected class XORCipherOutputStream extends ChunkedCipherOutputStream { + private class XORCipherOutputStream extends ChunkedCipherOutputStream { + private final int _initialOffset; + private int _recordStart = 0; + private int _recordEnd = 0; + private boolean _isPlain = false; + + public XORCipherOutputStream(OutputStream stream, int initialPos) throws IOException, GeneralSecurityException { + super(stream, -1); + _initialOffset = initialPos; + } + + public XORCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + super(dir, -1); + _initialOffset = 0; + } @Override protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) @@ -91,9 +120,67 @@ public class XOREncryptor extends Encryptor implements Cloneable { XOREncryptor.this.createEncryptionInfoEntry(dir); } - public XORCipherOutputStream(DirectoryNode dir) - throws IOException, GeneralSecurityException { - super(dir, 512); + @Override + public void setNextRecordSize(int recordSize, boolean isPlain) { + if (_recordEnd > 0 && !_isPlain) { + // encrypt last record + invokeCipher((int)getPos(), true); + } + _recordStart = (int)getTotalPos()+4; + _recordEnd = _recordStart+recordSize; + _isPlain = isPlain; } + + @Override + public void flush() throws IOException { + setNextRecordSize(0, true); + super.flush(); + } + + @Override + protected int invokeCipher(int posInChunk, boolean doFinal) { + if (posInChunk == 0) { + return 0; + } + + final int start = Math.max(posInChunk-(_recordEnd-_recordStart), 0); + + final BitSet plainBytes = getPlainByteFlags(); + final byte xorArray[] = getEncryptionInfo().getEncryptor().getSecretKey().getEncoded(); + final byte chunk[] = getChunk(); + final byte plain[] = (plainBytes.isEmpty()) ? null : chunk.clone(); + + /* + * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 + * + * The initial value for XorArrayIndex is as follows: + * XorArrayIndex = (FileOffset + Data.Length) % 16 + * + * The FileOffset variable in this context is the stream offset into the Workbook stream at + * the time we are about to write each of the bytes of the record data. + * This (the value) is then incremented after each byte is written. + */ + // ... also need to handle invocation in case of a filled chunk + int xorArrayIndex = _recordEnd+(start-_recordStart); + + for (int i=start; i < posInChunk; i++) { + byte value = chunk[i]; + value ^= xorArray[(xorArrayIndex++) & 0x0F]; + value = rotateLeft(value, 8-3); + chunk[i] = value; + } + + for (int i = plainBytes.nextSetBit(start); i >= 0 && i < posInChunk; i = plainBytes.nextSetBit(i+1)) { + chunk[i] = plain[i]; + } + + return posInChunk; + } + + private byte rotateLeft(byte bits, int shift) { + return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); + } + + } } diff --git a/src/java/org/apache/poi/poifs/filesystem/DocumentInputStream.java b/src/java/org/apache/poi/poifs/filesystem/DocumentInputStream.java index 672a4aa11..ffd6a1a85 100644 --- a/src/java/org/apache/poi/poifs/filesystem/DocumentInputStream.java +++ b/src/java/org/apache/poi/poifs/filesystem/DocumentInputStream.java @@ -189,4 +189,9 @@ public class DocumentInputStream extends InputStream implements LittleEndianInpu int i = readInt(); return i & 0xFFFFFFFFL; } + + @Override + public void readPlain(byte[] buf, int off, int len) { + readFully(buf, off, len); + } } diff --git a/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java b/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java index 4594343bf..2c5fe70b7 100644 --- a/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java +++ b/src/java/org/apache/poi/util/LittleEndianByteArrayInputStream.java @@ -104,4 +104,9 @@ public final class LittleEndianByteArrayInputStream extends ByteArrayInputStream checkPosition(buffer.length); read(buffer, 0, buffer.length); } + + @Override + public void readPlain(byte[] buf, int off, int len) { + readFully(buf, off, len); + } } diff --git a/src/java/org/apache/poi/util/LittleEndianInput.java b/src/java/org/apache/poi/util/LittleEndianInput.java index d8db24797..c140c275d 100644 --- a/src/java/org/apache/poi/util/LittleEndianInput.java +++ b/src/java/org/apache/poi/util/LittleEndianInput.java @@ -16,10 +16,7 @@ ==================================================================== */ package org.apache.poi.util; -/** - * - * @author Josh Micich - */ + public interface LittleEndianInput { int available(); byte readByte(); @@ -31,4 +28,14 @@ public interface LittleEndianInput { double readDouble(); void readFully(byte[] buf); void readFully(byte[] buf, int off, int len); + + /** + * Usually acts the same as {@link #readFully(byte[], int, int)}, but + * for an encrypted stream the raw (unencrypted) data is filled + * + * @param buf the byte array to receive the bytes + * @param off the start offset into the byte array + * @param len the amount of bytes to fill + */ + void readPlain(byte[] buf, int off, int len); } diff --git a/src/java/org/apache/poi/util/LittleEndianInputStream.java b/src/java/org/apache/poi/util/LittleEndianInputStream.java index da42caf3d..3062c50ff 100644 --- a/src/java/org/apache/poi/util/LittleEndianInputStream.java +++ b/src/java/org/apache/poi/util/LittleEndianInputStream.java @@ -34,7 +34,8 @@ public class LittleEndianInputStream extends FilterInputStream implements Little super(is); } - public int available() { + @Override + public int available() { try { return super.available(); } catch (IOException e) { @@ -42,11 +43,13 @@ public class LittleEndianInputStream extends FilterInputStream implements Little } } - public byte readByte() { + @Override + public byte readByte() { return (byte)readUByte(); } - public int readUByte() { + @Override + public int readUByte() { byte buf[] = new byte[1]; try { checkEOF(read(buf), 1); @@ -56,11 +59,13 @@ public class LittleEndianInputStream extends FilterInputStream implements Little return LittleEndian.getUByte(buf); } - public double readDouble() { + @Override + public double readDouble() { return Double.longBitsToDouble(readLong()); } - public int readInt() { + @Override + public int readInt() { byte buf[] = new byte[LittleEndianConsts.INT_SIZE]; try { checkEOF(read(buf), buf.length); @@ -82,7 +87,8 @@ public class LittleEndianInputStream extends FilterInputStream implements Little return retNum & 0x00FFFFFFFFL; } - public long readLong() { + @Override + public long readLong() { byte buf[] = new byte[LittleEndianConsts.LONG_SIZE]; try { checkEOF(read(buf), LittleEndianConsts.LONG_SIZE); @@ -92,11 +98,13 @@ public class LittleEndianInputStream extends FilterInputStream implements Little return LittleEndian.getLong(buf); } - public short readShort() { + @Override + public short readShort() { return (short)readUShort(); } - public int readUShort() { + @Override + public int readUShort() { byte buf[] = new byte[LittleEndianConsts.SHORT_SIZE]; try { checkEOF(read(buf), LittleEndianConsts.SHORT_SIZE); @@ -112,15 +120,22 @@ public class LittleEndianInputStream extends FilterInputStream implements Little } } - public void readFully(byte[] buf) { + @Override + public void readFully(byte[] buf) { readFully(buf, 0, buf.length); } - public void readFully(byte[] buf, int off, int len) { + @Override + public void readFully(byte[] buf, int off, int len) { try { checkEOF(read(buf, off, len), len); } catch (IOException e) { throw new RuntimeException(e); } } + + @Override + public void readPlain(byte[] buf, int off, int len) { + readFully(buf, off, len); + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java index ece68eef7..c2dd42098 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShowEncrypted.java @@ -169,7 +169,7 @@ public class HSLFSlideShowEncrypted implements Closeable { if (cyos == null) { enc.setChunkSize(-1); - cyos = enc.getDataStream(plainStream); + cyos = enc.getDataStream(plainStream, 0); } cyos.initCipherForBlock(persistId, false); } catch (Exception e) { @@ -314,7 +314,7 @@ public class HSLFSlideShowEncrypted implements Closeable { try { enc.setChunkSize(-1); - ccos = enc.getDataStream(los); + ccos = enc.getDataStream(los, 0); int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset)); int recType = LittleEndian.getUShort(pictstream, offset+2); final int rlen = (int)LittleEndian.getUInt(pictstream, offset+4); diff --git a/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java index 0e441e195..4cfdfcf24 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java +++ b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java @@ -151,11 +151,12 @@ public final class TestRecordFactoryInputStream { /** - * makes sure the record stream starts with {@link BOFRecord} and then {@link WindowOneRecord} - * The second record is gets decrypted so this method also checks its content. + * makes sure the record stream starts with {@link BOFRecord}, {@link FilePassRecord} and then {@link WindowOneRecord} + * The third record is decrypted so this method also checks its content. */ private void confirmReadInitialRecords(RecordFactoryInputStream rfis) { assertEquals(BOFRecord.class, rfis.nextRecord().getClass()); + FilePassRecord recFP = (FilePassRecord) rfis.nextRecord(); WindowOneRecord rec1 = (WindowOneRecord) rfis.nextRecord(); assertArrayEquals(HexRead.readFromString(SAMPLE_WINDOW1),rec1.serialize()); } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java index e7618073b..3baafa8b3 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java @@ -17,8 +17,7 @@ package org.apache.poi.hssf.usermodel; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.apache.poi.POITestCase.assertContains; import java.io.IOException; @@ -38,25 +37,34 @@ public class TestCryptoAPI { @Test public void bug59857() throws IOException { - Biff8EncryptionKey.setCurrentUserPassword("abc"); - HSSFWorkbook wb1 = ssTests.openSampleWorkbook("xor-encryption-abc.xls"); - String textExpected = "Sheet1\n1\n2\n3\n"; - String textActual = new ExcelExtractor(wb1).getText(); - assertEquals(textExpected, textActual); - wb1.close(); + // XOR-Obfuscation + // TODO: XOR-Obfuscation is currently flawed - although the de-/obfuscation initially works, + // it suddenly differs from the result of encrypted files via Office ... + // and only very small files can be opened without file validation errors + validateContent("xor-encryption-abc.xls", "abc", "Sheet1\n1\n2\n3\n"); - Biff8EncryptionKey.setCurrentUserPassword("password"); - HSSFWorkbook wb2 = ssTests.openSampleWorkbook("password.xls"); - textExpected = "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed."; - textActual = new ExcelExtractor(wb2).getText(); - assertTrue(textActual.contains(textExpected)); - wb2.close(); + // BinaryRC4 + validateContent("password.xls", "password", "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed."); - Biff8EncryptionKey.setCurrentUserPassword("freedom"); - HSSFWorkbook wb3 = ssTests.openSampleWorkbook("35897-type4.xls"); - textExpected = "Sheet1\nhello there!\n"; - textActual = new ExcelExtractor(wb3).getText(); - assertEquals(textExpected, textActual); - wb3.close(); + // CryptoAPI + validateContent("35897-type4.xls", "freedom", "Sheet1\nhello there!\n"); + } + + private void validateContent(String wbFile, String password, String textExpected) throws IOException { + Biff8EncryptionKey.setCurrentUserPassword(password); + HSSFWorkbook wb = ssTests.openSampleWorkbook(wbFile); + ExcelExtractor ee1 = new ExcelExtractor(wb); + String textActual = ee1.getText(); + assertContains(textActual, textExpected); + + Biff8EncryptionKey.setCurrentUserPassword("bla"); + HSSFWorkbook wbBla = ssTests.writeOutAndReadBack(wb); + ExcelExtractor ee2 = new ExcelExtractor(wbBla); + textActual = ee2.getText(); + assertContains(textActual, textExpected); + ee2.close(); + ee1.close(); + wbBla.close(); + wb.close(); } } From 71db60b890282623048f7453ca367826d2b9c72a Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 19 Aug 2016 20:57:56 +0000 Subject: [PATCH 7/9] merge trunk to branch git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1756967 13f79535-47bb-0310-9956-ffa450edef68 --- build.xml | 15 +- sonar/examples/pom.xml | 2 +- sonar/excelant/pom.xml | 2 +- sonar/main/pom.xml | 2 +- sonar/ooxml-schema-encryption/pom.xml | 2 +- sonar/ooxml-schema-security/pom.xml | 2 +- sonar/ooxml-schema/pom.xml | 2 +- sonar/ooxml/pom.xml | 2 +- sonar/pom.xml | 2 +- sonar/scratchpad/pom.xml | 2 +- .../poi/xslf/usermodel/XSLFTextParagraph.java | 2150 +++++++++-------- .../TestSXSSFSheetAutoSizeColumn.java | 2 +- .../hslf/extractor/PowerPointExtractor.java | 41 +- .../apache/poi/hslf/model/HeadersFooters.java | 4 +- .../src/org/apache/poi/hwpf/HWPFDocument.java | 84 +- .../hwpf/sprm/SectionSprmUncompressor.java | 98 +- .../poi/hslf/extractor/TestExtractor.java | 4 +- .../poi/hwpf/usermodel/TestHWPFWrite.java | 67 +- src/testcases/org/apache/poi/POITestCase.java | 16 + .../org/apache/poi/TestPOITestCase.java | 126 + 20 files changed, 1415 insertions(+), 1210 deletions(-) create mode 100644 src/testcases/org/apache/poi/TestPOITestCase.java diff --git a/build.xml b/build.xml index 2eddf722f..f40ff4c77 100644 --- a/build.xml +++ b/build.xml @@ -40,7 +40,7 @@ under the License. The Apache POI project Ant build. - + @@ -283,13 +283,10 @@ under the License. NOTE: we did not update to 3.x yet because it requires Java 7, but we are still supporting Java 6 currently =========================================================================================================== --> - - + + - - - - + @@ -441,7 +438,7 @@ under the License. This is POI ${version.id} - Java Version ${ant.java.version} + Java Version ${ant.java.version}/${java.version} Timestamp ${DSTAMP} The main targets of interest are: - clean Erase all build work products (ie. everything in the build directory) @@ -480,7 +477,9 @@ under the License. + + var rel = ("REL_"+project.getProperty("version.id")).toUpperCase().replace(/\W/g,"_"); diff --git a/sonar/examples/pom.xml b/sonar/examples/pom.xml index e0e6a24a1..cac1dc446 100644 --- a/sonar/examples/pom.xml +++ b/sonar/examples/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-examples jar diff --git a/sonar/excelant/pom.xml b/sonar/excelant/pom.xml index db1a4513d..2cbeec728 100644 --- a/sonar/excelant/pom.xml +++ b/sonar/excelant/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-excelant jar diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index ffc9499d9..26a8f2ba4 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-main jar diff --git a/sonar/ooxml-schema-encryption/pom.xml b/sonar/ooxml-schema-encryption/pom.xml index 1366a2bad..d704a6525 100644 --- a/sonar/ooxml-schema-encryption/pom.xml +++ b/sonar/ooxml-schema-encryption/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema-encryption diff --git a/sonar/ooxml-schema-security/pom.xml b/sonar/ooxml-schema-security/pom.xml index 336bf88fb..7aae600f4 100644 --- a/sonar/ooxml-schema-security/pom.xml +++ b/sonar/ooxml-schema-security/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema-security diff --git a/sonar/ooxml-schema/pom.xml b/sonar/ooxml-schema/pom.xml index c6e7d7d85..e1d6526da 100644 --- a/sonar/ooxml-schema/pom.xml +++ b/sonar/ooxml-schema/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema diff --git a/sonar/ooxml/pom.xml b/sonar/ooxml/pom.xml index 3a0079cf6..305695ff1 100644 --- a/sonar/ooxml/pom.xml +++ b/sonar/ooxml/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-ooxml jar diff --git a/sonar/pom.xml b/sonar/pom.xml index 508a02343..5acb3a5da 100644 --- a/sonar/pom.xml +++ b/sonar/pom.xml @@ -3,7 +3,7 @@ org.apache.poi poi-parent pom - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT Apache POI - the Java API for Microsoft Documents Maven build of Apache POI for Sonar checks http://poi.apache.org/ diff --git a/sonar/scratchpad/pom.xml b/sonar/scratchpad/pom.xml index 790299cbe..91ede69a1 100644 --- a/sonar/scratchpad/pom.xml +++ b/sonar/scratchpad/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta3-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-scratchpad jar diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java index edd17dec5..38c298275 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java @@ -1,1071 +1,1079 @@ -/* ==================================================================== - 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.xslf.usermodel; - -import java.awt.Color; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.apache.poi.sl.draw.DrawPaint; -import org.apache.poi.sl.usermodel.AutoNumberingScheme; -import org.apache.poi.sl.usermodel.PaintStyle; -import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint; -import org.apache.poi.sl.usermodel.TextParagraph; -import org.apache.poi.util.Beta; -import org.apache.poi.util.Internal; -import org.apache.poi.util.Units; -import org.apache.poi.xslf.model.ParagraphPropertyFetcher; -import org.apache.xmlbeans.XmlCursor; -import org.apache.xmlbeans.XmlObject; -import org.openxmlformats.schemas.drawingml.x2006.main.CTColor; -import org.openxmlformats.schemas.drawingml.x2006.main.CTRegularTextRun; -import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextAutonumberBullet; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBulletSizePercent; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBulletSizePoint; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharBullet; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextField; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextFont; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextLineBreak; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextNormalAutofit; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextSpacing; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextTabStop; -import org.openxmlformats.schemas.drawingml.x2006.main.CTTextTabStopList; -import org.openxmlformats.schemas.drawingml.x2006.main.STTextAlignType; -import org.openxmlformats.schemas.drawingml.x2006.main.STTextAutonumberScheme; -import org.openxmlformats.schemas.drawingml.x2006.main.STTextFontAlignType; -import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; -import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; - -/** - * Represents a paragraph of text within the containing text body. - * The paragraph is the highest level text separation mechanism. - * - * @since POI-3.8 - */ -@Beta -public class XSLFTextParagraph implements TextParagraph { - private final CTTextParagraph _p; - private final List _runs; - private final XSLFTextShape _shape; - - XSLFTextParagraph(CTTextParagraph p, XSLFTextShape shape){ - _p = p; - _runs = new ArrayList(); - _shape = shape; - - for(XmlObject ch : _p.selectPath("*")){ - if(ch instanceof CTRegularTextRun){ - CTRegularTextRun r = (CTRegularTextRun)ch; - _runs.add(newTextRun(r)); - } else if (ch instanceof CTTextLineBreak){ - CTTextLineBreak br = (CTTextLineBreak)ch; - CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); - r.setRPr(br.getRPr()); - r.setT("\n"); - _runs.add(newTextRun(r)); - } else if (ch instanceof CTTextField){ - CTTextField f = (CTTextField)ch; - CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); - r.setRPr(f.getRPr()); - r.setT(f.getT()); - _runs.add(newTextRun(r)); - } - } - } - - public String getText(){ - StringBuilder out = new StringBuilder(); - for (XSLFTextRun r : _runs) { - out.append(r.getRawText()); - } - return out.toString(); - } - - String getRenderableText(){ - StringBuilder out = new StringBuilder(); - for (XSLFTextRun r : _runs) { - out.append(r.getRenderableText()); - } - return out.toString(); - } - - @Internal - public CTTextParagraph getXmlObject(){ - return _p; - } - - public XSLFTextShape getParentShape() { - return _shape; - - } - - @Override - public List getTextRuns(){ - return _runs; - } - - public Iterator iterator(){ - return _runs.iterator(); - } - - /** - * Add a new run of text - * - * @return a new run of text - */ - public XSLFTextRun addNewTextRun(){ - CTRegularTextRun r = _p.addNewR(); - CTTextCharacterProperties rPr = r.addNewRPr(); - rPr.setLang("en-US"); - XSLFTextRun run = newTextRun(r); - _runs.add(run); - return run; - } - - /** - * Insert a line break - * - * @return text run representing this line break ('\n') - */ - public XSLFTextRun addLineBreak(){ - CTTextLineBreak br = _p.addNewBr(); - CTTextCharacterProperties brProps = br.addNewRPr(); - if(_runs.size() > 0){ - // by default line break has the font size of the last text run - CTTextCharacterProperties prevRun = _runs.get(_runs.size() - 1).getRPr(true); - brProps.set(prevRun); - } - CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); - r.setRPr(brProps); - r.setT("\n"); - XSLFTextRun run = new XSLFLineBreak(r, this, brProps); - _runs.add(run); - return run; - } - - @Override - public TextAlign getTextAlign(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetAlgn()){ - TextAlign val = TextAlign.values()[props.getAlgn().intValue() - 1]; - setValue(val); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - @Override - public void setTextAlign(TextAlign align) { - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(align == null) { - if(pr.isSetAlgn()) pr.unsetAlgn(); - } else { - pr.setAlgn(STTextAlignType.Enum.forInt(align.ordinal() + 1)); - } - } - - @Override - public FontAlign getFontAlign(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetFontAlgn()){ - FontAlign val = FontAlign.values()[props.getFontAlgn().intValue() - 1]; - setValue(val); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - /** - * Specifies the font alignment that is to be applied to the paragraph. - * Possible values for this include auto, top, center, baseline and bottom. - * see {@link org.apache.poi.sl.usermodel.TextParagraph.FontAlign}. - * - * @param align font align - */ - public void setFontAlign(FontAlign align){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(align == null) { - if(pr.isSetFontAlgn()) pr.unsetFontAlgn(); - } else { - pr.setFontAlgn(STTextFontAlignType.Enum.forInt(align.ordinal() + 1)); - } - } - - - - /** - * @return the font to be used on bullet characters within a given paragraph - */ - public String getBulletFont(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetBuFont()){ - setValue(props.getBuFont().getTypeface()); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - public void setBulletFont(String typeface){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextFont font = pr.isSetBuFont() ? pr.getBuFont() : pr.addNewBuFont(); - font.setTypeface(typeface); - } - - /** - * @return the character to be used in place of the standard bullet point - */ - public String getBulletCharacter(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetBuChar()){ - setValue(props.getBuChar().getChar()); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - public void setBulletCharacter(String str){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextCharBullet c = pr.isSetBuChar() ? pr.getBuChar() : pr.addNewBuChar(); - c.setChar(str); - } - - /** - * - * @return the color of bullet characters within a given paragraph. - * A null value means to use the text font color. - */ - public PaintStyle getBulletFontColor(){ - final XSLFTheme theme = getParentShape().getSheet().getTheme(); - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetBuClr()){ - XSLFColor c = new XSLFColor(props.getBuClr(), theme, null); - setValue(c.getColor()); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - Color col = fetcher.getValue(); - return (col == null) ? null : DrawPaint.createSolidPaint(col); - } - - public void setBulletFontColor(Color color) { - setBulletFontColor(DrawPaint.createSolidPaint(color)); - } - - - /** - * Set the color to be used on bullet characters within a given paragraph. - * - * @param color the bullet color - */ - public void setBulletFontColor(PaintStyle color) { - if (!(color instanceof SolidPaint)) { - throw new IllegalArgumentException("Currently XSLF only supports SolidPaint"); - } - - // TODO: implement setting bullet color to null - SolidPaint sp = (SolidPaint)color; - Color col = DrawPaint.applyColorTransform(sp.getSolidColor()); - - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTColor c = pr.isSetBuClr() ? pr.getBuClr() : pr.addNewBuClr(); - CTSRgbColor clr = c.isSetSrgbClr() ? c.getSrgbClr() : c.addNewSrgbClr(); - clr.setVal(new byte[]{(byte) col.getRed(), (byte) col.getGreen(), (byte) col.getBlue()}); - } - - /** - * Returns the bullet size that is to be used within a paragraph. - * This may be specified in two different ways, percentage spacing and font point spacing: - *

- * If bulletSize >= 0, then bulletSize is a percentage of the font size. - * If bulletSize < 0, then it specifies the size in points - *

- * - * @return the bullet size - */ - public Double getBulletFontSize(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetBuSzPct()){ - setValue(props.getBuSzPct().getVal() * 0.001); - return true; - } - if(props.isSetBuSzPts()){ - setValue( - props.getBuSzPts().getVal() * 0.01); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - /** - * Sets the bullet size that is to be used within a paragraph. - * This may be specified in two different ways, percentage spacing and font point spacing: - *

- * If bulletSize >= 0, then bulletSize is a percentage of the font size. - * If bulletSize < 0, then it specifies the size in points - *

- */ - public void setBulletFontSize(double bulletSize){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - - if(bulletSize >= 0) { - CTTextBulletSizePercent pt = pr.isSetBuSzPct() ? pr.getBuSzPct() : pr.addNewBuSzPct(); - pt.setVal((int)(bulletSize*1000)); - if(pr.isSetBuSzPts()) pr.unsetBuSzPts(); - } else { - CTTextBulletSizePoint pt = pr.isSetBuSzPts() ? pr.getBuSzPts() : pr.addNewBuSzPts(); - pt.setVal((int)(-bulletSize*100)); - if(pr.isSetBuSzPct()) pr.unsetBuSzPct(); - } - } - - /** - * @return the auto numbering scheme, or null if not defined - */ - public AutoNumberingScheme getAutoNumberingScheme() { - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { - public boolean fetch(CTTextParagraphProperties props) { - if (props.isSetBuAutoNum()) { - AutoNumberingScheme ans = AutoNumberingScheme.forOoxmlID(props.getBuAutoNum().getType().intValue()); - if (ans != null) { - setValue(ans); - return true; - } - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - /** - * @return the auto numbering starting number, or null if not defined - */ - public Integer getAutoNumberingStartAt() { - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { - public boolean fetch(CTTextParagraphProperties props) { - if (props.isSetBuAutoNum()) { - if (props.getBuAutoNum().isSetStartAt()) { - setValue(props.getBuAutoNum().getStartAt()); - return true; - } - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - - @Override - public void setIndent(Double indent){ - if ((indent == null) && !_p.isSetPPr()) return; - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(indent == null) { - if(pr.isSetIndent()) pr.unsetIndent(); - } else { - pr.setIndent(Units.toEMU(indent)); - } - } - - @Override - public Double getIndent() { - - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetIndent()){ - setValue(Units.toPoints(props.getIndent())); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - - return fetcher.getValue(); - } - - @Override - public void setLeftMargin(Double leftMargin){ - if (leftMargin == null && !_p.isSetPPr()) return; - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if (leftMargin == null) { - if(pr.isSetMarL()) pr.unsetMarL(); - } else { - pr.setMarL(Units.toEMU(leftMargin)); - } - - } - - /** - * @return the left margin (in points) of the paragraph, null if unset - */ - @Override - public Double getLeftMargin(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetMarL()){ - double val = Units.toPoints(props.getMarL()); - setValue(val); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - // if the marL attribute is omitted, then a value of 347663 is implied - return fetcher.getValue(); - } - - @Override - public void setRightMargin(Double rightMargin){ - if (rightMargin == null && !_p.isSetPPr()) return; - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(rightMargin == null) { - if(pr.isSetMarR()) pr.unsetMarR(); - } else { - pr.setMarR(Units.toEMU(rightMargin)); - } - } - - /** - * - * @return the right margin of the paragraph, null if unset - */ - @Override - public Double getRightMargin(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetMarR()){ - double val = Units.toPoints(props.getMarR()); - setValue(val); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - @Override - public Double getDefaultTabSize(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetDefTabSz()){ - double val = Units.toPoints(props.getDefTabSz()); - setValue(val); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - public double getTabStop(final int idx){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetTabLst()){ - CTTextTabStopList tabStops = props.getTabLst(); - if(idx < tabStops.sizeOfTabArray() ) { - CTTextTabStop ts = tabStops.getTabArray(idx); - double val = Units.toPoints(ts.getPos()); - setValue(val); - return true; - } - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue() == null ? 0. : fetcher.getValue(); - } - - public void addTabStop(double value){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextTabStopList tabStops = pr.isSetTabLst() ? pr.getTabLst() : pr.addNewTabLst(); - tabStops.addNewTab().setPos(Units.toEMU(value)); - } - - @Override - public void setLineSpacing(Double lineSpacing){ - if (lineSpacing == null && !_p.isSetPPr()) return; - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(lineSpacing == null) { - if (pr.isSetLnSpc()) pr.unsetLnSpc(); - } else { - CTTextSpacing spc = (pr.isSetLnSpc()) ? pr.getLnSpc() : pr.addNewLnSpc(); - if (lineSpacing >= 0) { - (spc.isSetSpcPct() ? spc.getSpcPct() : spc.addNewSpcPct()).setVal((int)(lineSpacing*1000)); - if (spc.isSetSpcPts()) spc.unsetSpcPts(); - } else { - (spc.isSetSpcPts() ? spc.getSpcPts() : spc.addNewSpcPts()).setVal((int)(-lineSpacing*100)); - if (spc.isSetSpcPct()) spc.unsetSpcPct(); - } - } - } - - @Override - public Double getLineSpacing(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetLnSpc()){ - CTTextSpacing spc = props.getLnSpc(); - - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - - Double lnSpc = fetcher.getValue(); - if (lnSpc != null && lnSpc > 0) { - // check if the percentage value is scaled - CTTextNormalAutofit normAutofit = getParentShape().getTextBodyPr().getNormAutofit(); - if(normAutofit != null) { - double scale = 1 - (double)normAutofit.getLnSpcReduction() / 100000; - lnSpc *= scale; - } - } - - return lnSpc; - } - - @Override - public void setSpaceBefore(Double spaceBefore){ - if (spaceBefore == null && !_p.isSetPPr()) { - return; - } - - // unset the space before on null input - if (spaceBefore == null) { - if(_p.getPPr().isSetSpcBef()) { - _p.getPPr().unsetSpcBef(); - } - return; - } - - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextSpacing spc = CTTextSpacing.Factory.newInstance(); - - if(spaceBefore >= 0) { - spc.addNewSpcPct().setVal((int)(spaceBefore*1000)); - } else { - spc.addNewSpcPts().setVal((int)(-spaceBefore*100)); - } - pr.setSpcBef(spc); - } - - @Override - public Double getSpaceBefore(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetSpcBef()){ - CTTextSpacing spc = props.getSpcBef(); - - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - - return fetcher.getValue(); - } - - @Override - public void setSpaceAfter(Double spaceAfter){ - if (spaceAfter == null && !_p.isSetPPr()) { - return; - } - - // unset the space before on null input - if (spaceAfter == null) { - if(_p.getPPr().isSetSpcAft()) { - _p.getPPr().unsetSpcAft(); - } - return; - } - - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextSpacing spc = CTTextSpacing.Factory.newInstance(); - - if(spaceAfter >= 0) { - spc.addNewSpcPct().setVal((int)(spaceAfter*1000)); - } else { - spc.addNewSpcPts().setVal((int)(-spaceAfter*100)); - } - pr.setSpcAft(spc); - } - - @Override - public Double getSpaceAfter(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetSpcAft()){ - CTTextSpacing spc = props.getSpcAft(); - - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue(); - } - - @Override - public void setIndentLevel(int level){ - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - pr.setLvl(level); - } - - @Override - public int getIndentLevel() { - CTTextParagraphProperties pr = _p.getPPr(); - return (pr == null || !pr.isSetLvl()) ? 0 : pr.getLvl(); - } - - /** - * Returns whether this paragraph has bullets - */ - public boolean isBullet() { - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetBuNone()) { - setValue(false); - return true; - } - if(props.isSetBuFont() || props.isSetBuChar()){ - setValue(true); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - return fetcher.getValue() == null ? false : fetcher.getValue(); - } - - /** - * - * @param flag whether text in this paragraph has bullets - */ - public void setBullet(boolean flag) { - if(isBullet() == flag) return; - - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - if(flag) { - pr.addNewBuFont().setTypeface("Arial"); - pr.addNewBuChar().setChar("\u2022"); - } else { - if (pr.isSetBuFont()) pr.unsetBuFont(); - if (pr.isSetBuChar()) pr.unsetBuChar(); - if (pr.isSetBuAutoNum()) pr.unsetBuAutoNum(); - if (pr.isSetBuBlip()) pr.unsetBuBlip(); - if (pr.isSetBuClr()) pr.unsetBuClr(); - if (pr.isSetBuClrTx()) pr.unsetBuClrTx(); - if (pr.isSetBuFont()) pr.unsetBuFont(); - if (pr.isSetBuFontTx()) pr.unsetBuFontTx(); - if (pr.isSetBuSzPct()) pr.unsetBuSzPct(); - if (pr.isSetBuSzPts()) pr.unsetBuSzPts(); - if (pr.isSetBuSzTx()) pr.unsetBuSzTx(); - pr.addNewBuNone(); - } - } - - /** - * Specifies that automatic numbered bullet points should be applied to this paragraph - * - * @param scheme type of auto-numbering - * @param startAt the number that will start number for a given sequence of automatically - numbered bullets (1-based). - */ - public void setBulletAutoNumber(AutoNumberingScheme scheme, int startAt) { - if(startAt < 1) throw new IllegalArgumentException("Start Number must be greater or equal that 1") ; - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextAutonumberBullet lst = pr.isSetBuAutoNum() ? pr.getBuAutoNum() : pr.addNewBuAutoNum(); - lst.setType(STTextAutonumberScheme.Enum.forInt(scheme.ooxmlId)); - lst.setStartAt(startAt); - } - - @Override - public String toString(){ - return "[" + getClass() + "]" + getText(); - } - - - /* package */ CTTextParagraphProperties getDefaultMasterStyle(){ - CTPlaceholder ph = _shape.getCTPlaceholder(); - String defaultStyleSelector; - switch(ph == null ? -1 : ph.getType().intValue()) { - case STPlaceholderType.INT_TITLE: - case STPlaceholderType.INT_CTR_TITLE: - defaultStyleSelector = "titleStyle"; - break; - case -1: // no placeholder means plain text box - case STPlaceholderType.INT_FTR: - case STPlaceholderType.INT_SLD_NUM: - case STPlaceholderType.INT_DT: - defaultStyleSelector = "otherStyle"; - break; - default: - defaultStyleSelector = "bodyStyle"; - break; - } - int level = getIndentLevel(); - - // wind up and find the root master sheet which must be slide master - final String nsPML = "http://schemas.openxmlformats.org/presentationml/2006/main"; - final String nsDML = "http://schemas.openxmlformats.org/drawingml/2006/main"; - XSLFSheet masterSheet = _shape.getSheet(); - for (XSLFSheet m = masterSheet; m != null; m = (XSLFSheet)m.getMasterSheet()) { - masterSheet = m; - XmlObject xo = masterSheet.getXmlObject(); - XmlCursor cur = xo.newCursor(); - try { - cur.push(); - if ((cur.toChild(nsPML, "txStyles") && cur.toChild(nsPML, defaultStyleSelector)) || - (cur.pop() && cur.toChild(nsPML, "notesStyle"))) { - while (level >= 0) { - cur.push(); - if (cur.toChild(nsDML, "lvl" +(level+1)+ "pPr")) { - return (CTTextParagraphProperties)cur.getObject(); - } - cur.pop(); - level--; - } - } - } finally { - cur.dispose(); - } - } - - return null; - } - - private boolean fetchParagraphProperty(ParagraphPropertyFetcher visitor){ - boolean ok = false; - XSLFTextShape shape = getParentShape(); - XSLFSheet sheet = shape.getSheet(); - - if(_p.isSetPPr()) ok = visitor.fetch(_p.getPPr()); - if (ok) return true; - - ok = shape.fetchShapeProperty(visitor); - if (ok) return true; - - - CTPlaceholder ph = shape.getCTPlaceholder(); - if(ph == null){ - // if it is a plain text box then take defaults from presentation.xml - @SuppressWarnings("resource") - XMLSlideShow ppt = sheet.getSlideShow(); - CTTextParagraphProperties themeProps = ppt.getDefaultParagraphStyle(getIndentLevel()); - if (themeProps != null) ok = visitor.fetch(themeProps); - } - if (ok) return true; - - // defaults for placeholders are defined in the slide master - CTTextParagraphProperties defaultProps = getDefaultMasterStyle(); - // TODO: determine master shape - if(defaultProps != null) ok = visitor.fetch(defaultProps); - if (ok) return true; - - return false; - } - - void copy(XSLFTextParagraph other){ - if (other == this) return; - - CTTextParagraph thisP = getXmlObject(); - CTTextParagraph otherP = other.getXmlObject(); - - if (thisP.isSetPPr()) thisP.unsetPPr(); - if (thisP.isSetEndParaRPr()) thisP.unsetEndParaRPr(); - - _runs.clear(); - for (int i=thisP.sizeOfBrArray(); i>0; i--) { - thisP.removeBr(i-1); - } - for (int i=thisP.sizeOfRArray(); i>0; i--) { - thisP.removeR(i-1); - } - for (int i=thisP.sizeOfFldArray(); i>0; i--) { - thisP.removeFld(i-1); - } - - XmlCursor thisC = thisP.newCursor(); - thisC.toEndToken(); - XmlCursor otherC = otherP.newCursor(); - otherC.copyXmlContents(thisC); - otherC.dispose(); - thisC.dispose(); - - List otherRs = other.getTextRuns(); - int i=0; - for(CTRegularTextRun rtr : thisP.getRArray()) { - XSLFTextRun run = newTextRun(rtr); - run.copy(otherRs.get(i++)); - _runs.add(run); - } - - - // set properties again, in case we are based on a different - // template - TextAlign srcAlign = other.getTextAlign(); - if(srcAlign != getTextAlign()){ - setTextAlign(srcAlign); - } - - boolean isBullet = other.isBullet(); - if(isBullet != isBullet()){ - setBullet(isBullet); - if(isBullet) { - String buFont = other.getBulletFont(); - if(buFont != null && !buFont.equals(getBulletFont())){ - setBulletFont(buFont); - } - String buChar = other.getBulletCharacter(); - if(buChar != null && !buChar.equals(getBulletCharacter())){ - setBulletCharacter(buChar); - } - PaintStyle buColor = other.getBulletFontColor(); - if(buColor != null && !buColor.equals(getBulletFontColor())){ - setBulletFontColor(buColor); - } - Double buSize = other.getBulletFontSize(); - if(!doubleEquals(buSize, getBulletFontSize())){ - setBulletFontSize(buSize); - } - } - } - - Double leftMargin = other.getLeftMargin(); - if (!doubleEquals(leftMargin, getLeftMargin())){ - setLeftMargin(leftMargin); - } - - Double indent = other.getIndent(); - if (!doubleEquals(indent, getIndent())) { - setIndent(indent); - } - - Double spaceAfter = other.getSpaceAfter(); - if (!doubleEquals(spaceAfter, getSpaceAfter())) { - setSpaceAfter(spaceAfter); - } - - Double spaceBefore = other.getSpaceBefore(); - if (!doubleEquals(spaceBefore, getSpaceBefore())) { - setSpaceBefore(spaceBefore); - } - - Double lineSpacing = other.getLineSpacing(); - if (!doubleEquals(lineSpacing, getLineSpacing())) { - setLineSpacing(lineSpacing); - } - } - - private static boolean doubleEquals(Double d1, Double d2) { - return (d1 == d2 || (d1 != null && d1.equals(d2))); - } - - @Override - public Double getDefaultFontSize() { - CTTextCharacterProperties endPr = _p.getEndParaRPr(); - if (endPr == null || !endPr.isSetSz()) { - endPr = getDefaultMasterStyle().getDefRPr(); - } - return (endPr == null || !endPr.isSetSz()) ? 12 : (endPr.getSz() / 100.); - } - - @Override - public String getDefaultFontFamily() { - return (_runs.isEmpty() ? "Arial" : _runs.get(0).getFontFamily()); - } - - @Override - public BulletStyle getBulletStyle() { - if (!isBullet()) return null; - return new BulletStyle(){ - @Override - public String getBulletCharacter() { - return XSLFTextParagraph.this.getBulletCharacter(); - } - - @Override - public String getBulletFont() { - return XSLFTextParagraph.this.getBulletFont(); - } - - @Override - public Double getBulletFontSize() { - return XSLFTextParagraph.this.getBulletFontSize(); - } - - @Override - public PaintStyle getBulletFontColor() { - return XSLFTextParagraph.this.getBulletFontColor(); - } - - @Override - public void setBulletFontColor(Color color) { - setBulletFontColor(DrawPaint.createSolidPaint(color)); - } - - @Override - public void setBulletFontColor(PaintStyle color) { - XSLFTextParagraph.this.setBulletFontColor(color); - } - - @Override - public AutoNumberingScheme getAutoNumberingScheme() { - return XSLFTextParagraph.this.getAutoNumberingScheme(); - } - - @Override - public Integer getAutoNumberingStartAt() { - return XSLFTextParagraph.this.getAutoNumberingStartAt(); - } - - }; - } - - @Override - public void setBulletStyle(Object... styles) { - if (styles.length == 0) { - setBullet(false); - } else { - setBullet(true); - for (Object ostyle : styles) { - if (ostyle instanceof Number) { - setBulletFontSize(((Number)ostyle).doubleValue()); - } else if (ostyle instanceof Color) { - setBulletFontColor((Color)ostyle); - } else if (ostyle instanceof Character) { - setBulletCharacter(ostyle.toString()); - } else if (ostyle instanceof String) { - setBulletFont((String)ostyle); - } else if (ostyle instanceof AutoNumberingScheme) { - setBulletAutoNumber((AutoNumberingScheme)ostyle, 0); - } - } - } - } - - /** - * Helper method for appending text and keeping paragraph and character properties. - * The character properties are moved to the end paragraph marker - */ - /* package */ void clearButKeepProperties() { - CTTextParagraph thisP = getXmlObject(); - for (int i=thisP.sizeOfBrArray(); i>0; i--) { - thisP.removeBr(i-1); - } - for (int i=thisP.sizeOfFldArray(); i>0; i--) { - thisP.removeFld(i-1); - } - if (!_runs.isEmpty()) { - int size = _runs.size(); - XSLFTextRun lastRun = _runs.get(size-1); - CTTextCharacterProperties cpOther = lastRun.getRPr(false); - if (cpOther != null) { - if (thisP.isSetEndParaRPr()) { - thisP.unsetEndParaRPr(); - } - CTTextCharacterProperties cp = thisP.addNewEndParaRPr(); - cp.set(cpOther); - } - for (int i=size; i>0; i--) { - thisP.removeR(i-1); - } - _runs.clear(); - } - } - - @Override - public boolean isHeaderOrFooter() { - CTPlaceholder ph = _shape.getCTPlaceholder(); - int phId = (ph == null ? -1 : ph.getType().intValue()); - switch (phId) { - case STPlaceholderType.INT_SLD_NUM: - case STPlaceholderType.INT_DT: - case STPlaceholderType.INT_FTR: - case STPlaceholderType.INT_HDR: - return true; - default: - return false; - } - } - - /** - * Helper method to allow subclasses to provide their own text run - * - * @param r the xml reference - * - * @return a new text paragraph - * - * @since POI 3.15-beta2 - */ - protected XSLFTextRun newTextRun(CTRegularTextRun r) { - return new XSLFTextRun(r, this); - } -} \ No newline at end of file +/* ==================================================================== + 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.xslf.usermodel; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.poi.sl.draw.DrawPaint; +import org.apache.poi.sl.usermodel.AutoNumberingScheme; +import org.apache.poi.sl.usermodel.PaintStyle; +import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.util.Beta; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Units; +import org.apache.poi.xslf.model.ParagraphPropertyFetcher; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlObject; +import org.openxmlformats.schemas.drawingml.x2006.main.CTColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTRegularTextRun; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextAutonumberBullet; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBulletSizePercent; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBulletSizePoint; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharBullet; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextField; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextFont; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextLineBreak; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextNormalAutofit; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextSpacing; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextTabStop; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextTabStopList; +import org.openxmlformats.schemas.drawingml.x2006.main.STTextAlignType; +import org.openxmlformats.schemas.drawingml.x2006.main.STTextAutonumberScheme; +import org.openxmlformats.schemas.drawingml.x2006.main.STTextFontAlignType; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; + +/** + * Represents a paragraph of text within the containing text body. + * The paragraph is the highest level text separation mechanism. + * + * @since POI-3.8 + */ +@Beta +public class XSLFTextParagraph implements TextParagraph { + private final CTTextParagraph _p; + private final List _runs; + private final XSLFTextShape _shape; + + XSLFTextParagraph(CTTextParagraph p, XSLFTextShape shape){ + _p = p; + _runs = new ArrayList(); + _shape = shape; + + for(XmlObject ch : _p.selectPath("*")){ + if(ch instanceof CTRegularTextRun){ + CTRegularTextRun r = (CTRegularTextRun)ch; + _runs.add(newTextRun(r)); + } else if (ch instanceof CTTextLineBreak){ + CTTextLineBreak br = (CTTextLineBreak)ch; + CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); + r.setRPr(br.getRPr()); + r.setT("\n"); + _runs.add(newTextRun(r)); + } else if (ch instanceof CTTextField){ + CTTextField f = (CTTextField)ch; + CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); + r.setRPr(f.getRPr()); + r.setT(f.getT()); + _runs.add(newTextRun(r)); + } + } + } + + public String getText(){ + StringBuilder out = new StringBuilder(); + for (XSLFTextRun r : _runs) { + out.append(r.getRawText()); + } + return out.toString(); + } + + String getRenderableText(){ + StringBuilder out = new StringBuilder(); + for (XSLFTextRun r : _runs) { + out.append(r.getRenderableText()); + } + return out.toString(); + } + + @Internal + public CTTextParagraph getXmlObject(){ + return _p; + } + + public XSLFTextShape getParentShape() { + return _shape; + + } + + @Override + public List getTextRuns(){ + return _runs; + } + + public Iterator iterator(){ + return _runs.iterator(); + } + + /** + * Add a new run of text + * + * @return a new run of text + */ + public XSLFTextRun addNewTextRun(){ + CTRegularTextRun r = _p.addNewR(); + CTTextCharacterProperties rPr = r.addNewRPr(); + rPr.setLang("en-US"); + XSLFTextRun run = newTextRun(r); + _runs.add(run); + return run; + } + + /** + * Insert a line break + * + * @return text run representing this line break ('\n') + */ + public XSLFTextRun addLineBreak(){ + CTTextLineBreak br = _p.addNewBr(); + CTTextCharacterProperties brProps = br.addNewRPr(); + if(_runs.size() > 0){ + // by default line break has the font size of the last text run + CTTextCharacterProperties prevRun = _runs.get(_runs.size() - 1).getRPr(true); + brProps.set(prevRun); + } + CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); + r.setRPr(brProps); + r.setT("\n"); + XSLFTextRun run = new XSLFLineBreak(r, this, brProps); + _runs.add(run); + return run; + } + + @Override + public TextAlign getTextAlign(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetAlgn()){ + TextAlign val = TextAlign.values()[props.getAlgn().intValue() - 1]; + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + @Override + public void setTextAlign(TextAlign align) { + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(align == null) { + if(pr.isSetAlgn()) pr.unsetAlgn(); + } else { + pr.setAlgn(STTextAlignType.Enum.forInt(align.ordinal() + 1)); + } + } + + @Override + public FontAlign getFontAlign(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetFontAlgn()){ + FontAlign val = FontAlign.values()[props.getFontAlgn().intValue() - 1]; + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + /** + * Specifies the font alignment that is to be applied to the paragraph. + * Possible values for this include auto, top, center, baseline and bottom. + * see {@link org.apache.poi.sl.usermodel.TextParagraph.FontAlign}. + * + * @param align font align + */ + public void setFontAlign(FontAlign align){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(align == null) { + if(pr.isSetFontAlgn()) pr.unsetFontAlgn(); + } else { + pr.setFontAlgn(STTextFontAlignType.Enum.forInt(align.ordinal() + 1)); + } + } + + + + /** + * @return the font to be used on bullet characters within a given paragraph + */ + public String getBulletFont(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuFont()){ + setValue(props.getBuFont().getTypeface()); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + public void setBulletFont(String typeface){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextFont font = pr.isSetBuFont() ? pr.getBuFont() : pr.addNewBuFont(); + font.setTypeface(typeface); + } + + /** + * @return the character to be used in place of the standard bullet point + */ + public String getBulletCharacter(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuChar()){ + setValue(props.getBuChar().getChar()); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + public void setBulletCharacter(String str){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextCharBullet c = pr.isSetBuChar() ? pr.getBuChar() : pr.addNewBuChar(); + c.setChar(str); + } + + /** + * + * @return the color of bullet characters within a given paragraph. + * A null value means to use the text font color. + */ + public PaintStyle getBulletFontColor(){ + final XSLFTheme theme = getParentShape().getSheet().getTheme(); + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuClr()){ + XSLFColor c = new XSLFColor(props.getBuClr(), theme, null); + setValue(c.getColor()); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + Color col = fetcher.getValue(); + return (col == null) ? null : DrawPaint.createSolidPaint(col); + } + + public void setBulletFontColor(Color color) { + setBulletFontColor(DrawPaint.createSolidPaint(color)); + } + + + /** + * Set the color to be used on bullet characters within a given paragraph. + * + * @param color the bullet color + */ + public void setBulletFontColor(PaintStyle color) { + if (!(color instanceof SolidPaint)) { + throw new IllegalArgumentException("Currently XSLF only supports SolidPaint"); + } + + // TODO: implement setting bullet color to null + SolidPaint sp = (SolidPaint)color; + Color col = DrawPaint.applyColorTransform(sp.getSolidColor()); + + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTColor c = pr.isSetBuClr() ? pr.getBuClr() : pr.addNewBuClr(); + CTSRgbColor clr = c.isSetSrgbClr() ? c.getSrgbClr() : c.addNewSrgbClr(); + clr.setVal(new byte[]{(byte) col.getRed(), (byte) col.getGreen(), (byte) col.getBlue()}); + } + + /** + * Returns the bullet size that is to be used within a paragraph. + * This may be specified in two different ways, percentage spacing and font point spacing: + *

+ * If bulletSize >= 0, then bulletSize is a percentage of the font size. + * If bulletSize < 0, then it specifies the size in points + *

+ * + * @return the bullet size + */ + public Double getBulletFontSize(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuSzPct()){ + setValue(props.getBuSzPct().getVal() * 0.001); + return true; + } + if(props.isSetBuSzPts()){ + setValue( - props.getBuSzPts().getVal() * 0.01); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + /** + * Sets the bullet size that is to be used within a paragraph. + * This may be specified in two different ways, percentage spacing and font point spacing: + *

+ * If bulletSize >= 0, then bulletSize is a percentage of the font size. + * If bulletSize < 0, then it specifies the size in points + *

+ */ + public void setBulletFontSize(double bulletSize){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + + if(bulletSize >= 0) { + CTTextBulletSizePercent pt = pr.isSetBuSzPct() ? pr.getBuSzPct() : pr.addNewBuSzPct(); + pt.setVal((int)(bulletSize*1000)); + if(pr.isSetBuSzPts()) pr.unsetBuSzPts(); + } else { + CTTextBulletSizePoint pt = pr.isSetBuSzPts() ? pr.getBuSzPts() : pr.addNewBuSzPts(); + pt.setVal((int)(-bulletSize*100)); + if(pr.isSetBuSzPct()) pr.unsetBuSzPct(); + } + } + + /** + * @return the auto numbering scheme, or null if not defined + */ + public AutoNumberingScheme getAutoNumberingScheme() { + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { + public boolean fetch(CTTextParagraphProperties props) { + if (props.isSetBuAutoNum()) { + AutoNumberingScheme ans = AutoNumberingScheme.forOoxmlID(props.getBuAutoNum().getType().intValue()); + if (ans != null) { + setValue(ans); + return true; + } + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + /** + * @return the auto numbering starting number, or null if not defined + */ + public Integer getAutoNumberingStartAt() { + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { + public boolean fetch(CTTextParagraphProperties props) { + if (props.isSetBuAutoNum()) { + if (props.getBuAutoNum().isSetStartAt()) { + setValue(props.getBuAutoNum().getStartAt()); + return true; + } + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + + @Override + public void setIndent(Double indent){ + if ((indent == null) && !_p.isSetPPr()) return; + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(indent == null) { + if(pr.isSetIndent()) pr.unsetIndent(); + } else { + pr.setIndent(Units.toEMU(indent)); + } + } + + @Override + public Double getIndent() { + + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetIndent()){ + setValue(Units.toPoints(props.getIndent())); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + + return fetcher.getValue(); + } + + @Override + public void setLeftMargin(Double leftMargin){ + if (leftMargin == null && !_p.isSetPPr()) return; + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if (leftMargin == null) { + if(pr.isSetMarL()) pr.unsetMarL(); + } else { + pr.setMarL(Units.toEMU(leftMargin)); + } + + } + + /** + * @return the left margin (in points) of the paragraph, null if unset + */ + @Override + public Double getLeftMargin(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetMarL()){ + double val = Units.toPoints(props.getMarL()); + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + // if the marL attribute is omitted, then a value of 347663 is implied + return fetcher.getValue(); + } + + @Override + public void setRightMargin(Double rightMargin){ + if (rightMargin == null && !_p.isSetPPr()) return; + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(rightMargin == null) { + if(pr.isSetMarR()) pr.unsetMarR(); + } else { + pr.setMarR(Units.toEMU(rightMargin)); + } + } + + /** + * + * @return the right margin of the paragraph, null if unset + */ + @Override + public Double getRightMargin(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetMarR()){ + double val = Units.toPoints(props.getMarR()); + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + @Override + public Double getDefaultTabSize(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetDefTabSz()){ + double val = Units.toPoints(props.getDefTabSz()); + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + public double getTabStop(final int idx){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetTabLst()){ + CTTextTabStopList tabStops = props.getTabLst(); + if(idx < tabStops.sizeOfTabArray() ) { + CTTextTabStop ts = tabStops.getTabArray(idx); + double val = Units.toPoints(ts.getPos()); + setValue(val); + return true; + } + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 0. : fetcher.getValue(); + } + + public void addTabStop(double value){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextTabStopList tabStops = pr.isSetTabLst() ? pr.getTabLst() : pr.addNewTabLst(); + tabStops.addNewTab().setPos(Units.toEMU(value)); + } + + @Override + public void setLineSpacing(Double lineSpacing){ + if (lineSpacing == null && !_p.isSetPPr()) return; + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(lineSpacing == null) { + if (pr.isSetLnSpc()) pr.unsetLnSpc(); + } else { + CTTextSpacing spc = (pr.isSetLnSpc()) ? pr.getLnSpc() : pr.addNewLnSpc(); + if (lineSpacing >= 0) { + (spc.isSetSpcPct() ? spc.getSpcPct() : spc.addNewSpcPct()).setVal((int)(lineSpacing*1000)); + if (spc.isSetSpcPts()) spc.unsetSpcPts(); + } else { + (spc.isSetSpcPts() ? spc.getSpcPts() : spc.addNewSpcPts()).setVal((int)(-lineSpacing*100)); + if (spc.isSetSpcPct()) spc.unsetSpcPct(); + } + } + } + + @Override + public Double getLineSpacing(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetLnSpc()){ + CTTextSpacing spc = props.getLnSpc(); + + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + + Double lnSpc = fetcher.getValue(); + if (lnSpc != null && lnSpc > 0) { + // check if the percentage value is scaled + CTTextNormalAutofit normAutofit = getParentShape().getTextBodyPr().getNormAutofit(); + if(normAutofit != null) { + double scale = 1 - (double)normAutofit.getLnSpcReduction() / 100000; + lnSpc *= scale; + } + } + + return lnSpc; + } + + @Override + public void setSpaceBefore(Double spaceBefore){ + if (spaceBefore == null && !_p.isSetPPr()) { + return; + } + + // unset the space before on null input + if (spaceBefore == null) { + if(_p.getPPr().isSetSpcBef()) { + _p.getPPr().unsetSpcBef(); + } + return; + } + + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextSpacing spc = CTTextSpacing.Factory.newInstance(); + + if(spaceBefore >= 0) { + spc.addNewSpcPct().setVal((int)(spaceBefore*1000)); + } else { + spc.addNewSpcPts().setVal((int)(-spaceBefore*100)); + } + pr.setSpcBef(spc); + } + + @Override + public Double getSpaceBefore(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetSpcBef()){ + CTTextSpacing spc = props.getSpcBef(); + + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + + return fetcher.getValue(); + } + + @Override + public void setSpaceAfter(Double spaceAfter){ + if (spaceAfter == null && !_p.isSetPPr()) { + return; + } + + // unset the space before on null input + if (spaceAfter == null) { + if(_p.getPPr().isSetSpcAft()) { + _p.getPPr().unsetSpcAft(); + } + return; + } + + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextSpacing spc = CTTextSpacing.Factory.newInstance(); + + if(spaceAfter >= 0) { + spc.addNewSpcPct().setVal((int)(spaceAfter*1000)); + } else { + spc.addNewSpcPts().setVal((int)(-spaceAfter*100)); + } + pr.setSpcAft(spc); + } + + @Override + public Double getSpaceAfter(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetSpcAft()){ + CTTextSpacing spc = props.getSpcAft(); + + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + @Override + public void setIndentLevel(int level){ + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + pr.setLvl(level); + } + + @Override + public int getIndentLevel() { + CTTextParagraphProperties pr = _p.getPPr(); + return (pr == null || !pr.isSetLvl()) ? 0 : pr.getLvl(); + } + + /** + * Returns whether this paragraph has bullets + */ + public boolean isBullet() { + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuNone()) { + setValue(false); + return true; + } + if(props.isSetBuFont() || props.isSetBuChar()){ + setValue(true); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); + } + + /** + * + * @param flag whether text in this paragraph has bullets + */ + public void setBullet(boolean flag) { + if(isBullet() == flag) return; + + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + if(flag) { + pr.addNewBuFont().setTypeface("Arial"); + pr.addNewBuChar().setChar("\u2022"); + } else { + if (pr.isSetBuFont()) pr.unsetBuFont(); + if (pr.isSetBuChar()) pr.unsetBuChar(); + if (pr.isSetBuAutoNum()) pr.unsetBuAutoNum(); + if (pr.isSetBuBlip()) pr.unsetBuBlip(); + if (pr.isSetBuClr()) pr.unsetBuClr(); + if (pr.isSetBuClrTx()) pr.unsetBuClrTx(); + if (pr.isSetBuFont()) pr.unsetBuFont(); + if (pr.isSetBuFontTx()) pr.unsetBuFontTx(); + if (pr.isSetBuSzPct()) pr.unsetBuSzPct(); + if (pr.isSetBuSzPts()) pr.unsetBuSzPts(); + if (pr.isSetBuSzTx()) pr.unsetBuSzTx(); + pr.addNewBuNone(); + } + } + + /** + * Specifies that automatic numbered bullet points should be applied to this paragraph + * + * @param scheme type of auto-numbering + * @param startAt the number that will start number for a given sequence of automatically + numbered bullets (1-based). + */ + public void setBulletAutoNumber(AutoNumberingScheme scheme, int startAt) { + if(startAt < 1) throw new IllegalArgumentException("Start Number must be greater or equal that 1") ; + CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); + CTTextAutonumberBullet lst = pr.isSetBuAutoNum() ? pr.getBuAutoNum() : pr.addNewBuAutoNum(); + lst.setType(STTextAutonumberScheme.Enum.forInt(scheme.ooxmlId)); + lst.setStartAt(startAt); + } + + @Override + public String toString(){ + return "[" + getClass() + "]" + getText(); + } + + + /** + * @return master style text paragraph properties, or null if + * there are no master slides or the master slides do not contain a text paragraph + */ + /* package */ CTTextParagraphProperties getDefaultMasterStyle(){ + CTPlaceholder ph = _shape.getCTPlaceholder(); + String defaultStyleSelector; + switch(ph == null ? -1 : ph.getType().intValue()) { + case STPlaceholderType.INT_TITLE: + case STPlaceholderType.INT_CTR_TITLE: + defaultStyleSelector = "titleStyle"; + break; + case -1: // no placeholder means plain text box + case STPlaceholderType.INT_FTR: + case STPlaceholderType.INT_SLD_NUM: + case STPlaceholderType.INT_DT: + defaultStyleSelector = "otherStyle"; + break; + default: + defaultStyleSelector = "bodyStyle"; + break; + } + int level = getIndentLevel(); + + // wind up and find the root master sheet which must be slide master + final String nsPML = "http://schemas.openxmlformats.org/presentationml/2006/main"; + final String nsDML = "http://schemas.openxmlformats.org/drawingml/2006/main"; + XSLFSheet masterSheet = _shape.getSheet(); + for (XSLFSheet m = masterSheet; m != null; m = (XSLFSheet)m.getMasterSheet()) { + masterSheet = m; + XmlObject xo = masterSheet.getXmlObject(); + XmlCursor cur = xo.newCursor(); + try { + cur.push(); + if ((cur.toChild(nsPML, "txStyles") && cur.toChild(nsPML, defaultStyleSelector)) || + (cur.pop() && cur.toChild(nsPML, "notesStyle"))) { + while (level >= 0) { + cur.push(); + if (cur.toChild(nsDML, "lvl" +(level+1)+ "pPr")) { + return (CTTextParagraphProperties)cur.getObject(); + } + cur.pop(); + level--; + } + } + } finally { + cur.dispose(); + } + } + + return null; + } + + private boolean fetchParagraphProperty(ParagraphPropertyFetcher visitor){ + boolean ok = false; + XSLFTextShape shape = getParentShape(); + XSLFSheet sheet = shape.getSheet(); + + if(_p.isSetPPr()) ok = visitor.fetch(_p.getPPr()); + if (ok) return true; + + ok = shape.fetchShapeProperty(visitor); + if (ok) return true; + + + CTPlaceholder ph = shape.getCTPlaceholder(); + if(ph == null){ + // if it is a plain text box then take defaults from presentation.xml + @SuppressWarnings("resource") + XMLSlideShow ppt = sheet.getSlideShow(); + CTTextParagraphProperties themeProps = ppt.getDefaultParagraphStyle(getIndentLevel()); + if (themeProps != null) ok = visitor.fetch(themeProps); + } + if (ok) return true; + + // defaults for placeholders are defined in the slide master + CTTextParagraphProperties defaultProps = getDefaultMasterStyle(); + // TODO: determine master shape + if(defaultProps != null) ok = visitor.fetch(defaultProps); + if (ok) return true; + + return false; + } + + void copy(XSLFTextParagraph other){ + if (other == this) return; + + CTTextParagraph thisP = getXmlObject(); + CTTextParagraph otherP = other.getXmlObject(); + + if (thisP.isSetPPr()) thisP.unsetPPr(); + if (thisP.isSetEndParaRPr()) thisP.unsetEndParaRPr(); + + _runs.clear(); + for (int i=thisP.sizeOfBrArray(); i>0; i--) { + thisP.removeBr(i-1); + } + for (int i=thisP.sizeOfRArray(); i>0; i--) { + thisP.removeR(i-1); + } + for (int i=thisP.sizeOfFldArray(); i>0; i--) { + thisP.removeFld(i-1); + } + + XmlCursor thisC = thisP.newCursor(); + thisC.toEndToken(); + XmlCursor otherC = otherP.newCursor(); + otherC.copyXmlContents(thisC); + otherC.dispose(); + thisC.dispose(); + + List otherRs = other.getTextRuns(); + int i=0; + for(CTRegularTextRun rtr : thisP.getRArray()) { + XSLFTextRun run = newTextRun(rtr); + run.copy(otherRs.get(i++)); + _runs.add(run); + } + + + // set properties again, in case we are based on a different + // template + TextAlign srcAlign = other.getTextAlign(); + if(srcAlign != getTextAlign()){ + setTextAlign(srcAlign); + } + + boolean isBullet = other.isBullet(); + if(isBullet != isBullet()){ + setBullet(isBullet); + if(isBullet) { + String buFont = other.getBulletFont(); + if(buFont != null && !buFont.equals(getBulletFont())){ + setBulletFont(buFont); + } + String buChar = other.getBulletCharacter(); + if(buChar != null && !buChar.equals(getBulletCharacter())){ + setBulletCharacter(buChar); + } + PaintStyle buColor = other.getBulletFontColor(); + if(buColor != null && !buColor.equals(getBulletFontColor())){ + setBulletFontColor(buColor); + } + Double buSize = other.getBulletFontSize(); + if(!doubleEquals(buSize, getBulletFontSize())){ + setBulletFontSize(buSize); + } + } + } + + Double leftMargin = other.getLeftMargin(); + if (!doubleEquals(leftMargin, getLeftMargin())){ + setLeftMargin(leftMargin); + } + + Double indent = other.getIndent(); + if (!doubleEquals(indent, getIndent())) { + setIndent(indent); + } + + Double spaceAfter = other.getSpaceAfter(); + if (!doubleEquals(spaceAfter, getSpaceAfter())) { + setSpaceAfter(spaceAfter); + } + + Double spaceBefore = other.getSpaceBefore(); + if (!doubleEquals(spaceBefore, getSpaceBefore())) { + setSpaceBefore(spaceBefore); + } + + Double lineSpacing = other.getLineSpacing(); + if (!doubleEquals(lineSpacing, getLineSpacing())) { + setLineSpacing(lineSpacing); + } + } + + private static boolean doubleEquals(Double d1, Double d2) { + return (d1 == d2 || (d1 != null && d1.equals(d2))); + } + + @Override + public Double getDefaultFontSize() { + CTTextCharacterProperties endPr = _p.getEndParaRPr(); + if (endPr == null || !endPr.isSetSz()) { + // inherit the font size from the master style + CTTextParagraphProperties masterStyle = getDefaultMasterStyle(); + if (masterStyle != null) { + endPr = masterStyle.getDefRPr(); + } + } + return (endPr == null || !endPr.isSetSz()) ? 12 : (endPr.getSz() / 100.); + } + + @Override + public String getDefaultFontFamily() { + return (_runs.isEmpty() ? "Arial" : _runs.get(0).getFontFamily()); + } + + @Override + public BulletStyle getBulletStyle() { + if (!isBullet()) return null; + return new BulletStyle(){ + @Override + public String getBulletCharacter() { + return XSLFTextParagraph.this.getBulletCharacter(); + } + + @Override + public String getBulletFont() { + return XSLFTextParagraph.this.getBulletFont(); + } + + @Override + public Double getBulletFontSize() { + return XSLFTextParagraph.this.getBulletFontSize(); + } + + @Override + public PaintStyle getBulletFontColor() { + return XSLFTextParagraph.this.getBulletFontColor(); + } + + @Override + public void setBulletFontColor(Color color) { + setBulletFontColor(DrawPaint.createSolidPaint(color)); + } + + @Override + public void setBulletFontColor(PaintStyle color) { + XSLFTextParagraph.this.setBulletFontColor(color); + } + + @Override + public AutoNumberingScheme getAutoNumberingScheme() { + return XSLFTextParagraph.this.getAutoNumberingScheme(); + } + + @Override + public Integer getAutoNumberingStartAt() { + return XSLFTextParagraph.this.getAutoNumberingStartAt(); + } + + }; + } + + @Override + public void setBulletStyle(Object... styles) { + if (styles.length == 0) { + setBullet(false); + } else { + setBullet(true); + for (Object ostyle : styles) { + if (ostyle instanceof Number) { + setBulletFontSize(((Number)ostyle).doubleValue()); + } else if (ostyle instanceof Color) { + setBulletFontColor((Color)ostyle); + } else if (ostyle instanceof Character) { + setBulletCharacter(ostyle.toString()); + } else if (ostyle instanceof String) { + setBulletFont((String)ostyle); + } else if (ostyle instanceof AutoNumberingScheme) { + setBulletAutoNumber((AutoNumberingScheme)ostyle, 0); + } + } + } + } + + /** + * Helper method for appending text and keeping paragraph and character properties. + * The character properties are moved to the end paragraph marker + */ + /* package */ void clearButKeepProperties() { + CTTextParagraph thisP = getXmlObject(); + for (int i=thisP.sizeOfBrArray(); i>0; i--) { + thisP.removeBr(i-1); + } + for (int i=thisP.sizeOfFldArray(); i>0; i--) { + thisP.removeFld(i-1); + } + if (!_runs.isEmpty()) { + int size = _runs.size(); + XSLFTextRun lastRun = _runs.get(size-1); + CTTextCharacterProperties cpOther = lastRun.getRPr(false); + if (cpOther != null) { + if (thisP.isSetEndParaRPr()) { + thisP.unsetEndParaRPr(); + } + CTTextCharacterProperties cp = thisP.addNewEndParaRPr(); + cp.set(cpOther); + } + for (int i=size; i>0; i--) { + thisP.removeR(i-1); + } + _runs.clear(); + } + } + + @Override + public boolean isHeaderOrFooter() { + CTPlaceholder ph = _shape.getCTPlaceholder(); + int phId = (ph == null ? -1 : ph.getType().intValue()); + switch (phId) { + case STPlaceholderType.INT_SLD_NUM: + case STPlaceholderType.INT_DT: + case STPlaceholderType.INT_FTR: + case STPlaceholderType.INT_HDR: + return true; + default: + return false; + } + } + + /** + * Helper method to allow subclasses to provide their own text run + * + * @param r the xml reference + * + * @return a new text paragraph + * + * @since POI 3.15-beta2 + */ + protected XSLFTextRun newTextRun(CTRegularTextRun r) { + return new XSLFTextRun(r, this); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFSheetAutoSizeColumn.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFSheetAutoSizeColumn.java index 9685b950b..15acbfca0 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFSheetAutoSizeColumn.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFSheetAutoSizeColumn.java @@ -63,7 +63,7 @@ public class TestSXSSFSheetAutoSizeColumn { // longCellValue ends up with approx. column width 10_000 (on my machine) // so shortCellValue can be expected to be < 5000 for all fonts // and longCellValue can be expected to be > 5000 for all fonts - private static final int COLUMN_WIDTH_THRESHOLD_BETWEEN_SHORT_AND_LONG = 5000; + private static final int COLUMN_WIDTH_THRESHOLD_BETWEEN_SHORT_AND_LONG = 4000; private static final int MAX_COLUMN_WIDTH = 255*256; private static final SortedSet columns; diff --git a/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java b/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java index f248ab633..44aa8a2d3 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java +++ b/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java @@ -32,9 +32,9 @@ import org.apache.poi.poifs.filesystem.*; * @author Nick Burch */ public final class PowerPointExtractor extends POIOLE2TextExtractor { - private HSLFSlideShowImpl _hslfshow; - private HSLFSlideShow _show; - private List _slides; + private final HSLFSlideShowImpl _hslfshow; + private final HSLFSlideShow _show; + private final List _slides; private boolean _slidesByDefault = true; private boolean _notesByDefault = false; @@ -240,9 +240,9 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { } // Slide header, if set - ret.append(headerText); + ret.append(headerText); - // Slide text + // Slide text textRunsToText(ret, slide.getTextParagraphs()); // Table text @@ -256,14 +256,13 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { // Comments, if requested and present if (getCommentText) { - Comment[] comments = slide.getComments(); - for (int j = 0; j < comments.length; j++) { - ret.append(comments[j].getAuthor() + " - " + comments[j].getText() + "\n"); + for (Comment comment : slide.getComments()) { + ret.append(comment.getAuthor() + " - " + comment.getText() + "\n"); } } } if (getNoteText) { - ret.append("\n"); + ret.append('\n'); } } @@ -271,7 +270,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { // Not currently using _notes, as that can have the notes of // master sheets in. Grab Slide list, then work from there, // but ensure no duplicates - HashSet seenNotes = new HashSet(); + Set seenNotes = new HashSet(); String headerText = ""; String footerText = ""; HeadersFooters hf = _show.getNotesHeadersFooters(); @@ -285,8 +284,8 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { } - for (int i = 0; i < _slides.size(); i++) { - HSLFNotes notes = _slides.get(i).getNotes(); + for (HSLFSlide slide : _slides) { + HSLFNotes notes = slide.getNotes(); if (notes == null) { continue; } @@ -297,13 +296,13 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { seenNotes.add(id); // Repeat the Notes header, if set - ret.append(headerText); + ret.append(headerText); // Notes text - textRunsToText(ret, notes.getTextParagraphs()); + textRunsToText(ret, notes.getTextParagraphs()); // Repeat the notes footer, if set - ret.append(footerText); + ret.append(footerText); } } @@ -315,16 +314,18 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { } private void extractTableText(StringBuffer ret, HSLFTable table) { - for (int row = 0; row < table.getNumberOfRows(); row++){ - for (int col = 0; col < table.getNumberOfColumns(); col++){ + final int nrows = table.getNumberOfRows(); + final int ncols = table.getNumberOfColumns(); + for (int row = 0; row < nrows; row++){ + for (int col = 0; col < ncols; col++){ HSLFTableCell cell = table.getCell(row, col); //defensive null checks; don't know if they're necessary if (cell != null){ String txt = cell.getText(); txt = (txt == null) ? "" : txt; ret.append(txt); - if (col < table.getNumberOfColumns()-1){ - ret.append("\t"); + if (col < ncols-1){ + ret.append('\t'); } } } @@ -339,7 +340,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { for (List lp : paragraphs) { ret.append(HSLFTextParagraph.getText(lp)); if (ret.length() > 0 && ret.charAt(ret.length()-1) != '\n') { - ret.append("\n"); + ret.append('\n'); } } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java b/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java index f34cd4be7..9bfa5f325 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java @@ -36,6 +36,8 @@ import org.apache.poi.hslf.usermodel.HSLFTextShape; */ public final class HeadersFooters { + private static final String _ppt2007tag = "___PPT12"; + private final HeadersFootersContainer _container; private final HSLFSheet _sheet; private final boolean _ppt2007; @@ -54,7 +56,7 @@ public final class HeadersFooters { // detect if this ppt was saved in Office2007 String tag = ppt.getSlideMasters().get(0).getProgrammableTag(); - _ppt2007 = "___PPT12".equals(tag); + _ppt2007 = _ppt2007tag.equals(tag); SheetContainer sc = _sheet.getSheetContainer(); HeadersFootersContainer hdd = (HeadersFootersContainer)sc.findFirstOfType(RecordTypes.HeadersFooters.typeID); diff --git a/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java b/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java index fe22c03ef..ce9d58f03 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/HWPFDocument.java @@ -22,32 +22,10 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Iterator; import org.apache.poi.hpsf.DocumentSummaryInformation; import org.apache.poi.hpsf.SummaryInformation; -import org.apache.poi.hwpf.model.BookmarksTables; -import org.apache.poi.hwpf.model.CHPBinTable; -import org.apache.poi.hwpf.model.ComplexFileTable; -import org.apache.poi.hwpf.model.DocumentProperties; -import org.apache.poi.hwpf.model.EscherRecordHolder; -import org.apache.poi.hwpf.model.FSPADocumentPart; -import org.apache.poi.hwpf.model.FSPATable; -import org.apache.poi.hwpf.model.FieldsTables; -import org.apache.poi.hwpf.model.FontTable; -import org.apache.poi.hwpf.model.ListTables; -import org.apache.poi.hwpf.model.NoteType; -import org.apache.poi.hwpf.model.NotesTables; -import org.apache.poi.hwpf.model.PAPBinTable; -import org.apache.poi.hwpf.model.PicturesTable; -import org.apache.poi.hwpf.model.RevisionMarkAuthorTable; -import org.apache.poi.hwpf.model.SavedByTable; -import org.apache.poi.hwpf.model.SectionTable; -import org.apache.poi.hwpf.model.SinglentonTextPiece; -import org.apache.poi.hwpf.model.StyleSheet; -import org.apache.poi.hwpf.model.SubdocumentType; -import org.apache.poi.hwpf.model.TextPiece; -import org.apache.poi.hwpf.model.TextPieceTable; +import org.apache.poi.hwpf.model.*; import org.apache.poi.hwpf.model.io.HWPFFileSystem; import org.apache.poi.hwpf.model.io.HWPFOutputStream; import org.apache.poi.hwpf.usermodel.Bookmarks; @@ -572,12 +550,27 @@ public final class HWPFDocument extends HWPFDocumentCore { } /** - * Warning - not currently implemented for HWPF! + * Write out the word file that is represented by this class, to the + * currently open {@link File}, via the writeable {@link POIFSFileSystem} + * it was opened as. + * + *

This will fail (with an {@link IllegalStateException} if the + * Document was opened read-only, opened from an {@link InputStream} + * instead of a File, or if this is not the root document. For those cases, + * you must use {@link #write(OutputStream)} or {@link #write(File)} to + * write to a brand new document. + * + * @since 3.15 */ @Override public void write() throws IOException { - // TODO Implement - throw new IllegalStateException("Coming soon!"); + validateInPlaceWritePossible(); + + // Update the Document+Properties streams in the file + write(directory.getFileSystem(), false); + + // Sync with the File on disk + directory.getFileSystem().writeFilesystem(); } /** @@ -912,23 +905,18 @@ public final class HWPFDocument extends HWPFDocumentCore { dataBuf = tempBuf; } - // create new document preserving order of entries - // TODO Check "copyOtherEntries" and tweak behaviour based on that - // TODO That's needed for in-place write + // Create a new document preserving order of entries / Update existing boolean docWritten = false; boolean dataWritten = false; boolean objectPoolWritten = false; boolean tableWritten = false; boolean propertiesWritten = false; - for ( Iterator iter = directory.getEntries(); iter.hasNext(); ) - { - Entry entry = iter.next(); + for (Entry entry : directory) { if ( entry.getName().equals( STREAM_WORD_DOCUMENT ) ) { if ( !docWritten ) { - pfs.createDocument( new ByteArrayInputStream( mainBuf ), - STREAM_WORD_DOCUMENT ); + write(pfs, mainBuf, STREAM_WORD_DOCUMENT); docWritten = true; } } @@ -936,7 +924,11 @@ public final class HWPFDocument extends HWPFDocumentCore { { if ( !objectPoolWritten ) { - _objectPool.writeTo( pfs.getRoot() ); + if ( copyOtherEntries ) { + _objectPool.writeTo( pfs.getRoot() ); + } else { + // Object pool is already there, no need to change/copy + } objectPoolWritten = true; } } @@ -945,8 +937,7 @@ public final class HWPFDocument extends HWPFDocumentCore { { if ( !tableWritten ) { - pfs.createDocument( new ByteArrayInputStream( tableBuf ), - STREAM_TABLE_1 ); + write(pfs, tableBuf, STREAM_TABLE_1); tableWritten = true; } } @@ -965,29 +956,25 @@ public final class HWPFDocument extends HWPFDocumentCore { { if ( !dataWritten ) { - pfs.createDocument( new ByteArrayInputStream( dataBuf ), - STREAM_DATA ); + write(pfs, dataBuf, STREAM_DATA); dataWritten = true; } } - else + else if ( copyOtherEntries ) { EntryUtils.copyNodeRecursively( entry, pfs.getRoot() ); } } if ( !docWritten ) - pfs.createDocument( new ByteArrayInputStream( mainBuf ), - STREAM_WORD_DOCUMENT ); + write(pfs, mainBuf, STREAM_WORD_DOCUMENT); if ( !tableWritten ) - pfs.createDocument( new ByteArrayInputStream( tableBuf ), - STREAM_TABLE_1 ); + write(pfs, tableBuf, STREAM_TABLE_1); if ( !propertiesWritten ) writeProperties( pfs ); if ( !dataWritten ) - pfs.createDocument( new ByteArrayInputStream( dataBuf ), - STREAM_DATA ); - if ( !objectPoolWritten ) + write(pfs, dataBuf, STREAM_DATA); + if ( !objectPoolWritten && copyOtherEntries ) _objectPool.writeTo( pfs.getRoot() ); this.directory = pfs.getRoot(); @@ -1000,6 +987,9 @@ public final class HWPFDocument extends HWPFDocumentCore { this._tableStream = tableStream.toByteArray(); this._dataStream = dataBuf; } + private static void write(NPOIFSFileSystem pfs, byte[] data, String name) throws IOException { + pfs.createOrUpdateDocument(new ByteArrayInputStream(data), name); + } @Internal public byte[] getDataStream() diff --git a/src/scratchpad/src/org/apache/poi/hwpf/sprm/SectionSprmUncompressor.java b/src/scratchpad/src/org/apache/poi/hwpf/sprm/SectionSprmUncompressor.java index bc578720e..0b186b56b 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/sprm/SectionSprmUncompressor.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/sprm/SectionSprmUncompressor.java @@ -24,11 +24,12 @@ import org.apache.poi.util.Internal; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; + @Internal public final class SectionSprmUncompressor extends SprmUncompressor { private static final POILogger logger = POILogFactory.getLogger(SectionSprmUncompressor.class); - + public SectionSprmUncompressor() { } @@ -58,15 +59,14 @@ public final class SectionSprmUncompressor extends SprmUncompressor */ static void unCompressSEPOperation (SectionProperties newSEP, SprmOperation sprm) { - final int operation = sprm.getOperation(); - final int operand = sprm.getOperand(); + int operation = sprm.getOperation(); switch (operation) { case 0: - newSEP.setCnsPgn ((byte) operand); + newSEP.setCnsPgn ((byte) sprm.getOperand()); break; case 0x1: - newSEP.setIHeadingPgn ((byte) operand); + newSEP.setIHeadingPgn ((byte) sprm.getOperand()); break; case 0x2: byte[] buf = new byte[sprm.size() - 3]; @@ -80,110 +80,110 @@ public final class SectionSprmUncompressor extends SprmUncompressor //not quite sure break; case 0x5: - newSEP.setFEvenlySpaced (getFlag (operand)); + newSEP.setFEvenlySpaced (getFlag (sprm.getOperand())); break; case 0x6: - newSEP.setFUnlocked (getFlag (operand)); + newSEP.setFUnlocked (getFlag (sprm.getOperand())); break; case 0x7: - newSEP.setDmBinFirst ((short) operand); + newSEP.setDmBinFirst ((short) sprm.getOperand()); break; case 0x8: - newSEP.setDmBinOther ((short) operand); + newSEP.setDmBinOther ((short) sprm.getOperand()); break; case 0x9: - newSEP.setBkc ((byte) operand); + newSEP.setBkc ((byte) sprm.getOperand()); break; case 0xa: - newSEP.setFTitlePage (getFlag (operand)); + newSEP.setFTitlePage (getFlag (sprm.getOperand())); break; case 0xb: - newSEP.setCcolM1 ((short) operand); + newSEP.setCcolM1 ((short) sprm.getOperand()); break; case 0xc: - newSEP.setDxaColumns (operand); + newSEP.setDxaColumns (sprm.getOperand()); break; case 0xd: - newSEP.setFAutoPgn (getFlag (operand)); + newSEP.setFAutoPgn (getFlag (sprm.getOperand())); break; case 0xe: - newSEP.setNfcPgn ((byte) operand); + newSEP.setNfcPgn ((byte) sprm.getOperand()); break; case 0xf: - newSEP.setDyaPgn ((short) operand); + newSEP.setDyaPgn ((short) sprm.getOperand()); break; case 0x10: - newSEP.setDxaPgn ((short) operand); + newSEP.setDxaPgn ((short) sprm.getOperand()); break; case 0x11: - newSEP.setFPgnRestart (getFlag (operand)); + newSEP.setFPgnRestart (getFlag (sprm.getOperand())); break; case 0x12: - newSEP.setFEndNote (getFlag (operand)); + newSEP.setFEndNote (getFlag (sprm.getOperand())); break; case 0x13: - newSEP.setLnc ((byte) operand); + newSEP.setLnc ((byte) sprm.getOperand()); break; case 0x14: - newSEP.setGrpfIhdt ((byte) operand); + newSEP.setGrpfIhdt ((byte) sprm.getOperand()); break; case 0x15: - newSEP.setNLnnMod ((short) operand); + newSEP.setNLnnMod ((short) sprm.getOperand()); break; case 0x16: - newSEP.setDxaLnn (operand); + newSEP.setDxaLnn (sprm.getOperand()); break; case 0x17: - newSEP.setDyaHdrTop (operand); + newSEP.setDyaHdrTop (sprm.getOperand()); break; case 0x18: - newSEP.setDyaHdrBottom (operand); + newSEP.setDyaHdrBottom (sprm.getOperand()); break; case 0x19: - newSEP.setFLBetween (getFlag (operand)); + newSEP.setFLBetween (getFlag (sprm.getOperand())); break; case 0x1a: - newSEP.setVjc ((byte) operand); + newSEP.setVjc ((byte) sprm.getOperand()); break; case 0x1b: - newSEP.setLnnMin ((short) operand); + newSEP.setLnnMin ((short) sprm.getOperand()); break; case 0x1c: - newSEP.setPgnStart ((short) operand); + newSEP.setPgnStart ((short) sprm.getOperand()); break; case 0x1d: - newSEP.setDmOrientPage( operand != 0 ); + newSEP.setDmOrientPage( sprm.getOperand() != 0 ); break; case 0x1e: //nothing break; case 0x1f: - newSEP.setXaPage (operand); + newSEP.setXaPage (sprm.getOperand()); break; case 0x20: - newSEP.setYaPage (operand); + newSEP.setYaPage (sprm.getOperand()); break; case 0x21: - newSEP.setDxaLeft (operand); + newSEP.setDxaLeft (sprm.getOperand()); break; case 0x22: - newSEP.setDxaRight (operand); + newSEP.setDxaRight (sprm.getOperand()); break; case 0x23: - newSEP.setDyaTop (operand); + newSEP.setDyaTop (sprm.getOperand()); break; case 0x24: - newSEP.setDyaBottom (operand); + newSEP.setDyaBottom (sprm.getOperand()); break; case 0x25: - newSEP.setDzaGutter (operand); + newSEP.setDzaGutter (sprm.getOperand()); break; case 0x26: - newSEP.setDmPaperReq ((short) operand); + newSEP.setDmPaperReq ((short) sprm.getOperand()); break; case 0x27: - newSEP.setFPropMark (getFlag (operand)); + newSEP.setFPropMark (getFlag (sprm.getOperand())); break; case 0x28: break; @@ -204,40 +204,40 @@ public final class SectionSprmUncompressor extends SprmUncompressor newSEP.setBrcRight(new BorderCode(sprm.getGrpprl(), sprm.getGrpprlOffset())); break; case 0x2f: - newSEP.setPgbProp (operand); + newSEP.setPgbProp (sprm.getOperand()); break; case 0x30: - newSEP.setDxtCharSpace (operand); + newSEP.setDxtCharSpace (sprm.getOperand()); break; case 0x31: - newSEP.setDyaLinePitch (operand); + newSEP.setDyaLinePitch (sprm.getOperand()); break; case 0x33: - newSEP.setWTextFlow ((short) operand); + newSEP.setWTextFlow ((short) sprm.getOperand()); break; case 0x3C: // [MS-DOC], v20140721, 2.6.4, sprmSRncFtn - newSEP.setRncFtn((short) operand); + newSEP.setRncFtn((short) sprm.getOperand()); break; case 0x3E: // [MS-DOC], v20140721, 2.6.4, sprmSRncEdn - newSEP.setRncEdn((short) operand); + newSEP.setRncEdn((short) sprm.getOperand()); break; case 0x3F: // [MS-DOC], v20140721, 2.6.4, sprmSNFtn - newSEP.setNFtn(operand); + newSEP.setNFtn((int) sprm.getOperand()); break; case 0x40: // [MS-DOC], v20140721, 2.6.4, sprmSNFtnRef - newSEP.setNfcFtnRef(operand); + newSEP.setNfcFtnRef((int) sprm.getOperand()); break; case 0x41: // [MS-DOC], v20140721, 2.6.4, sprmSNEdn - newSEP.setNEdn(operand); + newSEP.setNEdn((int) sprm.getOperand()); break; case 0x42: // [MS-DOC], v20140721, 2.6.4, sprmSNEdnRef - newSEP.setNfcEdnRef(operand); + newSEP.setNfcEdnRef((int) sprm.getOperand()); break; default: logger.log(POILogger.INFO, "Unsupported Sprm operation: " + operation + " (" + HexDump.byteToHex(operation) + ")"); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java index 5b78d1484..5e6b71928 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java @@ -18,6 +18,7 @@ package org.apache.poi.hslf.extractor; import static org.apache.poi.POITestCase.assertContains; +import static org.apache.poi.POITestCase.assertContainsIgnoreCase; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -26,7 +27,6 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.List; -import java.util.Locale; import org.apache.poi.POIDataSamples; import org.apache.poi.hslf.model.OLEShape; @@ -355,7 +355,7 @@ public final class TestExtractor { ppe = new PowerPointExtractor(hslf); text = ppe.getText(); - assertContains(text.toLowerCase(Locale.ROOT), "master"); + assertContainsIgnoreCase(text, "master"); assertContains(text, masterText); } diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java index 95d7919e3..a45b031da 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestHWPFWrite.java @@ -21,11 +21,16 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; +import org.apache.poi.POIDataSamples; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.HWPFTestCase; import org.apache.poi.hwpf.HWPFTestDataSamples; +import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.TempFile; /** @@ -75,7 +80,65 @@ public final class TestHWPFWrite extends HWPFTestCase { r = doc.getRange(); assertEquals("I am a test document\r", r.getParagraph(0).text()); doc.close(); - } + } - // TODO In-place write positive and negative checks + /** + * Writing to the file we opened from - note, uses a temp file to + * avoid changing our test files! + */ + @SuppressWarnings("resource") + public void testInPlaceWrite() throws Exception { + // Setup as a copy of a known-good file + final File file = TempFile.createTempFile("TestDocument", ".doc"); + IOUtils.copy( + POIDataSamples.getDocumentInstance().openResourceAsStream("SampleDoc.doc"), + new FileOutputStream(file) + ); + + // Open from the temp file in read-write mode + HWPFDocument doc = new HWPFDocument(new NPOIFSFileSystem(file, false).getRoot()); + Range r = doc.getRange(); + assertEquals("I am a test document\r", r.getParagraph(0).text()); + + // Change + r.replaceText("X XX a test document\r", false); + + // Save in-place, close, re-open and check + doc.write(); + doc.close(); + + doc = new HWPFDocument(new NPOIFSFileSystem(file).getRoot()); + assertEquals("X XX a test document\r", r.getParagraph(0).text()); + } + + @SuppressWarnings("resource") + public void testInvalidInPlaceWrite() throws Exception { + HWPFDocument doc; + + // Can't work for InputStream opened files + doc = new HWPFDocument( + POIDataSamples.getDocumentInstance().openResourceAsStream("SampleDoc.doc")); + try { + doc.write(); + fail("Shouldn't work for InputStream"); + } catch (IllegalStateException e) {} + + // Can't work for OPOIFS + OPOIFSFileSystem ofs = new OPOIFSFileSystem( + POIDataSamples.getDocumentInstance().openResourceAsStream("SampleDoc.doc")); + doc = new HWPFDocument(ofs.getRoot()); + try { + doc.write(); + fail("Shouldn't work for OPOIFSFileSystem"); + } catch (IllegalStateException e) {} + + // Can't work for Read-Only files + NPOIFSFileSystem fs = new NPOIFSFileSystem( + POIDataSamples.getDocumentInstance().getFile("SampleDoc.doc"), true); + doc = new HWPFDocument(fs.getRoot()); + try { + doc.write(); + fail("Shouldn't work for Read Only"); + } catch (IllegalStateException e) {} + } } diff --git a/src/testcases/org/apache/poi/POITestCase.java b/src/testcases/org/apache/poi/POITestCase.java index 8fa16c88a..786c6e112 100644 --- a/src/testcases/org/apache/poi/POITestCase.java +++ b/src/testcases/org/apache/poi/POITestCase.java @@ -32,13 +32,16 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.poi.util.SuppressForbidden; +import org.apache.poi.util.Internal; /** * Util class for POI JUnit TestCases, which provide additional features */ +@Internal public final class POITestCase { public static void assertContains(String haystack, String needle) { assertNotNull(haystack); @@ -47,6 +50,19 @@ public final class POITestCase { haystack.contains(needle) ); } + + public static void assertContainsIgnoreCase(String haystack, String needle, Locale locale) { + assertNotNull(haystack); + assertNotNull(needle); + String hay = haystack.toLowerCase(locale); + String n = needle.toLowerCase(locale); + assertTrue("Unable to find expected text '" + needle + "' in text:\n" + haystack, + hay.contains(n) + ); + } + public static void assertContainsIgnoreCase(String haystack, String needle) { + assertContainsIgnoreCase(haystack, needle, Locale.ROOT); + } public static void assertNotContained(String haystack, String needle) { assertNotNull(haystack); diff --git a/src/testcases/org/apache/poi/TestPOITestCase.java b/src/testcases/org/apache/poi/TestPOITestCase.java new file mode 100644 index 000000000..fd9f3d1ad --- /dev/null +++ b/src/testcases/org/apache/poi/TestPOITestCase.java @@ -0,0 +1,126 @@ +/* ==================================================================== + 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import org.apache.poi.POITestCase; +import org.junit.Ignore; +import org.junit.Test; + +/** + * A class for testing the POI Junit TestCase utility class + */ +public final class TestPOITestCase { + @Test + public void assertContains() { + POITestCase.assertContains("There is a needle in this haystack", "needle"); + /*try { + POITestCase.assertContains("There is gold in this haystack", "needle"); + fail("found a needle"); + } catch (final junit.framework.AssertionFailedError e) { + // expected + }*/ + } + + @Test + public void assertContainsIgnoreCase_Locale() { + POITestCase.assertContainsIgnoreCase("There is a Needle in this haystack", "needlE", Locale.ROOT); + // FIXME: test failing case + } + + @Test + public void assertContainsIgnoreCase() { + POITestCase.assertContainsIgnoreCase("There is a Needle in this haystack", "needlE"); + // FIXME: test failing case + } + + @Test + public void assertNotContained() { + POITestCase.assertNotContained("There is a needle in this haystack", "gold"); + // FIXME: test failing case + } + + @Test + public void assertMapContains() { + Map haystack = Collections.singletonMap("needle", "value"); + POITestCase.assertContains(haystack, "needle"); + // FIXME: test failing case + } + + + /** + * Utility method to get the value of a private/protected field. + * Only use this method in test cases!!! + */ + @Ignore + @Test + public void getFieldValue() { + /* + final Class clazz; + final T instance; + final Class fieldType; + final String fieldName; + + final R expected; + final R actual = POITestCase.getFieldValue(clazz, instance, fieldType, fieldName); + assertEquals(expected, actual); + */ + } + + /** + * Utility method to call a private/protected method. + * Only use this method in test cases!!! + */ + @Ignore + @Test + public void callMethod() { + /* + final Class clazz; + final T instance; + final Class returnType; + final String methodName; + final Class[] parameterTypes; + final Object[] parameters; + + final R expected; + final R actual = POITestCase.callMethod(clazz, instance, returnType, methodName, parameterTypes, parameters); + assertEquals(expected, actual); + */ + } + + /** + * Utility method to shallow compare all fields of the objects + * Only use this method in test cases!!! + */ + @Ignore + @Test + public void assertReflectEquals() throws Exception { + /* + final Object expected; + final Object actual; + POITestCase.assertReflectEquals(expected, actual); + */ + } +} From cda4f20482ad0e33db12f5ea5f1ba1fd4efb0ba7 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 19 Sep 2016 00:20:44 +0000 Subject: [PATCH 8/9] merge trunk to branch git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1761376 13f79535-47bb-0310-9956-ffa450edef68 --- KEYS | 55 +++--- build.xml | 17 +- sonar/examples/pom.xml | 2 +- sonar/excelant/pom.xml | 2 +- sonar/main/pom.xml | 2 +- sonar/ooxml-schema-encryption/pom.xml | 2 +- sonar/ooxml-schema-security/pom.xml | 2 +- sonar/ooxml-schema/pom.xml | 2 +- sonar/ooxml/pom.xml | 2 +- sonar/pom.xml | 2 +- sonar/scratchpad/pom.xml | 2 +- .../poi/hssf/view/SVTableCellRenderer.java | 4 +- .../poi/ss/examples/ExcelComparator.java | 16 +- .../apache/poi/ss/examples/html/ToHtml.java | 8 +- .../usermodel/examples/CreatePivotTable.java | 9 +- .../usermodel/examples/FillsAndColors.java | 6 +- .../org/apache/poi/TestAllFiles.java | 6 +- .../apache/poi/stress/XSSFFileHandler.java | 4 + .../poi/common/usermodel/Hyperlink.java | 7 + .../poi/hssf/model/HSSFFormulaParser.java | 7 +- .../hssf/usermodel/HSSFBorderFormatting.java | 65 ++++++- .../apache/poi/hssf/usermodel/HSSFCell.java | 4 +- .../poi/hssf/usermodel/HSSFCellStyle.java | 64 ++++++- .../poi/hssf/usermodel/HSSFClientAnchor.java | 13 ++ .../hssf/usermodel/HSSFCreationHelper.java | 2 + .../hssf/usermodel/HSSFEvaluationCell.java | 4 +- .../hssf/usermodel/HSSFFormulaEvaluator.java | 89 +-------- .../poi/hssf/usermodel/HSSFHyperlink.java | 2 + .../poi/poifs/nio/FileBackedDataSource.java | 5 +- .../apache/poi/sl/usermodel/SlideShow.java | 3 + .../org/apache/poi/ss/format/CellFormat.java | 32 ++-- .../poi/ss/formula/BaseFormulaEvaluator.java | 94 +++++++++- .../apache/poi/ss/formula/EvaluationCell.java | 2 + .../ss/formula/OperandClassTransformer.java | 6 +- .../eval/forked/ForkedEvaluationCell.java | 4 +- .../apache/poi/ss/formula/functions/DGet.java | 19 +- .../poi/ss/formula/functions/DStarRunner.java | 144 ++++++--------- .../poi/ss/usermodel/BorderFormatting.java | 58 +++++- .../poi/ss/usermodel/BuiltinFormats.java | 2 +- .../org/apache/poi/ss/usermodel/Cell.java | 31 ++-- .../apache/poi/ss/usermodel/CellStyle.java | 94 +++++++++- .../apache/poi/ss/usermodel/CellValue.java | 14 +- .../apache/poi/ss/usermodel/ClientAnchor.java | 13 ++ .../poi/ss/usermodel/CreationHelper.java | 9 +- .../ss/usermodel/DataConsolidateFunction.java | 4 +- .../poi/ss/usermodel/DataFormatter.java | 2 +- .../apache/poi/ss/usermodel/FormulaError.java | 6 +- .../poi/ss/usermodel/FormulaEvaluator.java | 1 + src/java/org/apache/poi/ss/usermodel/Row.java | 9 +- .../poi/ss/usermodel/charts/DataSources.java | 4 +- src/java/org/apache/poi/ss/util/CellUtil.java | 8 +- .../java/org/apache/poi/POIXMLDocument.java | 10 +- .../apache/poi/openxml4j/opc/OPCPackage.java | 17 +- .../apache/poi/openxml4j/opc/ZipPackage.java | 169 ++++++++++++------ .../poi/openxml4j/opc/internal/ZipHelper.java | 10 +- .../poi/openxml4j/util/ZipEntrySource.java | 5 + .../openxml4j/util/ZipFileZipEntrySource.java | 3 + .../util/ZipInputStreamZipEntrySource.java | 3 + .../poi/openxml4j/util/ZipSecureFile.java | 6 +- .../poi/xslf/usermodel/XMLSlideShow.java | 3 + .../poi/xslf/usermodel/XSLFHyperlink.java | 4 + .../poi/xslf/usermodel/XSLFTableRow.java | 29 ++- .../apache/poi/xssf/streaming/SXSSFCell.java | 3 + .../xssf/streaming/SXSSFCreationHelper.java | 2 + .../xssf/streaming/SXSSFFormulaEvaluator.java | 19 +- .../usermodel/BaseXSSFFormulaEvaluator.java | 78 +------- .../xssf/usermodel/XSSFBorderFormatting.java | 65 ++++++- .../apache/poi/xssf/usermodel/XSSFCell.java | 14 +- .../poi/xssf/usermodel/XSSFCellStyle.java | 64 ++++--- .../poi/xssf/usermodel/XSSFClientAnchor.java | 14 ++ .../xssf/usermodel/XSSFCreationHelper.java | 2 + .../xssf/usermodel/XSSFEvaluationCell.java | 2 +- .../xssf/usermodel/XSSFEvaluationSheet.java | 23 ++- .../xssf/usermodel/XSSFFormulaEvaluator.java | 29 +-- .../poi/xssf/usermodel/XSSFHyperlink.java | 1 + .../poi/xssf/usermodel/XSSFPivotTable.java | 74 ++++---- .../apache/poi/xssf/usermodel/XSSFSheet.java | 19 +- .../org/apache/poi/TestPOIXMLDocument.java | 37 +++- .../poi/extractor/TestExtractorFactory.java | 6 +- .../apache/poi/openxml4j/opc/TestPackage.java | 18 ++ .../poi/openxml4j/opc/TestZipPackage.java | 56 ++++++ .../poi/poifs/crypt/TestSecureTempZip.java | 8 + .../ss/formula/TestStructuredReferences.java | 4 +- .../poi/ss/formula/functions/TestProper.java | 2 +- .../poi/xslf/usermodel/TestXSLFTableRow.java | 131 ++++++++++++++ .../poi/xssf/streaming/TestSXSSFWorkbook.java | 40 +++++ .../xssf/usermodel/AllXSSFUsermodelTests.java | 2 +- .../usermodel/TestFormulaEvaluatorOnXSSF.java | 10 +- .../TestMultiSheetFormulaEvaluatorOnXSSF.java | 10 +- .../poi/xssf/usermodel/TestXSSFBugs.java | 4 +- .../poi/xssf/usermodel/TestXSSFCellStyle.java | 38 ++-- .../usermodel/TestXSSFFormulaEvaluation.java | 11 ++ .../xssf/usermodel/TestXSSFPivotTable.java | 64 ++++++- .../poi/xssf/usermodel/TestXSSFSheet.java | 45 +++++ .../usermodel/TestXSSFSheetShiftRows.java | 8 + .../poi/hslf/usermodel/HSLFSlideShow.java | 3 + .../hssf/converter/ExcelToFoConverter.java | 16 +- .../hssf/converter/ExcelToHtmlConverter.java | 8 +- .../org/apache/poi/ddf/TestEscherDump.java | 22 +-- .../poi/hssf/dev/BaseXLSIteratingTest.java | 20 +-- .../poi/hssf/model/TestFormulaParserEval.java | 2 +- .../hssf/record/TestSharedFormulaRecord.java | 2 +- .../apache/poi/hssf/usermodel/TestBugs.java | 6 +- .../poi/hssf/usermodel/TestCellStyle.java | 26 +-- .../TestHSSFConditionalFormatting.java | 18 +- .../usermodel/TestHSSFFormulaEvaluator.java | 4 +- .../poi/hssf/usermodel/TestHSSFOptimiser.java | 6 +- .../poi/hssf/usermodel/TestRowStyle.java | 8 +- .../poi/ss/formula/TestWorkbookEvaluator.java | 12 +- .../poi/ss/formula/atp/TestIfError.java | 6 +- .../eval/BaseTestCircularReferences.java | 14 +- .../poi/ss/formula/eval/TestFormulaBugs.java | 8 +- .../eval/TestFormulasFromSpreadsheet.java | 10 +- .../ss/formula/eval/TestMultiSheetEval.java | 10 +- .../poi/ss/formula/eval/TestPercentEval.java | 2 +- .../BaseTestFunctionsFromSpreadsheet.java | 6 +- .../poi/ss/formula/functions/TestAddress.java | 2 +- .../functions/TestCalendarFieldFunction.java | 2 +- .../poi/ss/formula/functions/TestClean.java | 2 +- .../poi/ss/formula/functions/TestDate.java | 2 +- .../poi/ss/formula/functions/TestFind.java | 4 +- .../poi/ss/formula/functions/TestFixed.java | 4 +- .../ss/formula/functions/TestIndirect.java | 4 +- .../poi/ss/formula/functions/TestIrr.java | 2 +- .../poi/ss/formula/functions/TestIsBlank.java | 4 +- .../poi/ss/formula/functions/TestMirr.java | 2 +- .../poi/ss/usermodel/BaseTestBorderStyle.java | 8 +- .../ss/usermodel/BaseTestBugzillaIssues.java | 4 +- .../apache/poi/ss/usermodel/BaseTestCell.java | 40 ++++- .../BaseTestConditionalFormatting.java | 58 +++--- .../usermodel/BaseTestFormulaEvaluator.java | 12 ++ .../poi/ss/usermodel/BaseTestWorkbook.java | 10 +- .../poi/ss/usermodel/TestDataFormatter.java | 21 +++ .../apache/poi/ss/util/BaseTestCellUtil.java | 4 +- .../org/apache/poi/util/NullOutputStream.java | 41 +++++ test-data/openxml4j/invalid.xlsx | Bin 0 -> 22 bytes test-data/slideshow/60003.ppt | Bin 0 -> 764928 bytes test-data/spreadsheet/52425.xlsx | Bin 0 -> 9439 bytes test-data/spreadsheet/DGet.xls | Bin 51712 -> 53760 bytes 139 files changed, 1760 insertions(+), 801 deletions(-) create mode 100644 src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTableRow.java create mode 100644 src/testcases/org/apache/poi/util/NullOutputStream.java create mode 100644 test-data/openxml4j/invalid.xlsx create mode 100644 test-data/slideshow/60003.ppt create mode 100644 test-data/spreadsheet/52425.xlsx diff --git a/KEYS b/KEYS index 811af6f51..6767cb00a 100644 --- a/KEYS +++ b/KEYS @@ -1753,10 +1753,11 @@ W45jdvBkYoPdQtS+8Vy+q0997zobctz8i5hfXzxg51/IuSU4uNtgr26XapsoLDur 7PoqJGZ6UdBVwyb0qZqs6KLGQkEyJ8358ivjJsjxUR8diK027wWnW/s= =/Vu1 -----END PGP PUBLIC KEY BLOCK----- -pub 2048R/26062CE3 2014-08-17 [expires: 2016-08-16] -uid Andreas Beeker -uid Andreas Beeker (kiwiwings) -sub 2048R/EE864ED6 2014-08-17 [expires: 2016-08-16] + +pub rsa2048/26062CE3 2014-08-17 [expires: 2018-08-21] +uid [ultimate] Andreas Beeker +uid [ultimate] Andreas Beeker (kiwiwings) +sub rsa2048/EE864ED6 2014-08-17 [expires: 2018-08-21] -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 @@ -1767,14 +1768,14 @@ ODjjNQtn+nYmOu7a8Xg3g3vTnikkuHZqWvK0O0VYouW/p1NHelUejQbOPuUKukD1 omzskuYgteTZ9Jn8efJMIymg9dGubuvN4HvUdEO0/u6K2MCZjIqNEPeqWIuZXeMb +4nGv2r0jSQAU94g3hueooqbUf+Mk2+H5O1d/h2Cii9qVvi6gELdVw9H+5Ir9AFc ynsmvxrPIxraBMPgrXmvPFOTlqlizyFv2O7pABEBAAG0JUFuZHJlYXMgQmVla2Vy -IDxraXdpd2luZ3NAYXBhY2hlLm9yZz6JAUEEEwECACsCGwMFCQPCZwAGCwkIBwMC -BhUIAgkKCwQWAgMBAh4BAheABQJUS6SzAhkBAAoJEKk+HEsmBizjjdYIALH3trht -6JTfPz2UsltFTcjzqLXG/I8Vq5hkr0s4ex1V7oNDXrBRBb1WC1pcEe1xqvQmoTyP -dUoX8lCyLOrt3IRpgjnH/4ACyenfOw6yHl6aaXX3HvseL3oHQTMpzkny+xVsrUzJ -wVCRVACPM49L6nRj4YRYaZqAn42x2peYHNcfFrvKsDtTnt4SbFQBlmuiDtlePM6a -9wzqXMFkX+9hcHp/SgOwDalpameoRQ/Itf1w2hN9dxYuaUWojo6b0XpChzkCHKix -uEKtjJXTovRlc6DzxT2Z5JMPhATiN3ZCo88lfd/kK8aMZgV4fruaq/QQRjNK/Hgn -6fP9jPARncSFHkCJAT4EEwECACgFAlPxECkCGwMFCQPCZwAGCwkIBwMCBhUIAgkK +IDxraXdpd2luZ3NAYXBhY2hlLm9yZz6JAUEEEwECACsCGwMGCwkIBwMCBhUIAgkK +CwQWAgMBAh4BAheAAhkBBQJXuYpTBQkHizsfAAoJEKk+HEsmBizjNa0H/AjJPguQ +WIn9AV/jstRN4OPM6eY7VUMG1DYoABRQSVsksPki5jZii0bI9VB3AUFgfXj0y6qk +CwQyKCJwZjcP3JuciJ5brQr/7D12hoTkYSCzCaECIpMoB7HWCpdoFusrgU2PUUwJ +i8xBTC+sLxIn3h5abTU68tnynCYhlA0mJ8zZ8CTvQJyEjidY1UgSohXClG2k/mo7 +z/IyW16x4dlpdkNfiBhL2v/5Ol7Vuz9g1lXvWvMdNQZ2PVK6w5dmCziCkih/qRgK +SUzn65ASEKiCN7afzUkCTdzrI71r3rOkJtlT1NWn2RAv6xT6AuhCPZzH2I3ImuFI +mkUKYhKzRN6AdmCJAT4EEwECACgFAlPxECkCGwMFCQPCZwAGCwkIBwMCBhUIAgkK CwQWAgMBAh4BAheAAAoJEKk+HEsmBizjvfYIALS1vlaqN/f7/YzpnOwlH2Wo4jpI jBrG7SqcdVQk3NGsXTXzzq5p7uTTzpEJW8ReZLGeYaTzqh1vH97uAPR6wL3GjHMZ F2jkC0wSHXxvh9Gyrdx3LA8NSO+BAG9ZfD6OGklsl7tFFEplLpfR1EsAKfbi0bAY @@ -1782,13 +1783,13 @@ F2jkC0wSHXxvh9Gyrdx3LA8NSO+BAG9ZfD6OGklsl7tFFEplLpfR1EsAKfbi0bAY 019qP30afGkvw+ZbIq8qbxJItObMuhn5xdI0YaMm2yudCfm2aGYSCnkrgNfuWzH6 WZ8n1fv45TGBUd2R6zPr13eH73AG1WXpapoD45yf/TFavRfnknU6xb7U3ZK0MkFu ZHJlYXMgQmVla2VyIChraXdpd2luZ3MpIDxhbmRyZWFzLmJlZWtlckBnbXguZGU+ -iQE+BBMBAgAoBQJT8LY0AhsDBQkDwmcABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIX -gAAKCRCpPhxLJgYs4wvFB/9hEV8LzHUpD0X/vM9pfW1ZbTl2GNiZfuMh6/KlqB3C -4gL3SLLRlk4mEPM3qlZWCqZiWVp5zz1C/j/+BLJW3c+j3rCUEAsobsR6HkxBr7N0 -LUnurbMXyiOvdg4lAS42Vi1JrydFdih1HCI3NiGMHAV/8NEdtmLt7Ue8Opx01R6r -wXBomX5BHJXnzbZRbCbdor/UnO8ff5K4jwd6yhY0BV+bI2uD+vGq9nMld0JGj7W7 -qB+HzOAxpINtHksQhtTV/PBWjHTCNhOdGh1zyoyvJSshm1NkvwBVZYMs/0ujApcq -xAXytGXonLiMmZMECOjxAigph8T4PCEKdIqdsVxkC2ybiQIcBBABCgAGBQJV5hIJ +iQE+BBMBAgAoAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCV7mKXAUJB4s7 +HwAKCRCpPhxLJgYs4zheCACgc3m2FH5kmXtYO44BdYYwdV2dyLMCxvVl7GUWqJF8 +wKmDWlUxBwrpzDBQXpmHyb+rqX/kvfEkH2wb9TZwginLecbZbMKubNUWUqGJBHQo +CaV8o6L/iEUJa5NXzY2OJCp32CHsmpefYkU+WgAnmTVe8Se7JEmJeu+2OfStV5m6 +zVK5xzlciYSc29LpA3dsv5hxE6YZ8kJBJaFyv2AvVzaouSR7nPNrdw3/jXaOz+Hb +VpP/CEf5IdvF/o37sv8o7WWcH1AjvMdGQNp6Zr5Te2E35V8PmpqLH4Z8W0/PXij2 +67i565JZc1Kmpqxm59jg1vs2X7rHNn0k+r9BFiCQC1LKiQIcBBABCgAGBQJV5hIJ AAoJEOGWdUUnufY1qh8P/03uvjuU1V9UZY9t/4J/K0wbU8Re9c/HfgmJrCn+wvDI OtxpOg3m07ZoIrosYEA2CIm+kLCYuNbzGSz6ZPZlpoq5FvxzO9OAYMO76r3ktxUw Snbxd9TCkjCCQ8RMxT/JGDBU77nAJPyhCUZF2/SyrXnexloNP9TR/IDQZNOXzlxR @@ -1805,14 +1806,14 @@ ulDgtZwWSin3Kd40VZDyKi6mqOp5ldz6AsZ2CSx1GfI9iVhfuFqOaiBLqpNpdvf9 nGL9OVoy1SdwTXgnboiIFtbTG3sVwD4x4qTRbmT22Ln/mIAICR2wxFBkzpbIQ7Mf R/zEgRh2VlRUUrWUsnYdOh0xfxuYgsnPCjpTY8hvEno3H6kzXKmj2GQJtawMVs5b Ro/GCM9lBBR/PAhB65ACzLmUUSsxjlmjZw0tCcOufg1RyAF/l6YVw1UOJaqXBfSP -eZkLQBj9p8VNpasX/acIfpEaZLE8QhoO11ajABEBAAGJASUEGAECAA8FAlPwtjQC -GwwFCQPCZwAACgkQqT4cSyYGLOPzLggAjHrdpMjZrHM5WqnxkJ1Eib1g14aScMGd -OIw9NOSQ2AGvJjbSy4xyMEUg3S14I73JGYtJu8g9YvCHbuWiyzySBIuGNinMI/Zj -ET/w1noqoNaSlIY4UfFh30g+OikEzP9WXmo0Scg0XH/fJhX1wCpM/TVlphX0yNGm -mkNBBqerRXC7Md4XOy001vvXZGM7vy+xOotyBOy/D4WNERSz3GVS3juCQGMWvMdq -KQa+qoiVaXWfFHwg8u4bSHunrzNja17GyaZHdCEmM9vFzlaqBkoLWCMwIcaKnX9s -tQJpFZwpzgut7DanaPcCDk7LMBbntaJwRC72M0qcj16SUAdAuGt1yQ== -=IonN +eZkLQBj9p8VNpasX/acIfpEaZLE8QhoO11ajABEBAAGJASUEGAECAA8CGwwFAle5 +inIFCQeLOz4ACgkQqT4cSyYGLOMvsAf+J2EyV9+GNqT8UmEU6OFnw/sdR1oE+vZ9 +fe4mifAfjQ+SKYf+MS0lU3lTuwcQKwFklePoYsvJEO7jNEgjTQ+zKiDSlV5yufSn +Idy8+sCYygPn5fSjGdRaMpCCfs5xrljLUPK5U8+vjeteRJW0o2/wmsYdHRz6A74B +kRq8kYu1M8VgZ6JD1YI/mp0mHTTB+H69/DNo6cA+7W/CibeTrffbJ35+OXGsJxqJ +b/QH/4lqsceNJtJThkHPQeM18R7/4t7Vhb5htOk2eB7coKzdYRKpHMzkm7elm8bI +uwsky9+6hIUMKD5hhc8G7g9lWOLSXCeNRUdqWTOfZaU5KOK70kKUeQ== +=PCbZ -----END PGP PUBLIC KEY BLOCK----- pub 4096R/B4812553 2014-02-26 [expires: 2019-02-25] diff --git a/build.xml b/build.xml index f40ff4c77..d3623395d 100644 --- a/build.xml +++ b/build.xml @@ -40,7 +40,7 @@ under the License. The Apache POI project Ant build. - + @@ -1092,7 +1092,7 @@ under the License. - + + - + + - + @@ -2430,12 +2432,13 @@ under the License. - + + diff --git a/sonar/examples/pom.xml b/sonar/examples/pom.xml index cac1dc446..ea53c0597 100644 --- a/sonar/examples/pom.xml +++ b/sonar/examples/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT poi-examples jar diff --git a/sonar/excelant/pom.xml b/sonar/excelant/pom.xml index 2cbeec728..a7b9408dc 100644 --- a/sonar/excelant/pom.xml +++ b/sonar/excelant/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT poi-excelant jar diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index 26a8f2ba4..36422a83c 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT poi-main jar diff --git a/sonar/ooxml-schema-encryption/pom.xml b/sonar/ooxml-schema-encryption/pom.xml index d704a6525..cbb2eff7d 100644 --- a/sonar/ooxml-schema-encryption/pom.xml +++ b/sonar/ooxml-schema-encryption/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT .. poi-ooxml-schema-encryption diff --git a/sonar/ooxml-schema-security/pom.xml b/sonar/ooxml-schema-security/pom.xml index 7aae600f4..a822c7343 100644 --- a/sonar/ooxml-schema-security/pom.xml +++ b/sonar/ooxml-schema-security/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT .. poi-ooxml-schema-security diff --git a/sonar/ooxml-schema/pom.xml b/sonar/ooxml-schema/pom.xml index e1d6526da..5e2b9fef5 100644 --- a/sonar/ooxml-schema/pom.xml +++ b/sonar/ooxml-schema/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT .. poi-ooxml-schema diff --git a/sonar/ooxml/pom.xml b/sonar/ooxml/pom.xml index 305695ff1..27636cdfa 100644 --- a/sonar/ooxml/pom.xml +++ b/sonar/ooxml/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT poi-ooxml jar diff --git a/sonar/pom.xml b/sonar/pom.xml index 5acb3a5da..8e2a9fc42 100644 --- a/sonar/pom.xml +++ b/sonar/pom.xml @@ -3,7 +3,7 @@ org.apache.poi poi-parent pom - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT Apache POI - the Java API for Microsoft Documents Maven build of Apache POI for Sonar checks http://poi.apache.org/ diff --git a/sonar/scratchpad/pom.xml b/sonar/scratchpad/pom.xml index 91ede69a1..6760762d6 100644 --- a/sonar/scratchpad/pom.xml +++ b/sonar/scratchpad/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.15-beta4-SNAPSHOT + 3.16-beta1-SNAPSHOT poi-scratchpad jar diff --git a/src/examples/src/org/apache/poi/hssf/view/SVTableCellRenderer.java b/src/examples/src/org/apache/poi/hssf/view/SVTableCellRenderer.java index ac0075b16..8a59b8306 100644 --- a/src/examples/src/org/apache/poi/hssf/view/SVTableCellRenderer.java +++ b/src/examples/src/org/apache/poi/hssf/view/SVTableCellRenderer.java @@ -160,8 +160,8 @@ public class SVTableCellRenderer extends JLabel SVTableUtils.getAWTColor(s.getRightBorderColor(), SVTableUtils.black), SVTableUtils.getAWTColor(s.getBottomBorderColor(), SVTableUtils.black), SVTableUtils.getAWTColor(s.getLeftBorderColor(), SVTableUtils.black), - s.getBorderTop(), s.getBorderRight(), - s.getBorderBottom(), s.getBorderLeft(), + s.getBorderTopEnum(), s.getBorderRightEnum(), + s.getBorderBottomEnum(), s.getBorderLeftEnum(), hasFocus); setBorder(cellBorder); isBorderSet=true; diff --git a/src/examples/src/org/apache/poi/ss/examples/ExcelComparator.java b/src/examples/src/org/apache/poi/ss/examples/ExcelComparator.java index dce4b0196..d6f846a6b 100644 --- a/src/examples/src/org/apache/poi/ss/examples/ExcelComparator.java +++ b/src/examples/src/org/apache/poi/ss/examples/ExcelComparator.java @@ -367,23 +367,23 @@ public class ExcelComparator { String borderName; switch (borderSide) { case 't': default: - b1 = style1.getBorderTop() == BorderStyle.THIN; - b2 = style2.getBorderTop() == BorderStyle.THIN; + b1 = style1.getBorderTopEnum() == BorderStyle.THIN; + b2 = style2.getBorderTopEnum() == BorderStyle.THIN; borderName = "TOP"; break; case 'b': - b1 = style1.getBorderBottom() == BorderStyle.THIN; - b2 = style2.getBorderBottom() == BorderStyle.THIN; + b1 = style1.getBorderBottomEnum() == BorderStyle.THIN; + b2 = style2.getBorderBottomEnum() == BorderStyle.THIN; borderName = "BOTTOM"; break; case 'l': - b1 = style1.getBorderLeft() == BorderStyle.THIN; - b2 = style2.getBorderLeft() == BorderStyle.THIN; + b1 = style1.getBorderLeftEnum() == BorderStyle.THIN; + b2 = style2.getBorderLeftEnum() == BorderStyle.THIN; borderName = "LEFT"; break; case 'r': - b1 = style1.getBorderRight() == BorderStyle.THIN; - b2 = style2.getBorderRight() == BorderStyle.THIN; + b1 = style1.getBorderRightEnum() == BorderStyle.THIN; + b2 = style2.getBorderRightEnum() == BorderStyle.THIN; borderName = "RIGHT"; break; } diff --git a/src/examples/src/org/apache/poi/ss/examples/html/ToHtml.java b/src/examples/src/org/apache/poi/ss/examples/html/ToHtml.java index c5fdc1da9..f5d2ecba8 100644 --- a/src/examples/src/org/apache/poi/ss/examples/html/ToHtml.java +++ b/src/examples/src/org/apache/poi/ss/examples/html/ToHtml.java @@ -298,10 +298,10 @@ public class ToHtml { } private void borderStyles(CellStyle style) { - styleOut("border-left", style.getBorderLeft(), BORDER); - styleOut("border-right", style.getBorderRight(), BORDER); - styleOut("border-top", style.getBorderTop(), BORDER); - styleOut("border-bottom", style.getBorderBottom(), BORDER); + styleOut("border-left", style.getBorderLeftEnum(), BORDER); + styleOut("border-right", style.getBorderRightEnum(), BORDER); + styleOut("border-top", style.getBorderTopEnum(), BORDER); + styleOut("border-bottom", style.getBorderBottomEnum(), BORDER); } private void fontStyle(CellStyle style) { diff --git a/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreatePivotTable.java b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreatePivotTable.java index 518ec37f4..a855feeda 100644 --- a/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreatePivotTable.java +++ b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreatePivotTable.java @@ -21,6 +21,7 @@ import java.io.FileOutputStream; import java.io.IOException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.DataConsolidateFunction; import org.apache.poi.ss.usermodel.Row; @@ -39,7 +40,11 @@ public class CreatePivotTable { //Create some data to build the pivot table on setCellData(sheet); - XSSFPivotTable pivotTable = sheet.createPivotTable(new AreaReference("A1:D4"), new CellReference("H5")); + AreaReference source = new AreaReference("A1:D4", SpreadsheetVersion.EXCEL2007); + CellReference position = new CellReference("H5"); + // Create a pivot table on this sheet, with H5 as the top-left cell.. + // The pivot table's data source is on the same sheet in A1:D4 + XSSFPivotTable pivotTable = sheet.createPivotTable(source, position); //Configure the pivot table //Use first column as row label pivotTable.addRowLabel(0); @@ -98,4 +103,4 @@ public class CreatePivotTable { Cell cell44 = row4.createCell(3); cell44.setCellValue("No"); } -} \ No newline at end of file +} diff --git a/src/examples/src/org/apache/poi/xssf/usermodel/examples/FillsAndColors.java b/src/examples/src/org/apache/poi/xssf/usermodel/examples/FillsAndColors.java index 019e14d1c..f5edd4a8e 100644 --- a/src/examples/src/org/apache/poi/xssf/usermodel/examples/FillsAndColors.java +++ b/src/examples/src/org/apache/poi/xssf/usermodel/examples/FillsAndColors.java @@ -32,13 +32,13 @@ public class FillsAndColors { Sheet sheet = wb.createSheet("new sheet"); // Create a row and put some cells in it. Rows are 0 based. - Row row = sheet.createRow((short) 1); + Row row = sheet.createRow(1); // Aqua background CellStyle style = wb.createCellStyle(); style.setFillBackgroundColor(IndexedColors.AQUA.getIndex()); style.setFillPattern(CellStyle.BIG_SPOTS); - Cell cell = row.createCell((short) 1); + Cell cell = row.createCell(1); cell.setCellValue(new XSSFRichTextString("X")); cell.setCellStyle(style); @@ -46,7 +46,7 @@ public class FillsAndColors { style = wb.createCellStyle(); style.setFillForegroundColor(IndexedColors.ORANGE.getIndex()); style.setFillPattern(CellStyle.SOLID_FOREGROUND); - cell = row.createCell((short) 2); + cell = row.createCell(2); cell.setCellValue(new XSSFRichTextString("X")); cell.setCellStyle(style); diff --git a/src/integrationtest/org/apache/poi/TestAllFiles.java b/src/integrationtest/org/apache/poi/TestAllFiles.java index 6183c185c..e364c9f28 100644 --- a/src/integrationtest/org/apache/poi/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/TestAllFiles.java @@ -240,6 +240,7 @@ public class TestAllFiles { EXPECTED_FAILURES.add("openxml4j/OPCCompliance_CoreProperties_OnlyOneCorePropertiesPartFAIL.docx"); EXPECTED_FAILURES.add("openxml4j/OPCCompliance_CoreProperties_UnauthorizedXMLLangAttributeFAIL.docx"); EXPECTED_FAILURES.add("openxml4j/OPCCompliance_DerivedPartNameFAIL.docx"); + EXPECTED_FAILURES.add("openxml4j/invalid.xlsx"); EXPECTED_FAILURES.add("spreadsheet/54764-2.xlsx"); // see TestXSSFBugs.bug54764() EXPECTED_FAILURES.add("spreadsheet/54764.xlsx"); // see TestXSSFBugs.bug54764() EXPECTED_FAILURES.add("spreadsheet/Simple.xlsb"); @@ -297,7 +298,10 @@ public class TestAllFiles { List files = new ArrayList(); for(String file : scanner.getIncludedFiles()) { file = file.replace('\\', '/'); // ... failures/handlers lookup doesn't work on windows otherwise - if (IGNORED.contains(file)) continue; + if (IGNORED.contains(file)) { + System.out.println("Ignoring " + file); + continue; + } FileHandler handler = HANDLERS.get(getExtension(file)); files.add(new Object[] { file, handler }); diff --git a/src/integrationtest/org/apache/poi/stress/XSSFFileHandler.java b/src/integrationtest/org/apache/poi/stress/XSSFFileHandler.java index be6039707..0e24f0486 100644 --- a/src/integrationtest/org/apache/poi/stress/XSSFFileHandler.java +++ b/src/integrationtest/org/apache/poi/stress/XSSFFileHandler.java @@ -30,6 +30,7 @@ import javax.xml.transform.TransformerException; import org.apache.poi.POIXMLException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException; import org.apache.poi.openxml4j.exceptions.OLE2NotOfficeXmlFileException; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; @@ -134,6 +135,9 @@ public class XSSFFileHandler extends SpreadsheetHandler { EXPECTED_ADDITIONAL_FAILURES.add("spreadsheet/Simple.xlsb"); // TODO: good to ignore? EXPECTED_ADDITIONAL_FAILURES.add("spreadsheet/sample-beta.xlsx"); + + // corrupt/invalid + EXPECTED_ADDITIONAL_FAILURES.add("openxml4j/invalid.xlsx"); } @SuppressWarnings("resource") diff --git a/src/java/org/apache/poi/common/usermodel/Hyperlink.java b/src/java/org/apache/poi/common/usermodel/Hyperlink.java index 132a3eb4c..abcdd112a 100644 --- a/src/java/org/apache/poi/common/usermodel/Hyperlink.java +++ b/src/java/org/apache/poi/common/usermodel/Hyperlink.java @@ -16,6 +16,8 @@ ==================================================================== */ package org.apache.poi.common.usermodel; +import org.apache.poi.util.Removal; + /** * Represents a hyperlink. */ @@ -25,6 +27,7 @@ public interface Hyperlink { * * @deprecated POI 3.15 beta 3. Use {@link HyperlinkType#URL} instead. */ + @Removal(version="3.17") public static final int LINK_URL = 1; // HyperlinkType.URL.getCode() /** @@ -32,6 +35,7 @@ public interface Hyperlink { * * @deprecated POI 3.15 beta 3. Use {@link HyperlinkType#DOCUMENT} instead. */ + @Removal(version="3.17") public static final int LINK_DOCUMENT = 2; // HyperlinkType.DOCUMENT.getCode() /** @@ -39,6 +43,7 @@ public interface Hyperlink { * * @deprecated POI 3.15 beta 3. Use {@link HyperlinkType#EMAIL} instead. */ + @Removal(version="3.17") public static final int LINK_EMAIL = 3; // HyperlinkType.EMAIL.getCode() /** @@ -46,6 +51,7 @@ public interface Hyperlink { * * @deprecated POI 3.15 beta 3. Use {@link HyperlinkType#FILE} instead. */ + @Removal(version="3.17") public static final int LINK_FILE = 4; // HyperlinkType.FILE.getCode() @@ -83,6 +89,7 @@ public interface Hyperlink { * @return the type of this hyperlink * @see HyperlinkType#forInt(int) * @deprecated POI 3.15 beta 3. Use {@link #getTypeEnum()} + * getType will return a HyperlinkType enum in the future. */ public int getType(); diff --git a/src/java/org/apache/poi/hssf/model/HSSFFormulaParser.java b/src/java/org/apache/poi/hssf/model/HSSFFormulaParser.java index b54b27ad4..774eb788f 100644 --- a/src/java/org/apache/poi/hssf/model/HSSFFormulaParser.java +++ b/src/java/org/apache/poi/hssf/model/HSSFFormulaParser.java @@ -17,8 +17,6 @@ package org.apache.poi.hssf.model; -import org.apache.poi.ss.formula.ptg.Ptg; -import org.apache.poi.util.Internal; import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.formula.FormulaParseException; @@ -26,6 +24,9 @@ import org.apache.poi.ss.formula.FormulaParser; import org.apache.poi.ss.formula.FormulaParsingWorkbook; import org.apache.poi.ss.formula.FormulaRenderer; import org.apache.poi.ss.formula.FormulaType; +import org.apache.poi.ss.formula.ptg.Ptg; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; /** * HSSF wrapper for the {@link FormulaParser} and {@link FormulaRenderer} @@ -61,6 +62,7 @@ public final class HSSFFormulaParser { * * @deprecated POI 3.15 beta 3. Use {@link #parse(String, HSSFWorkbook, FormulaType)} instead. */ + @Removal(version="3.17") public static Ptg[] parse(String formula, HSSFWorkbook workbook, int formulaType) throws FormulaParseException { return parse(formula, workbook, FormulaType.forInt(formulaType)); } @@ -87,6 +89,7 @@ public final class HSSFFormulaParser { * @throws FormulaParseException if the formula has incorrect syntax or is otherwise invalid * @deprecated POI 3.15 beta 3. Use {@link #parse(String, HSSFWorkbook, FormulaType, int)} instead. */ + @Removal(version="3.17") public static Ptg[] parse(String formula, HSSFWorkbook workbook, int formulaType, int sheetIndex) throws FormulaParseException { return parse(formula, workbook, FormulaType.forInt(formulaType), sheetIndex); } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFBorderFormatting.java b/src/java/org/apache/poi/hssf/usermodel/HSSFBorderFormatting.java index d06c9f179..c4113b615 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFBorderFormatting.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFBorderFormatting.java @@ -42,28 +42,83 @@ public final class HSSFBorderFormatting implements org.apache.poi.ss.usermodel.B return borderFormatting; } + /** + * @deprecated POI 3.15. Use {@link #getBorderBottomEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderBottom() { + public short getBorderBottom() { + return (short)borderFormatting.getBorderBottom(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderBottomEnum() { return BorderStyle.valueOf((short)borderFormatting.getBorderBottom()); } + /** + * @deprecated POI 3.15. Use {@link #getBorderDiagonalEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderDiagonal() { + public short getBorderDiagonal() { + return (short)borderFormatting.getBorderDiagonal(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderDiagonalEnum() { return BorderStyle.valueOf((short)borderFormatting.getBorderDiagonal()); } + /** + * @deprecated POI 3.15. Use {@link #getBorderLeftEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderLeft() { + public short getBorderLeft() { + return (short)borderFormatting.getBorderLeft(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderLeftEnum() { return BorderStyle.valueOf((short)borderFormatting.getBorderLeft()); } + /** + * @deprecated POI 3.15. Use {@link #getBorderRightEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderRight() { + public short getBorderRight() { + return (short)borderFormatting.getBorderRight(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderRightEnum() { return BorderStyle.valueOf((short)borderFormatting.getBorderRight()); } + /** + * @deprecated POI 3.15. Use {@link #getBorderTopEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderTop() { + public short getBorderTop() { + return (short)borderFormatting.getBorderTop(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderTopEnum() { return BorderStyle.valueOf((short)borderFormatting.getBorderTop()); } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 44427fce7..cfe998e05 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -446,6 +446,7 @@ public class HSSFCell implements Cell { * * Will return {@link CellType} in a future version of POI. * For forwards compatibility, do not hard-code cell type literals in your code. + * @deprecated 3.15. Will be return a {@link CellType} enum in the future. */ @Override public int getCellType() @@ -459,7 +460,6 @@ public class HSSFCell implements Cell { * @deprecated POI 3.15 beta 3 * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCellTypeEnum() { @@ -1154,6 +1154,7 @@ public class HSSFCell implements Cell { * @return one of ({@link CellType#NUMERIC}, {@link CellType#STRING}, * {@link CellType#BOOLEAN}, {@link CellType#ERROR}) depending * on the cached value of the formula + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCachedFormulaResultType() { @@ -1169,7 +1170,6 @@ public class HSSFCell implements Cell { * @deprecated POI 3.15 beta 3 * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCachedFormulaResultTypeEnum() { if (_cellType != CellType.FORMULA) { diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java index 6e6c05504..0fd7cf2ad 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCellStyle.java @@ -32,6 +32,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.util.Removal; /** * High level representation of the style of a cell in a sheet of a workbook. @@ -254,6 +255,7 @@ public final class HSSFCellStyle implements CellStyle { * @see #ALIGN_CENTER_SELECTION * @deprecated POI 3.15 beta 3. Use {@link #setAlignment(HorizontalAlignment)} instead. */ + @Removal(version="3.17") @Override public void setAlignment(short align) { @@ -329,6 +331,7 @@ public final class HSSFCellStyle implements CellStyle { * @see VerticalAlignment * @deprecated POI 3.15 beta 3. Use {@link #setVerticalAlignment(VerticalAlignment)} instead. */ + @Removal(version="3.17") @Override public void setVerticalAlignment(short align) { @@ -459,6 +462,7 @@ public final class HSSFCellStyle implements CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link HSSFCellStyle#setBorderLeft(BorderStyle)} instead. */ + @Removal(version="3.17") @Override public void setBorderLeft(short border) { @@ -469,6 +473,7 @@ public final class HSSFCellStyle implements CellStyle { /** * set the type of border to use for the left border of the cell * @param border type + * @since POI 3.15 */ @Override public void setBorderLeft(BorderStyle border) @@ -479,9 +484,20 @@ public final class HSSFCellStyle implements CellStyle { /** * get the type of border to use for the left border of the cell * @return border type + * @deprecated POI 3.15. Will return a BorderStyle enum in the future. Use {@link #getBorderLeftEnum()}. */ @Override - public BorderStyle getBorderLeft() + public short getBorderLeft() + { + return _format.getBorderLeft(); + } + /** + * get the type of border to use for the left border of the cell + * @return border type + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderLeftEnum() { return BorderStyle.valueOf(_format.getBorderLeft()); } @@ -505,6 +521,7 @@ public final class HSSFCellStyle implements CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link HSSFCellStyle#setBorderRight(BorderStyle)} instead. */ + @Removal(version="3.17") @Override public void setBorderRight(short border) { @@ -515,6 +532,7 @@ public final class HSSFCellStyle implements CellStyle { /** * set the type of border to use for the right border of the cell * @param border type + * @since POI 3.15 */ @Override public void setBorderRight(BorderStyle border) @@ -525,9 +543,20 @@ public final class HSSFCellStyle implements CellStyle { /** * get the type of border to use for the right border of the cell * @return border type + * @deprecated POI 3.15. Will return a BorderStyle enum in the future. Use {@link #getBorderRightEnum()}. */ @Override - public BorderStyle getBorderRight() + public short getBorderRight() + { + return _format.getBorderRight(); + } + /** + * get the type of border to use for the right border of the cell + * @return border type + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderRightEnum() { return BorderStyle.valueOf(_format.getBorderRight()); } @@ -551,6 +580,7 @@ public final class HSSFCellStyle implements CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link HSSFCellStyle#setBorderTop(BorderStyle)} instead. */ + @Removal(version="3.17") @Override public void setBorderTop(short border) { @@ -561,6 +591,7 @@ public final class HSSFCellStyle implements CellStyle { /** * set the type of border to use for the top border of the cell * @param border type + * @since POI 3.15 */ @Override public void setBorderTop(BorderStyle border) @@ -571,9 +602,20 @@ public final class HSSFCellStyle implements CellStyle { /** * get the type of border to use for the top border of the cell * @return border type + * @deprecated POI 3.15. Will return a BorderStyle enum in the future. Use {@link #getBorderTopEnum()}. */ @Override - public BorderStyle getBorderTop() + public short getBorderTop() + { + return _format.getBorderTop(); + } + /** + * get the type of border to use for the top border of the cell + * @return border type + * @since 3.15 + */ + @Override + public BorderStyle getBorderTopEnum() { return BorderStyle.valueOf(_format.getBorderTop()); } @@ -597,6 +639,7 @@ public final class HSSFCellStyle implements CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link HSSFCellStyle#setBorderBottom(BorderStyle)} instead. */ + @Removal(version="3.17") @Override public void setBorderBottom(short border) { @@ -607,6 +650,7 @@ public final class HSSFCellStyle implements CellStyle { /** * set the type of border to use for the bottom border of the cell * @param border type + * @since 3.15 beta 2 */ @Override public void setBorderBottom(BorderStyle border) @@ -617,9 +661,20 @@ public final class HSSFCellStyle implements CellStyle { /** * get the type of border to use for the bottom border of the cell * @return border type + * @deprecated POI 3.15. Will return a BorderStyle enum in the future. Use {@link #getBorderBottomEnum()}. */ @Override - public BorderStyle getBorderBottom() + public short getBorderBottom() + { + return _format.getBorderBottom(); + } + /** + * get the type of border to use for the bottom border of the cell + * @return border type + * @since 3.15 + */ + @Override + public BorderStyle getBorderBottomEnum() { return BorderStyle.valueOf(_format.getBorderBottom()); } @@ -733,6 +788,7 @@ public final class HSSFCellStyle implements CellStyle { * @param fp fill pattern (set to 1 to fill w/foreground color) * @deprecated POI 3.15 beta 3. Use {@link #setFillPattern(FillPatternType)} instead. */ + @Removal(version="3.17") @Override public void setFillPattern(short fp) { diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFClientAnchor.java b/src/java/org/apache/poi/hssf/usermodel/HSSFClientAnchor.java index 4b8a25e30..d4c8c6d68 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFClientAnchor.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFClientAnchor.java @@ -21,6 +21,7 @@ import org.apache.poi.ddf.EscherClientAnchorRecord; import org.apache.poi.ddf.EscherRecord; import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.util.Removal; /** * A client anchor is attached to an excel worksheet. It anchors against a @@ -250,6 +251,7 @@ public final class HSSFClientAnchor extends HSSFAnchor implements ClientAnchor { /** * Gets the anchor type + * Changed from returning an int to an enum in POI 3.14 beta 1. * @return the anchor type */ @Override @@ -260,11 +262,22 @@ public final class HSSFClientAnchor extends HSSFAnchor implements ClientAnchor { /** * Sets the anchor type * @param anchorType the anchor type to set + * @since POI 3.14 */ @Override public void setAnchorType(AnchorType anchorType) { _escherClientAnchor.setFlag(anchorType.value); } + /** + * Sets the anchor type + * @param anchorType the anchor type to set + * @deprecated POI 3.15. Use {@link #setAnchorType(AnchorType)} instead. + */ + @Removal(version="3.17") + @Override + public void setAnchorType(int anchorType) { + _escherClientAnchor.setFlag((short) anchorType); + } private void checkRange(int value, int minRange, int maxRange, String varName) { if (value < minRange || value > maxRange) diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCreationHelper.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCreationHelper.java index 7ce7ef1be..863d1a004 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCreationHelper.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCreationHelper.java @@ -21,6 +21,7 @@ import org.apache.poi.common.usermodel.HyperlinkType; import org.apache.poi.hssf.record.common.ExtendedColor; import org.apache.poi.ss.usermodel.CreationHelper; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; public class HSSFCreationHelper implements CreationHelper { private final HSSFWorkbook workbook; @@ -50,6 +51,7 @@ public class HSSFCreationHelper implements CreationHelper { * @deprecated POI 3.15 beta 3. Use {@link #createHyperlink(HyperlinkType)} instead. */ @Deprecated + @Removal(version="3.17") @Override public HSSFHyperlink createHyperlink(int type) { return new HSSFHyperlink(type); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java index b524cc149..1771b2e35 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java @@ -55,6 +55,7 @@ final class HSSFEvaluationCell implements EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCellType() { @@ -65,7 +66,6 @@ final class HSSFEvaluationCell implements EvaluationCell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCellTypeEnum() { return _cell.getCellTypeEnum(); @@ -99,6 +99,7 @@ final class HSSFEvaluationCell implements EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type of cached formula result + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCachedFormulaResultType() { @@ -109,7 +110,6 @@ final class HSSFEvaluationCell implements EvaluationCell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCachedFormulaResultTypeEnum() { return _cell.getCachedFormulaResultTypeEnum(); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index 8d7d781f9..20c7ffa1e 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -30,11 +30,10 @@ import org.apache.poi.ss.formula.eval.StringValueEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.udf.UDFFinder; import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.RichTextString; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.util.Internal; /** * Evaluates formula cells.

@@ -81,6 +80,11 @@ public class HSSFFormulaEvaluator extends BaseFormulaEvaluator { public static HSSFFormulaEvaluator create(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { return new HSSFFormulaEvaluator(workbook, stabilityClassifier, udfFinder); } + + @Override + protected RichTextString createRichTextString(String str) { + return new HSSFRichTextString(str); + } /** @@ -136,87 +140,10 @@ public class HSSFFormulaEvaluator extends BaseFormulaEvaluator { public void notifySetFormula(Cell cell) { _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell((HSSFCell)cell)); } - - /** - * If cell contains formula, it evaluates the formula, and saves the result of the formula. The - * cell remains as a formula cell. If the cell does not contain formula, rather than throwing an - * exception, this method returns {@link CellType#_NONE} and leaves the cell unchanged. - * - * Note that the type of the formula result is returned, so you know what kind of - * cached formula result is also stored with the formula. - *

-     * CellType evaluatedCellType = evaluator.evaluateFormulaCell(cell);
-     * 
- * Be aware that your cell will hold both the formula, and the result. If you want the cell - * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} - * @param cell The cell to evaluate - * @return {@link CellType#_NONE} for non-formula cells, or the type of the formula result - * @since POI 3.15 beta 3 - * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. - */ - @Internal - @Override - public CellType evaluateFormulaCellEnum(Cell cell) { - if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { - return CellType._NONE; - } - CellValue cv = evaluateFormulaCellValue(cell); - // cell remains a formula cell, but the cached value is changed - setCellValue(cell, cv); - return cv.getCellType(); - } - - /** - * If cell contains formula, it evaluates the formula, and - * puts the formula result back into the cell, in place - * of the old formula. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the same instance of HSSFCell is returned to - * allow chained calls like: - *
-     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
-     * 
- * Be aware that your cell value will be changed to hold the - * result of the formula. If you simply want the formula - * value computed for you, use {@link #evaluateFormulaCellEnum(Cell)}} - */ + @Override public HSSFCell evaluateInCell(Cell cell) { - if (cell == null) { - return null; - } - HSSFCell result = (HSSFCell) cell; - if (cell.getCellTypeEnum() == CellType.FORMULA) { - CellValue cv = evaluateFormulaCellValue(cell); - setCellValue(cell, cv); - setCellType(cell, cv); // cell will no longer be a formula cell - } - return result; - } - - private static void setCellValue(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); - switch (cellType) { - case BOOLEAN: - cell.setCellValue(cv.getBooleanValue()); - break; - case ERROR: - cell.setCellErrorValue(cv.getErrorValue()); - break; - case NUMERIC: - cell.setCellValue(cv.getNumberValue()); - break; - case STRING: - cell.setCellValue(new HSSFRichTextString(cv.getStringValue())); - break; - case BLANK: - // never happens - blanks eventually get translated to zero - case FORMULA: - // this will never happen, we have already evaluated the formula - default: - throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); - } + return (HSSFCell) super.evaluateInCell(cell); } /** diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java index 1847bfc0f..084a6bfe0 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java @@ -273,6 +273,8 @@ public class HSSFHyperlink implements Hyperlink { * * @return the type of this hyperlink * @see HyperlinkType#forInt + * @deprecated POI 3.15. Use {@link #getTypeEnum()} instead. + * getType will return a HyperlinkType enum in the future. */ @Override public int getType() { diff --git a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java index 1b8660644..47637daa3 100644 --- a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java +++ b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java @@ -94,6 +94,8 @@ public class FileBackedDataSource extends DataSource { if (writable) { dst = channel.map(FileChannel.MapMode.READ_WRITE, position, length); worked = 0; + // remember the buffer for cleanup if necessary + buffersToClean.add(dst); } else { // Read channel.position(position); @@ -109,9 +111,6 @@ public class FileBackedDataSource extends DataSource { // Ready it for reading dst.position(0); - // remember the buffer for cleanup if necessary - buffersToClean.add(dst); - // All done return dst; } diff --git a/src/java/org/apache/poi/sl/usermodel/SlideShow.java b/src/java/org/apache/poi/sl/usermodel/SlideShow.java index e46ce243f..228925d1c 100644 --- a/src/java/org/apache/poi/sl/usermodel/SlideShow.java +++ b/src/java/org/apache/poi/sl/usermodel/SlideShow.java @@ -83,6 +83,7 @@ public interface SlideShow< * @param format The format of the picture. * * @return the picture data reference. + * @since 3.15 beta 1 */ PictureData addPicture(InputStream is, PictureType format) throws IOException; @@ -93,6 +94,7 @@ public interface SlideShow< * @param format The format of the picture. * * @return the picture data reference + * @since 3.15 beta 1 */ PictureData addPicture(File pict, PictureType format) throws IOException; @@ -101,6 +103,7 @@ public interface SlideShow< * * @param pictureData The picture data to find in the SlideShow * @return {@code null} if picture data is not found in this slideshow + * @since 3.15 beta 3 */ PictureData findPictureData(byte[] pictureData); diff --git a/src/java/org/apache/poi/ss/format/CellFormat.java b/src/java/org/apache/poi/ss/format/CellFormat.java index e47d40c4f..bad849712 100644 --- a/src/java/org/apache/poi/ss/format/CellFormat.java +++ b/src/java/org/apache/poi/ss/format/CellFormat.java @@ -35,7 +35,6 @@ import org.apache.poi.ss.usermodel.ConditionalFormattingRule; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.util.DateFormatConverter; -import org.apache.poi.util.Internal; /** * Format a value according to the standard Excel behavior. This "standard" is @@ -44,28 +43,35 @@ import org.apache.poi.util.Internal; *

* An Excel format has up to four parts, separated by semicolons. Each part * specifies what to do with particular kinds of values, depending on the number - * of parts given:

One part (example: [Green]#.##)
If the - * value is a number, display according to this one part (example: green text, - * with up to two decimal points). If the value is text, display it as is. - *
Two parts (example: [Green]#.##;[Red]#.##)
If the value is a - * positive number or zero, display according to the first part (example: green + * of parts given: + *
+ *
One part (example: [Green]#.##)
+ *
If the value is a number, display according to this one part (example: green text, + * with up to two decimal points). If the value is text, display it as is.
+ * + *
Two parts (example: [Green]#.##;[Red]#.##)
+ *
If the value is a positive number or zero, display according to the first part (example: green * text, with up to two decimal points); if it is a negative number, display * according to the second part (example: red text, with up to two decimal - * points). If the value is text, display it as is.
Three parts (example: - * [Green]#.##;[Black]#.##;[Red]#.##)
If the value is a positive + * points). If the value is text, display it as is.
+ * + *
Three parts (example: [Green]#.##;[Black]#.##;[Red]#.##)
+ *
If the value is a positive * number, display according to the first part (example: green text, with up to * two decimal points); if it is zero, display according to the second part * (example: black text, with up to two decimal points); if it is a negative * number, display according to the third part (example: red text, with up to - * two decimal points). If the value is text, display it as is.
Four parts - * (example: [Green]#.##;[Black]#.##;[Red]#.##;[@])
If the value is - * a positive number, display according to the first part (example: green text, + * two decimal points). If the value is text, display it as is.
+ * + *
Four parts (example: [Green]#.##;[Black]#.##;[Red]#.##;[@])
+ *
If the value is a positive number, display according to the first part (example: green text, * with up to two decimal points); if it is zero, display according to the * second part (example: black text, with up to two decimal points); if it is a * negative number, display according to the third part (example: red text, with * up to two decimal points). If the value is text, display according to the * fourth part (example: text in the cell's usual color, with the text value - * surround by brackets).
+ * surround by brackets).
+ *
*

* A given format part may specify a given Locale, by including something * like [$$-409] or [$£-809] or [$-40C]. These @@ -421,6 +427,7 @@ public class CellFormat { * @param cell The cell. * * @return The ultimate type of this cell. + * @deprecated POI 3.15. This will return a CellType enum in the future */ public static int ultimateType(Cell cell) { return ultimateTypeEnum(cell).getCode(); @@ -439,7 +446,6 @@ public class CellFormat { * @deprecated POI 3.15 beta 3 * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") public static CellType ultimateTypeEnum(Cell cell) { CellType type = cell.getCellTypeEnum(); if (type == CellType.FORMULA) diff --git a/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java b/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java index 8746ba7fa..6cd19f5bf 100644 --- a/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/BaseFormulaEvaluator.java @@ -23,6 +23,7 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.RichTextString; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -108,6 +109,37 @@ public abstract class BaseFormulaEvaluator implements FormulaEvaluator, Workbook throw new IllegalStateException("Bad cell type (" + cell.getCellTypeEnum() + ")"); } } + + /** + * If cell contains formula, it evaluates the formula, and + * puts the formula result back into the cell, in place + * of the old formula. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + * Note that the same instance of HSSFCell is returned to + * allow chained calls like: + *

+     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
+     * 
+ * Be aware that your cell value will be changed to hold the + * result of the formula. If you simply want the formula + * value computed for you, use {@link #evaluateFormulaCellEnum(Cell)}} + * @param cell + * @return the {@code cell} that was passed in, allowing for chained calls + */ + @Override + public Cell evaluateInCell(Cell cell) { + if (cell == null) { + return null; + } + Cell result = cell; + if (cell.getCellTypeEnum() == CellType.FORMULA) { + CellValue cv = evaluateFormulaCellValue(cell); + setCellValue(cell, cv); + setCellType(cell, cv); // cell will no longer be a formula cell + } + return result; + } protected abstract CellValue evaluateFormulaCellValue(Cell cell); @@ -125,14 +157,47 @@ public abstract class BaseFormulaEvaluator implements FormulaEvaluator, Workbook * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} * @param cell The cell to evaluate * @return -1 for non-formula cells, or the type of the formula result + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int evaluateFormulaCell(Cell cell) { return evaluateFormulaCellEnum(cell).getCode(); } + + /** + * If cell contains formula, it evaluates the formula, + * and saves the result of the formula. The cell + * remains as a formula cell. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + * Note that the type of the formula result is returned, + * so you know what kind of value is also stored with + * the formula. + *
+     * CellType evaluatedCellType = evaluator.evaluateFormulaCellEnum(cell);
+     * 
+ * Be aware that your cell will hold both the formula, + * and the result. If you want the cell replaced with + * the result of the formula, use {@link #evaluate(org.apache.poi.ss.usermodel.Cell)} } + * @param cell The cell to evaluate + * @return The type of the formula result (the cell's type remains as CellType.FORMULA however) + * If cell is not a formula cell, returns {@link CellType#_NONE} rather than throwing an exception. + * @since POI 3.15 beta 3 + * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. + */ + @Override + public CellType evaluateFormulaCellEnum(Cell cell) { + if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { + return CellType._NONE; + } + CellValue cv = evaluateFormulaCellValue(cell); + // cell remains a formula cell, but the cached value is changed + setCellValue(cell, cv); + return cv.getCellTypeEnum(); + } protected static void setCellType(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); + CellType cellType = cv.getCellTypeEnum(); switch (cellType) { case BOOLEAN: case ERROR: @@ -150,6 +215,33 @@ public abstract class BaseFormulaEvaluator implements FormulaEvaluator, Workbook throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); } } + + protected abstract RichTextString createRichTextString(String str); + + protected void setCellValue(Cell cell, CellValue cv) { + CellType cellType = cv.getCellTypeEnum(); + switch (cellType) { + case BOOLEAN: + cell.setCellValue(cv.getBooleanValue()); + break; + case ERROR: + cell.setCellErrorValue(cv.getErrorValue()); + break; + case NUMERIC: + cell.setCellValue(cv.getNumberValue()); + break; + case STRING: + cell.setCellValue(createRichTextString(cv.getStringValue())); + break; + case BLANK: + // never happens - blanks eventually get translated to zero + case FORMULA: + // this will never happen, we have already evaluated the formula + default: + throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); + } + } + /** * Loops over all cells in all sheets of the supplied diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCell.java b/src/java/org/apache/poi/ss/formula/EvaluationCell.java index 3695fe280..a7e855c44 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationCell.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationCell.java @@ -42,6 +42,7 @@ public interface EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ int getCellType(); /** @@ -61,6 +62,7 @@ public interface EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type of cached formula result + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ int getCachedFormulaResultType(); /** diff --git a/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java b/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java index a45d5a2d1..8e438c966 100644 --- a/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java +++ b/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java @@ -28,6 +28,7 @@ import org.apache.poi.ss.formula.ptg.Ptg; import org.apache.poi.ss.formula.ptg.RangePtg; import org.apache.poi.ss.formula.ptg.UnionPtg; import org.apache.poi.ss.formula.ptg.ValueOperatorPtg; +import org.apache.poi.util.Removal; /** * This class performs 'operand class' transformation. Non-base tokens are classified into three @@ -59,7 +60,10 @@ final class OperandClassTransformer { private final FormulaType _formulaType; - /** @deprecated POI 3.15 beta 3. */ + /** + * @deprecated POI 3.15 beta 3. Use {@code OperandClassTransformer(FormulaType)} instead. + */ + @Removal(version="3.17") public OperandClassTransformer(int formulaType) { this(FormulaType.forInt(formulaType)); } diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java index 0521e0889..db01b0640 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java @@ -108,6 +108,7 @@ final class ForkedEvaluationCell implements EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCellType() { @@ -118,7 +119,6 @@ final class ForkedEvaluationCell implements EvaluationCell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCellTypeEnum() { return _cellType; @@ -160,6 +160,7 @@ final class ForkedEvaluationCell implements EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type of cached formula result + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCachedFormulaResultType() { @@ -170,7 +171,6 @@ final class ForkedEvaluationCell implements EvaluationCell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCachedFormulaResultTypeEnum() { return _masterCell.getCachedFormulaResultTypeEnum(); diff --git a/src/java/org/apache/poi/ss/formula/functions/DGet.java b/src/java/org/apache/poi/ss/formula/functions/DGet.java index 91a9934b5..0bf9cc262 100644 --- a/src/java/org/apache/poi/ss/formula/functions/DGet.java +++ b/src/java/org/apache/poi/ss/formula/functions/DGet.java @@ -17,7 +17,10 @@ package org.apache.poi.ss.formula.functions; +import org.apache.poi.ss.formula.eval.BlankEval; import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.OperandResolver; import org.apache.poi.ss.formula.eval.ValueEval; /** @@ -46,8 +49,18 @@ public final class DGet implements IDStarAlgorithm { public ValueEval getResult() { if(result == null) { return ErrorEval.VALUE_INVALID; - } else { - return result; - } + } else if(result instanceof BlankEval) { + return ErrorEval.VALUE_INVALID; + } else + try { + if(OperandResolver.coerceValueToString(OperandResolver.getSingleValue(result, 0, 0)).equals("")) { + return ErrorEval.VALUE_INVALID; + } + else { + return result; + } + } catch (EvaluationException e) { + return e.getErrorEval(); + } } } diff --git a/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java b/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java index 6a87a67a6..8418c4f74 100644 --- a/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java +++ b/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java @@ -17,13 +17,13 @@ package org.apache.poi.ss.formula.functions; -import org.apache.poi.ss.formula.TwoDEval; +import org.apache.poi.ss.formula.eval.AreaEval; import org.apache.poi.ss.formula.eval.BlankEval; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.EvaluationException; import org.apache.poi.ss.formula.eval.NotImplementedException; import org.apache.poi.ss.formula.eval.NumericValueEval; -import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.OperandResolver; import org.apache.poi.ss.formula.eval.StringEval; import org.apache.poi.ss.formula.eval.StringValueEval; import org.apache.poi.ss.formula.eval.ValueEval; @@ -62,11 +62,17 @@ public final class DStarRunner implements Function3Arg { public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval database, ValueEval filterColumn, ValueEval conditionDatabase) { // Input processing and error checks. - if(!(database instanceof TwoDEval) || !(conditionDatabase instanceof TwoDEval)) { + if(!(database instanceof AreaEval) || !(conditionDatabase instanceof AreaEval)) { return ErrorEval.VALUE_INVALID; } - TwoDEval db = (TwoDEval)database; - TwoDEval cdb = (TwoDEval)conditionDatabase; + AreaEval db = (AreaEval)database; + AreaEval cdb = (AreaEval)conditionDatabase; + + try { + filterColumn = OperandResolver.getSingleValue(filterColumn, srcRowIndex, srcColumnIndex); + } catch (EvaluationException e) { + return e.getErrorEval(); + } int fc; try { @@ -100,15 +106,11 @@ public final class DStarRunner implements Function3Arg { } // Filter each entry. if(matches) { - try { - ValueEval currentValueEval = solveReference(db.getValue(row, fc)); - // Pass the match to the algorithm and conditionally abort the search. - boolean shouldContinue = algorithm.processMatch(currentValueEval); - if(! shouldContinue) { - break; - } - } catch (EvaluationException e) { - return e.getErrorEval(); + ValueEval currentValueEval = resolveReference(db, row, fc); + // Pass the match to the algorithm and conditionally abort the search. + boolean shouldContinue = algorithm.processMatch(currentValueEval); + if(! shouldContinue) { + break; } } } @@ -126,56 +128,16 @@ public final class DStarRunner implements Function3Arg { } /** - * Resolve reference(-chains) until we have a normal value. + * * - * @param field a ValueEval which can be a RefEval. - * @return a ValueEval which is guaranteed not to be a RefEval - * @throws EvaluationException If a multi-sheet reference was found along the way. - */ - private static ValueEval solveReference(ValueEval field) throws EvaluationException { - if (field instanceof RefEval) { - RefEval refEval = (RefEval)field; - if (refEval.getNumberOfSheets() > 1) { - throw new EvaluationException(ErrorEval.VALUE_INVALID); - } - return solveReference(refEval.getInnerValueEval(refEval.getFirstSheetIndex())); - } - else { - return field; - } - } - - /** - * Returns the first column index that matches the given name. The name can either be - * a string or an integer, when it's an integer, then the respective column - * (1 based index) is returned. - * @param nameValueEval - * @param db - * @return the first column index that matches the given name (or int) + * @param nameValueEval Must not be a RefEval or AreaEval. Thus make sure resolveReference() is called on the value first! + * @param db Database + * @return Corresponding column number. * @throws EvaluationException */ - @SuppressWarnings("unused") - private static int getColumnForTag(ValueEval nameValueEval, TwoDEval db) + private static int getColumnForName(ValueEval nameValueEval, AreaEval db) throws EvaluationException { - int resultColumn = -1; - - // Numbers as column indicator are allowed, check that. - if(nameValueEval instanceof NumericValueEval) { - double doubleResultColumn = ((NumericValueEval)nameValueEval).getNumberValue(); - resultColumn = (int)doubleResultColumn; - // Floating comparisions are usually not possible, but should work for 0.0. - if(doubleResultColumn - resultColumn != 0.0) - throw new EvaluationException(ErrorEval.VALUE_INVALID); - resultColumn -= 1; // Numbers are 1-based not 0-based. - } else { - resultColumn = getColumnForName(nameValueEval, db); - } - return resultColumn; - } - - private static int getColumnForName(ValueEval nameValueEval, TwoDEval db) - throws EvaluationException { - String name = getStringFromValueEval(nameValueEval); + String name = OperandResolver.coerceValueToString(nameValueEval); return getColumnForString(db, name); } @@ -187,16 +149,19 @@ public final class DStarRunner implements Function3Arg { * @return Corresponding column number. * @throws EvaluationException If it's not possible to turn all headings into strings. */ - private static int getColumnForString(TwoDEval db,String name) + private static int getColumnForString(AreaEval db,String name) throws EvaluationException { int resultColumn = -1; final int width = db.getWidth(); for(int column = 0; column < width; ++column) { - ValueEval columnNameValueEval = db.getValue(0, column); - if(solveReference(columnNameValueEval) instanceof BlankEval) { + ValueEval columnNameValueEval = resolveReference(db, 0, column); + if(columnNameValueEval instanceof BlankEval) { continue; } - String columnName = getStringFromValueEval(columnNameValueEval); + if(columnNameValueEval instanceof ErrorEval) { + continue; + } + String columnName = OperandResolver.coerceValueToString(columnNameValueEval); if(name.equals(columnName)) { resultColumn = column; break; @@ -215,7 +180,7 @@ public final class DStarRunner implements Function3Arg { * @throws EvaluationException If references could not be resolved or comparison * operators and operands didn't match. */ - private static boolean fullfillsConditions(TwoDEval db, int row, TwoDEval cdb) + private static boolean fullfillsConditions(AreaEval db, int row, AreaEval cdb) throws EvaluationException { // Only one row must match to accept the input, so rows are ORed. // Each row is made up of cells where each cell is a condition, @@ -229,20 +194,15 @@ public final class DStarRunner implements Function3Arg { // special column that accepts formulas. boolean columnCondition = true; ValueEval condition = null; - try { - // The condition to apply. - condition = solveReference(cdb.getValue(conditionRow, column)); - } catch (java.lang.RuntimeException e) { - // It might be a special formula, then it is ok if it fails. - columnCondition = false; - } + + // The condition to apply. + condition = resolveReference(cdb, conditionRow, column); + // If the condition is empty it matches. if(condition instanceof BlankEval) continue; // The column in the DB to apply the condition to. - ValueEval targetHeader = solveReference(cdb.getValue(0, column)); - targetHeader = solveReference(targetHeader); - + ValueEval targetHeader = resolveReference(cdb, 0, column); if(!(targetHeader instanceof StringValueEval)) { throw new EvaluationException(ErrorEval.VALUE_INVALID); @@ -254,14 +214,14 @@ public final class DStarRunner implements Function3Arg { if(columnCondition == true) { // normal column condition // Should not throw, checked above. - ValueEval value = db.getValue( - row, getColumnForName(targetHeader, db)); + ValueEval value = resolveReference(db, row, getColumnForName(targetHeader, db)); if(!testNormalCondition(value, condition)) { matches = false; break; } } else { // It's a special formula condition. - if(getStringFromValueEval(condition).isEmpty()) { + // TODO: Check whether the condition cell contains a formula and return #VALUE! if it doesn't. + if(OperandResolver.coerceValueToString(condition).isEmpty()) { throw new EvaluationException(ErrorEval.VALUE_INVALID); } throw new NotImplementedException( @@ -328,7 +288,7 @@ public final class DStarRunner implements Function3Arg { if(itsANumber) { return testNumericCondition(value, operator.equal, stringOrNumber); } else { // It's a string. - String valueString = value instanceof BlankEval ? "" : getStringFromValueEval(value); + String valueString = value instanceof BlankEval ? "" : OperandResolver.coerceValueToString(value); return stringOrNumber.equals(valueString); } } else { // It's a text starts-with condition. @@ -336,7 +296,7 @@ public final class DStarRunner implements Function3Arg { return value instanceof StringEval; } else { - String valueString = value instanceof BlankEval ? "" : getStringFromValueEval(value); + String valueString = value instanceof BlankEval ? "" : OperandResolver.coerceValueToString(value); return valueString.startsWith(conditionString); } } @@ -424,20 +384,20 @@ public final class DStarRunner implements Function3Arg { return null; } } - + /** - * Takes a ValueEval and tries to retrieve a String value from it. - * It tries to resolve references if there are any. + * Resolve a ValueEval that's in an AreaEval. * - * @param value ValueEval to retrieve the string from. - * @return String corresponding to the given ValueEval. - * @throws EvaluationException If it's not possible to retrieve a String value. + * @param db AreaEval from which the cell to resolve is retrieved. + * @param dbRow Relative row in the AreaEval. + * @param dbCol Relative column in the AreaEval. + * @return A ValueEval that is a NumberEval, StringEval, BoolEval, BlankEval or ErrorEval. */ - private static String getStringFromValueEval(ValueEval value) - throws EvaluationException { - value = solveReference(value); - if(!(value instanceof StringValueEval)) - throw new EvaluationException(ErrorEval.VALUE_INVALID); - return ((StringValueEval)value).getStringValue(); + private static ValueEval resolveReference(AreaEval db, int dbRow, int dbCol) { + try { + return OperandResolver.getSingleValue(db.getValue(dbRow, dbCol), db.getFirstRow()+dbRow, db.getFirstColumn()+dbCol); + } catch (EvaluationException e) { + return e.getErrorEval(); + } } } diff --git a/src/java/org/apache/poi/ss/usermodel/BorderFormatting.java b/src/java/org/apache/poi/ss/usermodel/BorderFormatting.java index 059cdd065..949cc3d00 100644 --- a/src/java/org/apache/poi/ss/usermodel/BorderFormatting.java +++ b/src/java/org/apache/poi/ss/usermodel/BorderFormatting.java @@ -19,6 +19,8 @@ package org.apache.poi.ss.usermodel; +import org.apache.poi.util.Removal; + /** * High level representation for Border Formatting component * of Conditional Formatting settings @@ -27,84 +29,128 @@ public interface BorderFormatting { /** No border * @deprecated 3.15 beta 2. Use {@link BorderStyle#NONE} */ + @Removal(version="3.17") short BORDER_NONE = 0x0; /** Thin border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THIN} */ + @Removal(version="3.17") short BORDER_THIN = 0x1; /** Medium border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM} */ + @Removal(version="3.17") short BORDER_MEDIUM = 0x2; /** dash border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASHED} */ + @Removal(version="3.17") short BORDER_DASHED = 0x3; /** dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOTTED} */ + @Removal(version="3.17") short BORDER_DOTTED = 0x4; /** Thick border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THICK} */ + @Removal(version="3.17") short BORDER_THICK = 0x5; /** double-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOUBLE} */ + @Removal(version="3.17") short BORDER_DOUBLE = 0x6; /** hair-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#HAIR} */ + @Removal(version="3.17") short BORDER_HAIR = 0x7; /** Medium dashed border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASHED} */ + @Removal(version="3.17") short BORDER_MEDIUM_DASHED = 0x8; - /** dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT} */ + @Removal(version="3.17") short BORDER_DASH_DOT = 0x9; /** medium dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT} */ + @Removal(version="3.17") short BORDER_MEDIUM_DASH_DOT = 0xA; /** dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT_DOT} */ + @Removal(version="3.17") short BORDER_DASH_DOT_DOT = 0xB; /** medium dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT_DOT} */ + @Removal(version="3.17") short BORDER_MEDIUM_DASH_DOT_DOT = 0xC; /** slanted dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#SLANTED_DASH_DOT} */ + @Removal(version="3.17") short BORDER_SLANTED_DASH_DOT = 0xD; - BorderStyle getBorderBottom(); + /** + * @deprecated POI 3.15. Use {@link #getBorderBottomEnum()}. + * This method will return an BorderStyle enum in the future. + */ + short getBorderBottom(); + /** @since POI 3.15 */ + BorderStyle getBorderBottomEnum(); - BorderStyle getBorderDiagonal(); + /** + * @deprecated POI 3.15. Use {@link #getBorderDiagonalEnum()}. + * This method will return an BorderStyle enum in the future. + */ + short getBorderDiagonal(); + /** @since POI 3.15 */ + BorderStyle getBorderDiagonalEnum(); - BorderStyle getBorderLeft(); + /** + * @deprecated POI 3.15. Use {@link #getBorderLeftEnum()}. + * This method will return an BorderStyle enum in the future. + */ + short getBorderLeft(); + /** @since POI 3.15 */ + BorderStyle getBorderLeftEnum(); - BorderStyle getBorderRight(); + /** + * @deprecated POI 3.15. Use {@link #getBorderRightEnum()}. + * This method will return an BorderStyle enum in the future. + */ + short getBorderRight(); + /** @since POI 3.15 */ + BorderStyle getBorderRightEnum(); - BorderStyle getBorderTop(); + /** + * @deprecated POI 3.15. Use {@link #getBorderTopEnum()}. + * This method will return an BorderStyle enum in the future. + */ + short getBorderTop(); + /** @since POI 3.15 */ + BorderStyle getBorderTopEnum(); + short getBottomBorderColor(); Color getBottomBorderColorColor(); diff --git a/src/java/org/apache/poi/ss/usermodel/BuiltinFormats.java b/src/java/org/apache/poi/ss/usermodel/BuiltinFormats.java index 708f2f66a..8d1188178 100644 --- a/src/java/org/apache/poi/ss/usermodel/BuiltinFormats.java +++ b/src/java/org/apache/poi/ss/usermodel/BuiltinFormats.java @@ -75,7 +75,7 @@ public final class BuiltinFormats { "#,##0.00", "\"$\"#,##0_);(\"$\"#,##0)", "\"$\"#,##0_);[Red](\"$\"#,##0)", - "\"$\"#,##,00_);(\"$\"#,##0.00)", + "\"$\"#,##0.00_);(\"$\"#,##0.00)", "\"$\"#,##0.00_);[Red](\"$\"#,##0.00)", "0%", "0.00%", diff --git a/src/java/org/apache/poi/ss/usermodel/Cell.java b/src/java/org/apache/poi/ss/usermodel/Cell.java index 95840a6c0..13d860823 100644 --- a/src/java/org/apache/poi/ss/usermodel/Cell.java +++ b/src/java/org/apache/poi/ss/usermodel/Cell.java @@ -24,6 +24,7 @@ import org.apache.poi.ss.formula.FormulaParseException; import org.apache.poi.ss.util.CellAddress; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; /** * High level representation of a cell in a row of a spreadsheet. @@ -46,7 +47,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#NUMERIC} instead. */ - CellType CELL_TYPE_NUMERIC = CellType.NUMERIC; + @Removal(version="4.0") + int CELL_TYPE_NUMERIC = 0; //CellType.NUMERIC.getCode(); /** * String Cell type (1) @@ -54,7 +56,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#STRING} instead. */ - CellType CELL_TYPE_STRING = CellType.STRING; + @Removal(version="4.0") + int CELL_TYPE_STRING = 1; //CellType.STRING.getCode(); /** * Formula Cell type (2) @@ -62,7 +65,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#FORMULA} instead. */ - CellType CELL_TYPE_FORMULA = CellType.FORMULA; + @Removal(version="4.0") + int CELL_TYPE_FORMULA = 2; //CellType.FORMULA.getCode(); /** * Blank Cell type (3) @@ -70,7 +74,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#BLANK} instead. */ - CellType CELL_TYPE_BLANK = CellType.BLANK; + @Removal(version="4.0") + int CELL_TYPE_BLANK = 3; //CellType.BLANK.getCode(); /** * Boolean Cell type (4) @@ -78,7 +83,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#BOOLEAN} instead. */ - CellType CELL_TYPE_BOOLEAN = CellType.BOOLEAN; + @Removal(version="4.0") + int CELL_TYPE_BOOLEAN = 4; //CellType.BOOLEAN.getCode(); /** * Error Cell type (5) @@ -86,7 +92,8 @@ public interface Cell { * @see #getCellType() * @deprecated POI 3.15 beta 3. Use {@link CellType#ERROR} instead. */ - CellType CELL_TYPE_ERROR = CellType.ERROR; + @Removal(version="4.0") + int CELL_TYPE_ERROR = 5; //CellType.ERROR.getCode(); /** * Returns column index of this cell @@ -136,6 +143,7 @@ public interface Cell { * @see CellType#ERROR * @deprecated POI 3.15 beta 3. Use {@link #setCellType(CellType)} instead. */ + @Removal(version="4.0") void setCellType(int cellType); /** * Set the cells type (numeric, formula or string). @@ -155,10 +163,11 @@ public interface Cell { /** * Return the cell type. * - * Will return {@link CellType} in a future version of POI. + * Will return {@link CellType} in version 4.0 of POI. * For forwards compatibility, do not hard-code cell type literals in your code. * * @return the cell type + * @deprecated POI 3.15. Will return a {@link CellType} enum in the future. */ int getCellType(); @@ -168,9 +177,9 @@ public interface Cell { * @return the cell type * @since POI 3.15 beta 3 * @deprecated POI 3.15 beta 3 - * Will be deleted when we make the CellType enum transition. See bug 59791. + * Will be renamed to getCellType() when we make the CellType enum transition in POI 4.0. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") + @Removal(version="4.2") CellType getCellTypeEnum(); /** @@ -182,6 +191,7 @@ public interface Cell { * @return one of ({@link CellType#NUMERIC}, {@link CellType#STRING}, * {@link CellType#BOOLEAN}, {@link CellType#ERROR}) depending * on the cached value of the formula + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ int getCachedFormulaResultType(); @@ -192,9 +202,8 @@ public interface Cell { * on the cached value of the formula * @since POI 3.15 beta 3 * @deprecated POI 3.15 beta 3 - * Will be deleted when we make the CellType enum transition. See bug 59791. + * Will be renamed to getCachedFormulaResultType() when we make the CellType enum transition in POI 4.0. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") CellType getCachedFormulaResultTypeEnum(); /** diff --git a/src/java/org/apache/poi/ss/usermodel/CellStyle.java b/src/java/org/apache/poi/ss/usermodel/CellStyle.java index b6c971979..df3d57259 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellStyle.java +++ b/src/java/org/apache/poi/ss/usermodel/CellStyle.java @@ -17,270 +17,316 @@ package org.apache.poi.ss.usermodel; +import org.apache.poi.util.Removal; + public interface CellStyle { /** * general (normal) horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#GENERAL} instead. */ + @Removal(version="3.17") static final short ALIGN_GENERAL = 0x0; //HorizontalAlignment.GENERAL.getCode(); /** * left-justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#LEFT} instead. */ + @Removal(version="3.17") static final short ALIGN_LEFT = 0x1; //HorizontalAlignment.LEFT.getCode(); /** * center horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#CENTER} instead. */ + @Removal(version="3.17") static final short ALIGN_CENTER = 0x2; //HorizontalAlignment.CENTER.getCode(); /** * right-justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#RIGHT} instead. */ + @Removal(version="3.17") static final short ALIGN_RIGHT = 0x3; //HorizontalAlignment.RIGHT.getCode(); /** * fill? horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#FILL} instead. */ + @Removal(version="3.17") static final short ALIGN_FILL = 0x4; //HorizontalAlignment.FILL.getCode(); /** * justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#JUSTIFY} instead. */ + @Removal(version="3.17") static final short ALIGN_JUSTIFY = 0x5; //HorizontalAlignment.JUSTIFY.getCode(); /** * center-selection? horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#CENTER_SELECTION} instead. */ + @Removal(version="3.17") static final short ALIGN_CENTER_SELECTION = 0x6; //HorizontalAlignment.CENTER_SELECTION.getCode(); /** * top-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#TOP} instead. */ + @Removal(version="3.17") static final short VERTICAL_TOP = 0x0; //VerticalAlignment.TOP.getCode(); /** * center-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#CENTER} instead. */ + @Removal(version="3.17") static final short VERTICAL_CENTER = 0x1; //VerticalAlignment.CENTER.getCode(); /** * bottom-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#BOTTOM} instead. */ + @Removal(version="3.17") static final short VERTICAL_BOTTOM = 0x2; //VerticalAlignment.BOTTOM.getCode(); /** * vertically justified vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#JUSTIFY} instead. */ + @Removal(version="3.17") static final short VERTICAL_JUSTIFY = 0x3; //VerticalAlignment.JUSTIFY.getCode(); /** * No border * @deprecated 3.15 beta 2. Use {@link BorderStyle#NONE} instead. */ + @Removal(version="3.17") static final short BORDER_NONE = 0x0; //BorderStyle.NONE.getCode(); /** * Thin border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THIN} instead. */ + @Removal(version="3.17") static final short BORDER_THIN = 0x1; //BorderStyle.THIN.getCode(); /** * Medium border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM} instead. */ + @Removal(version="3.17") static final short BORDER_MEDIUM = 0x2; //BorderStyle.MEDIUM.getCode(); /** * dash border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASHED} instead. */ + @Removal(version="3.17") static final short BORDER_DASHED = 0x3; //BorderStyle.DASHED.getCode(); /** * dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOTTED} instead. */ + @Removal(version="3.17") static final short BORDER_DOTTED = 0x4; //BorderStyle.DOTTED.getCode(); /** * Thick border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THICK} instead. */ + @Removal(version="3.17") static final short BORDER_THICK = 0x5; //BorderStyle.THICK.getCode(); /** * double-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOUBLE} instead. */ + @Removal(version="3.17") static final short BORDER_DOUBLE = 0x6; //BorderStyle.DOUBLE.getCode(); /** * hair-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#HAIR} instead. */ + @Removal(version="3.17") static final short BORDER_HAIR = 0x7; //BorderStyle.HAIR.getCode(); /** * Medium dashed border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASHED} instead. */ + @Removal(version="3.17") static final short BORDER_MEDIUM_DASHED = 0x8; //BorderStyle.MEDIUM_DASHED.getCode(); /** * dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT} instead. */ + @Removal(version="3.17") static final short BORDER_DASH_DOT = 0x9; //BorderStyle.DASH_DOT.getCode(); /** * medium dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT} instead. */ + @Removal(version="3.17") static final short BORDER_MEDIUM_DASH_DOT = 0xA; //BorderStyle.MEDIUM_DASH_DOT.getCode(); /** * dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT_DOT} instead. */ + @Removal(version="3.17") static final short BORDER_DASH_DOT_DOT = 0xB; //BorderStyle.DASH_DOT_DOT.getCode(); /** * medium dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT_DOT} instead. */ + @Removal(version="3.17") static final short BORDER_MEDIUM_DASH_DOT_DOT = 0xC; //BorderStyle.MEDIUM_DASH_DOT_DOT.getCode(); /** * slanted dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#SLANTED_DASH_DOT} instead. */ + @Removal(version="3.17") static final short BORDER_SLANTED_DASH_DOT = 0xD; //BorderStyle.SLANTED_DASH_DOT.getCode(); /** * Fill Pattern: No background * @deprecated 3.15 beta 3. Use {@link FillPatternType#NO_FILL} instead. */ + @Removal(version="3.17") static final short NO_FILL = 0; //FillPatternType.NO_FILL.getCode(); /** * Fill Pattern: Solidly filled * @deprecated 3.15 beta 3. Use {@link FillPatternType#SOLID_FOREGROUND} instead. */ + @Removal(version="3.17") static final short SOLID_FOREGROUND = 1; //FillPatternType.SOLID_FOREGROUND.getCode(); /** * Fill Pattern: Small fine dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#FINE_DOTS} instead. */ + @Removal(version="3.17") static final short FINE_DOTS = 2; //FillPatternType.FINE_DOTS.getCode(); /** * Fill Pattern: Wide dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#ALT_BARS} instead. */ + @Removal(version="3.17") static final short ALT_BARS = 3; //FillPatternType.ALT_BARS.getCode(); /** * Fill Pattern: Sparse dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#SPARSE_DOTS} instead. */ + @Removal(version="3.17") static final short SPARSE_DOTS = 4; //FillPatternType.SPARSE_DOTS.getCode(); /** * Fill Pattern: Thick horizontal bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_HORZ_BANDS} instead. */ + @Removal(version="3.17") static final short THICK_HORZ_BANDS = 5; //FillPatternType.THICK_HORZ_BANDS.getCode(); /** * Fill Pattern: Thick vertical bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_VERT_BANDS} instead. */ + @Removal(version="3.17") static final short THICK_VERT_BANDS = 6; //FillPatternType.THICK_VERT_BANDS.getCode(); /** * Fill Pattern: Thick backward facing diagonals * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_BACKWARD_DIAG} instead. */ + @Removal(version="3.17") static final short THICK_BACKWARD_DIAG = 7; //FillPatternType.THICK_BACKWARD_DIAG.getCode(); /** * Fill Pattern: Thick forward facing diagonals * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_FORWARD_DIAG} instead. */ + @Removal(version="3.17") static final short THICK_FORWARD_DIAG = 8; //FillPatternType.THICK_FORWARD_DIAG.getCode(); /** * Fill Pattern: Large spots * @deprecated 3.15 beta 3. Use {@link FillPatternType#BIG_SPOTS} instead. */ + @Removal(version="3.17") static final short BIG_SPOTS = 9; //FillPatternType.BIG_SPOTS.getCode(); /** * Fill Pattern: Brick-like layout * @deprecated 3.15 beta 3. Use {@link FillPatternType#BRICKS} instead. */ + @Removal(version="3.17") static final short BRICKS = 10; //FillPatternType.BRICKS.getCode(); /** * Fill Pattern: Thin horizontal bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_HORZ_BANDS} instead. */ + @Removal(version="3.17") static final short THIN_HORZ_BANDS = 11; //FillPatternType.THIN_HORZ_BANDS.getCode(); /** * Fill Pattern: Thin vertical bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_VERT_BANDS} instead. */ + @Removal(version="3.17") static final short THIN_VERT_BANDS = 12; //FillPatternType.THIN_VERT_BANDS.getCode(); /** * Fill Pattern: Thin backward diagonal * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_BACKWARD_DIAG} instead. */ + @Removal(version="3.17") static final short THIN_BACKWARD_DIAG = 13; //FillPatternType.THIN_BACKWARD_DIAG.getCode(); /** * Fill Pattern: Thin forward diagonal * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_FORWARD_DIAG} instead. */ + @Removal(version="3.17") static final short THIN_FORWARD_DIAG = 14; //FillPatternType.THIN_FORWARD_DIAG.getCode(); /** * Fill Pattern: Squares * @deprecated 3.15 beta 3. Use {@link FillPatternType#SQUARES} instead. */ + @Removal(version="3.17") static final short SQUARES = 15; //FillPatternType.SQUARES.getCode(); /** * Fill Pattern: Diamonds * @deprecated 3.15 beta 3. Use {@link FillPatternType#DIAMONDS} instead. */ + @Removal(version="3.17") static final short DIAMONDS = 16; //FillPatternType.DIAMONDS.getCode(); /** * Fill Pattern: Less Dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#LESS_DOTS} instead. */ + @Removal(version="3.17") static final short LESS_DOTS = 17; //FillPatternType.LESS_DOTS.getCode(); /** * Fill Pattern: Least Dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#LEAST_DOTS} instead. */ + @Removal(version="3.17") static final short LEAST_DOTS = 18; //FillPatternType.LEAST_DOTS.getCode(); /** @@ -495,19 +541,29 @@ public interface CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link #setBorderLeft(BorderStyle)} instead */ + @Removal(version="3.17") void setBorderLeft(short border); /** * set the type of border to use for the left border of the cell * @param border type + * @since POI 3.15 */ void setBorderLeft(BorderStyle border); /** * get the type of border to use for the left border of the cell * @return border type + * @deprecated POI 3.15. Use {@link #getBorderLeftEnum()} instead. + * This will return a BorderStyle enum in the future. */ - BorderStyle getBorderLeft(); + short getBorderLeft(); + /** + * get the type of border to use for the left border of the cell + * @return border type + * @since POI 3.15 + */ + BorderStyle getBorderLeftEnum(); /** * set the type of border to use for the right border of the cell @@ -528,19 +584,29 @@ public interface CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link #setBorderRight(BorderStyle)} instead */ + @Removal(version="3.17") void setBorderRight(short border); /** * set the type of border to use for the right border of the cell * @param border type + * @since POI 3.15 */ void setBorderRight(BorderStyle border); /** * get the type of border to use for the right border of the cell * @return border type + * @deprecated POI 3.15. Use {@link #getBorderRightEnum()} instead. + * This will return a BorderStyle enum in the future. */ - BorderStyle getBorderRight(); + short getBorderRight(); + /** + * get the type of border to use for the right border of the cell + * @return border type + * @since POI 3.15 + */ + BorderStyle getBorderRightEnum(); /** * set the type of border to use for the top border of the cell @@ -561,19 +627,29 @@ public interface CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link #setBorderTop(BorderStyle)} instead */ + @Removal(version="3.17") void setBorderTop(short border); /** * set the type of border to use for the top border of the cell * @param border type + * @since POI 3.15 */ void setBorderTop(BorderStyle border); /** * get the type of border to use for the top border of the cell * @return border type + * @deprecated POI 3.15. Use {@link #getBorderTopEnum()} instead. + * This will return a BorderStyle enum in the future. */ - BorderStyle getBorderTop(); + short getBorderTop(); + /** + * get the type of border to use for the top border of the cell + * @return border type + * @since POI 3.15 + */ + BorderStyle getBorderTopEnum(); /** * set the type of border to use for the bottom border of the cell @@ -594,19 +670,29 @@ public interface CellStyle { * @see #BORDER_SLANTED_DASH_DOT * @deprecated 3.15 beta 2. Use {@link #setBorderBottom(BorderStyle)} instead. */ + @Removal(version="3.17") void setBorderBottom(short border); /** * set the type of border to use for the bottom border of the cell * @param border type + * @since POI 3.15 */ void setBorderBottom(BorderStyle border); /** * get the type of border to use for the bottom border of the cell * @return border type + * @deprecated POI 3.15. Use {@link #getBorderBottomEnum()} instead. + * This will return a BorderStyle enum in the future. */ - BorderStyle getBorderBottom(); + short getBorderBottom(); + /** + * get the type of border to use for the bottom border of the cell + * @return border type + * @since POI 3.15 + */ + BorderStyle getBorderBottomEnum(); /** * set the color to use for the left border diff --git a/src/java/org/apache/poi/ss/usermodel/CellValue.java b/src/java/org/apache/poi/ss/usermodel/CellValue.java index 52fea225a..301accfca 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellValue.java +++ b/src/java/org/apache/poi/ss/usermodel/CellValue.java @@ -78,10 +78,22 @@ public final class CellValue { } /** * @return Returns the cellType. + * @since POI 3.15 */ - public CellType getCellType() { + public CellType getCellTypeEnum() { return _cellType; } + /** + * @return Returns the cellType. + * @deprecated POI 3.15. Use {@link #getCellTypeEnum()} instead. + * In the future, the signature of this method will be changed to return a + * {@link CellType}. + */ + @Deprecated + public int getCellType() { + return _cellType.getCode(); + } + /** * @return Returns the errorValue. */ diff --git a/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java b/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java index 672c1e230..702970c69 100644 --- a/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java +++ b/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java @@ -17,6 +17,7 @@ package org.apache.poi.ss.usermodel; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; /** * A client anchor is attached to an excel worksheet. It anchors against a @@ -35,6 +36,7 @@ public interface ClientAnchor { *

* @deprecated since POI 3.14beta1 (circa 2015-11-24). Use {@link AnchorType#MOVE_AND_RESIZE} instead. */ + @Removal(version="3.17") public static final AnchorType MOVE_AND_RESIZE = AnchorType.MOVE_AND_RESIZE; /** @@ -50,6 +52,7 @@ public interface ClientAnchor { *

* @deprecated since POI 3.14beta1 (circa 2015-11-24). Use {@link AnchorType#MOVE_DONT_RESIZE} instead. */ + @Removal(version="3.17") public static final AnchorType MOVE_DONT_RESIZE = AnchorType.MOVE_DONT_RESIZE; /** @@ -66,6 +69,7 @@ public interface ClientAnchor { *

* @deprecated since POI 3.14beta1 (circa 2015-11-24). Use {@link AnchorType#DONT_MOVE_AND_RESIZE} instead. */ + @Removal(version="3.17") public static final AnchorType DONT_MOVE_AND_RESIZE = AnchorType.DONT_MOVE_AND_RESIZE; /** @@ -288,11 +292,20 @@ public interface ClientAnchor { /** * Sets the anchor type * @param anchorType the anchor type to set + * @since POI 3.14 */ public void setAnchorType( AnchorType anchorType ); + /** + * Sets the anchor type + * @param anchorType the anchor type to set + * @deprecated POI 3.15. Use {@link #setAnchorType(AnchorType)} instead. + */ + @Removal(version="3.17") + public void setAnchorType( int anchorType ); /** * Gets the anchor type + * Changed from returning an int to an enum in POI 3.14 beta 1. * @return the anchor type */ public AnchorType getAnchorType(); diff --git a/src/java/org/apache/poi/ss/usermodel/CreationHelper.java b/src/java/org/apache/poi/ss/usermodel/CreationHelper.java index 25e1cdaae..53462c1d4 100644 --- a/src/java/org/apache/poi/ss/usermodel/CreationHelper.java +++ b/src/java/org/apache/poi/ss/usermodel/CreationHelper.java @@ -17,18 +17,18 @@ package org.apache.poi.ss.usermodel; import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.util.Removal; /** * An object that handles instantiating concrete * classes of the various instances one needs for * HSSF and XSSF. - * Works around a major shortcoming in Java, where we - * can't have static methods on interfaces or abstract + * Works around a limitation in Java where we + * cannot have static methods on interfaces or abstract * classes. * This allows you to get the appropriate class for * a given interface, without you having to worry - * about if you're dealing with HSSF or XSSF, despite - * Java being quite rubbish. + * about if you're dealing with HSSF or XSSF. */ public interface CreationHelper { /** @@ -46,6 +46,7 @@ public interface CreationHelper { * Creates a new Hyperlink, of the given type * @deprecated POI 3.15 beta 3. Use {@link #createHyperlink(HyperlinkType)} instead. */ + @Removal(version="3.17") @Deprecated Hyperlink createHyperlink(int type); diff --git a/src/java/org/apache/poi/ss/usermodel/DataConsolidateFunction.java b/src/java/org/apache/poi/ss/usermodel/DataConsolidateFunction.java index f54fb4f7b..d40a8d11c 100644 --- a/src/java/org/apache/poi/ss/usermodel/DataConsolidateFunction.java +++ b/src/java/org/apache/poi/ss/usermodel/DataConsolidateFunction.java @@ -38,8 +38,8 @@ public enum DataConsolidateFunction { VAR(10, "Var"), VARP(11, "Varp"); - private int value; - private String name; + private final int value; + private final String name; DataConsolidateFunction(int value, String name) { this.value = value; diff --git a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java index ee6583937..61a5092f2 100644 --- a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java +++ b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java @@ -895,7 +895,7 @@ public class DataFormatter implements Observer { return cell.getRichStringCellValue().getString(); case BOOLEAN : - return String.valueOf(cell.getBooleanCellValue()); + return cell.getBooleanCellValue() ? "TRUE" : "FALSE"; case BLANK : return ""; case ERROR: diff --git a/src/java/org/apache/poi/ss/usermodel/FormulaError.java b/src/java/org/apache/poi/ss/usermodel/FormulaError.java index 583ca06d9..fa102299e 100644 --- a/src/java/org/apache/poi/ss/usermodel/FormulaError.java +++ b/src/java/org/apache/poi/ss/usermodel/FormulaError.java @@ -166,19 +166,19 @@ public enum FormulaError { return false; } - public static FormulaError forInt(byte type){ + public static FormulaError forInt(byte type) throws IllegalArgumentException { FormulaError err = bmap.get(type); if(err == null) throw new IllegalArgumentException("Unknown error type: " + type); return err; } - public static FormulaError forInt(int type){ + public static FormulaError forInt(int type) throws IllegalArgumentException { FormulaError err = imap.get(type); if(err == null) err = bmap.get((byte)type); if(err == null) throw new IllegalArgumentException("Unknown error type: " + type); return err; } - public static FormulaError forString(String code){ + public static FormulaError forString(String code) throws IllegalArgumentException { FormulaError err = smap.get(code); if(err == null) throw new IllegalArgumentException("Unknown error code: " + code); return err; diff --git a/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java b/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java index be75ff9d5..357bd073a 100644 --- a/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java +++ b/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java @@ -101,6 +101,7 @@ public interface FormulaEvaluator { * or one of {@link CellType#NUMERIC}, {@link CellType#STRING}, * {@link CellType#BOOLEAN}, {@link CellType#ERROR} * Note: the cell's type remains as CellType.FORMULA however. + * @deprecated 3.15. Will return a {@link CellType} enum in the future */ int evaluateFormulaCell(Cell cell); diff --git a/src/java/org/apache/poi/ss/usermodel/Row.java b/src/java/org/apache/poi/ss/usermodel/Row.java index b17edcd30..f58775704 100644 --- a/src/java/org/apache/poi/ss/usermodel/Row.java +++ b/src/java/org/apache/poi/ss/usermodel/Row.java @@ -19,6 +19,8 @@ package org.apache.poi.ss.usermodel; import java.util.Iterator; +import org.apache.poi.util.Removal; + /** * High level representation of a row of a spreadsheet. */ @@ -240,8 +242,10 @@ public interface Row extends Iterable { CREATE_NULL_AS_BLANK(3); /** - * @deprecated as of POI 3.15-beta2, scheduled for removal in 3.17 - the id has no function and will be removed + * @deprecated as of POI 3.15-beta2, scheduled for removal in 3.17 - the id has no function and will be removed. + * The {@code id} is only kept only for backwards compatibility with applications that hard-coded the number */ + @Removal(version="3.17") @Deprecated public final int id; private MissingCellPolicy(int id) { @@ -254,6 +258,7 @@ public interface Row extends Iterable { * * @deprecated as of POI 3.15-beta2, scheduled for removal in 3.17 - use the MissingCellPolicy enum **/ + @Removal(version="3.17") @Deprecated public static final MissingCellPolicy RETURN_NULL_AND_BLANK = MissingCellPolicy.RETURN_NULL_AND_BLANK; /** @@ -261,6 +266,7 @@ public interface Row extends Iterable { * * @deprecated as of POI 3.15-beta2, scheduled for removal in 3.17 - use the MissingCellPolicy enum **/ + @Removal(version="3.17") @Deprecated public static final MissingCellPolicy RETURN_BLANK_AS_NULL = MissingCellPolicy.RETURN_BLANK_AS_NULL; /** @@ -268,6 +274,7 @@ public interface Row extends Iterable { * * @deprecated as of POI 3.15-beta2, scheduled for removal in 3.17 - use the MissingCellPolicy enum **/ + @Removal(version="3.17") @Deprecated public static final MissingCellPolicy CREATE_NULL_AS_BLANK = MissingCellPolicy.CREATE_NULL_AS_BLANK; diff --git a/src/java/org/apache/poi/ss/usermodel/charts/DataSources.java b/src/java/org/apache/poi/ss/usermodel/charts/DataSources.java index 3bf891e0a..75e7fd9a8 100644 --- a/src/java/org/apache/poi/ss/usermodel/charts/DataSources.java +++ b/src/java/org/apache/poi/ss/usermodel/charts/DataSources.java @@ -42,7 +42,7 @@ public class DataSources { return new AbstractCellRangeDataSource(sheet, cellRangeAddress) { public Number getPointAt(int index) { CellValue cellValue = getCellValueAt(index); - if (cellValue != null && cellValue.getCellType() == CellType.NUMERIC) { + if (cellValue != null && cellValue.getCellTypeEnum() == CellType.NUMERIC) { return Double.valueOf(cellValue.getNumberValue()); } else { return null; @@ -59,7 +59,7 @@ public class DataSources { return new AbstractCellRangeDataSource(sheet, cellRangeAddress) { public String getPointAt(int index) { CellValue cellValue = getCellValueAt(index); - if (cellValue != null && cellValue.getCellType() == CellType.STRING) { + if (cellValue != null && cellValue.getCellTypeEnum() == CellType.STRING) { return cellValue.getStringValue(); } else { return null; diff --git a/src/java/org/apache/poi/ss/util/CellUtil.java b/src/java/org/apache/poi/ss/util/CellUtil.java index 4f446c10d..3dad83aab 100644 --- a/src/java/org/apache/poi/ss/util/CellUtil.java +++ b/src/java/org/apache/poi/ss/util/CellUtil.java @@ -405,10 +405,10 @@ public final class CellUtil { Map properties = new HashMap(); put(properties, ALIGNMENT, style.getAlignmentEnum()); put(properties, VERTICAL_ALIGNMENT, style.getVerticalAlignmentEnum()); - put(properties, BORDER_BOTTOM, style.getBorderBottom()); - put(properties, BORDER_LEFT, style.getBorderLeft()); - put(properties, BORDER_RIGHT, style.getBorderRight()); - put(properties, BORDER_TOP, style.getBorderTop()); + put(properties, BORDER_BOTTOM, style.getBorderBottomEnum()); + put(properties, BORDER_LEFT, style.getBorderLeftEnum()); + put(properties, BORDER_RIGHT, style.getBorderRightEnum()); + put(properties, BORDER_TOP, style.getBorderTopEnum()); put(properties, BOTTOM_BORDER_COLOR, style.getBottomBorderColor()); put(properties, DATA_FORMAT, style.getDataFormat()); put(properties, FILL_PATTERN, style.getFillPatternEnum()); diff --git a/src/ooxml/java/org/apache/poi/POIXMLDocument.java b/src/ooxml/java/org/apache/poi/POIXMLDocument.java index 4ec3d442e..93afa6163 100644 --- a/src/ooxml/java/org/apache/poi/POIXMLDocument.java +++ b/src/ooxml/java/org/apache/poi/POIXMLDocument.java @@ -40,6 +40,7 @@ import org.apache.xmlbeans.impl.common.SystemCache; /** * This holds the common functionality for all POI OOXML Document classes. */ +// TODO: implements AutoCloseable in Java 7+ when POI drops support for Java 6. public abstract class POIXMLDocument extends POIXMLDocumentPart implements Closeable { public static final String DOCUMENT_CREATOR = "Apache POI"; @@ -230,6 +231,11 @@ public abstract class POIXMLDocument extends POIXMLDocumentPart implements Close */ @SuppressWarnings("resource") public final void write(OutputStream stream) throws IOException { + OPCPackage p = getPackage(); + if(p == null) { + throw new IOException("Cannot write data, document seems to have been closed already"); + } + //force all children to commit their changes into the underlying OOXML Package // TODO Shouldn't they be committing to the new one instead? Set context = new HashSet(); @@ -239,10 +245,6 @@ public abstract class POIXMLDocument extends POIXMLDocumentPart implements Close //save extended and custom properties getProperties().commit(); - OPCPackage p = getPackage(); - if(p == null) { - throw new IOException("Cannot write data, document seems to have been closed already"); - } p.save(stream); } } diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java index e029d7dec..b7720ee42 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java @@ -248,9 +248,10 @@ public abstract class OPCPackage implements RelationshipSource, Closeable { * @throws InvalidFormatException * If the specified file doesn't exist, and a parsing error * occur. + * @throws InvalidOperationException */ public static OPCPackage open(String path, PackageAccess access) - throws InvalidFormatException { + throws InvalidFormatException, InvalidOperationException { if (path == null || "".equals(path.trim())) { throw new IllegalArgumentException("'path' must be given"); } @@ -261,8 +262,20 @@ public abstract class OPCPackage implements RelationshipSource, Closeable { } OPCPackage pack = new ZipPackage(path, access); + boolean success = false; if (pack.partList == null && access != PackageAccess.WRITE) { - pack.getParts(); + try { + pack.getParts(); + success = true; + } finally { + if (! success) { + try { + pack.close(); + } catch (final IOException e) { + throw new InvalidOperationException("Could not close OPCPackage while cleaning up", e); + } + } + } } pack.originalPackagePath = new File(path).getAbsolutePath(); return pack; diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/ZipPackage.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/ZipPackage.java index fe4d6f1ed..33333c3c5 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/ZipPackage.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/ZipPackage.java @@ -19,6 +19,7 @@ package org.apache.poi.openxml4j.opc; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -52,7 +53,10 @@ import org.apache.poi.util.TempFile; * Physical zip package. */ public final class ZipPackage extends OPCPackage { - private static POILogger logger = POILogFactory.getLogger(ZipPackage.class); + private static final String MIMETYPE = "mimetype"; + private static final String SETTINGS_XML = "settings.xml"; + + private static final POILogger logger = POILogFactory.getLogger(ZipPackage.class); /** * Zip archive, as either a file on disk, @@ -85,12 +89,22 @@ public final class ZipPackage extends OPCPackage { * @throws IllegalArgumentException * If the specified input stream not an instance of * ZipInputStream. + * @throws IOException + * if input stream cannot be opened, read, or closed */ ZipPackage(InputStream in, PackageAccess access) throws IOException { super(access); - @SuppressWarnings("resource") ThresholdInputStream zis = ZipHelper.openZipStream(in); - this.zipArchive = new ZipInputStreamZipEntrySource(zis); + try { + this.zipArchive = new ZipInputStreamZipEntrySource(zis); + } catch (final IOException e) { + try { + zis.close(); + } catch (final IOException e2) { + throw new IOException("Failed to close zip input stream while cleaning up. " + e.getMessage(), e2); + } + throw new IOException("Failed to read zip entry source", e); + } } /** @@ -100,8 +114,9 @@ public final class ZipPackage extends OPCPackage { * The path of the file to open or create. * @param access * The package access mode. + * @throws InvalidOperationException */ - ZipPackage(String path, PackageAccess access) { + ZipPackage(String path, PackageAccess access) throws InvalidOperationException { this(new File(path), access); } @@ -112,9 +127,9 @@ public final class ZipPackage extends OPCPackage { * The file to open or create. * @param access * The package access mode. + * @throws InvalidOperationException */ - @SuppressWarnings("resource") - ZipPackage(File file, PackageAccess access) { + ZipPackage(File file, PackageAccess access) throws InvalidOperationException { super(access); ZipEntrySource ze; @@ -127,36 +142,72 @@ public final class ZipPackage extends OPCPackage { throw new InvalidOperationException("Can't open the specified file: '" + file + "'", e); } logger.log(POILogger.ERROR, "Error in zip file "+file+" - falling back to stream processing (i.e. ignoring zip central directory)"); - // some zips can't be opened via ZipFile in JDK6, as the central directory - // contains either non-latin entries or the compression type can't be handled - // the workaround is to iterate over the stream and not the directory - FileInputStream fis = null; - ThresholdInputStream zis = null; - try { - fis = new FileInputStream(file); - zis = ZipHelper.openZipStream(fis); - ze = new ZipInputStreamZipEntrySource(zis); - } catch (IOException e2) { - if (zis != null) { - try { - zis.close(); - } catch (IOException e3) { - throw new InvalidOperationException("Can't open the specified file: '" + file + "'"+ - " and couldn't close the file input stream", e); - } - } else if (fis != null) { - try { - fis.close(); - } catch (IOException e3) { - throw new InvalidOperationException("Can't open the specified file: '" + file + "'"+ - " and couldn't close the file input stream", e); - } - } - throw new InvalidOperationException("Can't open the specified file: '" + file + "'", e); - } + ze = openZipEntrySourceStream(file); } this.zipArchive = ze; } + + private static ZipEntrySource openZipEntrySourceStream(File file) throws InvalidOperationException { + final FileInputStream fis; + // Acquire a resource that is needed to read the next level of openZipEntrySourceStream + try { + // open the file input stream + fis = new FileInputStream(file); + } catch (final FileNotFoundException e) { + // If the source cannot be acquired, abort (no resources to free at this level) + throw new InvalidOperationException("Can't open the specified file input stream from file: '" + file + "'", e); + } + + // If an error occurs while reading the next level of openZipEntrySourceStream, free the acquired resource + try { + // read from the file input stream + return openZipEntrySourceStream(fis); + } catch (final Exception e) { + try { + // abort: close the file input stream + fis.close(); + } catch (final IOException e2) { + throw new InvalidOperationException("Could not close the specified file input stream from file: '" + file + "'", e2); + } + throw new InvalidOperationException("Failed to read the file input stream from file: '" + file + "'", e); + } + } + + private static ZipEntrySource openZipEntrySourceStream(FileInputStream fis) throws InvalidOperationException { + final ThresholdInputStream zis; + // Acquire a resource that is needed to read the next level of openZipEntrySourceStream + try { + // open the zip input stream + zis = ZipHelper.openZipStream(fis); + } catch (final IOException e) { + // If the source cannot be acquired, abort (no resources to free at this level) + throw new InvalidOperationException("Could not open the file input stream", e); + } + + // If an error occurs while reading the next level of openZipEntrySourceStream, free the acquired resource + try { + // read from the zip input stream + return openZipEntrySourceStream(zis); + } catch (final Exception e) { + try { + // abort: close the zip input stream + zis.close(); + } catch (final IOException e2) { + throw new InvalidOperationException("Failed to read the zip entry source stream and could not close the zip input stream", e2); + } + throw new InvalidOperationException("Failed to read the zip entry source stream", e); + } + } + + private static ZipEntrySource openZipEntrySourceStream(ThresholdInputStream zis) throws InvalidOperationException { + // Acquire the final level resource. If this is acquired successfully, the zip package was read successfully from the input stream + try { + // open the zip entry source stream + return new ZipInputStreamZipEntrySource(zis); + } catch (IOException e) { + throw new InvalidOperationException("Could not open the specified zip entry source stream", e); + } + } /** * Constructor. Opens a Zip based Open XML document from @@ -206,7 +257,7 @@ public final class ZipPackage extends OPCPackage { this.contentTypeManager = new ZipContentTypeManager( getZipArchive().getInputStream(entry), this); } catch (IOException e) { - throw new InvalidFormatException(e.getMessage()); + throw new InvalidFormatException(e.getMessage(), e); } break; } @@ -220,11 +271,12 @@ public final class ZipPackage extends OPCPackage { boolean hasSettingsXML = false; entries = this.zipArchive.getEntries(); while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.getName().equals("mimetype")) { + final ZipEntry entry = entries.nextElement(); + final String name = entry.getName(); + if (MIMETYPE.equals(name)) { hasMimetype = true; } - if (entry.getName().equals("settings.xml")) { + if (SETTINGS_XML.equals(name)) { hasSettingsXML = true; } numEntries++; @@ -259,10 +311,10 @@ public final class ZipPackage extends OPCPackage { String contentType = contentTypeManager.getContentType(partName); if (contentType != null && contentType.equals(ContentTypes.RELATIONSHIPS_PART)) { try { - partList.put(partName, new ZipPackagePart(this, entry, - partName, contentType)); + PackagePart part = new ZipPackagePart(this, entry, partName, contentType); + partList.put(partName, part); } catch (InvalidOperationException e) { - throw new InvalidFormatException(e.getMessage()); + throw new InvalidFormatException(e.getMessage(), e); } } } @@ -274,17 +326,16 @@ public final class ZipPackage extends OPCPackage { PackagePartName partName = buildPartName(entry); if(partName == null) continue; - String contentType = contentTypeManager - .getContentType(partName); + String contentType = contentTypeManager.getContentType(partName); if (contentType != null && contentType.equals(ContentTypes.RELATIONSHIPS_PART)) { // Already handled } else if (contentType != null) { try { - partList.put(partName, new ZipPackagePart(this, entry, - partName, contentType)); + PackagePart part = new ZipPackagePart(this, entry, partName, contentType); + partList.put(partName, part); } catch (InvalidOperationException e) { - throw new InvalidFormatException(e.getMessage()); + throw new InvalidFormatException(e.getMessage(), e); } } else { throw new InvalidFormatException( @@ -392,20 +443,22 @@ public final class ZipPackage extends OPCPackage { // Save the final package to a temporary file try { save(tempFile); - - // Close the current zip file, so we can - // overwrite it on all platforms - this.zipArchive.close(); - // Copy the new file over the old one - FileHelper.copyFile(tempFile, targetFile); } finally { - // Either the save operation succeed or not, we delete the - // temporary file - if (!tempFile.delete()) { - logger - .log(POILogger.WARN,"The temporary file: '" - + targetFile.getAbsolutePath() - + "' cannot be deleted ! Make sure that no other application use it."); + try { + // Close the current zip file, so we can + // overwrite it on all platforms + this.zipArchive.close(); + // Copy the new file over the old one + FileHelper.copyFile(tempFile, targetFile); + } finally { + // Either the save operation succeed or not, we delete the + // temporary file + if (!tempFile.delete()) { + logger + .log(POILogger.WARN,"The temporary file: '" + + targetFile.getAbsolutePath() + + "' cannot be deleted ! Make sure that no other application use it."); + } } } } else { diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ZipHelper.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ZipHelper.java index 632d2af26..b674b3ad2 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ZipHelper.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ZipHelper.java @@ -19,6 +19,7 @@ package org.apache.poi.openxml4j.opc.internal; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; @@ -239,10 +240,15 @@ public final class ZipHelper { * @param file * The file to open. * @return The zip archive freshly open. + * @throws IOException if the zip file cannot be opened or closed to read the header signature + * @throws NotOfficeXmlFileException if stream does not start with zip header signature */ - public static ZipFile openZipFile(File file) throws IOException { + public static ZipFile openZipFile(File file) throws IOException, NotOfficeXmlFileException { if (!file.exists()) { - return null; + throw new FileNotFoundException("File does not exist"); + } + if (file.isDirectory()) { + throw new IOException("File is a directory"); } // Peek at the first few bytes to sanity check diff --git a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java index 1d64ffe4e..51ad32ce6 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java @@ -45,4 +45,9 @@ public interface ZipEntrySource { * resources may be freed */ public void close() throws IOException; + + /** + * Has close been called already? + */ + public boolean isClosed(); } diff --git a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipFileZipEntrySource.java b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipFileZipEntrySource.java index f4117f44b..09317d361 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipFileZipEntrySource.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipFileZipEntrySource.java @@ -39,6 +39,9 @@ public class ZipFileZipEntrySource implements ZipEntrySource { } zipArchive = null; } + public boolean isClosed() { + return (zipArchive == null); + } public Enumeration getEntries() { if (zipArchive == null) diff --git a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipInputStreamZipEntrySource.java b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipInputStreamZipEntrySource.java index 36b69ac25..4c2b9df3e 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipInputStreamZipEntrySource.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipInputStreamZipEntrySource.java @@ -76,6 +76,9 @@ public class ZipInputStreamZipEntrySource implements ZipEntrySource { // Free the memory zipEntries = null; } + public boolean isClosed() { + return (zipEntries == null); + } /** * Why oh why oh why are Iterator and Enumeration diff --git a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipSecureFile.java b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipSecureFile.java index 13369a5ed..9000656e5 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipSecureFile.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipSecureFile.java @@ -134,15 +134,15 @@ public class ZipSecureFile extends ZipFile { return MAX_TEXT_SIZE; } - public ZipSecureFile(File file, int mode) throws IOException { + public ZipSecureFile(File file, int mode) throws ZipException, IOException { super(file, mode); } - public ZipSecureFile(File file) throws IOException { + public ZipSecureFile(File file) throws ZipException, IOException { super(file); } - public ZipSecureFile(String name) throws IOException { + public ZipSecureFile(String name) throws ZipException, IOException { super(name); } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java index fe969d36b..14b656b53 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java @@ -471,6 +471,7 @@ implements SlideShow { * @param format The format of the picture * * @return the picture data + * @since 3.15 beta 2 */ @Override public XSLFPictureData addPicture(InputStream is, PictureType format) throws IOException @@ -486,6 +487,7 @@ implements SlideShow { * @param format The format of the picture. * * @return the picture data + * @since 3.15 beta 2 */ @Override public XSLFPictureData addPicture(File pict, PictureType format) throws IOException @@ -507,6 +509,7 @@ implements SlideShow { * * @param pictureData The picture data to find in the SlideShow * @return {@code null} if picture data is not found in this slideshow + * @since 3.15 beta 2 */ @Override public XSLFPictureData findPictureData(byte[] pictureData) { diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFHyperlink.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFHyperlink.java index 09b883c00..2f300623f 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFHyperlink.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFHyperlink.java @@ -69,6 +69,10 @@ public class XSLFHyperlink implements Hyperlink { _link.setTooltip(label); } + /* (non-Javadoc) + * @deprecated POI 3.15. Use {@link #getTypeEnum()} instead. + * Will return a HyperlinkType enum in the future + */ @Override public int getType() { return getTypeEnum().getCode(); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTableRow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTableRow.java index 5326d9e78..c4529c4aa 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTableRow.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTableRow.java @@ -32,9 +32,9 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTTableRow; * Represents a table in a .pptx presentation */ public class XSLFTableRow implements Iterable { - private CTTableRow _row; - private List _cells; - private XSLFTable _table; + private final CTTableRow _row; + private final List _cells; + private final XSLFTable _table; /*package*/ XSLFTableRow(CTTableRow row, XSLFTable table){ _row = row; @@ -78,6 +78,29 @@ public class XSLFTableRow implements Iterable { _table.updateRowColIndexes(); return cell; } + + /** + * Merge cells of a table row, inclusive. + * Indices are 0-based. + * + * @param firstCol 0-based index of first column to merge, inclusive + * @param lastCol 0-based index of last column to merge, inclusive + */ + public void mergeCells(int firstCol, int lastCol) + { + if (firstCol >= lastCol) { + throw new IllegalArgumentException( + "Cannot merge, first column >= last column : " + + firstCol + " >= " + lastCol + ); + } + final int colSpan = (lastCol - firstCol) + 1; + + _cells.get(firstCol).setGridSpan(colSpan); + for (final XSLFTableCell cell : _cells.subList(firstCol+1, lastCol+1)) { + cell.setHMerge(true); + } + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java index 6174af54b..99a4648a7 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCell.java @@ -43,6 +43,7 @@ import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.NotImplemented; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.Removal; import org.apache.poi.xssf.usermodel.XSSFHyperlink; import org.apache.poi.xssf.usermodel.XSSFRichTextString; @@ -61,6 +62,8 @@ public class SXSSFCell implements Cell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ + @Removal(version="3.17") + @Deprecated public SXSSFCell(SXSSFRow row, int cellType) { this(row, CellType.forInt((cellType))); diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCreationHelper.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCreationHelper.java index 201879702..f2687ff1d 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCreationHelper.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFCreationHelper.java @@ -26,6 +26,7 @@ import org.apache.poi.ss.usermodel.Hyperlink; import org.apache.poi.util.Internal; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.Removal; import org.apache.poi.xssf.usermodel.XSSFCreationHelper; import org.apache.poi.xssf.usermodel.XSSFRichTextString; @@ -72,6 +73,7 @@ public class SXSSFCreationHelper implements CreationHelper { * @deprecated POI 3.15 beta 3. Use {@link #createHyperlink(HyperlinkType)} instead. */ @Deprecated + @Removal(version="3.17") @Override public Hyperlink createHyperlink(int type) { return helper.createHyperlink(type); diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java index ac72bc7f3..0ac776ffc 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java @@ -72,24 +72,9 @@ public final class SXSSFFormulaEvaluator extends BaseXSSFFormulaEvaluator { return new SXSSFEvaluationCell((SXSSFCell)cell); } - /** - * If cell contains formula, it evaluates the formula, and - * puts the formula result back into the cell, in place - * of the old formula. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the same instance of SXSSFCell is returned to - * allow chained calls like: - *
-     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
-     * 
- * Be aware that your cell value will be changed to hold the - * result of the formula. If you simply want the formula - * value computed for you, use {@link #evaluateFormulaCellEnum(org.apache.poi.ss.usermodel.Cell)} } - */ + @Override public SXSSFCell evaluateInCell(Cell cell) { - doEvaluateInCell(cell); - return (SXSSFCell)cell; + return (SXSSFCell) super.evaluateInCell(cell); } /** diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java index c6c030b95..780126c3d 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java @@ -28,7 +28,7 @@ import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; -import org.apache.poi.util.Internal; +import org.apache.poi.ss.usermodel.RichTextString; /** * Internal POI use only - parent of XSSF and SXSSF formula evaluators @@ -37,6 +37,10 @@ public abstract class BaseXSSFFormulaEvaluator extends BaseFormulaEvaluator { protected BaseXSSFFormulaEvaluator(WorkbookEvaluator bookEvaluator) { super(bookEvaluator); } + @Override + protected RichTextString createRichTextString(String str) { + return new XSSFRichTextString(str); + } public void notifySetFormula(Cell cell) { _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell)); @@ -48,78 +52,6 @@ public abstract class BaseXSSFFormulaEvaluator extends BaseFormulaEvaluator { _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell)); } - /** - * If cell contains formula, it evaluates the formula, - * and saves the result of the formula. The cell - * remains as a formula cell. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the type of the formula result is returned, - * so you know what kind of value is also stored with - * the formula. - *
-     * CellType evaluatedCellType = evaluator.evaluateFormulaCellEnum(cell);
-     * 
- * Be aware that your cell will hold both the formula, - * and the result. If you want the cell replaced with - * the result of the formula, use {@link #evaluate(org.apache.poi.ss.usermodel.Cell)} } - * @param cell The cell to evaluate - * @return The type of the formula result (the cell's type remains as CellType.FORMULA however) - * If cell is not a formula cell, returns {@link CellType#_NONE} rather than throwing an exception. - * @since POI 3.15 beta 3 - * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. - */ - @Internal(since="POI 3.15 beta 3") - public CellType evaluateFormulaCellEnum(Cell cell) { - if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { - return CellType._NONE; - } - CellValue cv = evaluateFormulaCellValue(cell); - // cell remains a formula cell, but the cached value is changed - setCellValue(cell, cv); - return cv.getCellType(); - } - - /** - * If cell contains formula, it evaluates the formula, and - * puts the formula result back into the cell, in place - * of the old formula. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - */ - protected void doEvaluateInCell(Cell cell) { - if (cell == null) return; - if (cell.getCellTypeEnum() == CellType.FORMULA) { - CellValue cv = evaluateFormulaCellValue(cell); - setCellType(cell, cv); // cell will no longer be a formula cell - setCellValue(cell, cv); - } - } - - private static void setCellValue(Cell cell, CellValue cv) { - CellType cellType = cv.getCellType(); - switch (cellType) { - case BOOLEAN: - cell.setCellValue(cv.getBooleanValue()); - break; - case ERROR: - cell.setCellErrorValue(cv.getErrorValue()); - break; - case NUMERIC: - cell.setCellValue(cv.getNumberValue()); - break; - case STRING: - cell.setCellValue(new XSSFRichTextString(cv.getStringValue())); - break; - case BLANK: - // never happens - blanks eventually get translated to zero - case FORMULA: - // this will never happen, we have already evaluated the formula - default: - throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); - } - } - /** * Turns a XSSFCell / SXSSFCell into a XSSFEvaluationCell */ diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFBorderFormatting.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFBorderFormatting.java index e48fe948e..edea8cda9 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFBorderFormatting.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFBorderFormatting.java @@ -35,32 +35,87 @@ public class XSSFBorderFormatting implements BorderFormatting { _border = border; } + /** + * @deprecated POI 3.15. Use {@link #getBorderBottomEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderBottom() { + public short getBorderBottom() { + return getBorderBottomEnum().getCode(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderBottomEnum() { STBorderStyle.Enum ptrn = _border.isSetBottom() ? _border.getBottom().getStyle() : null; return ptrn == null ? BorderStyle.NONE : BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } + /** + * @deprecated POI 3.15. Use {@link #getBorderDiagonalEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderDiagonal() { + public short getBorderDiagonal() { + return getBorderDiagonalEnum().getCode(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderDiagonalEnum() { STBorderStyle.Enum ptrn = _border.isSetDiagonal() ? _border.getDiagonal().getStyle() : null; return ptrn == null ? BorderStyle.NONE : BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } + /** + * @deprecated POI 3.15. Use {@link #getBorderLeftEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderLeft() { + public short getBorderLeft() { + return getBorderLeftEnum().getCode(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderLeftEnum() { STBorderStyle.Enum ptrn = _border.isSetLeft() ? _border.getLeft().getStyle() : null; return ptrn == null ? BorderStyle.NONE : BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } + /** + * @deprecated POI 3.15. Use {@link #getBorderRightEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderRight() { + public short getBorderRight() { + return getBorderRightEnum().getCode(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderRightEnum() { STBorderStyle.Enum ptrn = _border.isSetRight() ? _border.getRight().getStyle() : null; return ptrn == null ? BorderStyle.NONE : BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } + /** + * @deprecated POI 3.15. Use {@link #getBorderTopEnum()}. + * This method will return an BorderStyle enum in the future. + */ @Override - public BorderStyle getBorderTop() { + public short getBorderTop() { + return getBorderTopEnum().getCode(); + } + /** + * @since POI 3.15 + */ + @Override + public BorderStyle getBorderTopEnum() { STBorderStyle.Enum ptrn = _border.isSetTop() ? _border.getTop().getStyle() : null; return ptrn == null ? BorderStyle.NONE : BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java index 4aa2f037e..c7932d167 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java @@ -668,6 +668,7 @@ public final class XSSFCell implements Cell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return the cell type + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCellType() { @@ -699,6 +700,7 @@ public final class XSSFCell implements Cell { * @return one of ({@link CellType#NUMERIC}, {@link CellType#STRING}, * {@link CellType#BOOLEAN}, {@link CellType#ERROR}) depending * on the cached value of the formula + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCachedFormulaResultType() { @@ -714,7 +716,6 @@ public final class XSSFCell implements Cell { * @deprecated POI 3.15 beta 3 * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCachedFormulaResultTypeEnum() { if (! isFormulaCell()) { @@ -826,7 +827,7 @@ public final class XSSFCell implements Cell { * @throws IllegalStateException if the cell type returned by {@link #getCellTypeEnum()} isn't {@link CellType#ERROR} * @see FormulaError */ - public String getErrorCellString() { + public String getErrorCellString() throws IllegalStateException { CellType cellType = getBaseCellType(true); if(cellType != CellType.ERROR) throw typeMismatch(CellType.ERROR, cellType, false); @@ -844,13 +845,16 @@ public final class XSSFCell implements Cell { * @see FormulaError */ @Override - public byte getErrorCellValue() { + public byte getErrorCellValue() throws IllegalStateException { String code = getErrorCellString(); if (code == null) { return 0; } - - return FormulaError.forString(code).getCode(); + try { + return FormulaError.forString(code).getCode(); + } catch (final IllegalArgumentException e) { + throw new IllegalStateException("Unexpected error code", e); + } } /** diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCellStyle.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCellStyle.java index 760c21537..9eec21ec6 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCellStyle.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCellStyle.java @@ -28,6 +28,7 @@ import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.VerticalAlignment; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.xssf.model.StylesTable; import org.apache.poi.xssf.model.ThemesTable; import org.apache.poi.xssf.usermodel.extensions.XSSFCellAlignment; @@ -251,11 +252,13 @@ public class XSSFCellStyle implements CellStyle { /** * Get the type of border to use for the bottom border of the cell + * Will be removed when {@link #getBorderBottom()} returns a BorderStyle enum * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} + * @since POI 3.15 */ @Override - public BorderStyle getBorderBottom() { + public BorderStyle getBorderBottomEnum() { if(!_cellXf.getApplyBorder()) return BorderStyle.NONE; int idx = (int)_cellXf.getBorderId(); @@ -266,24 +269,26 @@ public class XSSFCellStyle implements CellStyle { } return BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } - /** * Get the type of border to use for the bottom border of the cell + * This will return a BorderStyle enum in the future. * - * @return border type as Java enum - * @deprecated 3.15 beta 2. Use {@link #getBorderBottom} + * @return border type code + * @deprecated 3.15 beta 2. Use {@link #getBorderBottomEnum()} */ - public BorderStyle getBorderBottomEnum() { - return getBorderBottom(); + public short getBorderBottom() { + return getBorderBottomEnum().getCode(); } /** * Get the type of border to use for the left border of the cell + * Will be removed when {@link #getBorderLeft()} returns a BorderStyle enum * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} + * @since POI 3.15 */ @Override - public BorderStyle getBorderLeft() { + public BorderStyle getBorderLeftEnum() { if(!_cellXf.getApplyBorder()) return BorderStyle.NONE; int idx = (int)_cellXf.getBorderId(); @@ -297,21 +302,24 @@ public class XSSFCellStyle implements CellStyle { /** * Get the type of border to use for the left border of the cell + * This will return a BorderStyle enum in the future. * - * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} - * @deprecated 3.15 beta 2. Use {@link #getBorderLeft} + * @return border type code + * @deprecated 3.15 beta 2. Use {@link #getBorderLeftEnum()} */ - public BorderStyle getBorderLeftEnum() { - return getBorderLeft(); + public short getBorderLeft() { + return getBorderLeftEnum().getCode(); } /** * Get the type of border to use for the right border of the cell + * Will be removed when {@link #getBorderRight()} returns a BorderStyle enum * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} + * @since POI 3.15 */ @Override - public BorderStyle getBorderRight() { + public BorderStyle getBorderRightEnum() { if(!_cellXf.getApplyBorder()) return BorderStyle.NONE; int idx = (int)_cellXf.getBorderId(); @@ -322,24 +330,26 @@ public class XSSFCellStyle implements CellStyle { } return BorderStyle.valueOf((short)(ptrn.intValue() - 1)); } - /** * Get the type of border to use for the right border of the cell + * This will return a BorderStyle enum in the future. * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} - * @deprecated 3.15 beta 2. Use {@link #getBorderRight} + * @deprecated 3.15 beta 2. Use {@link #getBorderRightEnum()} instead */ - public BorderStyle getBorderRightEnum() { - return getBorderRight(); + public short getBorderRight() { + return getBorderRightEnum().getCode(); } /** * Get the type of border to use for the top border of the cell + * Will be removed when {@link #getBorderTop()} returns a BorderStyle enum * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} + * @since POI 3.15 */ @Override - public BorderStyle getBorderTop() { + public BorderStyle getBorderTopEnum() { if(!_cellXf.getApplyBorder()) return BorderStyle.NONE; int idx = (int)_cellXf.getBorderId(); @@ -350,15 +360,15 @@ public class XSSFCellStyle implements CellStyle { } return BorderStyle.valueOf((short) (ptrn.intValue() - 1)); } - /** * Get the type of border to use for the top border of the cell + * This will return a BorderStyle enum in the future. * * @return border type, default value is {@link org.apache.poi.ss.usermodel.BorderStyle#NONE} - * @deprecated 3.15 beta 2. Use {@link #getBorderTop} + * @deprecated 3.15 beta 2. Use {@link #getBorderTopEnum()} instead. */ - public BorderStyle getBorderTopEnum() { - return getBorderTop(); + public short getBorderTop() { + return getBorderTopEnum().getCode(); } /** @@ -775,6 +785,7 @@ public class XSSFCellStyle implements CellStyle { * @see org.apache.poi.ss.usermodel.CellStyle#ALIGN_CENTER_SELECTION * @deprecated POI 3.15 beta 3. Use {@link #setAlignment(HorizontalAlignment)} instead. */ + @Removal(version="3.17") @Override public void setAlignment(short align) { setAlignment(HorizontalAlignment.forInt(align)); @@ -796,6 +807,7 @@ public class XSSFCellStyle implements CellStyle { * @param border the type of border to use * @deprecated 3.15 beta 2. Use {@link #setBorderBottom(BorderStyle)} */ + @Removal(version="3.17") @Override public void setBorderBottom(short border) { setBorderBottom(BorderStyle.valueOf(border)); @@ -806,6 +818,7 @@ public class XSSFCellStyle implements CellStyle { * * @param border - type of border to use * @see org.apache.poi.ss.usermodel.BorderStyle + * @since POI 3.15 */ @Override public void setBorderBottom(BorderStyle border) { @@ -825,6 +838,7 @@ public class XSSFCellStyle implements CellStyle { * @param border the type of border to use * @deprecated 3.15 beta 2. Use {@link #setBorderLeft(BorderStyle)} */ + @Removal(version="3.17") @Override public void setBorderLeft(short border) { setBorderLeft(BorderStyle.valueOf(border)); @@ -834,6 +848,7 @@ public class XSSFCellStyle implements CellStyle { * Set the type of border to use for the left border of the cell * * @param border the type of border to use + * @since POI 3.15 */ @Override public void setBorderLeft(BorderStyle border) { @@ -854,6 +869,7 @@ public class XSSFCellStyle implements CellStyle { * @param border the type of border to use * @deprecated 3.15 beta 2. Use {@link #setBorderRight(BorderStyle)} */ + @Removal(version="3.17") @Override public void setBorderRight(short border) { setBorderRight(BorderStyle.valueOf(border)); @@ -863,6 +879,7 @@ public class XSSFCellStyle implements CellStyle { * Set the type of border to use for the right border of the cell * * @param border the type of border to use + * @since POI 3.15 */ @Override public void setBorderRight(BorderStyle border) { @@ -883,6 +900,7 @@ public class XSSFCellStyle implements CellStyle { * @param border the type of border to use * @deprecated 3.15 beta 2. Use {@link #setBorderTop(BorderStyle)} */ + @Removal(version="3.17") @Override public void setBorderTop(short border) { setBorderTop(BorderStyle.valueOf(border)); @@ -892,6 +910,7 @@ public class XSSFCellStyle implements CellStyle { * Set the type of border to use for the top border of the cell * * @param border the type of border to use + * @since POI 3.15 */ @Override public void setBorderTop(BorderStyle border) { @@ -1121,7 +1140,9 @@ public class XSSFCellStyle implements CellStyle { * @see #setFillBackgroundColor(short) * @see #setFillForegroundColor(short) * @param fp fill pattern (set to {@link org.apache.poi.ss.usermodel.CellStyle#SOLID_FOREGROUND} to fill w/foreground color) + * @deprecated POI 3.15. Use {@link #setFillPattern(FillPatternType)} instead. */ + @Removal(version="3.17") @Override public void setFillPattern(short fp) { setFillPattern(FillPatternType.forInt(fp)); @@ -1335,6 +1356,7 @@ public class XSSFCellStyle implements CellStyle { * @see org.apache.poi.ss.usermodel.VerticalAlignment * @deprecated POI 3.15 beta 3. Use {@link #setVerticalAlignment(VerticalAlignment)} instead. */ + @Removal(version="3.17") @Override public void setVerticalAlignment(short align) { setVerticalAlignment(VerticalAlignment.forInt(align)); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java index 8045912ab..df47fc227 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java @@ -19,6 +19,7 @@ package org.apache.poi.xssf.usermodel; import org.apache.poi.ss.usermodel.ClientAnchor; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; /** @@ -218,15 +219,28 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { /** * Sets the anchor type * @param anchorType the anchor type to set + * @since POI 3.14 */ @Override public void setAnchorType( AnchorType anchorType ) { this.anchorType = anchorType; } + /** + * Sets the anchor type + * @param anchorType the anchor type to set + * @deprecated POI 3.15. Use {@link #setAnchorType(AnchorType)} instead + */ + @Removal(version="3.17") + @Override + public void setAnchorType( int anchorType ) + { + this.anchorType = AnchorType.byId(anchorType); + } /** * Gets the anchor type + * Changed from returning an int to an enum in POI 3.14 beta 1. * @return the anchor type */ @Override diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCreationHelper.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCreationHelper.java index 63dc78069..cb11a73b2 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCreationHelper.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCreationHelper.java @@ -20,6 +20,7 @@ import org.apache.poi.common.usermodel.HyperlinkType; import org.apache.poi.ss.usermodel.CreationHelper; import org.apache.poi.ss.usermodel.Hyperlink; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; public class XSSFCreationHelper implements CreationHelper { private final XSSFWorkbook workbook; @@ -61,6 +62,7 @@ public class XSSFCreationHelper implements CreationHelper { * @deprecated POI 3.15 beta 3. Use {@link #createHyperlink(HyperlinkType)} instead. */ @Deprecated + @Removal(version="3.17") @Override public XSSFHyperlink createHyperlink(int type) { return new XSSFHyperlink(type); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java index e85b58017..129052e79 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java @@ -58,6 +58,7 @@ final class XSSFEvaluationCell implements EvaluationCell { * For forwards compatibility, do not hard-code cell type literals in your code. * * @return cell type + * @deprecated 3.15. Will return a {@link CellType} enum in the future. */ @Override public int getCellType() { @@ -68,7 +69,6 @@ final class XSSFEvaluationCell implements EvaluationCell { * @deprecated POI 3.15 beta 3. * Will be deleted when we make the CellType enum transition. See bug 59791. */ - @Internal(since="POI 3.15 beta 3") @Override public CellType getCellTypeEnum() { return _cell.getCellTypeEnum(); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java index c25dfb5d6..7d9d0286f 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java @@ -67,7 +67,28 @@ final class XSSFEvaluationSheet implements EvaluationSheet { } } - return _cellCache.get(new CellKey(rowIndex, columnIndex)); + final CellKey key = new CellKey(rowIndex, columnIndex); + EvaluationCell evalcell = _cellCache.get(key); + + // If cache is stale, update cache with this one cell + // This is a compromise between rebuilding the entire cache + // (which would quickly defeat the benefit of the cache) + // and not caching at all. + // See bug 59958: Add cells on the fly to the evaluation sheet cache on cache miss + if (evalcell == null) { + XSSFRow row = _xs.getRow(rowIndex); + if (row == null) { + return null; + } + XSSFCell cell = row.getCell(columnIndex); + if (cell == null) { + return null; + } + evalcell = new XSSFEvaluationCell(cell, this); + _cellCache.put(key, evalcell); + } + + return evalcell; } private static class CellKey { diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java index fc456b7e5..e63f47b46 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java @@ -23,6 +23,8 @@ import org.apache.poi.ss.formula.IStabilityClassifier; import org.apache.poi.ss.formula.WorkbookEvaluator; import org.apache.poi.ss.formula.udf.UDFFinder; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.CellValue; /** * Evaluates formula cells.

@@ -55,27 +57,6 @@ public final class XSSFFormulaEvaluator extends BaseXSSFFormulaEvaluator { return new XSSFFormulaEvaluator(workbook, stabilityClassifier, udfFinder); } - /** - * If cell contains formula, it evaluates the formula, and - * puts the formula result back into the cell, in place - * of the old formula. - * Else if cell does not contain formula, this method leaves - * the cell unchanged. - * Note that the same instance of XSSFCell is returned to - * allow chained calls like: - *

-     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
-     * 
- * Be aware that your cell value will be changed to hold the - * result of the formula. If you simply want the formula - * value computed for you, use {@link #evaluateFormulaCellEnum(org.apache.poi.ss.usermodel.Cell)} } - * @param cell - */ - public XSSFCell evaluateInCell(Cell cell) { - doEvaluateInCell(cell); - return (XSSFCell)cell; - } - /** * Loops over all cells in all sheets of the supplied * workbook. @@ -90,6 +71,12 @@ public final class XSSFFormulaEvaluator extends BaseXSSFFormulaEvaluator { public static void evaluateAllFormulaCells(XSSFWorkbook wb) { BaseFormulaEvaluator.evaluateAllFormulaCells(wb); } + + @Override + public XSSFCell evaluateInCell(Cell cell) { + return (XSSFCell) super.evaluateInCell(cell); + } + /** * Loops over all cells in all sheets of the supplied * workbook. diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFHyperlink.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFHyperlink.java index 1f1c8421f..9d26a6658 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFHyperlink.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFHyperlink.java @@ -168,6 +168,7 @@ public class XSSFHyperlink implements Hyperlink { * @return the type of this hyperlink * @see HyperlinkType#forInt * @deprecated POI 3.15 beta 3. Use {@link #getTypeEnum()} instead. + * getType will return a HyperlinkType enum in the future. */ @Override public int getType() { diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java index 9f6864919..bf85f65de 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java @@ -30,6 +30,7 @@ import javax.xml.namespace.QName; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataConsolidateFunction; @@ -213,24 +214,43 @@ public class XSSFPivotTable extends POIXMLDocumentPart { } protected AreaReference getPivotArea() { - AreaReference pivotArea = new AreaReference(getPivotCacheDefinition(). - getCTPivotCacheDefinition().getCacheSource().getWorksheetSource().getRef()); + AreaReference pivotArea = new AreaReference( + getPivotCacheDefinition() + .getCTPivotCacheDefinition() + .getCacheSource() + .getWorksheetSource() + .getRef(), + SpreadsheetVersion.EXCEL2007); return pivotArea; } + + /** + * Verify column index (relative to first column in pivot area) is within the + * pivot area + * + * @param columnIndex + * @throws IndexOutOfBoundsException + */ + private void checkColumnIndex(int columnIndex) throws IndexOutOfBoundsException { + AreaReference pivotArea = getPivotArea(); + int size = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol() + 1; + + if (columnIndex < 0 || columnIndex >= size) { + throw new IndexOutOfBoundsException("Column Index: " + columnIndex + ", Size: " + size); + } + } /** * Add a row label using data from the given column. - * @param columnIndex the index of the column to be used as row label. + * @param columnIndex the index of the source column to be used as row label. + * {@code columnIndex} is 0-based indexed and relative to the first column in the source. */ @Beta public void addRowLabel(int columnIndex) { + checkColumnIndex(columnIndex); + AreaReference pivotArea = getPivotArea(); - int lastRowIndex = pivotArea.getLastCell().getRow() - pivotArea.getFirstCell().getRow(); - int lastColIndex = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol(); - - if(columnIndex > lastColIndex) { - throw new IndexOutOfBoundsException(); - } + final int lastRowIndex = pivotArea.getLastCell().getRow() - pivotArea.getFirstCell().getRow(); CTPivotFields pivotFields = pivotTableDefinition.getPivotFields(); CTPivotField pivotField = CTPivotField.Factory.newInstance(); @@ -238,7 +258,7 @@ public class XSSFPivotTable extends POIXMLDocumentPart { pivotField.setAxis(STAxis.AXIS_ROW); pivotField.setShowAll(false); - for(int i = 0; i <= lastRowIndex; i++) { + for (int i = 0; i <= lastRowIndex; i++) { items.addNewItem().setT(STItemType.DEFAULT); } items.setCount(items.sizeOfItemArray()); @@ -270,7 +290,8 @@ public class XSSFPivotTable extends POIXMLDocumentPart { /** * Add a column label using data from the given column and specified function - * @param columnIndex the index of the column to be used as column label. + * @param columnIndex the index of the source column to be used as column label. + * {@code columnIndex} is 0-based indexed and relative to the first column in the source. * @param function the function to be used on the data * The following functions exists: * Sum, Count, Average, Max, Min, Product, Count numbers, StdDev, StdDevp, Var, Varp @@ -278,12 +299,7 @@ public class XSSFPivotTable extends POIXMLDocumentPart { */ @Beta public void addColumnLabel(DataConsolidateFunction function, int columnIndex, String valueFieldName) { - AreaReference pivotArea = getPivotArea(); - int lastColIndex = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol(); - - if(columnIndex > lastColIndex && columnIndex < 0) { - throw new IndexOutOfBoundsException(); - } + checkColumnIndex(columnIndex); addDataColumn(columnIndex, true); addDataField(function, columnIndex, valueFieldName); @@ -303,7 +319,8 @@ public class XSSFPivotTable extends POIXMLDocumentPart { /** * Add a column label using data from the given column and specified function - * @param columnIndex the index of the column to be used as column label. + * @param columnIndex the index of the source column to be used as column label + * {@code columnIndex} is 0-based indexed and relative to the first column in the source.. * @param function the function to be used on the data * The following functions exists: * Sum, Count, Average, Max, Min, Product, Count numbers, StdDev, StdDevp, Var, Varp @@ -323,12 +340,10 @@ public class XSSFPivotTable extends POIXMLDocumentPart { */ @Beta private void addDataField(DataConsolidateFunction function, int columnIndex, String valueFieldName) { + checkColumnIndex(columnIndex); + AreaReference pivotArea = getPivotArea(); - int lastColIndex = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol(); - - if(columnIndex > lastColIndex && columnIndex < 0) { - throw new IndexOutOfBoundsException(); - } + CTDataFields dataFields; if(pivotTableDefinition.getDataFields() != null) { dataFields = pivotTableDefinition.getDataFields(); @@ -352,11 +367,8 @@ public class XSSFPivotTable extends POIXMLDocumentPart { */ @Beta public void addDataColumn(int columnIndex, boolean isDataField) { - AreaReference pivotArea = getPivotArea(); - int lastColIndex = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol(); - if(columnIndex > lastColIndex && columnIndex < 0) { - throw new IndexOutOfBoundsException(); - } + checkColumnIndex(columnIndex); + CTPivotFields pivotFields = pivotTableDefinition.getPivotFields(); CTPivotField pivotField = CTPivotField.Factory.newInstance(); @@ -371,13 +383,11 @@ public class XSSFPivotTable extends POIXMLDocumentPart { */ @Beta public void addReportFilter(int columnIndex) { + checkColumnIndex(columnIndex); + AreaReference pivotArea = getPivotArea(); - int lastColIndex = pivotArea.getLastCell().getCol() - pivotArea.getFirstCell().getCol(); int lastRowIndex = pivotArea.getLastCell().getRow() - pivotArea.getFirstCell().getRow(); - if(columnIndex > lastColIndex && columnIndex < 0) { - throw new IndexOutOfBoundsException(); - } CTPivotFields pivotFields = pivotTableDefinition.getPivotFields(); CTPivotField pivotField = CTPivotField.Factory.newInstance(); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index 5f1da12ec..d72dca445 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -591,20 +591,21 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } } else { //search the referenced drawing in the list of the sheet's relations + final String id = ctDrawing.getId(); for (RelationPart rp : getRelationParts()){ POIXMLDocumentPart p = rp.getDocumentPart(); if(p instanceof XSSFVMLDrawing) { XSSFVMLDrawing dr = (XSSFVMLDrawing)p; String drId = rp.getRelationship().getId(); - if(drId.equals(ctDrawing.getId())){ + if (drId.equals(id)) { drawing = dr; break; } - break; + // do not break here since drawing has not been found yet (see bug 52425) } } if(drawing == null){ - logger.log(POILogger.ERROR, "Can't find VML drawing with id=" + ctDrawing.getId() + " in the list of the sheet's relationships"); + logger.log(POILogger.ERROR, "Can't find VML drawing with id=" + id + " in the list of the sheet's relationships"); } } return drawing; @@ -4164,9 +4165,9 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { * @return The pivot table */ @Beta - public XSSFPivotTable createPivotTable(AreaReference source, CellReference position, Sheet sourceSheet){ - - if(source.getFirstCell().getSheetName() != null && !source.getFirstCell().getSheetName().equals(sourceSheet.getSheetName())) { + public XSSFPivotTable createPivotTable(AreaReference source, CellReference position, Sheet sourceSheet) { + final String sourceSheetName = source.getFirstCell().getSheetName(); + if(sourceSheetName != null && !sourceSheetName.equalsIgnoreCase(sourceSheet.getSheetName())) { throw new IllegalArgumentException("The area is referenced in another sheet than the " + "defined source sheet " + sourceSheet.getSheetName() + "."); } @@ -4192,8 +4193,10 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { */ @Beta public XSSFPivotTable createPivotTable(AreaReference source, CellReference position){ - if(source.getFirstCell().getSheetName() != null && !source.getFirstCell().getSheetName().equals(this.getSheetName())) { - return createPivotTable(source, position, getWorkbook().getSheet(source.getFirstCell().getSheetName())); + final String sourceSheetName = source.getFirstCell().getSheetName(); + if(sourceSheetName != null && !sourceSheetName.equalsIgnoreCase(this.getSheetName())) { + final XSSFSheet sourceSheet = getWorkbook().getSheet(sourceSheetName); + return createPivotTable(source, position, sourceSheet); } return createPivotTable(source, position, this); } diff --git a/src/ooxml/testcases/org/apache/poi/TestPOIXMLDocument.java b/src/ooxml/testcases/org/apache/poi/TestPOIXMLDocument.java index 9d25d8e05..16b28a450 100644 --- a/src/ooxml/testcases/org/apache/poi/TestPOIXMLDocument.java +++ b/src/ooxml/testcases/org/apache/poi/TestPOIXMLDocument.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; import java.io.File; import java.io.FileOutputStream; @@ -33,9 +34,11 @@ import java.util.HashSet; import java.util.List; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; +import org.apache.poi.util.NullOutputStream; import org.apache.poi.util.PackageHelper; import org.apache.poi.util.TempFile; import org.junit.Test; @@ -120,12 +123,43 @@ public final class TestPOIXMLDocument { FileOutputStream out = new FileOutputStream(tmp); doc.write(out); out.close(); + + // Should not be able to write to an output stream that has been closed + try { + doc.write(out); + fail("Should not be able to write to an output stream that has been closed."); + } catch (final OpenXML4JRuntimeException e) { + // FIXME: A better exception class (IOException?) and message should be raised + // indicating that the document could not be written because the output stream is closed. + // see {@link org.apache.poi.openxml4j.opc.ZipPackage#saveImpl(java.io.OutputStream)} + if (e.getMessage().matches("Fail to save: an error occurs while saving the package : The part .+ fail to be saved in the stream with marshaller .+")) { + // expected + } else { + throw e; + } + } + + // Should not be able to write a document that has been closed doc.close(); + try { + doc.write(new NullOutputStream()); + fail("Should not be able to write a document that has been closed."); + } catch (final IOException e) { + if (e.getMessage().equals("Cannot write data, document seems to have been closed already")) { + // expected + } else { + throw e; + } + } + + // Should be able to close a document multiple times, though subsequent closes will have no effect. + doc.close(); + @SuppressWarnings("resource") OPCPackage pkg2 = OPCPackage.open(tmp.getAbsolutePath()); + doc = new OPCParser(pkg1); try { - doc = new OPCParser(pkg1); doc.parse(new TestFactory()); context = new HashMap(); traverse(doc, context); @@ -150,6 +184,7 @@ public final class TestPOIXMLDocument { } } finally { doc.close(); + pkg1.close(); pkg2.close(); } } diff --git a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java index 0aa022332..9d206f719 100644 --- a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java +++ b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java @@ -682,9 +682,12 @@ public class TestExtractorFactory { // Text try { ExtractorFactory.createExtractor(OPCPackage.open(txt.toString())); - fail(); + fail("TestExtractorFactory.testPackage() failed on " + txt.toString()); } catch(UnsupportedFileFormatException e) { // Good + } catch (Exception e) { + System.out.println("TestExtractorFactory.testPackage() failed on " + txt.toString()); + throw e; } } @@ -942,6 +945,7 @@ public class TestExtractorFactory { "openxml4j/OPCCompliance_CoreProperties_OnlyOneCorePropertiesPartFAIL.docx", "openxml4j/OPCCompliance_CoreProperties_UnauthorizedXMLLangAttributeFAIL.docx", "openxml4j/OPCCompliance_DerivedPartNameFAIL.docx", + "openxml4j/invalid.xlsx", "spreadsheet/54764-2.xlsx", // see TestXSSFBugs.bug54764() "spreadsheet/54764.xlsx", // see TestXSSFBugs.bug54764() "spreadsheet/Simple.xlsb", diff --git a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackage.java b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackage.java index d84ecab81..5f83bc52d 100644 --- a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackage.java +++ b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackage.java @@ -943,4 +943,22 @@ public final class TestPackage { ZipSecureFile.setMaxTextSize(before); } } + + // bug 60128 + @Test + public void testCorruptFile() throws IOException { + OPCPackage pkg = null; + File file = OpenXML4JTestDataSamples.getSampleFile("invalid.xlsx"); + try { + pkg = OPCPackage.open(file, PackageAccess.READ); + } catch (Exception e) { + System.out.println(e.getClass().getName()); + System.out.println(e.getMessage()); + e.printStackTrace(); + } finally { + if (pkg != null) { + pkg.close(); + } + } + } } diff --git a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestZipPackage.java b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestZipPackage.java index 7f174e07a..0989d10cb 100644 --- a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestZipPackage.java +++ b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestZipPackage.java @@ -33,11 +33,14 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import org.apache.poi.POIDataSamples; import org.apache.poi.POITextExtractor; import org.apache.poi.POIXMLException; import org.apache.poi.extractor.ExtractorFactory; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.openxml4j.OpenXML4JTestDataSamples; +import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException; +import org.apache.poi.openxml4j.exceptions.ODFNotOfficeXmlFileException; import org.apache.poi.sl.usermodel.SlideShow; import org.apache.poi.sl.usermodel.SlideShowFactory; import org.apache.poi.ss.usermodel.Workbook; @@ -181,6 +184,7 @@ public class TestZipPackage { public void testClosingStreamOnException() throws IOException { InputStream is = OpenXML4JTestDataSamples.openSampleStream("dcterms_bug_56479.zip"); File tmp = File.createTempFile("poi-test-truncated-zip", ""); + // create a corrupted zip file by truncating a valid zip file to the first 100 bytes OutputStream os = new FileOutputStream(tmp); for (int i = 0; i < 100; i++) { os.write(is.read()); @@ -189,11 +193,63 @@ public class TestZipPackage { os.close(); is.close(); + // feed the corrupted zip file to OPCPackage try { OPCPackage.open(tmp, PackageAccess.READ); } catch (Exception e) { + // expected: the zip file is invalid + // this test does not care if open() throws an exception or not. } + // If the stream is not closed on exception, it will keep a file descriptor to tmp, + // and requests to the OS to delete the file will fail. assertTrue("Can't delete tmp file", tmp.delete()); + } + + /** + * If ZipPackage is passed an invalid file, a call to close + * (eg from the OPCPackage open method) should tidy up the + * stream / file the broken file is being read from. + * See bug #60128 for more + */ + @Test + public void testTidyStreamOnInvalidFile() throws Exception { + // Spreadsheet has a good mix of alternate file types + POIDataSamples files = POIDataSamples.getSpreadSheetInstance(); + + File[] notValidF = new File[] { + files.getFile("SampleSS.ods"), files.getFile("SampleSS.txt") + }; + InputStream[] notValidS = new InputStream[] { + files.openResourceAsStream("SampleSS.ods"), files.openResourceAsStream("SampleSS.txt") + }; + for (File notValid : notValidF) { + ZipPackage pkg = new ZipPackage(notValid, PackageAccess.READ); + assertNotNull(pkg.getZipArchive()); + assertFalse(pkg.getZipArchive().isClosed()); + try { + pkg.getParts(); + fail("Shouldn't work"); + } catch (ODFNotOfficeXmlFileException e) { + } catch (NotOfficeXmlFileException ne) {} + pkg.close(); + + assertNotNull(pkg.getZipArchive()); + assertTrue(pkg.getZipArchive().isClosed()); + } + for (InputStream notValid : notValidS) { + ZipPackage pkg = new ZipPackage(notValid, PackageAccess.READ); + assertNotNull(pkg.getZipArchive()); + assertFalse(pkg.getZipArchive().isClosed()); + try { + pkg.getParts(); + fail("Shouldn't work"); + } catch (ODFNotOfficeXmlFileException e) { + } catch (NotOfficeXmlFileException ne) {} + pkg.close(); + + assertNotNull(pkg.getZipArchive()); + assertTrue(pkg.getZipArchive().isClosed()); + } } } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java index 4d4c5df34..868a38227 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java @@ -149,10 +149,12 @@ public class TestSecureTempZip { static class AesZipFileZipEntrySource implements ZipEntrySource { final ZipFile zipFile; final Cipher ci; + boolean closed; AesZipFileZipEntrySource(ZipFile zipFile, Cipher ci) { this.zipFile = zipFile; this.ci = ci; + this.closed = false; } /** @@ -172,6 +174,12 @@ public class TestSecureTempZip { @Override public void close() throws IOException { zipFile.close(); + closed = true; + } + + @Override + public boolean isClosed() { + return closed; } } } diff --git a/src/ooxml/testcases/org/apache/poi/ss/formula/TestStructuredReferences.java b/src/ooxml/testcases/org/apache/poi/ss/formula/TestStructuredReferences.java index 4e176257f..b60664ad0 100644 --- a/src/ooxml/testcases/org/apache/poi/ss/formula/TestStructuredReferences.java +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/TestStructuredReferences.java @@ -110,7 +110,7 @@ public class TestStructuredReferences { private static void confirm(FormulaEvaluator fe, Cell cell, double expectedResult) { fe.clearAllCachedResultValues(); CellValue cv = fe.evaluate(cell); - if (cv.getCellType() != CellType.NUMERIC) { + if (cv.getCellTypeEnum() != CellType.NUMERIC) { fail("expected numeric cell type but got " + cv.formatAsString()); } assertEquals(expectedResult, cv.getNumberValue(), 0.0); @@ -119,7 +119,7 @@ public class TestStructuredReferences { private static void confirm(FormulaEvaluator fe, Cell cell, String expectedResult) { fe.clearAllCachedResultValues(); CellValue cv = fe.evaluate(cell); - if (cv.getCellType() != CellType.STRING) { + if (cv.getCellTypeEnum() != CellType.STRING) { fail("expected String cell type but got " + cv.formatAsString()); } assertEquals(expectedResult, cv.getStringValue()); diff --git a/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java index d86079eca..a24fd8879 100644 --- a/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java @@ -96,7 +96,7 @@ public final class TestProper { cell11.setCellFormula(formulaText); evaluator.clearAllCachedResultValues(); CellValue cv = evaluator.evaluate(cell11); - if (cv.getCellType() != CellType.STRING) { + if (cv.getCellTypeEnum() != CellType.STRING) { throw new AssertionFailedError("Wrong result type: " + cv.formatAsString()); } String actualValue = cv.getStringValue(); diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTableRow.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTableRow.java new file mode 100644 index 000000000..8a0892819 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTableRow.java @@ -0,0 +1,131 @@ +/* ==================================================================== + 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.xslf.usermodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.List; + +import org.apache.poi.xslf.XSLFTestDataSamples; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTableCell; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTableRow; + +public class TestXSLFTableRow { + + private static XMLSlideShow ppt; + private static XSLFTable tbl; + private static XSLFTableRow row; + + /** Copied from {@link TestXSLFTable#testRead()} */ + @Before + public void setUp() throws IOException { + ppt = XSLFTestDataSamples.openSampleDocument("shapes.pptx"); + + XSLFSlide slide = ppt.getSlides().get(3); + List shapes = slide.getShapes(); + tbl = (XSLFTable)shapes.get(0); + List rows = tbl.getRows(); + row = rows.get(0); + } + + @After + public void tearDown() throws IOException { + ppt.getPackage().revert(); + ppt.close(); + } + + + @Test + public void constructor() { + XSLFTableRow row2 = new XSLFTableRow(row.getXmlObject(), tbl); + assertSame(row.getXmlObject(), row2.getXmlObject()); + assertEquals(row.getHeight(), row2.getHeight(), 1e-16); + } + + @Test + public void testHeight() { + final double h = 10.0; + row.setHeight(h); + assertEquals(h, row.getHeight(), 1e-16); + } + + /** copied from {@link TestXSLFTable#testCreate()} */ + @Test + public void getCells() { + List cells = row.getCells(); + assertNotNull(cells); + assertEquals(3, cells.size()); + } + + @Test + public void testIterator() { + int i = 0; + for (XSLFTableCell cell : row) { + i++; + assertEquals("header"+i, cell.getText()); + } + assertEquals(3, i); + } + + /** copied from {@link TestXSLFTable#testCreate()} */ + @Test + public void addCell() { + XSLFTableCell cell = row.addCell(); + assertNotNull(cell); + + assertNotNull(cell.getXmlObject()); + // by default table cell has no borders + CTTableCell tc = (CTTableCell)cell.getXmlObject(); + assertTrue(tc.getTcPr().getLnB().isSetNoFill()); + assertTrue(tc.getTcPr().getLnT().isSetNoFill()); + assertTrue(tc.getTcPr().getLnL().isSetNoFill()); + assertTrue(tc.getTcPr().getLnR().isSetNoFill()); + } + + @Test + public void mergeCells() { + try { + row.mergeCells(0, 0); + fail("expected IllegalArgumentException when merging fewer than 2 columns"); + } catch (final IllegalArgumentException e) { + // expected + } + + row.mergeCells(0, 1); + List cells = row.getCells(); + //the top-left cell of a merged region is not regarded as merged + assertFalse("top-left cell of merged region", cells.get(0).isMerged()); + assertTrue("inside merged region", cells.get(1).isMerged()); + assertFalse("outside merged region", cells.get(2).isMerged()); + } + + @Test + public void getXmlObject() { + CTTableRow ctrow = row.getXmlObject(); + assertNotNull(ctrow); + } + +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbook.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbook.java index 67ebe1ef0..7b5a67de5 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbook.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbook.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -35,6 +36,8 @@ import java.util.Arrays; import org.apache.poi.POIDataSamples; import org.apache.poi.POITestCase; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; import org.apache.poi.ss.usermodel.BaseTestXWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; @@ -43,6 +46,7 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.ss.util.CellReference; +import org.apache.poi.util.NullOutputStream; import org.apache.poi.xssf.SXSSFITestDataProvider; import org.apache.poi.xssf.XSSFTestDataSamples; import org.apache.poi.xssf.model.SharedStringsTable; @@ -535,4 +539,40 @@ public final class TestSXSSFWorkbook extends BaseTestXWorkbook { swb.dispose(); swb.close(); } + + /** + * To avoid accident changes to the template, you should be able + * to create a SXSSFWorkbook from a read-only XSSF one, then + * change + save that (only). See bug #60010 + * TODO Fix this to work! + */ + @Test + @Ignore + public void createFromReadOnlyWorkbook() throws Exception { + File input = XSSFTestDataSamples.getSampleFile("sample.xlsx"); + OPCPackage pkg = OPCPackage.open(input, PackageAccess.READ); + XSSFWorkbook xssf = new XSSFWorkbook(pkg); + SXSSFWorkbook wb = new SXSSFWorkbook(xssf, 2); + + String sheetName = "Test SXSSF"; + Sheet s = wb.createSheet(sheetName); + for (int i=0; i<10; i++) { + Row r = s.createRow(i); + r.createCell(0).setCellValue(true); + r.createCell(1).setCellValue(2.4); + r.createCell(2).setCellValue("Test Row " + i); + } + assertEquals(10, s.getLastRowNum()); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + wb.write(bos); + wb.dispose(); + wb.close(); + + xssf = new XSSFWorkbook(new ByteArrayInputStream(bos.toByteArray())); + s = xssf.getSheet(sheetName); + assertEquals(10, s.getLastRowNum()); + assertEquals(true, s.getRow(0).getCell(0).getBooleanCellValue()); + assertEquals("Test Row 9", s.getRow(9).getCell(2).getStringCellValue()); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java index 96b363283..6ecad0a74 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java @@ -59,7 +59,7 @@ import org.junit.runners.Suite; TestXSSFSheetComments.class, TestColumnHelper.class, TestHeaderFooterHelper.class, - TestXSSFPivotTable.class, + //TestXSSFPivotTable.class, //converted to junit4 TestForkedEvaluator.class }) public final class AllXSSFUsermodelTests { diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestFormulaEvaluatorOnXSSF.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestFormulaEvaluatorOnXSSF.java index 4cbc7c4e6..ad8b49028 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestFormulaEvaluatorOnXSSF.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestFormulaEvaluatorOnXSSF.java @@ -205,14 +205,14 @@ public final class TestFormulaEvaluatorOnXSSF { final CellType expectedCellType = expValue.getCellTypeEnum(); switch (expectedCellType) { case BLANK: - assertEquals(msg, CellType.BLANK, actValue.getCellType()); + assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum()); break; case BOOLEAN: - assertEquals(msg, CellType.BOOLEAN, actValue.getCellType()); + assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum()); assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue()); break; case ERROR: - assertEquals(msg, CellType.ERROR, actValue.getCellType()); + assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum()); // if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values // assertEquals(msg, expValue.getErrorCellValue(), actValue.getErrorValue()); // } @@ -220,14 +220,14 @@ public final class TestFormulaEvaluatorOnXSSF { case FORMULA: // will never be used, since we will call method after formula evaluation fail("Cannot expect formula as result of formula evaluation: " + msg); case NUMERIC: - assertEquals(msg, CellType.NUMERIC, actValue.getCellType()); + assertEquals(msg, CellType.NUMERIC, actValue.getCellTypeEnum()); TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); // double delta = Math.abs(expValue.getNumericCellValue()-actValue.getNumberValue()); // double pctExpValue = Math.abs(0.00001*expValue.getNumericCellValue()); // assertTrue(msg, delta <= pctExpValue); break; case STRING: - assertEquals(msg, CellType.STRING, actValue.getCellType()); + assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum()); assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue()); break; default: diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java index f33d96e63..b8926a3cf 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java @@ -188,14 +188,14 @@ public final class TestMultiSheetFormulaEvaluatorOnXSSF { final CellType expectedCellType = expValue.getCellTypeEnum(); switch (expectedCellType) { case BLANK: - assertEquals(msg, CellType.BLANK, actValue.getCellType()); + assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum()); break; case BOOLEAN: - assertEquals(msg, CellType.BOOLEAN, actValue.getCellType()); + assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum()); assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue()); break; case ERROR: - assertEquals(msg, CellType.ERROR, actValue.getCellType()); + assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum()); // if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values // assertEquals(msg, expected.getErrorCellValue(), actual.getErrorValue()); // } @@ -203,14 +203,14 @@ public final class TestMultiSheetFormulaEvaluatorOnXSSF { case FORMULA: // will never be used, since we will call method after formula evaluation fail("Cannot expect formula as result of formula evaluation: " + msg); case NUMERIC: - assertEquals(msg, CellType.NUMERIC, actValue.getCellType()); + assertEquals(msg, CellType.NUMERIC, actValue.getCellTypeEnum()); TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); // double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); // double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); // assertTrue(msg, delta <= pctExpected); break; case STRING: - assertEquals(msg, CellType.STRING, actValue.getCellType()); + assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum()); assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue()); break; default: diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java index 2be21e830..e695f385f 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java @@ -304,7 +304,7 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { if(c.getCellTypeEnum() == CellType.FORMULA) { CellValue cv = eval.evaluate(c); - if(cv.getCellType() == CellType.NUMERIC) { + if(cv.getCellTypeEnum() == CellType.NUMERIC) { // assert that the calculated value agrees with // the cached formula result calculated by Excel String formula = c.getCellFormula(); @@ -2187,7 +2187,7 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { assertEquals("E4+E5", cell.getCellFormula()); CellValue value = evaluator.evaluate(cell); - assertEquals(CellType.ERROR, value.getCellType()); + assertEquals(CellType.ERROR, value.getCellTypeEnum()); assertEquals(-60, value.getErrorValue()); assertEquals("~CIRCULAR~REF~", FormulaError.forInt(value.getErrorValue()).getString()); assertEquals("CIRCULAR_REF", FormulaError.forInt(value.getErrorValue()).toString()); diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFCellStyle.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFCellStyle.java index 94e40a3a7..c907d40db 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFCellStyle.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFCellStyle.java @@ -104,11 +104,11 @@ public class TestXSSFCellStyle { @Test public void testGetSetBorderBottom() { //default values - assertEquals(BorderStyle.NONE, cellStyle.getBorderBottom()); + assertEquals(BorderStyle.NONE, cellStyle.getBorderBottomEnum()); int num = stylesTable.getBorders().size(); cellStyle.setBorderBottom(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderBottom()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderBottomEnum()); //a new border has been added assertEquals(num + 1, stylesTable.getBorders().size()); //id of the created border @@ -122,7 +122,7 @@ public class TestXSSFCellStyle { //setting the same border multiple times should not change borderId for (int i = 0; i < 3; i++) { cellStyle.setBorderBottom(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderBottom()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderBottomEnum()); } assertEquals(borderId, cellStyle.getCoreXf().getBorderId()); assertEquals(num, stylesTable.getBorders().size()); @@ -139,11 +139,11 @@ public class TestXSSFCellStyle { @Test public void testGetSetBorderRight() { //default values - assertEquals(BorderStyle.NONE, cellStyle.getBorderRight()); + assertEquals(BorderStyle.NONE, cellStyle.getBorderRightEnum()); int num = stylesTable.getBorders().size(); cellStyle.setBorderRight(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderRight()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderRightEnum()); //a new border has been added assertEquals(num + 1, stylesTable.getBorders().size()); //id of the created border @@ -157,7 +157,7 @@ public class TestXSSFCellStyle { //setting the same border multiple times should not change borderId for (int i = 0; i < 3; i++) { cellStyle.setBorderRight(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderRight()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderRightEnum()); } assertEquals(borderId, cellStyle.getCoreXf().getBorderId()); assertEquals(num, stylesTable.getBorders().size()); @@ -174,11 +174,11 @@ public class TestXSSFCellStyle { @Test public void testGetSetBorderLeft() { //default values - assertEquals(BorderStyle.NONE, cellStyle.getBorderLeft()); + assertEquals(BorderStyle.NONE, cellStyle.getBorderLeftEnum()); int num = stylesTable.getBorders().size(); cellStyle.setBorderLeft(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderLeft()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderLeftEnum()); //a new border has been added assertEquals(num + 1, stylesTable.getBorders().size()); //id of the created border @@ -192,7 +192,7 @@ public class TestXSSFCellStyle { //setting the same border multiple times should not change borderId for (int i = 0; i < 3; i++) { cellStyle.setBorderLeft(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderLeft()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderLeftEnum()); } assertEquals(borderId, cellStyle.getCoreXf().getBorderId()); assertEquals(num, stylesTable.getBorders().size()); @@ -209,11 +209,11 @@ public class TestXSSFCellStyle { @Test public void testGetSetBorderTop() { //default values - assertEquals(BorderStyle.NONE, cellStyle.getBorderTop()); + assertEquals(BorderStyle.NONE, cellStyle.getBorderTopEnum()); int num = stylesTable.getBorders().size(); cellStyle.setBorderTop(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderTop()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderTopEnum()); //a new border has been added assertEquals(num + 1, stylesTable.getBorders().size()); //id of the created border @@ -227,7 +227,7 @@ public class TestXSSFCellStyle { //setting the same border multiple times should not change borderId for (int i = 0; i < 3; i++) { cellStyle.setBorderTop(BorderStyle.MEDIUM); - assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderTop()); + assertEquals(BorderStyle.MEDIUM, cellStyle.getBorderTopEnum()); } assertEquals(borderId, cellStyle.getCoreXf().getBorderId()); assertEquals(num, stylesTable.getBorders().size()); @@ -243,7 +243,7 @@ public class TestXSSFCellStyle { private void testGetSetBorderXMLBean(BorderStyle border, STBorderStyle.Enum expected) { cellStyle.setBorderTop(border); - assertEquals(border, cellStyle.getBorderTop()); + assertEquals(border, cellStyle.getBorderTopEnum()); int borderId = (int)cellStyle.getCoreXf().getBorderId(); assertTrue(borderId > 0); //check changes in the underlying xml bean @@ -256,7 +256,7 @@ public class TestXSSFCellStyle { @Test public void testGetSetBorderNone() { cellStyle.setBorderTop(BorderStyle.NONE); - assertEquals(BorderStyle.NONE, cellStyle.getBorderTop()); + assertEquals(BorderStyle.NONE, cellStyle.getBorderTopEnum()); int borderId = (int)cellStyle.getCoreXf().getBorderId(); assertTrue(borderId > 0); //check changes in the underlying xml bean @@ -562,10 +562,10 @@ public class TestXSSFCellStyle { assertEquals(style2.getRightBorderColor(), style1.getRightBorderColor()); assertEquals(style2.getBottomBorderColor(), style1.getBottomBorderColor()); - assertEquals(style2.getBorderBottom(), style1.getBorderBottom()); - assertEquals(style2.getBorderLeft(), style1.getBorderLeft()); - assertEquals(style2.getBorderRight(), style1.getBorderRight()); - assertEquals(style2.getBorderTop(), style1.getBorderTop()); + assertEquals(style2.getBorderBottomEnum(), style1.getBorderBottomEnum()); + assertEquals(style2.getBorderLeftEnum(), style1.getBorderLeftEnum()); + assertEquals(style2.getBorderRightEnum(), style1.getBorderRightEnum()); + assertEquals(style2.getBorderTopEnum(), style1.getBorderTopEnum()); wb2.close(); } @@ -999,7 +999,7 @@ public class TestXSSFCellStyle { Workbook copy = XSSFTestDataSamples.writeOutAndReadBack(target); // previously this failed because the border-element was not copied over - copy.getCellStyleAt((short)1).getBorderBottom(); + copy.getCellStyleAt((short)1).getBorderBottomEnum(); copy.close(); diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java index 6dcaef960..84a4aa680 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java @@ -682,4 +682,15 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { value = evaluator.evaluate(cell); assertEquals(1, value.getNumberValue(), 0.001); } + + @Test + public void evaluateInCellReturnsSameDataType() throws IOException { + XSSFWorkbook wb = new XSSFWorkbook(); + wb.createSheet().createRow(0).createCell(0); + XSSFFormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + XSSFCell cell = wb.getSheetAt(0).getRow(0).getCell(0); + XSSFCell same = evaluator.evaluateInCell(cell); + assertSame(cell, same); + wb.close(); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java index 55fe4a4fa..851ca33d6 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java @@ -16,30 +16,39 @@ ==================================================================== */ package org.apache.poi.xssf.usermodel; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.io.IOException; + import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataConsolidateFunction; import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; +import org.apache.poi.xssf.XSSFITestDataProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPageField; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPageFields; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPivotFields; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPivotTableDefinition; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STDataConsolidateFunction; -import junit.framework.TestCase; - -public class TestXSSFPivotTable extends TestCase { +public class TestXSSFPivotTable { + private static final XSSFITestDataProvider _testDataProvider = XSSFITestDataProvider.instance; + private XSSFWorkbook wb; private XSSFPivotTable pivotTable; private XSSFPivotTable offsetPivotTable; private Cell offsetOuterCell; - @Override + @Before public void setUp(){ - Workbook wb = new XSSFWorkbook(); - XSSFSheet sheet = (XSSFSheet) wb.createSheet(); + wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.createSheet(); Row row1 = sheet.createRow(0); // Create a cell and put a value in it. @@ -72,10 +81,10 @@ public class TestXSSFPivotTable extends TestCase { Cell cell12 = row1.createCell(3); cell12.setCellValue(12.12); - AreaReference source = new AreaReference("A1:C2"); + AreaReference source = new AreaReference("A1:C2", _testDataProvider.getSpreadsheetVersion()); pivotTable = sheet.createPivotTable(source, new CellReference("H5")); - XSSFSheet offsetSheet = (XSSFSheet) wb.createSheet(); + XSSFSheet offsetSheet = wb.createSheet(); Row tableRow_1 = offsetSheet.createRow(1); offsetOuterCell = tableRow_1.createCell(1); @@ -114,11 +123,19 @@ public class TestXSSFPivotTable extends TestCase { AreaReference offsetSource = new AreaReference(new CellReference("C2"), new CellReference("E4")); offsetPivotTable = offsetSheet.createPivotTable(offsetSource, new CellReference("C6")); } + + @After + public void tearDown() throws IOException { + XSSFWorkbook wb2 = _testDataProvider.writeOutAndReadBack(wb); + wb.close(); + wb2.close(); + } /** * Verify that when creating a row label it's created on the correct row * and the count is increased by one. */ + @Test public void testAddRowLabelToPivotTable() { int columnIndex = 0; @@ -141,6 +158,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's not possible to create a row label outside of the referenced area. */ + @Test public void testAddRowLabelOutOfRangeThrowsException() { int columnIndex = 5; @@ -155,6 +173,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that when creating one column label, no col fields are being created. */ + @Test public void testAddOneColumnLabelToPivotTableDoesNotCreateColField() { int columnIndex = 0; @@ -167,6 +186,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's possible to create three column labels with different DataConsolidateFunction */ + @Test public void testAddThreeDifferentColumnLabelsToPivotTable() { int columnOne = 0; int columnTwo = 1; @@ -184,6 +204,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's possible to create three column labels with the same DataConsolidateFunction */ + @Test public void testAddThreeSametColumnLabelsToPivotTable() { int columnOne = 0; int columnTwo = 1; @@ -200,6 +221,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that when creating two column labels, a col field is being created and X is set to -2. */ + @Test public void testAddTwoColumnLabelsToPivotTable() { int columnOne = 0; int columnTwo = 1; @@ -214,6 +236,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that a data field is created when creating a data column */ + @Test public void testColumnLabelCreatesDataField() { int columnIndex = 0; @@ -229,6 +252,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's possible to set a custom name when creating a data column */ + @Test public void testColumnLabelSetCustomName() { int columnIndex = 0; @@ -245,6 +269,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's not possible to create a column label outside of the referenced area. */ + @Test public void testAddColumnLabelOutOfRangeThrowsException() { int columnIndex = 5; @@ -260,6 +285,7 @@ public class TestXSSFPivotTable extends TestCase { * Verify when creating a data column set to a data field, the data field with the corresponding * column index will be set to true. */ + @Test public void testAddDataColumn() { int columnIndex = 0; boolean isDataField = true; @@ -272,6 +298,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's not possible to create a data column outside of the referenced area. */ + @Test public void testAddDataColumnOutOfRangeThrowsException() { int columnIndex = 5; boolean isDataField = true; @@ -301,6 +328,7 @@ public class TestXSSFPivotTable extends TestCase { /** * Verify that it's not possible to create a new filter outside of the referenced area. */ + @Test public void testAddReportFilterOutOfRangeThrowsException() { int columnIndex = 5; try { @@ -315,10 +343,28 @@ public class TestXSSFPivotTable extends TestCase { * Verify that the Pivot Table operates only within the referenced area, even when the * first column of the referenced area is not index 0. */ + @Test public void testAddDataColumnWithOffsetData() { offsetPivotTable.addColumnLabel(DataConsolidateFunction.SUM, 1); assertEquals(CellType.NUMERIC, offsetOuterCell.getCellTypeEnum()); offsetPivotTable.addColumnLabel(DataConsolidateFunction.SUM, 0); } + + @Test + public void testPivotTableSheetNamesAreCaseInsensitive() { + wb.setSheetName(0, "original"); + wb.setSheetName(1, "offset"); + XSSFSheet original = wb.getSheet("OriginaL"); + XSSFSheet offset = wb.getSheet("OffseT"); + // assume sheets are accessible via case-insensitive name + assertNotNull(original); + assertNotNull(offset); + + AreaReference source = new AreaReference("ORIGinal!A1:C2", _testDataProvider.getSpreadsheetVersion()); + // create a pivot table on the same sheet, case insensitive + original.createPivotTable(source, new CellReference("W1")); + // create a pivot table on a different sheet, case insensitive + offset.createPivotTable(source, new CellReference("W1")); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java index 689e999bc..3f7be066d 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java @@ -47,6 +47,9 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellCopyPolicy; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Drawing; import org.apache.poi.ss.usermodel.FormulaError; import org.apache.poi.ss.usermodel.IgnoredErrorType; import org.apache.poi.ss.usermodel.IndexedColors; @@ -1975,4 +1978,46 @@ public final class TestXSSFSheet extends BaseTestXSheet { wb.close(); } } + + /** + * See bug #52425 + */ + @Test + public void testInsertCommentsToClonedSheet() { + Workbook wb = XSSFTestDataSamples.openSampleWorkbook("52425.xlsx"); + CreationHelper helper = wb.getCreationHelper(); + Sheet sheet2 = wb.createSheet("Sheet 2"); + Sheet sheet3 = wb.cloneSheet(0); + wb.setSheetName(2, "Sheet 3"); + + // Adding Comment to new created Sheet 2 + addComments(helper, sheet2); + // Adding Comment to cloned Sheet 3 + addComments(helper, sheet3); + } + + private void addComments(CreationHelper helper, Sheet sheet) { + Drawing drawing = sheet.createDrawingPatriarch(); + + for (int i = 0; i < 2; i++) { + ClientAnchor anchor = helper.createClientAnchor(); + anchor.setCol1(0); + anchor.setRow1(0 + i); + anchor.setCol2(2); + anchor.setRow2(3 + i); + + Comment comment = drawing.createCellComment(anchor); + comment.setString(helper.createRichTextString("BugTesting")); + + Row row = sheet.getRow(0 + i); + if (row == null) + row = sheet.createRow(0 + i); + Cell cell = row.getCell(0); + if (cell == null) + cell = row.createCell(0); + + cell.setCellComment(comment); + } + + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java index 028735323..3e2cd5403 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java @@ -51,6 +51,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { // TODO - support shifting of page breaks } + /** Error occurred at FormulaShifter#rowMoveAreaPtg while shift rows upward. */ @Test public void testBug54524() throws IOException { XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("54524.xlsx"); @@ -66,6 +67,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { workbook.close(); } + /** negative row shift causes corrupted data or throws exception */ @Test public void testBug53798() throws IOException { // NOTE that for HSSF (.xls) negative shifts combined with positive ones do work as expected @@ -124,6 +126,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { } } + /** negative row shift causes corrupted data or throws exception */ @Test public void testBug53798a() throws IOException { Workbook wb = XSSFTestDataSamples.openSampleWorkbook("53798.xlsx"); @@ -152,6 +155,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { read.close(); } + /** Shifting rows with comment result - Unreadable content error and comment deletion */ @Test public void testBug56017() throws IOException { Workbook wb = XSSFTestDataSamples.openSampleWorkbook("56017.xlsx"); @@ -193,6 +197,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { wbBack.close(); } + /** Moving the active sheet and deleting the others results in a corrupted file */ @Test public void test57171() throws IOException { Workbook wb = XSSFTestDataSamples.openSampleWorkbook("57171_57163_57165.xlsx"); @@ -210,6 +215,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { wbRead.close(); } + /** Cannot delete an arbitrary sheet in an XLS workbook (only the last one) */ @Test public void test57163() throws IOException { Workbook wb = XSSFTestDataSamples.openSampleWorkbook("57171_57163_57165.xlsx"); @@ -314,6 +320,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { wb.close(); } + /** Failed to clone a sheet from an Excel 2010 */ @Test public void test57165() throws IOException { Workbook wb = XSSFTestDataSamples.openSampleWorkbook("57171_57163_57165.xlsx"); @@ -341,6 +348,7 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { } } + /** Shifting rows with cell comments only shifts comments from first such cell. Other cell comments not shifted */ @Test public void testBug57828_OnlyOneCommentShiftedInRow() throws IOException { XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("57828.xlsx"); diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java index ad0e5aa9a..a17c61143 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java @@ -804,6 +804,7 @@ public final class HSLFSlideShow implements SlideShow CellType.* + cell.setCellValue(5.0); + assertEquals(Cell.CELL_TYPE_NUMERIC, cell.getCellType()); + assertEquals(0, cell.getCellType()); //make sure that hard-coded int literals still work, even though users should be using the named constants + assertEquals(CellType.NUMERIC, cell.getCellTypeEnum()); // make sure old way and new way are compatible + + // make sure switch(int|Enum) still works. Cases must be statically resolvable in Java 6 ("constant expression required") + switch(cell.getCellType()) { + case Cell.CELL_TYPE_NUMERIC: + // expected + break; + case Cell.CELL_TYPE_STRING: + case Cell.CELL_TYPE_BOOLEAN: + case Cell.CELL_TYPE_ERROR: + case Cell.CELL_TYPE_FORMULA: + case Cell.CELL_TYPE_BLANK: + default: + fail("unexpected cell type: " + cell.getCellType()); + } + } } diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java index 5509cbf71..28cfb2cf7 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestConditionalFormatting.java @@ -363,10 +363,10 @@ public abstract class BaseTestConditionalFormatting { BorderFormatting r1bf = rule1.getBorderFormatting(); assertNotNull(r1bf); - assertEquals(BorderStyle.THIN, r1bf.getBorderBottom()); - assertEquals(BorderStyle.THICK,r1bf.getBorderTop()); - assertEquals(BorderStyle.DASHED,r1bf.getBorderLeft()); - assertEquals(BorderStyle.DOTTED,r1bf.getBorderRight()); + assertEquals(BorderStyle.THIN, r1bf.getBorderBottomEnum()); + assertEquals(BorderStyle.THICK,r1bf.getBorderTopEnum()); + assertEquals(BorderStyle.DASHED,r1bf.getBorderLeftEnum()); + assertEquals(BorderStyle.DOTTED,r1bf.getBorderRightEnum()); PatternFormatting r1pf = rule1.getPatternFormatting(); assertNotNull(r1pf); @@ -1021,19 +1021,19 @@ public abstract class BaseTestConditionalFormatting { for (BorderStyle border : BorderStyle.values()) { borderFmt.setBorderTop(border); - assertEquals(border, borderFmt.getBorderTop()); + assertEquals(border, borderFmt.getBorderTopEnum()); borderFmt.setBorderBottom(border); - assertEquals(border, borderFmt.getBorderBottom()); + assertEquals(border, borderFmt.getBorderBottomEnum()); borderFmt.setBorderLeft(border); - assertEquals(border, borderFmt.getBorderLeft()); + assertEquals(border, borderFmt.getBorderLeftEnum()); borderFmt.setBorderRight(border); - assertEquals(border, borderFmt.getBorderRight()); + assertEquals(border, borderFmt.getBorderRightEnum()); borderFmt.setBorderDiagonal(border); - assertEquals(border, borderFmt.getBorderDiagonal()); + assertEquals(border, borderFmt.getBorderDiagonalEnum()); } workbook.close(); @@ -1049,37 +1049,37 @@ public abstract class BaseTestConditionalFormatting { ConditionalFormattingRule rule1 = sheetCF.createConditionalFormattingRule(ComparisonOperator.EQUAL, "7"); BorderFormatting borderFmt = rule1.createBorderFormatting(); - assertEquals(BorderStyle.NONE, borderFmt.getBorderBottom()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderBottomEnum()); borderFmt.setBorderBottom(BorderStyle.DOTTED); - assertEquals(BorderStyle.DOTTED, borderFmt.getBorderBottom()); + assertEquals(BorderStyle.DOTTED, borderFmt.getBorderBottomEnum()); borderFmt.setBorderBottom(BorderStyle.NONE); - assertEquals(BorderStyle.NONE, borderFmt.getBorderBottom()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderBottomEnum()); borderFmt.setBorderBottom(BorderStyle.THICK); - assertEquals(BorderStyle.THICK, borderFmt.getBorderBottom()); + assertEquals(BorderStyle.THICK, borderFmt.getBorderBottomEnum()); - assertEquals(BorderStyle.NONE, borderFmt.getBorderTop()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderTopEnum()); borderFmt.setBorderTop(BorderStyle.DOTTED); - assertEquals(BorderStyle.DOTTED, borderFmt.getBorderTop()); + assertEquals(BorderStyle.DOTTED, borderFmt.getBorderTopEnum()); borderFmt.setBorderTop(BorderStyle.NONE); - assertEquals(BorderStyle.NONE, borderFmt.getBorderTop()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderTopEnum()); borderFmt.setBorderTop(BorderStyle.THICK); - assertEquals(BorderStyle.THICK, borderFmt.getBorderTop()); + assertEquals(BorderStyle.THICK, borderFmt.getBorderTopEnum()); - assertEquals(BorderStyle.NONE, borderFmt.getBorderLeft()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderLeftEnum()); borderFmt.setBorderLeft(BorderStyle.DOTTED); - assertEquals(BorderStyle.DOTTED, borderFmt.getBorderLeft()); + assertEquals(BorderStyle.DOTTED, borderFmt.getBorderLeftEnum()); borderFmt.setBorderLeft(BorderStyle.NONE); - assertEquals(BorderStyle.NONE, borderFmt.getBorderLeft()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderLeftEnum()); borderFmt.setBorderLeft(BorderStyle.THIN); - assertEquals(BorderStyle.THIN, borderFmt.getBorderLeft()); + assertEquals(BorderStyle.THIN, borderFmt.getBorderLeftEnum()); - assertEquals(BorderStyle.NONE, borderFmt.getBorderRight()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderRightEnum()); borderFmt.setBorderRight(BorderStyle.DOTTED); - assertEquals(BorderStyle.DOTTED, borderFmt.getBorderRight()); + assertEquals(BorderStyle.DOTTED, borderFmt.getBorderRightEnum()); borderFmt.setBorderRight(BorderStyle.NONE); - assertEquals(BorderStyle.NONE, borderFmt.getBorderRight()); + assertEquals(BorderStyle.NONE, borderFmt.getBorderRightEnum()); borderFmt.setBorderRight(BorderStyle.HAIR); - assertEquals(BorderStyle.HAIR, borderFmt.getBorderRight()); + assertEquals(BorderStyle.HAIR, borderFmt.getBorderRightEnum()); ConditionalFormattingRule [] cfRules = { rule1 }; @@ -1095,10 +1095,10 @@ public abstract class BaseTestConditionalFormatting { BorderFormatting r1fp = cf.getRule(0).getBorderFormatting(); assertNotNull(r1fp); - assertEquals(BorderStyle.THICK, r1fp.getBorderBottom()); - assertEquals(BorderStyle.THICK, r1fp.getBorderTop()); - assertEquals(BorderStyle.THIN, r1fp.getBorderLeft()); - assertEquals(BorderStyle.HAIR, r1fp.getBorderRight()); + assertEquals(BorderStyle.THICK, r1fp.getBorderBottomEnum()); + assertEquals(BorderStyle.THICK, r1fp.getBorderTopEnum()); + assertEquals(BorderStyle.THIN, r1fp.getBorderLeftEnum()); + assertEquals(BorderStyle.HAIR, r1fp.getBorderRightEnum()); workbook.close(); } diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluator.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluator.java index 8a16d3a64..c4922a7c8 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluator.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluator.java @@ -19,6 +19,7 @@ package org.apache.poi.ss.usermodel; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import java.io.IOException; @@ -326,4 +327,15 @@ public abstract class BaseTestFormulaEvaluator { wb.close(); } + + @Test + public void evaluateInCellReturnsSameCell() throws IOException { + Workbook wb = _testDataProvider.createWorkbook(); + wb.createSheet().createRow(0).createCell(0); + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + Cell cell = wb.getSheetAt(0).getRow(0).getCell(0); + Cell same = evaluator.evaluateInCell(cell); + assertSame(cell, same); + wb.close(); + } } \ No newline at end of file diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestWorkbook.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestWorkbook.java index 6290119f5..52f542281 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestWorkbook.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestWorkbook.java @@ -34,6 +34,7 @@ import java.util.Iterator; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.ss.ITestDataProvider; import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.util.NullOutputStream; import org.junit.Test; public abstract class BaseTestWorkbook { @@ -781,15 +782,6 @@ public abstract class BaseTestWorkbook { } } - protected static class NullOutputStream extends OutputStream { - public NullOutputStream() { - } - - @Override - public void write(int b) throws IOException { - } - } - @Test public void test58499() throws IOException { Workbook workbook = _testDataProvider.createWorkbook(); diff --git a/src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java b/src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java index 649bddf9b..50276ae28 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java +++ b/src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java @@ -597,6 +597,27 @@ public class TestDataFormatter { } } + @Test + public void testBoolean() throws IOException { + DataFormatter formatter = new DataFormatter(); + + // Create a spreadsheet with some TRUE/FALSE boolean values in it + Workbook wb = new HSSFWorkbook(); + try { + Sheet s = wb.createSheet(); + Row r = s.createRow(0); + Cell c = r.createCell(0); + + c.setCellValue(true); + assertEquals("TRUE", formatter.formatCellValue(c)); + + c.setCellValue(false); + assertEquals("FALSE", formatter.formatCellValue(c)); + } finally { + wb.close(); + } + } + /** * While we don't currently support using a locale code at * the start of a format string to format it differently, we diff --git a/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java b/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java index a63ce5a60..91c204da3 100644 --- a/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java +++ b/src/testcases/org/apache/poi/ss/util/BaseTestCellUtil.java @@ -99,11 +99,11 @@ public class BaseTestCellUtil { // A valid BorderStyle constant, as a Short CellUtil.setCellStyleProperty(c, CellUtil.BORDER_BOTTOM, BorderStyle.DASH_DOT.getCode()); - assertEquals(BorderStyle.DASH_DOT, c.getCellStyle().getBorderBottom()); + assertEquals(BorderStyle.DASH_DOT, c.getCellStyle().getBorderBottomEnum()); // A valid BorderStyle constant, as an Enum CellUtil.setCellStyleProperty(c, CellUtil.BORDER_TOP, BorderStyle.MEDIUM_DASH_DOT); - assertEquals(BorderStyle.MEDIUM_DASH_DOT, c.getCellStyle().getBorderTop()); + assertEquals(BorderStyle.MEDIUM_DASH_DOT, c.getCellStyle().getBorderTopEnum()); wb.close(); } diff --git a/src/testcases/org/apache/poi/util/NullOutputStream.java b/src/testcases/org/apache/poi/util/NullOutputStream.java new file mode 100644 index 000000000..ac76123a3 --- /dev/null +++ b/src/testcases/org/apache/poi/util/NullOutputStream.java @@ -0,0 +1,41 @@ +/* ==================================================================== + 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 java.io.OutputStream; + +/** + * Implementation of an OutputStream which does nothing, used + * to redirect stdout to avoid spamming the console with output + */ +public final class NullOutputStream extends OutputStream { + public NullOutputStream() { + } + + @Override + public void write(byte[] b, int off, int len) { + } + + @Override + public void write(int b) { + } + + @Override + public void write(byte[] b) { + } +} diff --git a/test-data/openxml4j/invalid.xlsx b/test-data/openxml4j/invalid.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..15cb0ecb3e219d1701294bfdf0fe3f5cb5d208e7 GIT binary patch literal 22 NcmWIWW@Tf*000g10H*)| literal 0 HcmV?d00001 diff --git a/test-data/slideshow/60003.ppt b/test-data/slideshow/60003.ppt new file mode 100644 index 0000000000000000000000000000000000000000..1a4cd2f48a81888a8fc96e26391e99978f45b03f GIT binary patch literal 764928 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

FHk&dMDNT*YRw%;lPkUv@$yx9~ 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%0AT< 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 literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/52425.xlsx b/test-data/spreadsheet/52425.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0659822f641489044f8688834d4691477ad54ce1 GIT binary patch literal 9439 zcmeHNg+VsMfk(gxAOTPS000%>+2g4_H#h)58xa7&1EAc~61BB) zGPZHjRdKg7c6`C&W^F~8jc^Z?1-J+M|G&%s@C=lw^xAv^<1|0t6clSw3w;}0RD?n* z?O_!oE%?!yuH#c0pUKsgyzHW)Ff~GwSSY{k2UCf-(%~uKJqA;mCrn68@Qg3=wzG}q zkIt@BjEMtF16&BDYv7II4sK@6T8>^lP}O46z{~MjBsGSY&|BnF*DQPirXDd>w_{yV zW_l%vg`}X(L(C|EI+i+Cv84to82c4k z9jnM)&xY_K3!9R`3XnYMdJEII+go^m;@=duS`|!l z3}c}z>?%4;VRaphtsGfder*3$)c;}a{L7=4#L6kOgE4~kq^^T|&Y-i=I3hBx!cvV? zD&DW2e#Nei$e|^fZKWs0Q33kHOZqf>U-!+-@K=cTD}Ee%S#w0A~k zpmRu+urFEcz;%J1Ku=R7rQJa;%~1@+bp>x_dgo~*Mi0a)aE4f(15xqvNP`IZQ+zeM zRiBx0 zo0l78I`B}t>l#^oJQ7Q5#k=ujmQU|drslYR&Nd|3{V?mqN3)9Kpg+UCgA=vAxM#oD zFN~sK4(7Z5sU#7>({>B68DWMg2@rq+=VrzFhn~3FI#?Rm+FJhbYk%qt9L%}FQ2w`< zl9+yrb})|kS>Sj7WG{Xfw7mG9sd$l^&Km$#6h%j3fJyAQkM5%1tdP``DU>p#Sw~uH zSHa~hdHnh_dC{2FtR4;Bvoh$usuy3gp8=%oiN9;EK6TKGi}UiE+%_qmju#Z_d4%0S zo|yfho|>FtyEXWF4s%b^h+JYVj4pCnnY!L1qVQYZEWRz3D^1a_6GK@=k7y5fZ00$d z^IUDCXt?WN2_>3K5h*5wGKU7WrazSY*nd92e-N7)sCG$euM=j8)yYrR;>BktNn6}q z*~z6q_<-+f<&fxi{Adp)!(N2F^ALCd02}t=Vf^?jGfLv*qGl*@g7y+FDR5ko(x?h z1M_~PpEDYtEUDGL;*x&Y?i^r{dtu?jHeQi)ZzhR0trw?7#IiTzqy`IJDCX%3W9w7d zSfk1&jS1z@?u}_&%bEAG*!iGv{G+!eMmVazwt5yG2MV8@cWUC$)Z#euAarShrEYd8 z<-&Ey^lu)til9m)6lmmsf}3WeK)JAb`iSfli`g?i`^*Q@j{hPvx6n#g&kbjS$X@W> z8zX|$d!u|&`d)orgok4R!h{178Y_q%+dS!z$ewVUR%}){{K)znJrtVSSe!bq1dOGg zrU>@Vwpnm^;DdqK3K8v3L=;afiIu(?`TEeAUoSpsk;$pkHc?8Q9|kETaa<XT)+C1!C|I=DPTZZA+ zU~E`{v4Qv(8ywAyjh!4>f9@bZ3`TgIaMUOmClF>=k^9f&MwHco4>hQY2>2m$KF@-XELlPd1iTqej$!z;hwgh^qDb(5D{gvuGRIbDZ1Uq=$DY%EaFak87*vL2Nq_X=fIzGOO#!*BRNXF*3?n0Bb^F4Hh#qfL4w=~-aeh+(#eYDm zHR+QAR%O6M%-1aUA+;KOnyJyUK>pv5kvcjT=}7^iOVt`Prz)Apro9z#sY1 zPfOusW^8TD`g8k}7TX#_VI-XREku`s7>=&jmL^uL2S^@PrZd}gR9m?#1=Yq;m8mfg zdK)2Bim`@o6;l&+(3(#ar;+ZjKfJN5ku!hR)B1&eV3MYE)h5-3l3AmjE$8C+c%zwf zK8}NkbvB~VG2ofJ`iM(05kr}O94<+m(X@*#7INOZEj!;>NH$ZOwX1Kvhxygb8A~=Z z?jkYo7r{lXf*lEw&ozC<0)8ePtmO~n7p4O8AouOk@6(ez z*zuxQm;$i+%a5NI{U3YgjBL1L!oL^7eb~DQY*<1*vE!a0F#5C}NicWVES!@_!m1dJ zS-c%9DyvTYWK}bKx7-N;K+Tnk?!XOukeP*RWFG?JmJ0nK^A4AG4&!0CsAHbm1C`Bv z-S2$Pd3=5~;E|UyIv)exKYl8JMUiJjH@zFI2Zd6yzVW&}oWlFI`k|X1J2R8-d}C#? zJq}sWq$WI)7nHI|rt5h%^+wlc`}$xdjVP+6e7QdI_9R%r`{Kfp7l%&EZSf*0%E#mK z{NnkW@84%>(3|42;mHJOA!~!Gp4(sbAfM&j+l(TyM|{Q&iKz^Zg>WZpl6S*;06H%y zE!v$cHkz~^p@d_)xqEjnG*IL@3Nz8@h8n=vbrbJYj%~hTbYL22Z+B;_#?Bmc{)!n8 zX=NA;f{4t#84}V69WWx+_g`~^I#~2s3d|Kv z;VBmaEb9pRd2EzdL8;xe+|Q;JLK|-7$37G}X^!zes%X*V&Cm73+Z6;Y@N+1X8M>K{ zqfheQc<0aW-s-e+HuryfKOLy(A4F8_T$vCE`&2nHF)>lc%KP16(dP#gEBLZ>(Mv{h}g_npy0yDD+B z_g!hnJ`V0el9Yk)@g15HU!r{<7GmOXLL2vl zl&u3DlpR2b*N*|Bo{iP!lvfyJk*R(jQY9=R!xGE~=}05H2I16DK!$zCao|5;Yh;%VKQ+;)^$s z5KVC5S_EFLET(u=MA|ovn;{N*4{As@wR@P&$g^p)3cthhhMmi4HW2xlhTg3+l)q6$ zX2?8SV@y_5)d*S@wvono2rSMH0hX|BsLdehn7yzpbdD|wL)?V}?@ z9oj=r4=G%x%gVSF*(VDG^^Av)skOO};;Uvo2wIl|9}6<6rh2!oK%1otH(WRKBKIbW zy;?OloGs7ZmWP1KE~iYB;L;_B#lIhjSHJgq6`CYzLnOh(!hYhLR}xZ;4TesyVS};Y zhUQhud~jn<$qP?nEO#DpZuk8s|H$;zWSlx z>)d&(1fWjo_yI7(WPa=GP!a@uBrD*c`^tmh{Z8xe;~Oqs{(ab$7qD!I<{$CRABoYg zDCcKdR1(`J4G)1uzRByrf%FPiwTSX=#gZiy6JfV8^#PfdobgNYb{X5S$E>_%ej6zRn=Av^Rgym@%*a9C z)!fF^k=4c8O6-qg2n*~uTt&%p28`oPu`P&lLdDu^&`4WSWDR=YHG{OJ7bRX1uU=Zh zKHh16x#e1!UPWg3Ia;da>Ug7NkEJ)bi|vw&_0o@mhu%|lOX5=8un3E9+fGTC=CbQB zih_>z8XdJUqp&KZe=B8CaB%(YNMe$c0@kOAN~l`q!mARZmmmEsjqPNEo0KA~=8cnM zLFk!>t5Ox7-AZd-xgF1nQ_@to^4lV4k2R?-jnZm45@PE##}S1kS1(`!?L=(&C*e0|D2rld=jsBXy!czt zd@JbE4!)>a=LtR!FtA!GZYaL2dTIis#)*4V36|dw&ZW_{ZJGjF?$cW2#JjN9u#cnVtBL{?ez{!zOxt+eINCTG8)Z&vGwk&8d7MoJfTyB0bx7ae8 z=v)!Ra9%jJB#BldfWy%~CgTkg|Sk_8S36Gg}K^%GYA6A7)YoJkz-tdwh+^Fh_e zP8Hz9L5+sg4_1RVYuh|6CFZ?{tCVxpp?i@CvvIAf%~d*4uN-NMNIS5(5+|teo_t#f zTFbLHzP~goklQs8)^y&OwieI#Rxt69^S4UtB0Gbd3FF`qEG)(RgM*Gv?pDT*KQfWr zcrDveFn;%Z%sE!#{0Hsv_;=E5rCZc;_X4V@4>sxKEeDc%TqMwz8XeR7Qy~Z;&9tW# zH@vrw6{E%ScE?iH`qCy`O(@{(N)>{*U<{u*f;luA!_X3x=dmW|Vwf#`^DR#!`0Zc% zk>akn>P2tT%Ttqpp5C)-DY8@>Nn*eg zJn)UruZ|thQm!5`;ks9KRPG(USB7~}s*34()~8wUEK@CRii!wdd!FzA|NaiMd7AjSfjdO}M zf*7$|$%{!9ZO^qI6uLZQ4u1tzAA=5E2&MOW(_QxRJCMaKM(qmNuQ%iRo-!{d_Rgaq znLsn1;nbn2yK-Kx@VWcSs#MW2*EZ&NV!Y|5FzeKW+Y#8k>>y9r!F?bQHQy*5DD%zV z<+RziB~Z}fasU3=%s``&hnCnJ!(>TQ=fj9*+BvIcH~SA$Hes|OerXgRiZ5S>O$&nD z(Vjb6b7`CMBk{g&6`Wz?OT#$4B1fEZ+<97sQML@;f89@cOej-%sMg;<$+!fbW+6(O zsS7ckUcMbANAf1ddjA%W;yp87JUchkqUg(4G;zNT3cvk)>GN+BJh!O7Gp6?@?%RpO z2>cbc(*HqZI|p-ElgZ(kv6ItJn|kLQ!eU@mpn1ur_Ubp&Y#Q4VP6V{?i zqQTR^3yLD(;lblY8zJ@auq7*+VOP3HfRM%5;VLkq4=7N+l)`TFn_RAJ-MA+Xmghf$ z<@q?Us-Tgrp`wGWog=Hho!x&LUH{esVKbK=+iwH&Y2s&ym)Q5tAgjl|pR$PY<^7m6 z$6h5E4Jgxup@`p457j%{l93K!rq?j$j0)ooX2*YiHAe_9#xbXARS(jDlfUY!d(pYO zH4{crBBWfVG|pQv2^amMcj!$A-6bFfLobGjRIP*tjd4)P^$VkRZjDUFc(;y{^m61? z&0}C$v-fTrZYxv&9_cAUneQ4kDdyBl9dJG8UBpBRHpQl>)JNs9%=l6{>`MIW_>ebL z>Nx~?6aDiNl2I*rOD6Bz&;)E>i!~$OkYbWmcJmQvl`@r#3QaMDQJxo4?h36M+r)>0 zI)#eiI4;z)E|SLGdtbMrQ5mXPDGq}qQ91kH8HHO5y*?c7rr!pjAS(D3as@f=@IBF8 zb->SE&`!priyq^WEWyOFsl;4v~ENZJM8jQ$kuodSIl`^!owuZHB9e-9NR1|*}bDM}Ru?_Yj(v_uw zw1L|90?A)W3F=weQu{xAP-^kj+l$j1A5COC+{Bf`63cLXOekdrpV^9&0QB0~V%n{G z-(G-!Tgbj`QZI>GJX0}N!x>K1_5Iobl3FqVfMXo5rnWUt^QT)0$C#TZhFNJ->WMqA(Ms* zjytk%m4KD$uJr=d?AJL++e}TbUhz0TDt_{WBi3oJhLi1$GSi3kZ~?^hcyjhp5d}8> z2&P*8?fP!bL|WmLL!oloss5I#bIXJAqvyx5Vj(^$Y8|`3E&Ix9nVa#z=1d9ZS8@Mp z01Ry%jQ_O&Kdj~-OL(lVOgk9Aa~`o(wAUleDaSFO20nqG8z&vq61ZZ6{=`Qrd$y(7 zoYxZBWB*{z@v!lmj&fOK(bJYTcXOw}!|*Hw4o;CxuRxvJEvL}?6v>1o5Fmroy(qPl z9l2iS)vmS=?1b9U3vp5-nGRZSMNtwBwlCoDk2IXtLA&A4m^wywDvYn=cHo9kH1rUC zQ3;86Ulo#tu7~4$icPal90S@Q99crhQAbSOK63#1i(>4T#!D(}=~iDiNL~8~J#i$@ zhWm#nYpllyuZST&k@K7itT#o4W3#1PSq=E<4#-Tz&y(CVJK7(A4wh>=koT(-6u*&H z*caFZz{PX`oIvM9m#1Doen%w3Qfxu0^^V<%@hyADr zMBtT{YlM>@U4%cXhj90p0srm$ia+P~&;4({wosJ$JHX%jy?+Y+*jK>Z*k8K8cLo36 z1N*DsIIN!e-#cP=aqjj?ej%O0J|Vf&Ik_u*x3vCC7yT0#5*8Z6 zhH$qedlvwDCjDiW{AH42?%}y{+|Xs!F!!+_^LMtjA-m z@0|19d%kndJ@?+Z&z#SD=33sFHQM0WwRQQ&0NmYmTT5p(UDFl_J+En=|6!5ID8p^zG43+oYBaH4}Jogq_SEJ<$vCS#jX4j_4a{$`>pKrKZRF{O$B9ylS%*M zc-yB8vE9dJ?4BuLyWFS5>68Ab;EsRupHN9d01Y~DL_-S;zKrhH2f?t1J_uc$3Wf`N zFlC&4H$4^nVx>5;Fr@7k3m2}@_Q?O;^uIBEzqvnD_5MCKUxeL8bU^PkUO*{+6A_*3 zZxmgtC?(&2na-~N&O$NO(I`Ibc*!gPjFHia6V#56MvoeKz{Vv@$l)<#d@?biA2kvQ zJvOe(PStbNQC*G-%>l?r4Y0JUwX^*xJ(kobh75foIyPunCc}j7ZQaphwm0b=NqxW= z(<3n>agx}Kd>*&Dl{{M7)zjHjq`I~@RapR=a}8i)+>d-M+Y+@ zz3kbs!vjpP;oa?9n>+#_qm#+WabrL?#>P@(`Vgg?f_OTbm^8*IPfAXvCZxO3iD-Xx z+{ohBD8?Ti2h9qwd_7F?bnRacyhQDAQQDFpUy0$!wi zeD*0|Z!xf|1$d?e*xE|o_<_33z*iOkr>T9N+7GDBQv1g(Kv@}ZzYQoT2R>=1VhaFk zwmt^*(M!)Z;CZUR-&4C_A<+9};9Jz*q4puQLmj~PX_Eg*ZDU2cZ2O(k!iW2TgAwr{ zGL`PzKj5pLAjd!H1KybgE_@rhII*3DaltCQ! z&`~w~pT1Xw~fC#7m}>wJxP`usW#=~vGOiW)|tW$Uo)AbL@VGAzo& z&ZHs50D|KF%NrUZ^Y#d#Lad%TWo|)(_6U9)Bm-?Y&CW?FfX#T8q*Fe|f`rl2VqqbL zSyBaIFFWI^6jtCXrg7DT1(f};3#lPwh4!dhlGVAOdV-qOphg$6m=N3DCWTl|EFpxQ zFT`5(irW|KSGw|1MV_?&9qd#u9WDXoR4?+-il2xJ7y8Vtm{vjbQYyM}*^vTh$McR9 z#5TO(NFf(iLD-aoRk^Tg!Y(*i*oD<#n~6&fSmy%kY2ZZ%Yjk0Y37dAXCKtAZFb;xK za){2ffDHT~vRT?T=G4UkyL7^+5x=`wtGzD%b+I|<%3>Yr#fnR{HEG3om~2{A!jo{A zYVOdb*&C7w&HPCiks7`W@_Uwcm{*wZ_~>ca3%0;1h80?U7l4yQS{4B zt(R+ey-gjgj&=d@HWO9bOmPdBY`R=XuV)(RS*7jL|?x8Wm+CZB!OER=!6#4RL5Qf;2L3?{UCJp{r zj9+cv!OjfIVZ61r$>fq5KAU0~C#yw#Hg$h3MPeNOjGt4>d-33`En!d^d`-M@wZmkC zLpgpB!8`^_Go`cUO6QagNyA)+mc+r|k^!zo%<XY}&4tXUc6?W3yb1Nmp+x z>oE#&W*3Z6ux)tG!Tjiy=P3ITqmRkiQn^bCI+Dpyqk~#2Jm} z1Wt*_30%c(BA5x4d4P`=h3WjA0=xc@Ma9F0`4P||ptnPNg8I~!qtBy`9V!5H1 z6N>K|Q?ZUH)&|mXyyV3^ ztks!nn{>pKtk#Fth-AH^SVtr)f5!0g9g(a7jWe6Hn6x6rM9lLrg)?EBDJhAHWMY$& z=%-9%Ql68NiD#dgl9FkKWJ)QfMT&`-=V7YN)Y?ob$+So^r6kkORY)mG#2;8plwu;2 zK0i4o`o5;w@@Y^^#5@mEeWu=K8kJ0tOrw(NXNqZ5GEFO{Q5lkzWIlWYXjc7>o7AyG z)-$_woI37-m83C=aBVX(CQVpD@iyBo>zt8sLgAR`I!O_2vB(3E89J^p+>~Tzlf&^#_F; zp8LW5vJcY_uN!(N-vjyH$M-zG*L|AJX8GF2*EGJC@imOEUA^?FFD}Ud delta 2383 zcmZ`)ZA_C_6n<{MpcQCg1xjnNL`ckFnGQeL1_%ncITb|tuzb|kR+~66VPR&3SzL4` zYNm35WSIj|KHMA?eX}J?_GA9ruZfA#jkpiDW&5!#+kPxF+oU`9?b}z1FSmJn?tPwf z&$;K^d+zNY*Y^+V&#h+r&aZJ90syY>yvC#$^Dt)P9(}-?wic3x>rC!S1N*5fmsVo2Cs``GR| zqp&M<%&Lxb$2eng^rhO-mrAqL)`;SsPd7+E*eqEU7!b6?r1jAzqk6O zZ3Xp{kV?Ic@BzDe>V$sbAnMzUc8;3W^8Wk8rhyI&6cOosX@fVWeC zZIwVXE&lb5K))F{SOxT0fU0U>>oVX9$r~j9A{is8-wdRu1OKk0jxhtssRcY%poyf9 zWN#gC+y<~Mz_Lu>7|GKlza;rprc|+gCe3uM87OY$=8g-}S#MC66Q+gU>;TTb1{^vK z+9)pdoD6)2%#s4**2bvEpz?qaM_1KXv6?bg7=J}tIK zi%nn=)v?I+V#QVsY>x)!)xg>`u=XdgXKkP~J>j@h1M>+?@(%20%)@VTuik^r{LcXw ze}1r(`ZaR@)Zi;74=Lgi_n)!z-v_HndCgFR(SvfN^9MtDd~~RSuy2OEGWL{=H4T@_ zqa6P2a2}r=E@d8RZrGaQZUKwAP}GKYnCMvM>QUbnl7|^onx>I*cl(kWnaJY4k)x3s zxY;9E(1r|b!ZBLhQJolBQ6pAb73VS)E1#JHIWYbZuWi_+0aY(p$YWwvB!c4YJWqfR!#{~q;4w&0?& z)OHH77RS|*6`y11ga>1*>XWRw zk&o(MA$H6su5o3@e9)sBR}?$Gq;&a6E1s@SlR+6fATaJ3FL|7)PKs1iMf>E#(=7^u$> zrk}yUznCtQ{)qOlf#}z!>W|;J`~7X{yJ*N$F&~S?#1kZ*9Pz}6Cq=v<;>8dzgy6*s vAZ~nd!HWysN8euh{QUf_!{_(f24)}Xx-ap6W_wv9@3|dG6VC{paJl^t&%dF} From 0a7cd3b68bb897440974ed7abea6d9ef82c1e130 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Wed, 28 Sep 2016 21:01:40 +0000 Subject: [PATCH 9/9] merge trunk to branch git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1762709 13f79535-47bb-0310-9956-ffa450edef68 --- KEYS | 99 +++++++++++++++ build.gradle | 21 ++++ build.xml | 4 +- doap_POI.rdf | 7 ++ sonar/examples/pom.xml | 2 +- sonar/excelant/pom.xml | 2 +- sonar/main/pom.xml | 2 +- sonar/ooxml-schema-encryption/pom.xml | 2 +- sonar/ooxml-schema-security/pom.xml | 2 +- sonar/ooxml-schema/pom.xml | 2 +- sonar/ooxml/pom.xml | 2 +- sonar/pom.xml | 2 +- sonar/scratchpad/pom.xml | 2 +- .../hssf/usermodel/examples/InCellLists.java | 54 ++++----- .../org/apache/poi/TestAllFiles.java | 1 + .../poi/hssf/usermodel/HSSFDataFormat.java | 3 +- .../hssf/usermodel/HSSFFormulaEvaluator.java | 87 +++++++++++-- .../poi/hssf/usermodel/HSSFWorkbook.java | 3 +- .../poi/poifs/nio/FileBackedDataSource.java | 25 ++-- .../poi/ss/formula/functions/Bin2Dec.java | 2 +- .../apache/poi/ss/usermodel/CellStyle.java | 96 +++++++-------- .../apache/poi/ss/usermodel/CellValue.java | 40 ++++-- .../org/apache/poi/ss/usermodel/Workbook.java | 29 ++--- .../org/apache/poi/ss/util/SheetUtil.java | 19 ++- .../org/apache/poi/util/CommonsLogger.java | 12 +- .../util/DefaultTempFileCreationStrategy.java | 15 ++- src/java/org/apache/poi/util/NullLogger.java | 17 ++- .../org/apache/poi/util/POILogFactory.java | 2 +- src/java/org/apache/poi/util/POILogger.java | 17 ++- .../org/apache/poi/util/SystemOutLogger.java | 6 +- .../poi/extractor/ExtractorFactory.java | 5 +- .../poi/poifs/crypt/dsig/SignatureConfig.java | 2 +- .../java/org/apache/poi/util/OOXMLLite.java | 1 + .../apache/poi/xslf/usermodel/XSLFShape.java | 6 +- .../poi/xssf/streaming/SXSSFWorkbook.java | 18 ++- .../usermodel/BaseXSSFFormulaEvaluator.java | 74 +++++++++++- .../usermodel/XSSFPivotCacheDefinition.java | 40 +++++- .../poi/xssf/usermodel/XSSFPivotTable.java | 32 ++--- .../apache/poi/xssf/usermodel/XSSFSheet.java | 114 +++++++++++++++--- .../xssf/usermodel/helpers/ColumnHelper.java | 1 + .../usermodel/helpers/XSSFRowShifter.java | 3 +- .../poi/extractor/TestExtractorFactory.java | 14 +++ .../poi/poifs/crypt/TestSignatureInfo.java | 7 +- .../poi/ss/formula/functions/TestProper.java | 1 - ...Table.java => BaseTestXSSFPivotTable.java} | 107 +++------------- .../poi/xssf/usermodel/TestXSSFBugs.java | 37 ++++++ .../usermodel/TestXSSFFormulaEvaluation.java | 11 -- .../usermodel/TestXSSFPivotTableName.java | 112 +++++++++++++++++ .../xssf/usermodel/TestXSSFPivotTableRef.java | 111 +++++++++++++++++ .../poi/xssf/usermodel/TestXSSFSheet.java | 23 +++- .../usermodel/TestXSSFSheetShiftRows.java | 53 +++++++- .../src/org/apache/poi/hmef/Attachment.java | 2 + .../poi/hwpf/converter/WordToFoUtils.java | 16 +-- .../apache/poi/hwpf/usermodel/TestBugs.java | 17 +++ src/testcases/org/apache/poi/POITestCase.java | 59 +++++++++ .../apache/poi/hssf/usermodel/TestBugs.java | 4 +- .../filesystem/TestPOIFSDocumentPath.java | 10 +- .../poi/poifs/macros/TestVBAMacroReader.java | 20 ++- .../poi/ss/formula/atp/TestIfError.java | 2 +- .../ss/usermodel/BaseTestBugzillaIssues.java | 3 - .../ss/usermodel/BaseTestSheetShiftRows.java | 30 ++++- .../BaseTestSheetUpdateArrayFormulas.java | 2 +- .../org/apache/poi/util/DummyPOILogger.java | 5 +- .../org/apache/poi/util/TestPOILogger.java | 4 +- test-data/diagram/44501a.vsd | Bin test-data/diagram/44501b.vsd | Bin test-data/diagram/44501d.vsd | Bin test-data/document/57843.doc | Bin 0 -> 8192 bytes test-data/document/60158.docm | Bin 0 -> 15617 bytes .../hsmf/logsat.com_signatures_valid.msg | Bin ...g_workarea_downloadasset.aspx_id=5864.pptx | Bin ...538ba7a204-programa_alianca_12-04-2007.ppt | Bin ...r.com.tvcamboriu.www_pps_Pensar_5b1_5d.ppt | Bin test-data/spreadsheet/45565.xls | Bin 0 -> 13824 bytes test-data/spreadsheet/59687.xlsx | Bin 0 -> 9580 bytes .../TestShiftRowSharedFormula.xlsx | Bin 0 -> 8431 bytes test-data/spreadsheet/Themes2.xls | Bin ...1B954F-5C07F98E_ooe_stat_download_bp10.xls | Bin ..._0011d159-1eeb-4b63-8833-867b0926e5f3.xlsx | Bin .../spreadsheet/noSharedStringTable.xlsx | Bin 80 files changed, 1167 insertions(+), 356 deletions(-) create mode 100644 build.gradle rename src/ooxml/testcases/org/apache/poi/xssf/usermodel/{TestXSSFPivotTable.java => BaseTestXSSFPivotTable.java} (74%) create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableName.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableRef.java mode change 100755 => 100644 test-data/diagram/44501a.vsd mode change 100755 => 100644 test-data/diagram/44501b.vsd mode change 100755 => 100644 test-data/diagram/44501d.vsd create mode 100644 test-data/document/57843.doc create mode 100644 test-data/document/60158.docm mode change 100755 => 100644 test-data/hsmf/logsat.com_signatures_valid.msg mode change 100755 => 100644 test-data/slideshow/aascu.org_workarea_downloadasset.aspx_id=5864.pptx mode change 100755 => 100644 test-data/slideshow/br.com.diversas.palestras_Nelson_20-_20Temas_20Diversos_20XXXVI_pmrg_462538ba7a204-programa_alianca_12-04-2007.ppt mode change 100755 => 100644 test-data/slideshow/br.com.tvcamboriu.www_pps_Pensar_5b1_5d.ppt create mode 100644 test-data/spreadsheet/45565.xls create mode 100644 test-data/spreadsheet/59687.xlsx create mode 100644 test-data/spreadsheet/TestShiftRowSharedFormula.xlsx mode change 100755 => 100644 test-data/spreadsheet/Themes2.xls mode change 100755 => 100644 test-data/spreadsheet/at.gv.land-oberoesterreich.www_cps_rde_xbcr_SID-4A1B954F-5C07F98E_ooe_stat_download_bp10.xls mode change 100755 => 100644 test-data/spreadsheet/craftonhills.edu_programreview_report.aspx_goalpriorityreport_0011d159-1eeb-4b63-8833-867b0926e5f3.xlsx mode change 100755 => 100644 test-data/spreadsheet/noSharedStringTable.xlsx diff --git a/KEYS b/KEYS index 6767cb00a..adc245d13 100644 --- a/KEYS +++ b/KEYS @@ -2386,3 +2386,102 @@ cCRRHYCeAZDU5UMxi0nonfT+060i2rLPSd7o0bstG7gb1pD99rKxL57M7uy+WBnk YobtRLhWHvHyWS4dKqiteeTwAqG2ZFOq98KCu1LXMKj42HqwOtB3L23HlhqVyA== =8e4G -----END PGP PUBLIC KEY BLOCK----- +pub 4096R/8BABDD6C 2015-10-10 [expires: 2017-10-09] +uid Javen O'Neal +uid Javen O'Neal +sub 4096R/309424B4 2015-10-10 [expires: 2017-10-09] + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.22 (GNU/Linux) + +mQINBFYZkgYBEAC28IT8XHE4bm5iXgL7COy7hmh8FS67hwfnEV08rm3f8tflYxe+ +tYdRUI8y5UIFJyX2138GsV7sjV+pBiEq02xEU5pl2/AfXF+GmtW7ErS2Tk4iQ0id +SEcnyvim5LtaFYyMjYC1mzr0MaiJqWHjw6Lxjjep3s40coAkauRIcnJQ0s1YQIqk +BPlAl0rDILE8Lix+IVokUxTizh9popwDW0T/z6gzdKDeE5FPeKYWPvs34bKwe2vm +KqL2qmBh3Tk7MbtKYD79pGzYkNSyVmfWIDTjc++lNmDWYt0QN6YlaXoV4ZCAbLk+ +raHU+5aKduZNnP25FnwTyt/Xm4Pl8RdROBzsmese2UuYrfsPaZrZkhhekE7Ttjk1 +EqTob/LmgR7KSwWGT6Y9PAyROIs50yw5T7wMjdz0+C9SUZHK5lhPnFawyamWM1de +Y/f5vEvbI2Xap614bg6EPObPSQh/1r6J+7EmrbSqRE0W8FlSK2cWmB8l724lBG9f +Y5bma/F5g8eL4xcOGkaw2VCBu3i/IRUoHBP1ndkAQfIHmlGlWFc9u6PtmFyZHJJ7 +Boks8g8M9aOcUvO+K/+gBWzGXO8n/NG10iItX9ubSFbvXCKJgK7cFXDdC8F/uHXO +vfDSTrwBffm7Czyxx8AgDHxGMysNCe5Pet7bU6bZL4ANnCPfMhgN7pp5bQARAQAB +tCBKYXZlbiBPJ05lYWwgPG9uZWFsakBhcGFjaGUub3JnPokCPwQTAQIAKQUCV+cS +OwIbAwUJA8GMagcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEOZnesaLq91s +DN4P/36DX8zeM8PK9x9lb3DogipXefiMyWOZb+p64Ah6iFIxDoTNP+meG3eiQP11 +T02TeI8tZKG6kuFM6fQb3GE6g/2TgHeWnrhek2KfHbOh6/nWvrAnsaEiu+vIBIFL +kiO0ydtu1DdNjWB0PR3bcnxF1GOX38cx37SjMR3n/eaIFnHqENzHNZgZ0sv38pYt +c0FDeoqpO89aAiCi+IWuLAhYFzU+GFdHxteFMtYGYDe5iI4EHjciIO4/Mq9MyQce +rYS/ADCJV0SWS+mPk3czKRdpklZiRoOqSiZcyTUJ7H2NFcdQuYjpLlmNvt0c95bR +fxaOhFWSSvZzkcZXr4u82VDc0rSvXW1q2vFZpbYJq7uDQZpBlkLBwlZ9gDaXHa6Y +5JB7RGWMmA+mfwSmfKxfCR0HdTldNsSic20bUlJjVjKkZR+Mco5SmAW0nb1OkpD7 +lDCx6x8aunuFbf2SGnIZxtGxm31rXE7bTib4wE5X9EECLfg9hTJxPdDqvKfRBVBQ +ZVtPdq3jzyh6CxM/bygfmUjqdP91hS+wfXYq1W7d4e8EphkCiqdMXgclFEbdGp8e ++8BDvbCIchLzZ5wOxL4Ntwo4bL2Zn0IJ/K6K1WoOnMzq+6xWZOGRJ28v8go2VqO/ +o0zWgSm6zEz2kAE9s9Y1by0xL40byg0AdJzG4h1cRPOsYeB0tCNKYXZlbiBPJ05l +YWwgPGphdmVub25lYWxAZ21haWwuY29tPokCQQQTAQIAKwIbAwUJA8GMagYLCQgH +AwIGFQgCCQoLBBYCAwECHgECF4AFAlfnEmUCGQEACgkQ5md6xour3WzZxBAAjLkr +z4Roxig47WGax6ppGqIGLTNEPPvNluIgtwZYfTW6MrzQIquju4o0QEIb8mOW9uke +LnR284t/5CU36EuVan0wWYwrwIJWtbZGz14GgtHsRyErllmYWKa0aYJ3kgY8JuNT +bK40g9RlrdLsYntwFlWQB9rL4nBPoWVWRllWOMcbhdF6/p9r2EmKO/CsTYdfolEh +dKdEtSgshQgvZxfgalanqb69/zc81RLpUQ0q1uiedrN0ghNd1+XCXeopKeeAoW9Y +LOIlQ3ALz9t+A3w4Ft5h3RZBHPwSPAgRv4fDv3y016rnPSna6wC5atlW9b1jxcnS +myP3kNcAXJDWwpQmMwYG8NJGCANwYRUNGcXlMvYbMpmuYHqc7kf+AbkG3H8Z1ktJ +jfwK8aw2ZX/AlINIQZ7fVJLDOkMapbjApUNOc7UoXx9Z8qXiVDizisRCJxy2Kj9h +pR+XMIwgPwCTgmQ+W+hQ50h7S4H9VVSfEe+H/+k6kHhRxEAZq5NGIzC7Mh8xvyHa +EII5tAS8fQwfnJ75URITUp2Vrdry5aDEZ90AIlYiQVuhUwFeNimjKOr8i9Zse7JC +MucIcmBsvwThVn22e6x14cNfc4KwqcWSQvxMuzqejHq4WTMf9Ln15G7nVMulyUTZ +hfJ4Spr45BEr+4UDMNqwIRQdIMXHa+JZGs3Nj7SJAhwEEwEKAAYFAlYaCkgACgkQ +ykkN1QqzdQMHbBAAm6Y0PaDBlhiYd+nCaxpNJGG0h2F/2nPLXNaGdTQSICD8B97i +J1Hvio0mEHnkbbm6e2s0rvxervgy4BuxgwzWgguaLQsALgyGpY2pHzsQAVQVVq6H +PQdwSOy731oO7R7CeJB/UIT02ne5WnLxxUN1e4qYLHp0+QFXOd4TPuTwPEG/Gewr +EVJN4C2k373eSsWwvXCBYe5UDLqPHrVTYnyU8uFmi4yOMbmIyyb1x7At6adc8FWQ +DQQt/0YElDe3m4Xmco4OMBem8i7QFchinhLIZPwNCR4aGrhbQYuq7C8JOWjEGFg8 +k322fOwAEopr3+dHemToGde5j2u7JnatAeu7CwNVMY4Z9s/wUChP7gXlNenKIgeD +hUsG9RroqpXTLKmjz+2fSCGT5o2rBgwRnwpTcDaFWZlVzb1r/KQxTRsEkvDfSU1Y +jixoqFJYBj/2fhl1EPF7gb5WK8tBZ8PqhPaJwXAynzSGEG6QLbYwxCoKfVvl8PZj +SGg4Dmca6QpUskYwuiMMZhJJDNxKn5t2NZFEilZVaITRtNxtyRm2xvbcPXsSjAdo +VXCqKX777rfPryPW4Yjw55a7uaQQe3M6d4ndgVP491tGBMfBTHa7hKei/R1S106C +2R+rrE3o42WnhM3AarlWRqaHzuJQuofAjtRv19Ibk6ahK/qFuZBBFAWnUiSJARwE +EAECAAYFAldOVSMACgkQ28vfLzZwP7I7OQgAnDlCoagIdBNHxGAPieZt5bJTj8Mv +DtoxOAJtUjbJPTu5iw23pi2b83xmYCNQQkJ86IiSX56kILr9SP1uscChjM8aqwBK +H5vMWAzHxdbTNFrjmCm/NAF+6ArFi5snlWf3phLPUIdhNhGA4jhklWMuy6rflujy +BCgtZSh0VbaU/02ir3/QXBc331VN+qbXoCxD8lFagj0rg4GLCWFFCPJUeAr/SmIp +v28UIVNDUI9lyCB4G3utjBhDpo0LHv+X+AXkD5V9kwZBE5NzahL/3AWjm9B9Vqb1 +JO1PVYv7sXqG25+xRMJssnsoGQYH75LSwoau3Yo1l8J35LOLYPUQfO9NYokCPgQT +AQIAKAUCVhmSBgIbAwUJA8GMagYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ +5md6xour3WxiVxAAkN6x14p905j7NUB0Ky/p3da9KeIp5uAyTR/CcE9pLvvdSMyd +xH8nE35EyHT84l7/yQHQ0wXxAFilBSGBt3C6Sth8J+11zxesUeDmJ1dX2Z0xRFz+ +qO7MBJ2ccU9pK20niPgaKuBBZsDoye9wKtli2UxOCid26CeLs5Y1gAcPuY9Hmzh3 +FnGhzrI5dP0HcE9lQXKA04vqviT1hxd+LlleCij1ItQR0ZYhV7z4SZp66vEfnh/R +mtaC5L0ZFfoAartLSVX6a16dQDqbLy/ukecQeQ2qWo+IkNQe+p23GR1FihBZeHxC +G+L9igu/cFgRLubMqHelwVCwL12Uazb2tG2zAnaTi5WgU0urvLqMRCuyQ40KlQ4h +ly6oOEKWcwGBhfTD+baBsHLfrVdikz4IiV8+7oQsb/xyuQEz66FYLptDpX3obgRR +ls0xjJ1YlZsmFg/NCJaAoPILBnS8wpbQp+T8yf9YcfczsMs9wALtM2rkLZqS6dn0 +yBekDL3ZsuiS7Ot6WM351nKCvDtpe1hF4CAkE2xhCLdRyvDHmbq4vBf0nyAvzTCI +hUzgQeUKDDjxcmH1yxGNXswlfllqK6QkI9HnV/AuTI6nEjlBRNmtLg7SijHmuAZd ++X8Sy/CibqCgY0J51FlylH+Cx8qQUaUB81KKimCohTejzrfUm3xUOvautFG5Ag0E +VhmSBgEQAOQbxM1ZydIHIlnKx6LEmQdujX7Ns60NxrGdhLLHoJusuTutjTEt50Ex +MHhn/PjYVeK1JU/gTh0fPM7il9xwj+cUBbOtBQ+E2sVXXMSBMj93+6ivLBVesq27 +R3ls63EMKHGcyYsIqHafBG9EvBwHBj+UNG3qKyEZv/NaX6UUoEYrvI4yx+z+ahew +K5RMAZ1qNUqtZfsDlKW3SF3rgRBBmVWowim1G6tB0HVor4YRA/iHwH6WIu04QqzG +A0uQpvptVJ0i1Hd9SoXisJUsovXSRTqj7+eFbILGywbvM2NYwF52lW9jbz3gu/Hb +O3uPY0xdshW/8F92FZRXzuMMv3O6I5SaVRRJE2oJVJHtteEpyrbkCqhaLzkSiNxf +sqzOw03gYICnxSDY3fqKBRKq84DMbiXbv4DwqeXWZXR2Yhs85Rn5cgoNqg5oyNwy +PlQOfqZzoiDle3SWs41pCM2po1tGgZXkeRqNedRI9V+c8ZacOrTPnjRL0Ae475n4 +EFP3Ajur7UaXRe57AiSn4B30E5/D0HC0SeeRWacFeAHJ8WGvf6wXNGoAmtJL+TRJ +iSqNVjZ1EhuwYJpjUgPEfesXyCc5U6qkHMHcn0rXVteG4mrn0/191CPtfamxpDM9 +hhTZ0WEbwFRwC41QQnRCO6EQfkPwZLC9BtSSuRVCI30617HiOFwDABEBAAGJAiUE +GAECAA8FAlYZkgYCGwwFCQPBjGoACgkQ5md6xour3WwC1Q//QnYoOLfPiSI/NVI6 +agjCECNdtpUfdiGy7sEH3FYpNQGu8LDahcmTsxxcp2LeXjZIhuJt/dRPAMC/teQq +ihZvdz5iuYwqg8I7ZtZh+qxqxvjwOwtKnELpoMpZyK81v4C2oLQAzNdMC18QTBt+ +L3RSMDdnPJ92GsCoYSGdLT0Jy16l/ShUQZ85EFUEjzFEDVnlLKpfZoqVCIULe0nj +NCyNY6txc6X4uChCB5ZtsLaHgUTm0I+wb+AX0wbEDELyldzkbfVPTxbCMQgkPx0E +W7ufcM3wx9sGT9I3FNOqZKHa8xq08be7z6OJZlsuw1NfeWG+UF9f6KZjH/zxIdtY +IDoVReAU5g/LfOQTHXpg+7eArlf/hVh57uFjPJxdh8wqKfFzIVSoksCwv3w3Hrca +7eh7Po46U6Tt6icWInBUthvOja0CgDojw+mm3GKvGMif/9YZXY/RHcc3t2CQDp/c +Shzcaly9QYj0eDujTQj7XFd/AAwdj8YWA4Ha2Peh4/oK4ugt7pKwt51MvYzSYDel +NFTn1hbTXTcj21i2/C9I6oqIhKt8c+St6Tge7PkGjq5BRqvY2L/IJmS5TmSerciL +bpjAhwE2YmGQ7oB+3V798HtAmceRNf8AY0GWrZswJlg7xUn+WJNwQ9uIHI1fxYHx +2Nr+AmDDs6ZOEI5zhwxioePw/Cg= +=9lKb +-----END PGP PUBLIC KEY BLOCK----- diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..20f98e820 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +/* ==================================================================== + 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. +==================================================================== */ + +// For help converting an Ant build to a Gradle build, see +// https://docs.gradle.org/current/userguide/ant.html + +ant.importBuild 'build.xml' diff --git a/build.xml b/build.xml index d3623395d..975cf5c50 100644 --- a/build.xml +++ b/build.xml @@ -40,7 +40,7 @@ under the License. The Apache POI project Ant build. - + @@ -78,7 +78,7 @@ under the License. JVM system properties for running tests, user.language and user.country are required as we have locale-sensitive formatters --> - + diff --git a/doap_POI.rdf b/doap_POI.rdf index 5369344cd..08106f476 100644 --- a/doap_POI.rdf +++ b/doap_POI.rdf @@ -35,6 +35,13 @@ Java + + + Apache POI 3.15 + 2016-09-21 + 3.15 + + Apache POI 3.14 diff --git a/sonar/examples/pom.xml b/sonar/examples/pom.xml index ea53c0597..cac1dc446 100644 --- a/sonar/examples/pom.xml +++ b/sonar/examples/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-examples jar diff --git a/sonar/excelant/pom.xml b/sonar/excelant/pom.xml index a7b9408dc..2cbeec728 100644 --- a/sonar/excelant/pom.xml +++ b/sonar/excelant/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-excelant jar diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index 36422a83c..26a8f2ba4 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-main jar diff --git a/sonar/ooxml-schema-encryption/pom.xml b/sonar/ooxml-schema-encryption/pom.xml index cbb2eff7d..d704a6525 100644 --- a/sonar/ooxml-schema-encryption/pom.xml +++ b/sonar/ooxml-schema-encryption/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema-encryption diff --git a/sonar/ooxml-schema-security/pom.xml b/sonar/ooxml-schema-security/pom.xml index a822c7343..7aae600f4 100644 --- a/sonar/ooxml-schema-security/pom.xml +++ b/sonar/ooxml-schema-security/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema-security diff --git a/sonar/ooxml-schema/pom.xml b/sonar/ooxml-schema/pom.xml index 5e2b9fef5..e1d6526da 100644 --- a/sonar/ooxml-schema/pom.xml +++ b/sonar/ooxml-schema/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT .. poi-ooxml-schema diff --git a/sonar/ooxml/pom.xml b/sonar/ooxml/pom.xml index 27636cdfa..305695ff1 100644 --- a/sonar/ooxml/pom.xml +++ b/sonar/ooxml/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-ooxml jar diff --git a/sonar/pom.xml b/sonar/pom.xml index 8e2a9fc42..5acb3a5da 100644 --- a/sonar/pom.xml +++ b/sonar/pom.xml @@ -3,7 +3,7 @@ org.apache.poi poi-parent pom - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT Apache POI - the Java API for Microsoft Documents Maven build of Apache POI for Sonar checks http://poi.apache.org/ diff --git a/sonar/scratchpad/pom.xml b/sonar/scratchpad/pom.xml index 6760762d6..91ede69a1 100644 --- a/sonar/scratchpad/pom.xml +++ b/sonar/scratchpad/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 3.16-beta1-SNAPSHOT + 3.15-beta4-SNAPSHOT poi-scratchpad jar diff --git a/src/examples/src/org/apache/poi/hssf/usermodel/examples/InCellLists.java b/src/examples/src/org/apache/poi/hssf/usermodel/examples/InCellLists.java index a666c6ab7..e1d4cbcbf 100644 --- a/src/examples/src/org/apache/poi/hssf/usermodel/examples/InCellLists.java +++ b/src/examples/src/org/apache/poi/hssf/usermodel/examples/InCellLists.java @@ -67,27 +67,21 @@ public class InCellLists { * the Excel spreadsheet file this code will create. */ public void demonstrateMethodCalls(String outputFilename) throws IOException { - HSSFWorkbook workbook = null; - HSSFSheet sheet = null; - HSSFRow row = null; - HSSFCell cell = null; - ArrayList multiLevelListItems = null; - ArrayList listItems = null; + HSSFWorkbook workbook = new HSSFWorkbook(); try { - workbook = new HSSFWorkbook(); - sheet = workbook.createSheet("In Cell Lists"); - row = sheet.createRow(0); + HSSFSheet sheet = workbook.createSheet("In Cell Lists"); + HSSFRow row = sheet.createRow(0); // Create a cell at A1 and insert a single, bulleted, item into // that cell. - cell = row.createCell(0); + HSSFCell cell = row.createCell(0); this.bulletedItemInCell(workbook, "List Item", cell); // Create a cell at A2 and insert a plain list - that is one // whose items are neither bulleted or numbered - into that cell. row = sheet.createRow(1); cell = row.createCell(0); - listItems = new ArrayList(); + ArrayList listItems = new ArrayList(); listItems.add("List Item One."); listItems.add("List Item Two."); listItems.add("List Item Three."); @@ -131,7 +125,7 @@ public class InCellLists { // to preserve order. row = sheet.createRow(4); cell = row.createCell(0); - multiLevelListItems = new ArrayList(); + ArrayList multiLevelListItems = new ArrayList(); listItems = new ArrayList(); listItems.add("ML List Item One - Sub Item One."); listItems.add("ML List Item One - Sub Item Two."); @@ -189,9 +183,7 @@ public class InCellLists { ioEx.printStackTrace(System.out); } finally { - if (workbook != null) { - workbook.close(); - } + workbook.close(); } } @@ -236,7 +228,7 @@ public class InCellLists { * will be written. */ public void listInCell(HSSFWorkbook workbook, ArrayList listItems, HSSFCell cell) { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); HSSFCellStyle wrapStyle = workbook.createCellStyle(); wrapStyle.setWrapText(true); for(String listItem : listItems) { @@ -269,7 +261,7 @@ public class InCellLists { HSSFCell cell, int startingValue, int increment) { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); int itemNumber = startingValue; // Note that again, an HSSFCellStye object is required and that // it's wrap text property should be set to 'true' @@ -278,7 +270,7 @@ public class InCellLists { // Note that the basic method is identical to the listInCell() method // with one difference; a number prefixed to the items text. for(String listItem : listItems) { - buffer.append(String.valueOf(itemNumber) + ". "); + buffer.append(itemNumber).append(". "); buffer.append(listItem); buffer.append("\n"); itemNumber += increment; @@ -303,7 +295,7 @@ public class InCellLists { public void bulletedListInCell(HSSFWorkbook workbook, ArrayList listItems, HSSFCell cell) { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); // Note that again, an HSSFCellStye object is required and that // it's wrap text property should be set to 'true' HSSFCellStyle wrapStyle = workbook.createCellStyle(); @@ -339,8 +331,7 @@ public class InCellLists { public void multiLevelListInCell(HSSFWorkbook workbook, ArrayList multiLevelListItems, HSSFCell cell) { - StringBuffer buffer = new StringBuffer(); - ArrayList lowerLevelItems = null; + StringBuilder buffer = new StringBuilder(); // Note that again, an HSSFCellStye object is required and that // it's wrap text property should be set to 'true' HSSFCellStyle wrapStyle = workbook.createCellStyle(); @@ -353,7 +344,7 @@ public class InCellLists { buffer.append("\n"); // and then an ArrayList whose elements encapsulate the text // for the lower level list items. - lowerLevelItems = multiLevelListItem.getLowerLevelItems(); + ArrayList lowerLevelItems = multiLevelListItem.getLowerLevelItems(); if(!(lowerLevelItems == null) && !(lowerLevelItems.isEmpty())) { for(String item : lowerLevelItems) { buffer.append(InCellLists.TAB); @@ -401,10 +392,8 @@ public class InCellLists { int highLevelIncrement, int lowLevelStartingValue, int lowLevelIncrement) { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); int highLevelItemNumber = highLevelStartingValue; - int lowLevelItemNumber = 0; - ArrayList lowerLevelItems = null; // Note that again, an HSSFCellStye object is required and that // it's wrap text property should be set to 'true' HSSFCellStyle wrapStyle = workbook.createCellStyle(); @@ -413,20 +402,20 @@ public class InCellLists { for(MultiLevelListItem multiLevelListItem : multiLevelListItems) { // For each element in the ArrayList, get the text for the high // level list item...... - buffer.append(String.valueOf(highLevelItemNumber)); + buffer.append(highLevelItemNumber); buffer.append(". "); buffer.append(multiLevelListItem.getItemText()); buffer.append("\n"); // and then an ArrayList whose elements encapsulate the text // for the lower level list items. - lowerLevelItems = multiLevelListItem.getLowerLevelItems(); + ArrayList lowerLevelItems = multiLevelListItem.getLowerLevelItems(); if(!(lowerLevelItems == null) && !(lowerLevelItems.isEmpty())) { - lowLevelItemNumber = lowLevelStartingValue; + int lowLevelItemNumber = lowLevelStartingValue; for(String item : lowerLevelItems) { buffer.append(InCellLists.TAB); - buffer.append(String.valueOf(highLevelItemNumber)); + buffer.append(highLevelItemNumber); buffer.append("."); - buffer.append(String.valueOf(lowLevelItemNumber)); + buffer.append(lowLevelItemNumber); buffer.append(" "); buffer.append(item); buffer.append("\n"); @@ -459,8 +448,7 @@ public class InCellLists { public void multiLevelBulletedListInCell(HSSFWorkbook workbook, ArrayList multiLevelListItems, HSSFCell cell) { - StringBuffer buffer = new StringBuffer(); - ArrayList lowerLevelItems = null; + StringBuilder buffer = new StringBuilder(); // Note that again, an HSSFCellStye object is required and that // it's wrap text property should be set to 'true' HSSFCellStyle wrapStyle = workbook.createCellStyle(); @@ -475,7 +463,7 @@ public class InCellLists { buffer.append("\n"); // and then an ArrayList whose elements encapsulate the text // for the lower level list items. - lowerLevelItems = multiLevelListItem.getLowerLevelItems(); + ArrayList lowerLevelItems = multiLevelListItem.getLowerLevelItems(); if(!(lowerLevelItems == null) && !(lowerLevelItems.isEmpty())) { for(String item : lowerLevelItems) { buffer.append(InCellLists.TAB); diff --git a/src/integrationtest/org/apache/poi/TestAllFiles.java b/src/integrationtest/org/apache/poi/TestAllFiles.java index e364c9f28..6def523db 100644 --- a/src/integrationtest/org/apache/poi/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/TestAllFiles.java @@ -232,6 +232,7 @@ public class TestAllFiles { EXPECTED_FAILURES.add("spreadsheet/43493.xls"); EXPECTED_FAILURES.add("spreadsheet/46904.xls"); EXPECTED_FAILURES.add("document/Bug50955.doc"); + EXPECTED_FAILURES.add("document/57843.doc"); EXPECTED_FAILURES.add("slideshow/PPT95.ppt"); EXPECTED_FAILURES.add("openxml4j/OPCCompliance_CoreProperties_DCTermsNamespaceLimitedUseFAIL.docx"); EXPECTED_FAILURES.add("openxml4j/OPCCompliance_CoreProperties_DoNotUseCompatibilityMarkupFAIL.docx"); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java index 6eecc7b2c..23fe8cad7 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java @@ -20,7 +20,6 @@ package org.apache.poi.hssf.usermodel; import java.util.Arrays; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Vector; import org.apache.poi.hssf.model.InternalWorkbook; @@ -90,7 +89,7 @@ public final class HSSFDataFormat implements DataFormat { public short getFormat(String pFormat) { // Normalise the format string String format; - if (pFormat.toUpperCase(Locale.ROOT).equals("TEXT")) { + if (pFormat.equalsIgnoreCase("TEXT")) { format = "@"; } else { format = pFormat; diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index 20c7ffa1e..fa5082635 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -32,7 +32,6 @@ import org.apache.poi.ss.formula.udf.UDFFinder; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.FormulaEvaluator; -import org.apache.poi.ss.usermodel.RichTextString; import org.apache.poi.ss.usermodel.Workbook; /** @@ -80,11 +79,6 @@ public class HSSFFormulaEvaluator extends BaseFormulaEvaluator { public static HSSFFormulaEvaluator create(HSSFWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { return new HSSFFormulaEvaluator(workbook, stabilityClassifier, udfFinder); } - - @Override - protected RichTextString createRichTextString(String str) { - return new HSSFRichTextString(str); - } /** @@ -140,10 +134,87 @@ public class HSSFFormulaEvaluator extends BaseFormulaEvaluator { public void notifySetFormula(Cell cell) { _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell((HSSFCell)cell)); } - + + /** + * If cell contains formula, it evaluates the formula, and saves the result of the formula. The + * cell remains as a formula cell. If the cell does not contain formula, rather than throwing an + * exception, this method returns {@link CellType#_NONE} and leaves the cell unchanged. + * + * Note that the type of the formula result is returned, so you know what kind of + * cached formula result is also stored with the formula. + *

+     * CellType evaluatedCellType = evaluator.evaluateFormulaCell(cell);
+     * 
+ * Be aware that your cell will hold both the formula, and the result. If you want the cell + * replaced with the result of the formula, use {@link #evaluateInCell(org.apache.poi.ss.usermodel.Cell)} + * @param cell The cell to evaluate + * @return {@link CellType#_NONE} for non-formula cells, or the type of the formula result + * @since POI 3.15 beta 3 + * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. + */ + @Internal + @Override + public CellType evaluateFormulaCellEnum(Cell cell) { + if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { + return CellType._NONE; + } + CellValue cv = evaluateFormulaCellValue(cell); + // cell remains a formula cell, but the cached value is changed + setCellValue(cell, cv); + return cv.getCellType(); + } + + /** + * If cell contains formula, it evaluates the formula, and + * puts the formula result back into the cell, in place + * of the old formula. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + * Note that the same instance of HSSFCell is returned to + * allow chained calls like: + *
+     * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
+     * 
+ * Be aware that your cell value will be changed to hold the + * result of the formula. If you simply want the formula + * value computed for you, use {@link #evaluateFormulaCellEnum(Cell)}} + */ @Override public HSSFCell evaluateInCell(Cell cell) { - return (HSSFCell) super.evaluateInCell(cell); + if (cell == null) { + return null; + } + HSSFCell result = (HSSFCell) cell; + if (cell.getCellTypeEnum() == CellType.FORMULA) { + CellValue cv = evaluateFormulaCellValue(cell); + setCellValue(cell, cv); + setCellType(cell, cv); // cell will no longer be a formula cell + } + return result; + } + + private static void setCellValue(Cell cell, CellValue cv) { + CellType cellType = cv.getCellType(); + switch (cellType) { + case BOOLEAN: + cell.setCellValue(cv.getBooleanValue()); + break; + case ERROR: + cell.setCellErrorValue(cv.getErrorValue()); + break; + case NUMERIC: + cell.setCellValue(cv.getNumberValue()); + break; + case STRING: + cell.setCellValue(new HSSFRichTextString(cv.getStringValue())); + break; + case BLANK: + // never happens - blanks eventually get translated to zero + case FORMULA: + // this will never happen, we have already evaluated the formula + default: + throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); + } } /** diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index 2800904af..b4339587b 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -963,8 +963,9 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss * Get the HSSFSheet object at the given index. * @param index of the sheet number (0-based physical & logical) * @return HSSFSheet at the provided index + * @throws IllegalArgumentException if the index is out of range (index + * < 0 || index >= getNumberOfSheets()). */ - @Override public HSSFSheet getSheetAt(int index) { diff --git a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java index 47637daa3..32a8701fb 100644 --- a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java +++ b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java @@ -88,27 +88,26 @@ public class FileBackedDataSource extends DataSource { throw new IndexOutOfBoundsException("Position " + position + " past the end of the file"); } - // Do we read or map (for read/write? + // Do we read or map (for read/write)? ByteBuffer dst; - int worked = -1; if (writable) { dst = channel.map(FileChannel.MapMode.READ_WRITE, position, length); - worked = 0; - // remember the buffer for cleanup if necessary - buffersToClean.add(dst); + + // remember this buffer for cleanup + buffersToClean.add(dst); } else { - // Read + // allocate the buffer on the heap if we cannot map the data in directly channel.position(position); dst = ByteBuffer.allocate(length); - worked = IOUtils.readFully(channel, dst); + + // Read the contents and check that we could read some data + int worked = IOUtils.readFully(channel, dst); + if(worked == -1) { + throw new IndexOutOfBoundsException("Position " + position + " past the end of the file"); + } } - // Check - if(worked == -1) { - throw new IndexOutOfBoundsException("Position " + position + " past the end of the file"); - } - - // Ready it for reading + // make it ready for reading dst.position(0); // All done diff --git a/src/java/org/apache/poi/ss/formula/functions/Bin2Dec.java b/src/java/org/apache/poi/ss/formula/functions/Bin2Dec.java index 9617877ca..cb206051c 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Bin2Dec.java +++ b/src/java/org/apache/poi/ss/formula/functions/Bin2Dec.java @@ -84,7 +84,7 @@ public class Bin2Dec extends Fixed1ArgFunction implements FreeRefFunction { //Add 1 to obtained number sum++; - value = "-" + String.valueOf(sum); + value = "-" + sum; } } catch (NumberFormatException e) { return ErrorEval.NUM_ERROR; diff --git a/src/java/org/apache/poi/ss/usermodel/CellStyle.java b/src/java/org/apache/poi/ss/usermodel/CellStyle.java index df3d57259..1eab8dec1 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellStyle.java +++ b/src/java/org/apache/poi/ss/usermodel/CellStyle.java @@ -26,308 +26,308 @@ public interface CellStyle { * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#GENERAL} instead. */ @Removal(version="3.17") - static final short ALIGN_GENERAL = 0x0; //HorizontalAlignment.GENERAL.getCode(); + short ALIGN_GENERAL = 0x0; //HorizontalAlignment.GENERAL.getCode(); /** * left-justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#LEFT} instead. */ @Removal(version="3.17") - static final short ALIGN_LEFT = 0x1; //HorizontalAlignment.LEFT.getCode(); + short ALIGN_LEFT = 0x1; //HorizontalAlignment.LEFT.getCode(); /** * center horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#CENTER} instead. */ @Removal(version="3.17") - static final short ALIGN_CENTER = 0x2; //HorizontalAlignment.CENTER.getCode(); + short ALIGN_CENTER = 0x2; //HorizontalAlignment.CENTER.getCode(); /** * right-justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#RIGHT} instead. */ @Removal(version="3.17") - static final short ALIGN_RIGHT = 0x3; //HorizontalAlignment.RIGHT.getCode(); + short ALIGN_RIGHT = 0x3; //HorizontalAlignment.RIGHT.getCode(); /** * fill? horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#FILL} instead. */ @Removal(version="3.17") - static final short ALIGN_FILL = 0x4; //HorizontalAlignment.FILL.getCode(); + short ALIGN_FILL = 0x4; //HorizontalAlignment.FILL.getCode(); /** * justified horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#JUSTIFY} instead. */ @Removal(version="3.17") - static final short ALIGN_JUSTIFY = 0x5; //HorizontalAlignment.JUSTIFY.getCode(); + short ALIGN_JUSTIFY = 0x5; //HorizontalAlignment.JUSTIFY.getCode(); /** * center-selection? horizontal alignment * @deprecated POI 3.15 beta 3. Use {@link HorizontalAlignment#CENTER_SELECTION} instead. */ @Removal(version="3.17") - static final short ALIGN_CENTER_SELECTION = 0x6; //HorizontalAlignment.CENTER_SELECTION.getCode(); + short ALIGN_CENTER_SELECTION = 0x6; //HorizontalAlignment.CENTER_SELECTION.getCode(); /** * top-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#TOP} instead. */ @Removal(version="3.17") - static final short VERTICAL_TOP = 0x0; //VerticalAlignment.TOP.getCode(); + short VERTICAL_TOP = 0x0; //VerticalAlignment.TOP.getCode(); /** * center-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#CENTER} instead. */ @Removal(version="3.17") - static final short VERTICAL_CENTER = 0x1; //VerticalAlignment.CENTER.getCode(); + short VERTICAL_CENTER = 0x1; //VerticalAlignment.CENTER.getCode(); /** * bottom-aligned vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#BOTTOM} instead. */ @Removal(version="3.17") - static final short VERTICAL_BOTTOM = 0x2; //VerticalAlignment.BOTTOM.getCode(); + short VERTICAL_BOTTOM = 0x2; //VerticalAlignment.BOTTOM.getCode(); /** * vertically justified vertical alignment * @deprecated POI 3.15 beta 3. Use {@link VerticalAlignment#JUSTIFY} instead. */ @Removal(version="3.17") - static final short VERTICAL_JUSTIFY = 0x3; //VerticalAlignment.JUSTIFY.getCode(); + short VERTICAL_JUSTIFY = 0x3; //VerticalAlignment.JUSTIFY.getCode(); /** * No border * @deprecated 3.15 beta 2. Use {@link BorderStyle#NONE} instead. */ @Removal(version="3.17") - static final short BORDER_NONE = 0x0; //BorderStyle.NONE.getCode(); + short BORDER_NONE = 0x0; //BorderStyle.NONE.getCode(); /** * Thin border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THIN} instead. */ @Removal(version="3.17") - static final short BORDER_THIN = 0x1; //BorderStyle.THIN.getCode(); + short BORDER_THIN = 0x1; //BorderStyle.THIN.getCode(); /** * Medium border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM} instead. */ @Removal(version="3.17") - static final short BORDER_MEDIUM = 0x2; //BorderStyle.MEDIUM.getCode(); + short BORDER_MEDIUM = 0x2; //BorderStyle.MEDIUM.getCode(); /** * dash border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASHED} instead. */ @Removal(version="3.17") - static final short BORDER_DASHED = 0x3; //BorderStyle.DASHED.getCode(); + short BORDER_DASHED = 0x3; //BorderStyle.DASHED.getCode(); /** * dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOTTED} instead. */ @Removal(version="3.17") - static final short BORDER_DOTTED = 0x4; //BorderStyle.DOTTED.getCode(); + short BORDER_DOTTED = 0x4; //BorderStyle.DOTTED.getCode(); /** * Thick border * @deprecated 3.15 beta 2. Use {@link BorderStyle#THICK} instead. */ @Removal(version="3.17") - static final short BORDER_THICK = 0x5; //BorderStyle.THICK.getCode(); + short BORDER_THICK = 0x5; //BorderStyle.THICK.getCode(); /** * double-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DOUBLE} instead. */ @Removal(version="3.17") - static final short BORDER_DOUBLE = 0x6; //BorderStyle.DOUBLE.getCode(); + short BORDER_DOUBLE = 0x6; //BorderStyle.DOUBLE.getCode(); /** * hair-line border * @deprecated 3.15 beta 2. Use {@link BorderStyle#HAIR} instead. */ @Removal(version="3.17") - static final short BORDER_HAIR = 0x7; //BorderStyle.HAIR.getCode(); + short BORDER_HAIR = 0x7; //BorderStyle.HAIR.getCode(); /** * Medium dashed border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASHED} instead. */ @Removal(version="3.17") - static final short BORDER_MEDIUM_DASHED = 0x8; //BorderStyle.MEDIUM_DASHED.getCode(); + short BORDER_MEDIUM_DASHED = 0x8; //BorderStyle.MEDIUM_DASHED.getCode(); /** * dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT} instead. */ @Removal(version="3.17") - static final short BORDER_DASH_DOT = 0x9; //BorderStyle.DASH_DOT.getCode(); + short BORDER_DASH_DOT = 0x9; //BorderStyle.DASH_DOT.getCode(); /** * medium dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT} instead. */ @Removal(version="3.17") - static final short BORDER_MEDIUM_DASH_DOT = 0xA; //BorderStyle.MEDIUM_DASH_DOT.getCode(); + short BORDER_MEDIUM_DASH_DOT = 0xA; //BorderStyle.MEDIUM_DASH_DOT.getCode(); /** * dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#DASH_DOT_DOT} instead. */ @Removal(version="3.17") - static final short BORDER_DASH_DOT_DOT = 0xB; //BorderStyle.DASH_DOT_DOT.getCode(); + short BORDER_DASH_DOT_DOT = 0xB; //BorderStyle.DASH_DOT_DOT.getCode(); /** * medium dash-dot-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#MEDIUM_DASH_DOT_DOT} instead. */ @Removal(version="3.17") - static final short BORDER_MEDIUM_DASH_DOT_DOT = 0xC; //BorderStyle.MEDIUM_DASH_DOT_DOT.getCode(); + short BORDER_MEDIUM_DASH_DOT_DOT = 0xC; //BorderStyle.MEDIUM_DASH_DOT_DOT.getCode(); /** * slanted dash-dot border * @deprecated 3.15 beta 2. Use {@link BorderStyle#SLANTED_DASH_DOT} instead. */ @Removal(version="3.17") - static final short BORDER_SLANTED_DASH_DOT = 0xD; //BorderStyle.SLANTED_DASH_DOT.getCode(); + short BORDER_SLANTED_DASH_DOT = 0xD; //BorderStyle.SLANTED_DASH_DOT.getCode(); /** * Fill Pattern: No background * @deprecated 3.15 beta 3. Use {@link FillPatternType#NO_FILL} instead. */ @Removal(version="3.17") - static final short NO_FILL = 0; //FillPatternType.NO_FILL.getCode(); + short NO_FILL = 0; //FillPatternType.NO_FILL.getCode(); /** * Fill Pattern: Solidly filled * @deprecated 3.15 beta 3. Use {@link FillPatternType#SOLID_FOREGROUND} instead. */ @Removal(version="3.17") - static final short SOLID_FOREGROUND = 1; //FillPatternType.SOLID_FOREGROUND.getCode(); + short SOLID_FOREGROUND = 1; //FillPatternType.SOLID_FOREGROUND.getCode(); /** * Fill Pattern: Small fine dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#FINE_DOTS} instead. */ @Removal(version="3.17") - static final short FINE_DOTS = 2; //FillPatternType.FINE_DOTS.getCode(); + short FINE_DOTS = 2; //FillPatternType.FINE_DOTS.getCode(); /** * Fill Pattern: Wide dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#ALT_BARS} instead. */ @Removal(version="3.17") - static final short ALT_BARS = 3; //FillPatternType.ALT_BARS.getCode(); + short ALT_BARS = 3; //FillPatternType.ALT_BARS.getCode(); /** * Fill Pattern: Sparse dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#SPARSE_DOTS} instead. */ @Removal(version="3.17") - static final short SPARSE_DOTS = 4; //FillPatternType.SPARSE_DOTS.getCode(); + short SPARSE_DOTS = 4; //FillPatternType.SPARSE_DOTS.getCode(); /** * Fill Pattern: Thick horizontal bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_HORZ_BANDS} instead. */ @Removal(version="3.17") - static final short THICK_HORZ_BANDS = 5; //FillPatternType.THICK_HORZ_BANDS.getCode(); + short THICK_HORZ_BANDS = 5; //FillPatternType.THICK_HORZ_BANDS.getCode(); /** * Fill Pattern: Thick vertical bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_VERT_BANDS} instead. */ @Removal(version="3.17") - static final short THICK_VERT_BANDS = 6; //FillPatternType.THICK_VERT_BANDS.getCode(); + short THICK_VERT_BANDS = 6; //FillPatternType.THICK_VERT_BANDS.getCode(); /** * Fill Pattern: Thick backward facing diagonals * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_BACKWARD_DIAG} instead. */ @Removal(version="3.17") - static final short THICK_BACKWARD_DIAG = 7; //FillPatternType.THICK_BACKWARD_DIAG.getCode(); + short THICK_BACKWARD_DIAG = 7; //FillPatternType.THICK_BACKWARD_DIAG.getCode(); /** * Fill Pattern: Thick forward facing diagonals * @deprecated 3.15 beta 3. Use {@link FillPatternType#THICK_FORWARD_DIAG} instead. */ @Removal(version="3.17") - static final short THICK_FORWARD_DIAG = 8; //FillPatternType.THICK_FORWARD_DIAG.getCode(); + short THICK_FORWARD_DIAG = 8; //FillPatternType.THICK_FORWARD_DIAG.getCode(); /** * Fill Pattern: Large spots * @deprecated 3.15 beta 3. Use {@link FillPatternType#BIG_SPOTS} instead. */ @Removal(version="3.17") - static final short BIG_SPOTS = 9; //FillPatternType.BIG_SPOTS.getCode(); + short BIG_SPOTS = 9; //FillPatternType.BIG_SPOTS.getCode(); /** * Fill Pattern: Brick-like layout * @deprecated 3.15 beta 3. Use {@link FillPatternType#BRICKS} instead. */ @Removal(version="3.17") - static final short BRICKS = 10; //FillPatternType.BRICKS.getCode(); + short BRICKS = 10; //FillPatternType.BRICKS.getCode(); /** * Fill Pattern: Thin horizontal bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_HORZ_BANDS} instead. */ @Removal(version="3.17") - static final short THIN_HORZ_BANDS = 11; //FillPatternType.THIN_HORZ_BANDS.getCode(); + short THIN_HORZ_BANDS = 11; //FillPatternType.THIN_HORZ_BANDS.getCode(); /** * Fill Pattern: Thin vertical bands * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_VERT_BANDS} instead. */ @Removal(version="3.17") - static final short THIN_VERT_BANDS = 12; //FillPatternType.THIN_VERT_BANDS.getCode(); + short THIN_VERT_BANDS = 12; //FillPatternType.THIN_VERT_BANDS.getCode(); /** * Fill Pattern: Thin backward diagonal * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_BACKWARD_DIAG} instead. */ @Removal(version="3.17") - static final short THIN_BACKWARD_DIAG = 13; //FillPatternType.THIN_BACKWARD_DIAG.getCode(); + short THIN_BACKWARD_DIAG = 13; //FillPatternType.THIN_BACKWARD_DIAG.getCode(); /** * Fill Pattern: Thin forward diagonal * @deprecated 3.15 beta 3. Use {@link FillPatternType#THIN_FORWARD_DIAG} instead. */ @Removal(version="3.17") - static final short THIN_FORWARD_DIAG = 14; //FillPatternType.THIN_FORWARD_DIAG.getCode(); + short THIN_FORWARD_DIAG = 14; //FillPatternType.THIN_FORWARD_DIAG.getCode(); /** * Fill Pattern: Squares * @deprecated 3.15 beta 3. Use {@link FillPatternType#SQUARES} instead. */ @Removal(version="3.17") - static final short SQUARES = 15; //FillPatternType.SQUARES.getCode(); + short SQUARES = 15; //FillPatternType.SQUARES.getCode(); /** * Fill Pattern: Diamonds * @deprecated 3.15 beta 3. Use {@link FillPatternType#DIAMONDS} instead. */ @Removal(version="3.17") - static final short DIAMONDS = 16; //FillPatternType.DIAMONDS.getCode(); + short DIAMONDS = 16; //FillPatternType.DIAMONDS.getCode(); /** * Fill Pattern: Less Dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#LESS_DOTS} instead. */ @Removal(version="3.17") - static final short LESS_DOTS = 17; //FillPatternType.LESS_DOTS.getCode(); + short LESS_DOTS = 17; //FillPatternType.LESS_DOTS.getCode(); /** * Fill Pattern: Least Dots * @deprecated 3.15 beta 3. Use {@link FillPatternType#LEAST_DOTS} instead. */ @Removal(version="3.17") - static final short LEAST_DOTS = 18; //FillPatternType.LEAST_DOTS.getCode(); + short LEAST_DOTS = 18; //FillPatternType.LEAST_DOTS.getCode(); /** * get the index within the Workbook (sequence within the collection of ExtnededFormat objects) @@ -353,7 +353,7 @@ public interface CellStyle { /** * Get the format string */ - public String getDataFormatString(); + String getDataFormatString(); /** * set the font for this style @@ -870,17 +870,17 @@ public interface CellStyle { * to be of the same type (HSSFCellStyle or * XSSFCellStyle) */ - public void cloneStyleFrom(CellStyle source); + void cloneStyleFrom(CellStyle source); /** * Controls if the Cell should be auto-sized * to shrink to fit if the text is too long */ - public void setShrinkToFit(boolean shrinkToFit); + void setShrinkToFit(boolean shrinkToFit); /** * Should the Cell be auto-sized by Excel to shrink * it to fit if this text is too long? */ - public boolean getShrinkToFit(); + boolean getShrinkToFit(); } diff --git a/src/java/org/apache/poi/ss/usermodel/CellValue.java b/src/java/org/apache/poi/ss/usermodel/CellValue.java index 301accfca..b10210882 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellValue.java +++ b/src/java/org/apache/poi/ss/usermodel/CellValue.java @@ -18,6 +18,8 @@ package org.apache.poi.ss.usermodel; import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; /** * Mimics the 'data view' of a cell. This allows formula evaluator @@ -47,12 +49,15 @@ public final class CellValue { public CellValue(double numberValue) { this(CellType.NUMERIC, numberValue, false, null, 0); } + public static CellValue valueOf(boolean booleanValue) { return booleanValue ? TRUE : FALSE; } + public CellValue(String stringValue) { this(CellType.STRING, 0.0, false, stringValue, 0); } + public static CellValue getError(int errorCode) { return new CellValue(CellType.ERROR, 0.0, false, null, errorCode); } @@ -64,30 +69,44 @@ public final class CellValue { public boolean getBooleanValue() { return _booleanValue; } + /** * @return Returns the numberValue. */ public double getNumberValue() { return _numberValue; } + /** * @return Returns the stringValue. */ public String getStringValue() { return _textValue; } + + /** + * Return the cell type. + * + * @return the cell type + * @since POI 3.15 + * @deprecated POI 3.15 + * Will be renamed to getCellTypeEnum() when we make the CellType enum transition in POI 4.0. See bug 59791. + */ + @Internal(since="POI 3.15 beta 3") + @Removal(version="4.2") + public CellType getCellTypeEnum() { + return _cellType; + } + /** - * @return Returns the cellType. - * @since POI 3.15 - */ - public CellType getCellTypeEnum() { - return _cellType; - } - /** - * @return Returns the cellType. + * Return the cell type. + * + * Will return {@link CellType} in version 4.0 of POI. + * For forwards compatibility, do not hard-code cell type literals in your code. + * + * @return the cell type + * * @deprecated POI 3.15. Use {@link #getCellTypeEnum()} instead. - * In the future, the signature of this method will be changed to return a - * {@link CellType}. */ @Deprecated public int getCellType() { @@ -100,6 +119,7 @@ public final class CellValue { public byte getErrorValue() { return (byte) _errorCode; } + public String toString() { StringBuffer sb = new StringBuffer(64); sb.append(getClass().getName()).append(" ["); diff --git a/src/java/org/apache/poi/ss/usermodel/Workbook.java b/src/java/org/apache/poi/ss/usermodel/Workbook.java index e52615fb1..f5043b727 100644 --- a/src/java/org/apache/poi/ss/usermodel/Workbook.java +++ b/src/java/org/apache/poi/ss/usermodel/Workbook.java @@ -35,22 +35,22 @@ import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; public interface Workbook extends Closeable, Iterable { /** Extended windows meta file */ - public static final int PICTURE_TYPE_EMF = 2; + int PICTURE_TYPE_EMF = 2; /** Windows Meta File */ - public static final int PICTURE_TYPE_WMF = 3; + int PICTURE_TYPE_WMF = 3; /** Mac PICT format */ - public static final int PICTURE_TYPE_PICT = 4; + int PICTURE_TYPE_PICT = 4; /** JPEG format */ - public static final int PICTURE_TYPE_JPEG = 5; + int PICTURE_TYPE_JPEG = 5; /** PNG format */ - public static final int PICTURE_TYPE_PNG = 6; + int PICTURE_TYPE_PNG = 6; /** Device independent bitmap */ - public static final int PICTURE_TYPE_DIB = 7; + int PICTURE_TYPE_DIB = 7; /** @@ -58,14 +58,14 @@ public interface Workbook extends Closeable, Iterable { * * @see #setSheetHidden(int, int) */ - public static final int SHEET_STATE_VISIBLE = 0; + int SHEET_STATE_VISIBLE = 0; /** * Indicates the book window is hidden, but can be shown by the user via the user interface. * * @see #setSheetHidden(int, int) */ - public static final int SHEET_STATE_HIDDEN = 1; + int SHEET_STATE_HIDDEN = 1; /** * Indicates the sheet is hidden and cannot be shown in the user interface (UI). @@ -77,7 +77,7 @@ public interface Workbook extends Closeable, Iterable { * * @see #setSheetHidden(int, int) */ - public static final int SHEET_STATE_VERY_HIDDEN = 2; + int SHEET_STATE_VERY_HIDDEN = 2; /** * Convenience method to get the active sheet. The active sheet is is the sheet @@ -169,7 +169,7 @@ public interface Workbook extends Closeable, Iterable { int getSheetIndex(Sheet sheet); /** - * Sreate an Sheet for this Workbook, adds it to the sheets and returns + * Create a Sheet for this Workbook, adds it to the sheets and returns * the high level representation. Use this to create new sheets. * * @return Sheet representing the new sheet. @@ -217,7 +217,7 @@ public interface Workbook extends Closeable, Iterable { * See {@link org.apache.poi.ss.util.WorkbookUtil#createSafeSheetName(String nameProposal)} * for a safe way to create valid names *

- * @param sheetname sheetname to set for the sheet. + * @param sheetname The name to set for the sheet. * @return Sheet representing the new sheet. * @throws IllegalArgumentException if the name is null or invalid * or workbook already contains a sheet with this name @@ -253,6 +253,8 @@ public interface Workbook extends Closeable, Iterable { * * @param index of the sheet number (0-based physical & logical) * @return Sheet at the provided index + * @throws IllegalArgumentException if the index is out of range (index + * < 0 || index >= getNumberOfSheets()). */ Sheet getSheetAt(int index); @@ -617,7 +619,7 @@ public interface Workbook extends Closeable, Iterable { * workbook values when the workbook is opened * @since 3.8 */ - public void setForceFormulaRecalculation(boolean value); + void setForceFormulaRecalculation(boolean value); /** * Whether Excel will be asked to recalculate all formulas when the workbook is opened. @@ -632,6 +634,5 @@ public interface Workbook extends Closeable, Iterable { * @return SpreadsheetVersion enum * @since 3.14 beta 2 */ - public SpreadsheetVersion getSpreadsheetVersion(); - + SpreadsheetVersion getSpreadsheetVersion(); } diff --git a/src/java/org/apache/poi/ss/util/SheetUtil.java b/src/java/org/apache/poi/ss/util/SheetUtil.java index d64dbeba8..f234380fe 100644 --- a/src/java/org/apache/poi/ss/util/SheetUtil.java +++ b/src/java/org/apache/poi/ss/util/SheetUtil.java @@ -128,7 +128,7 @@ public class SheetUtil { // We should only be checking merged regions if useMergedCells is true. Why are we doing this for-loop? int colspan = 1; for (CellRangeAddress region : sheet.getMergedRegions()) { - if (containsCell(region, row.getRowNum(), column)) { + if (region.isInRange(row.getRowNum(), column)) { if (!useMergedCells) { // If we're not using merged cells, skip this one and move on to the next. return -1; @@ -151,8 +151,8 @@ public class SheetUtil { if (cellType == CellType.STRING) { RichTextString rt = cell.getRichStringCellValue(); String[] lines = rt.getString().split("\\n"); - for (int i = 0; i < lines.length; i++) { - String txt = lines[i] + defaultChar; + for (String line : lines) { + String txt = line + defaultChar; AttributedString str = new AttributedString(txt); copyAttributes(font, str, 0, txt.length()); @@ -193,7 +193,7 @@ public class SheetUtil { * @param defaultCharWidth the width of a character using the default font in a workbook * @param colspan the number of columns that is spanned by the cell (1 if the cell is not part of a merged region) * @param style the cell style, which contains text rotation and indention information needed to compute the cell width - * @param width the minimum best-fit width. This algorithm will only return values greater than or equal to the minimum width. + * @param minWidth the minimum best-fit width. This algorithm will only return values greater than or equal to the minimum width. * @param str the text contained in the cell * @return the best fit cell width */ @@ -219,8 +219,7 @@ public class SheetUtil { } // frameWidth accounts for leading spaces which is excluded from bounds.getWidth() final double frameWidth = bounds.getX() + bounds.getWidth(); - final double width = Math.max(minWidth, ((frameWidth / colspan) / defaultCharWidth) + style.getIndention()); - return width; + return Math.max(minWidth, ((frameWidth / colspan) / defaultCharWidth) + style.getIndention()); } /** @@ -273,13 +272,12 @@ public class SheetUtil { AttributedString str = new AttributedString(String.valueOf(defaultChar)); copyAttributes(defaultFont, str, 0, 1); TextLayout layout = new TextLayout(str.getIterator(), fontRenderContext); - int defaultCharWidth = (int) layout.getAdvance(); - return defaultCharWidth; + return (int) layout.getAdvance(); } /** * Compute width of a single cell in a row - * Convenience method for {@link getCellWidth} + * Convenience method for {@link #getCellWidth} * * @param row the row that contains the cell of interest * @param column the column number of the cell whose width is to be calculated @@ -334,7 +332,7 @@ public class SheetUtil { private static void copyAttributes(Font font, AttributedString str, int startIdx, int endIdx) { str.addAttribute(TextAttribute.FAMILY, font.getFontName(), startIdx, endIdx); str.addAttribute(TextAttribute.SIZE, (float)font.getFontHeightInPoints()); - if (font.getBoldweight() == Font.BOLDWEIGHT_BOLD) str.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, startIdx, endIdx); + if (font.getBold()) str.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, startIdx, endIdx); if (font.getItalic() ) str.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, startIdx, endIdx); if (font.getUnderline() == Font.U_SINGLE ) str.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, startIdx, endIdx); } @@ -348,6 +346,7 @@ public class SheetUtil { * @return true if the range contains the cell [rowIx, colIx] * @deprecated 3.15 beta 2. Use {@link CellRangeAddressBase#isInRange(int, int)}. */ + @Deprecated public static boolean containsCell(CellRangeAddress cr, int rowIx, int colIx) { return cr.isInRange(rowIx, colIx); } diff --git a/src/java/org/apache/poi/util/CommonsLogger.java b/src/java/org/apache/poi/util/CommonsLogger.java index 1cbb9628c..a204fde17 100644 --- a/src/java/org/apache/poi/util/CommonsLogger.java +++ b/src/java/org/apache/poi/util/CommonsLogger.java @@ -30,7 +30,7 @@ import org.apache.commons.logging.LogFactory; */ public class CommonsLogger extends POILogger { - private static LogFactory _creator = LogFactory.getFactory(); + private static final LogFactory _creator = LogFactory.getFactory(); private Log log = null; @Override @@ -46,8 +46,10 @@ public class CommonsLogger extends POILogger * @param obj1 The object to log. */ @Override - public void log(final int level, final Object obj1) + protected void _log(final int level, final Object obj1) { + // FIXME: What happens if level is in between two levels (an even number)? + // Should this be `if (level >= FATAL) ...`? if(level==FATAL) { if(log.isFatalEnabled()) @@ -100,9 +102,11 @@ public class CommonsLogger extends POILogger * @param exception An exception to be logged */ @Override - public void log(final int level, final Object obj1, + protected void _log(final int level, final Object obj1, final Throwable exception) { + // FIXME: What happens if level is in between two levels (an even number)? + // Should this be `if (level >= FATAL) ...`? if(level==FATAL) { if(log.isFatalEnabled()) @@ -174,6 +178,8 @@ public class CommonsLogger extends POILogger @Override public boolean check(final int level) { + // FIXME: What happens if level is in between two levels (an even number)? + // Should this be `if (level >= FATAL) ...`? if(level==FATAL) { if(log.isFatalEnabled()) diff --git a/src/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java b/src/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java index ef9e8443a..6f4475287 100644 --- a/src/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java +++ b/src/java/org/apache/poi/util/DefaultTempFileCreationStrategy.java @@ -83,15 +83,22 @@ public class DefaultTempFileCreationStrategy implements TempFileCreationStrategy } /** - * Attempt to create a directory + * Attempt to create a directory, including any necessary parent directories. + * Does nothing if directory already exists. * - * @param directory - * @throws IOException + * @param directory the directory to create + * @throws IOException if unable to create temporary directory or it is not a directory */ private void createTempDirectory(File directory) throws IOException { - if (!(directory.exists() || directory.mkdirs()) || !directory.isDirectory()) { + // create directory if it doesn't exist + final boolean dirExists = (directory.exists() || directory.mkdirs()); + + if (!dirExists) { throw new IOException("Could not create temporary directory '" + directory + "'"); } + else if (!directory.isDirectory()) { + throw new IOException("Could not create temporary directory. '" + directory + "' exists but is not a directory."); + } } @Override diff --git a/src/java/org/apache/poi/util/NullLogger.java b/src/java/org/apache/poi/util/NullLogger.java index fe0979cfe..e21adfaa7 100644 --- a/src/java/org/apache/poi/util/NullLogger.java +++ b/src/java/org/apache/poi/util/NullLogger.java @@ -23,6 +23,7 @@ package org.apache.poi.util; * calls as cheap as possible by performing lazy evaluation of the log * message.

*/ +@Internal public class NullLogger extends POILogger { @Override public void initialize(final String cat) { @@ -37,7 +38,7 @@ public class NullLogger extends POILogger { */ @Override - public void log(final int level, final Object obj1) { + protected void _log(final int level, final Object obj1) { // do nothing } @@ -49,7 +50,19 @@ public class NullLogger extends POILogger { * @param exception An exception to be logged */ @Override - public void log(int level, Object obj1, final Throwable exception) { + protected void _log(int level, Object obj1, final Throwable exception) { + // do nothing + } + + /** + * Log a message. Lazily appends Object parameters together. + * If the last parameter is a {@link Throwable} it is logged specially. + * + * @param level One of DEBUG, INFO, WARN, ERROR, FATAL + * @param objs the objects to place in the message + */ + @Override + public void log(int level, Object... objs) { // do nothing } diff --git a/src/java/org/apache/poi/util/POILogFactory.java b/src/java/org/apache/poi/util/POILogFactory.java index ba2551de7..1e34ccd29 100644 --- a/src/java/org/apache/poi/util/POILogFactory.java +++ b/src/java/org/apache/poi/util/POILogFactory.java @@ -35,7 +35,7 @@ public final class POILogFactory { /** * Map of POILogger instances, with classes as keys */ - private static Map _loggers = new HashMap(); + private static final Map _loggers = new HashMap(); /** * A common instance of NullLogger, as it does nothing diff --git a/src/java/org/apache/poi/util/POILogger.java b/src/java/org/apache/poi/util/POILogger.java index 04198bfba..c54c75425 100644 --- a/src/java/org/apache/poi/util/POILogger.java +++ b/src/java/org/apache/poi/util/POILogger.java @@ -54,7 +54,7 @@ public abstract class POILogger { * @param level One of DEBUG, INFO, WARN, ERROR, FATAL * @param obj1 The object to log. This is converted to a string. */ - abstract protected void log(int level, Object obj1); + abstract protected void _log(int level, Object obj1); /** * Log a message @@ -63,11 +63,20 @@ public abstract class POILogger { * @param obj1 The object to log. This is converted to a string. * @param exception An exception to be logged */ - abstract protected void log(int level, Object obj1, final Throwable exception); + abstract protected void _log(int level, Object obj1, final Throwable exception); /** * Check if a logger is enabled to log at the specified level + * This allows code to avoid building strings or evaluating functions in + * the arguments to log. + * + * An example: + *

+     * if (logger.check(POILogger.INFO)) {
+     *     logger.log(POILogger.INFO, "Avoid concatenating " + " strings and evaluating " + functions());
+     * }
+     * 
* * @param level One of DEBUG, INFO, WARN, ERROR, FATAL */ @@ -98,9 +107,9 @@ public abstract class POILogger { // somehow this ambiguity works and doesn't lead to a loop, // but it's confusing ... if (lastEx == null) { - log(level, msg); + _log(level, msg); } else { - log(level, msg, lastEx); + _log(level, msg, lastEx); } } } diff --git a/src/java/org/apache/poi/util/SystemOutLogger.java b/src/java/org/apache/poi/util/SystemOutLogger.java index 36d96ca03..457f25410 100644 --- a/src/java/org/apache/poi/util/SystemOutLogger.java +++ b/src/java/org/apache/poi/util/SystemOutLogger.java @@ -42,9 +42,9 @@ public class SystemOutLogger extends POILogger * @param obj1 The object to log. */ @Override - public void log(final int level, final Object obj1) + protected void _log(final int level, final Object obj1) { - log(level, obj1, null); + _log(level, obj1, null); } /** @@ -56,7 +56,7 @@ public class SystemOutLogger extends POILogger */ @Override @SuppressForbidden("uses printStackTrace") - public void log(final int level, final Object obj1, + protected void _log(final int level, final Object obj1, final Throwable exception) { if (check(level)) { System.out.println("[" + _cat + "]" + LEVEL_STRINGS_SHORT[Math.min(LEVEL_STRINGS_SHORT.length-1, level)] + " " + obj1); diff --git a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java index a67e83e24..cdd35b359 100644 --- a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java +++ b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java @@ -33,7 +33,6 @@ import org.apache.poi.hsmf.datatypes.AttachmentChunks; import org.apache.poi.hsmf.extractor.OutlookTextExtactor; import org.apache.poi.hssf.extractor.ExcelExtractor; import org.apache.poi.hwpf.extractor.WordExtractor; -import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackageAccess; @@ -123,7 +122,7 @@ public class ExtractorFactory { return OLE2ExtractorFactory.getPreferEventExtractor(); } - public static POITextExtractor createExtractor(File f) throws IOException, InvalidFormatException, OpenXML4JException, XmlException { + public static POITextExtractor createExtractor(File f) throws IOException, OpenXML4JException, XmlException { NPOIFSFileSystem fs = null; try { fs = new NPOIFSFileSystem(f); @@ -163,7 +162,7 @@ public class ExtractorFactory { } } - public static POITextExtractor createExtractor(InputStream inp) throws IOException, InvalidFormatException, OpenXML4JException, XmlException { + public static POITextExtractor createExtractor(InputStream inp) throws IOException, OpenXML4JException, XmlException { // Figure out the kind of stream // If clearly doesn't do mark/reset, wrap up if (! inp.markSupported()) { diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java index 25efc1e55..6779d8a21 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -64,7 +64,7 @@ public class SignatureConfig { private static final POILogger LOG = POILogFactory.getLogger(SignatureConfig.class); - public static interface SignatureConfigurable { + public interface SignatureConfigurable { void setSignatureConfig(SignatureConfig signatureConfig); } diff --git a/src/ooxml/java/org/apache/poi/util/OOXMLLite.java b/src/ooxml/java/org/apache/poi/util/OOXMLLite.java index b32077123..afb60f600 100644 --- a/src/ooxml/java/org/apache/poi/util/OOXMLLite.java +++ b/src/ooxml/java/org/apache/poi/util/OOXMLLite.java @@ -96,6 +96,7 @@ public final class OOXMLLite { "BaseTestXSheet", "BaseTestXRow", "BaseTestXCell", + "BaseTestXSSFPivotTable", "TestSXSSFWorkbook\\$\\d", "TestSXSSFWorkbook\\$NullOutputStream", "TestUnfixedBugs", diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java index 0c4e4971d..70df3f806 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java @@ -134,7 +134,7 @@ public abstract class XSLFShape implements Shape { if (this instanceof PlaceableShape) { PlaceableShape ps = (PlaceableShape)this; - ps.setAnchor(((PlaceableShape)sh).getAnchor()); + ps.setAnchor(sh.getAnchor()); } @@ -257,7 +257,7 @@ public abstract class XSLFShape implements Shape { * Different types of placeholders are allowed and can be specified by using the placeholder * type attribute for this element * - * @param placeholder + * @param placeholder The shape to use as placeholder or null if no placeholder should be set. */ protected void setPlaceholder(Placeholder placeholder) { String xquery = "declare namespace p='"+PML_NS+"' .//*/p:nvPr"; @@ -494,7 +494,7 @@ public abstract class XSLFShape implements Shape { int idx = (int)fillRef.getIdx(); CTSchemeColor phClr = fillRef.getSchemeClr(); CTStyleMatrix matrix = theme.getXmlObject().getThemeElements().getFmtScheme(); - XmlObject styleLst = null; + final XmlObject styleLst; int childIdx; if (idx >= 1 && idx <= 999) { childIdx = idx-1; diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java index 331ad9a0f..6b9ce374f 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java @@ -907,7 +907,9 @@ public class SXSSFWorkbook implements Workbook { try { sheet.getSheetDataWriter().close(); } catch (IOException e) { - // ignore exception here + logger.log(POILogger.WARN, + "An exception occurred while closing sheet data writer for sheet " + + sheet.getSheetName() + ".", e); } } @@ -926,11 +928,8 @@ public class SXSSFWorkbook implements Workbook { @Override public void write(OutputStream stream) throws IOException { - for (SXSSFSheet sheet : _xFromSxHash.values()) - { - sheet.flushRows(); - } - + flushSheets(); + //Save the template File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx"); try @@ -956,6 +955,13 @@ public class SXSSFWorkbook implements Workbook { } } + protected void flushSheets() throws IOException { + for (SXSSFSheet sheet : _xFromSxHash.values()) + { + sheet.flushRows(); + } + } + /** * Dispose of temporary files backing this workbook on disk. * Calling this method will render the workbook unusable. diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java index 780126c3d..11fe3efad 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/BaseXSSFFormulaEvaluator.java @@ -28,7 +28,7 @@ import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellValue; -import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.util.Internal; /** * Internal POI use only - parent of XSSF and SXSSF formula evaluators @@ -52,6 +52,78 @@ public abstract class BaseXSSFFormulaEvaluator extends BaseFormulaEvaluator { _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell)); } + /** + * If cell contains formula, it evaluates the formula, + * and saves the result of the formula. The cell + * remains as a formula cell. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + * Note that the type of the formula result is returned, + * so you know what kind of value is also stored with + * the formula. + *
+     * CellType evaluatedCellType = evaluator.evaluateFormulaCellEnum(cell);
+     * 
+ * Be aware that your cell will hold both the formula, + * and the result. If you want the cell replaced with + * the result of the formula, use {@link #evaluate(org.apache.poi.ss.usermodel.Cell)} } + * @param cell The cell to evaluate + * @return The type of the formula result (the cell's type remains as CellType.FORMULA however) + * If cell is not a formula cell, returns {@link CellType#_NONE} rather than throwing an exception. + * @since POI 3.15 beta 3 + * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791. + */ + @Internal(since="POI 3.15 beta 3") + public CellType evaluateFormulaCellEnum(Cell cell) { + if (cell == null || cell.getCellTypeEnum() != CellType.FORMULA) { + return CellType._NONE; + } + CellValue cv = evaluateFormulaCellValue(cell); + // cell remains a formula cell, but the cached value is changed + setCellValue(cell, cv); + return cv.getCellType(); + } + + /** + * If cell contains formula, it evaluates the formula, and + * puts the formula result back into the cell, in place + * of the old formula. + * Else if cell does not contain formula, this method leaves + * the cell unchanged. + */ + protected void doEvaluateInCell(Cell cell) { + if (cell == null) return; + if (cell.getCellTypeEnum() == CellType.FORMULA) { + CellValue cv = evaluateFormulaCellValue(cell); + setCellType(cell, cv); // cell will no longer be a formula cell + setCellValue(cell, cv); + } + } + + private static void setCellValue(Cell cell, CellValue cv) { + CellType cellType = cv.getCellType(); + switch (cellType) { + case BOOLEAN: + cell.setCellValue(cv.getBooleanValue()); + break; + case ERROR: + cell.setCellErrorValue(cv.getErrorValue()); + break; + case NUMERIC: + cell.setCellValue(cv.getNumberValue()); + break; + case STRING: + cell.setCellValue(new XSSFRichTextString(cv.getStringValue())); + break; + case BLANK: + // never happens - blanks eventually get translated to zero + case FORMULA: + // this will never happen, we have already evaluated the formula + default: + throw new IllegalStateException("Unexpected cell value type (" + cellType + ")"); + } + } + /** * Turns a XSSFCell / SXSSFCell into a XSSFEvaluationCell */ diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotCacheDefinition.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotCacheDefinition.java index 8ab465aff..26735c782 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotCacheDefinition.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotCacheDefinition.java @@ -28,10 +28,13 @@ import javax.xml.namespace.QName; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Name; 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.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.util.Beta; @@ -41,6 +44,7 @@ import org.apache.xmlbeans.XmlOptions; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCacheField; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCacheFields; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPivotCacheDefinition; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheetSource; public class XSSFPivotCacheDefinition extends POIXMLDocumentPart{ @@ -116,6 +120,40 @@ public class XSSFPivotCacheDefinition extends POIXMLDocumentPart{ out.close(); } + /** + * Find the 2D base data area for the pivot table, either from its direct reference or named table/range. + * @return AreaReference representing the current area defined by the pivot table + * @throws IllegalArgumentException if the ref attribute is not contiguous or the name attribute is not found. + */ + @Beta + public AreaReference getPivotArea(Workbook wb) throws IllegalArgumentException { + final CTWorksheetSource wsSource = ctPivotCacheDefinition.getCacheSource().getWorksheetSource(); + + final String ref = wsSource.getRef(); + final String name = wsSource.getName(); + + if (ref == null && name == null) throw new IllegalArgumentException("Pivot cache must reference an area, named range, or table."); + + // this is the XML format, so tell the reference that. + if (ref != null) return new AreaReference(ref, SpreadsheetVersion.EXCEL2007); + + if (name != null) { + // named range or table? + final Name range = wb.getName(name); + if (range != null) return new AreaReference(range.getRefersToFormula(), SpreadsheetVersion.EXCEL2007); + // not a named range, check for a table. + // do this second, as tables are sheet-specific, but named ranges are not, and may not have a sheet name given. + final XSSFSheet sheet = (XSSFSheet) wb.getSheet(wsSource.getSheet()); + for (XSSFTable table : sheet.getTables()) { + if (table.getName().equals(name)) { //case-sensitive? + return new AreaReference(table.getStartCellReference(), table.getEndCellReference()); + } + } + } + + throw new IllegalArgumentException("Name '" + name + "' was not found."); + } + /** * Generates a cache field for each column in the reference area for the pivot table. * @param sheet The sheet where the data i collected from @@ -123,7 +161,7 @@ public class XSSFPivotCacheDefinition extends POIXMLDocumentPart{ @Beta protected void createCacheFields(Sheet sheet) { //Get values for start row, start and end column - AreaReference ar = new AreaReference(ctPivotCacheDefinition.getCacheSource().getWorksheetSource().getRef()); + AreaReference ar = getPivotArea(sheet.getWorkbook()); CellReference firstCell = ar.getFirstCell(); CellReference lastCell = ar.getLastCell(); int columnStart = firstCell.getCol(); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java index bf85f65de..e6f91afde 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFPivotTable.java @@ -30,11 +30,11 @@ import javax.xml.namespace.QName; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; -import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataConsolidateFunction; import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.util.Beta; @@ -214,13 +214,8 @@ public class XSSFPivotTable extends POIXMLDocumentPart { } protected AreaReference getPivotArea() { - AreaReference pivotArea = new AreaReference( - getPivotCacheDefinition() - .getCTPivotCacheDefinition() - .getCacheSource() - .getWorksheetSource() - .getRef(), - SpreadsheetVersion.EXCEL2007); + final Workbook wb = getDataSheet().getWorkbook(); + AreaReference pivotArea = getPivotCacheDefinition().getPivotArea(wb); return pivotArea; } @@ -419,12 +414,13 @@ public class XSSFPivotTable extends POIXMLDocumentPart { /** * Creates cacheSource and workSheetSource for pivot table and sets the source reference as well assets the location of the pivot table - * @param source Source for data for pivot table * @param position Position for pivot table in sheet * @param sourceSheet Sheet where the source will be collected from + * @param refConfig an configurator that knows how to configure pivot table references */ @Beta - protected void createSourceReferences(AreaReference source, CellReference position, Sheet sourceSheet){ + protected void createSourceReferences(CellReference position, Sheet sourceSheet, PivotTableReferenceConfigurator refConfig){ + //Get cell one to the right and one down from position, add both to AreaReference and set pivot table location. AreaReference destination = new AreaReference(position, new CellReference(position.getRow()+1, position.getCol()+1)); @@ -448,9 +444,8 @@ public class XSSFPivotTable extends POIXMLDocumentPart { worksheetSource.setSheet(sourceSheet.getSheetName()); setDataSheet(sourceSheet); - String[] firstCell = source.getFirstCell().getCellRefParts(); - String[] lastCell = source.getLastCell().getCellRefParts(); - worksheetSource.setRef(firstCell[2]+firstCell[1]+':'+lastCell[2]+lastCell[1]); + refConfig.configureReference(worksheetSource); + if (worksheetSource.getName() == null && worksheetSource.getRef() == null) throw new IllegalArgumentException("Pivot table source area reference or name must be specified."); } @Beta @@ -465,11 +460,20 @@ public class XSSFPivotTable extends POIXMLDocumentPart { int firstColumn = sourceArea.getFirstCell().getCol(); int lastColumn = sourceArea.getLastCell().getCol(); CTPivotField pivotField; - for(int i = 0; i<=lastColumn-firstColumn; i++) { + for(int i = firstColumn; i<=lastColumn; i++) { pivotField = pivotFields.addNewPivotField(); pivotField.setDataField(false); pivotField.setShowAll(false); } pivotFields.setCount(pivotFields.sizeOfPivotFieldArray()); } + + protected static interface PivotTableReferenceConfigurator { + + /** + * Configure the name or area reference for the pivot table + * @param wsSource CTWorksheetSource that needs the pivot source reference assignment + */ + public void configureReference(CTWorksheetSource wsSource); + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index d72dca445..bc2f071be 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -64,8 +64,10 @@ import org.apache.poi.ss.usermodel.Footer; import org.apache.poi.ss.usermodel.Header; import org.apache.poi.ss.usermodel.IgnoredErrorType; import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Table; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellAddress; import org.apache.poi.ss.util.CellRangeAddress; @@ -80,6 +82,7 @@ import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.poi.util.Removal; import org.apache.poi.xssf.model.CommentsTable; +import org.apache.poi.xssf.usermodel.XSSFPivotTable.PivotTableReferenceConfigurator; import org.apache.poi.xssf.usermodel.helpers.ColumnHelper; import org.apache.poi.xssf.usermodel.helpers.XSSFIgnoredErrorHelper; import org.apache.poi.xssf.usermodel.helpers.XSSFRowShifter; @@ -1925,15 +1928,17 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } // Performance optimization: explicit boxing is slightly faster than auto-unboxing, though may use more memory - final Integer rownumI = new Integer(row.getRowNum()); // NOSONAR - int idx = _rows.headMap(rownumI).size(); - _rows.remove(rownumI); + final int rowNum = row.getRowNum(); + final Integer rowNumI = new Integer(rowNum); // NOSONAR + // this is not the physical row number! + final int idx = _rows.headMap(rowNumI).size(); + _rows.remove(rowNumI); worksheet.getSheetData().removeRow(idx); // also remove any comment located in that row if(sheetComments != null) { for (CellAddress ref : getCellComments().keySet()) { - if (ref.getRow() == idx) { + if (ref.getRow() == rowNum) { sheetComments.removeComment(ref); } } @@ -4158,27 +4163,56 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } /** - * Create a pivot table and set area of source, source sheet and a position for pivot table - * @param source Area from where data will be collected - * @param position A reference to the cell where the table will start - * @param sourceSheet The sheet where source will be collected from + * Create a pivot table using the AreaReference range on sourceSheet, at the given position. + * If the source reference contains a sheet name, it must match the sourceSheet + * @param source location of pivot data + * @param position A reference to the top left cell where the pivot table will start + * @param sourceSheet The sheet containing the source data, if the source reference doesn't contain a sheet name + * @throws IllegalArgumentException if source references a sheet different than sourceSheet * @return The pivot table */ @Beta - public XSSFPivotTable createPivotTable(AreaReference source, CellReference position, Sheet sourceSheet) { + public XSSFPivotTable createPivotTable(final AreaReference source, CellReference position, Sheet sourceSheet) { final String sourceSheetName = source.getFirstCell().getSheetName(); if(sourceSheetName != null && !sourceSheetName.equalsIgnoreCase(sourceSheet.getSheetName())) { throw new IllegalArgumentException("The area is referenced in another sheet than the " + "defined source sheet " + sourceSheet.getSheetName() + "."); } + + return createPivotTable(position, sourceSheet, new PivotTableReferenceConfigurator() { + public void configureReference(CTWorksheetSource wsSource) { + final String[] firstCell = source.getFirstCell().getCellRefParts(); + final String firstRow = firstCell[1]; + final String firstCol = firstCell[2]; + final String[] lastCell = source.getLastCell().getCellRefParts(); + final String lastRow = lastCell[1]; + final String lastCol = lastCell[2]; + final String ref = firstCol+firstRow+':'+lastCol+lastRow; //or just source.formatAsString() + wsSource.setRef(ref); + } + }); + } + + /** + * Create a pivot table using the AreaReference or named/table range on sourceSheet, at the given position. + * If the source reference contains a sheet name, it must match the sourceSheet. + * @param sourceRef location of pivot data - mutually exclusive with SourceName + * @param sourceName range or table name for pivot data - mutually exclusive with SourceRef + * @param position A reference to the top left cell where the pivot table will start + * @param sourceSheet The sheet containing the source data, if the source reference doesn't contain a sheet name + * @throws IllegalArgumentException if source references a sheet different than sourceSheet + * @return The pivot table + */ + private XSSFPivotTable createPivotTable(CellReference position, Sheet sourceSheet, PivotTableReferenceConfigurator refConfig) { + XSSFPivotTable pivotTable = createPivotTable(); //Creates default settings for the pivot table pivotTable.setDefaultPivotTableDefinition(); //Set sources and references - pivotTable.createSourceReferences(source, position, sourceSheet); + pivotTable.createSourceReferences(position, sourceSheet, refConfig); - //Create cachefield/s and empty SharedItems + //Create cachefield/s and empty SharedItems - must be after creating references pivotTable.getPivotCacheDefinition().createCacheFields(sourceSheet); pivotTable.createDefaultDataColumns(); @@ -4186,9 +4220,10 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } /** - * Create a pivot table and set area of source and a position for pivot table - * @param source Area from where data will be collected - * @param position A reference to the cell where the table will start + * Create a pivot table using the AreaReference range, at the given position. + * If the source reference contains a sheet name, that sheet is used, otherwise this sheet is assumed as the source sheet. + * @param source location of pivot data + * @param position A reference to the top left cell where the pivot table will start * @return The pivot table */ @Beta @@ -4201,6 +4236,57 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { return createPivotTable(source, position, this); } + /** + * Create a pivot table using the Name range reference on sourceSheet, at the given position. + * If the source reference contains a sheet name, it must match the sourceSheet + * @param source location of pivot data + * @param position A reference to the top left cell where the pivot table will start + * @param sourceSheet The sheet containing the source data, if the source reference doesn't contain a sheet name + * @throws IllegalArgumentException if source references a sheet different than sourceSheet + * @return The pivot table + */ + @Beta + public XSSFPivotTable createPivotTable(final Name source, CellReference position, Sheet sourceSheet) { + if(source.getSheetName() != null && !source.getSheetName().equals(sourceSheet.getSheetName())) { + throw new IllegalArgumentException("The named range references another sheet than the " + + "defined source sheet " + sourceSheet.getSheetName() + "."); + } + + return createPivotTable(position, sourceSheet, new PivotTableReferenceConfigurator() { + public void configureReference(CTWorksheetSource wsSource) { + wsSource.setName(source.getNameName()); + } + }); + } + + /** + * Create a pivot table using the Name range, at the given position. + * If the source reference contains a sheet name, that sheet is used, otherwise this sheet is assumed as the source sheet. + * @param source location of pivot data + * @param position A reference to the top left cell where the pivot table will start + * @return The pivot table + */ + @Beta + public XSSFPivotTable createPivotTable(Name source, CellReference position) { + return createPivotTable(source, position, getWorkbook().getSheet(source.getSheetName())); + } + + /** + * Create a pivot table using the Table, at the given position. + * Tables are required to have a sheet reference, so no additional logic around reference sheet is needed. + * @param source location of pivot data + * @param position A reference to the top left cell where the pivot table will start + * @return The pivot table + */ + @Beta + public XSSFPivotTable createPivotTable(final Table source, CellReference position) { + return createPivotTable(position, getWorkbook().getSheet(source.getSheetName()), new PivotTableReferenceConfigurator() { + public void configureReference(CTWorksheetSource wsSource) { + wsSource.setName(source.getName()); + } + }); + } + /** * Returns all the pivot tables for this Sheet */ diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/ColumnHelper.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/ColumnHelper.java index 1e51e5e3f..b27100656 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/ColumnHelper.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/ColumnHelper.java @@ -319,6 +319,7 @@ public class ColumnHelper { } public int getIndexOfColumn(CTCols cols, CTCol searchCol) { + if (cols == null || searchCol == null) return -1; int i = 0; for (CTCol col : cols.getColArray()) { if (col.getMin() == searchCol.getMin() && col.getMax() == searchCol.getMax()) { diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java index d11ed1fa8..e55b014a7 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowShifter.java @@ -147,7 +147,8 @@ public final class XSSFRowShifter extends RowShifter { } - if (f.isSetRef()) { //Range of cells which the formula applies to. + //Range of cells which the formula applies to. + if (f.isSetRef()) { String ref = f.getRef(); String shiftedRef = shiftFormula(row, ref, shifter); if (shiftedRef != null) f.setRef(shiftedRef); diff --git a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java index 9d206f719..26fe68a63 100644 --- a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java +++ b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java @@ -16,6 +16,7 @@ ==================================================================== */ package org.apache.poi.extractor; +import static org.apache.poi.POITestCase.assertContains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -37,6 +38,7 @@ import org.apache.poi.hdgf.extractor.VisioTextExtractor; import org.apache.poi.hpbf.extractor.PublisherTextExtractor; import org.apache.poi.hslf.extractor.PowerPointExtractor; import org.apache.poi.hsmf.extractor.OutlookTextExtactor; +import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.OldExcelFormatException; import org.apache.poi.hssf.extractor.EventBasedExcelExtractor; import org.apache.poi.hssf.extractor.ExcelExtractor; @@ -1019,4 +1021,16 @@ public class TestExtractorFactory { // expected here } } + + // This bug is currently open. This test will fail with "expected error not thrown" when the bug has been fixed. + // When this happens, change this from @Test(expected=...) to @Test + // bug 45565: text within TextBoxes is extracted by ExcelExtractor and WordExtractor + @Test(expected=AssertionError.class) + public void test45565() throws Exception { + POITextExtractor extractor = ExtractorFactory.createExtractor(HSSFTestDataSamples.getSampleFile("45565.xls")); + String text = extractor.getText(); + assertContains(text, "testdoc"); + assertContains(text, "test phrase"); + extractor.close(); + } } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java index 77c91cdca..202b9e18e 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -55,6 +55,7 @@ import org.apache.poi.POIDataSamples; import org.apache.poi.POITestCase; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackageAccess; +import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.poifs.crypt.dsig.DigestInfo; import org.apache.poi.poifs.crypt.dsig.SignatureConfig; import org.apache.poi.poifs.crypt.dsig.SignatureInfo; @@ -99,7 +100,7 @@ public class TestSignatureInfo { public static void initBouncy() throws IOException { CryptoFunctions.registerBouncyCastle(); - /*** TODO : set cal to now ... only set to fixed date for debugging ... */ + // Set cal to now ... only set to fixed date for debugging ... cal = LocaleUtil.getLocaleCalendar(LocaleUtil.TIMEZONE_UTC); assertNotNull(cal); // cal.set(2014, 7, 6, 21, 42, 12); @@ -403,7 +404,9 @@ public class TestSignatureInfo { // verify Iterator spIter = si.getSignatureParts().iterator(); - assertTrue(spIter.hasNext()); + assertTrue("Had: " + si.getSignatureConfig().getOpcPackage(). + getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN), + spIter.hasNext()); SignaturePart sp = spIter.next(); boolean valid = sp.validate(); assertTrue(valid); diff --git a/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java index a24fd8879..921dc445e 100644 --- a/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestProper.java @@ -31,7 +31,6 @@ import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import junit.framework.AssertionFailedError; -import junit.framework.TestCase; import org.junit.Test; import static org.junit.Assert.assertEquals; diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/BaseTestXSSFPivotTable.java similarity index 74% rename from src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java rename to src/ooxml/testcases/org/apache/poi/xssf/usermodel/BaseTestXSSFPivotTable.java index 851ca33d6..445812566 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTable.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/BaseTestXSSFPivotTable.java @@ -16,16 +16,13 @@ ==================================================================== */ package org.apache.poi.xssf.usermodel; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.io.IOException; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataConsolidateFunction; -import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.XSSFITestDataProvider; @@ -38,97 +35,27 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPivotFields; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPivotTableDefinition; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STDataConsolidateFunction; -public class TestXSSFPivotTable { +public abstract class BaseTestXSSFPivotTable { private static final XSSFITestDataProvider _testDataProvider = XSSFITestDataProvider.instance; - private XSSFWorkbook wb; - private XSSFPivotTable pivotTable; - private XSSFPivotTable offsetPivotTable; - private Cell offsetOuterCell; + protected XSSFWorkbook wb; + protected XSSFPivotTable pivotTable; + protected XSSFPivotTable offsetPivotTable; + protected Cell offsetOuterCell; + /** + * required to set up the test pivot tables and cell reference, either by name or reference. + * @see junit.framework.TestCase#setUp() + */ @Before - public void setUp(){ - wb = new XSSFWorkbook(); - XSSFSheet sheet = wb.createSheet(); - - Row row1 = sheet.createRow(0); - // Create a cell and put a value in it. - Cell cell = row1.createCell(0); - cell.setCellValue("Names"); - Cell cell2 = row1.createCell(1); - cell2.setCellValue("#"); - Cell cell7 = row1.createCell(2); - cell7.setCellValue("Data"); - Cell cell10 = row1.createCell(3); - cell10.setCellValue("Value"); - - Row row2 = sheet.createRow(1); - Cell cell3 = row2.createCell(0); - cell3.setCellValue("Jan"); - Cell cell4 = row2.createCell(1); - cell4.setCellValue(10); - Cell cell8 = row2.createCell(2); - cell8.setCellValue("Apa"); - Cell cell11 = row1.createCell(3); - cell11.setCellValue(11.11); - - Row row3 = sheet.createRow(2); - Cell cell5 = row3.createCell(0); - cell5.setCellValue("Ben"); - Cell cell6 = row3.createCell(1); - cell6.setCellValue(9); - Cell cell9 = row3.createCell(2); - cell9.setCellValue("Bepa"); - Cell cell12 = row1.createCell(3); - cell12.setCellValue(12.12); - - AreaReference source = new AreaReference("A1:C2", _testDataProvider.getSpreadsheetVersion()); - pivotTable = sheet.createPivotTable(source, new CellReference("H5")); - - XSSFSheet offsetSheet = wb.createSheet(); - - Row tableRow_1 = offsetSheet.createRow(1); - offsetOuterCell = tableRow_1.createCell(1); - offsetOuterCell.setCellValue(-1); - Cell tableCell_1_1 = tableRow_1.createCell(2); - tableCell_1_1.setCellValue("Row #"); - Cell tableCell_1_2 = tableRow_1.createCell(3); - tableCell_1_2.setCellValue("Exponent"); - Cell tableCell_1_3 = tableRow_1.createCell(4); - tableCell_1_3.setCellValue("10^Exponent"); - - Row tableRow_2 = offsetSheet.createRow(2); - Cell tableCell_2_1 = tableRow_2.createCell(2); - tableCell_2_1.setCellValue(0); - Cell tableCell_2_2 = tableRow_2.createCell(3); - tableCell_2_2.setCellValue(0); - Cell tableCell_2_3 = tableRow_2.createCell(4); - tableCell_2_3.setCellValue(1); - - Row tableRow_3= offsetSheet.createRow(3); - Cell tableCell_3_1 = tableRow_3.createCell(2); - tableCell_3_1.setCellValue(1); - Cell tableCell_3_2 = tableRow_3.createCell(3); - tableCell_3_2.setCellValue(1); - Cell tableCell_3_3 = tableRow_3.createCell(4); - tableCell_3_3.setCellValue(10); - - Row tableRow_4 = offsetSheet.createRow(4); - Cell tableCell_4_1 = tableRow_4.createCell(2); - tableCell_4_1.setCellValue(2); - Cell tableCell_4_2 = tableRow_4.createCell(3); - tableCell_4_2.setCellValue(2); - Cell tableCell_4_3 = tableRow_4.createCell(4); - tableCell_4_3.setCellValue(100); - - AreaReference offsetSource = new AreaReference(new CellReference("C2"), new CellReference("E4")); - offsetPivotTable = offsetSheet.createPivotTable(offsetSource, new CellReference("C6")); - } + public abstract void setUp(); @After public void tearDown() throws IOException { - XSSFWorkbook wb2 = _testDataProvider.writeOutAndReadBack(wb); - wb.close(); - wb2.close(); + if (wb != null) { + XSSFWorkbook wb2 = _testDataProvider.writeOutAndReadBack(wb); + wb.close(); + wb2.close(); + } } /** @@ -155,6 +82,7 @@ public class TestXSSFPivotTable { assertEquals(0, (int)pivotTable.getRowLabelColumns().get(0)); assertEquals(1, (int)pivotTable.getRowLabelColumns().get(1)); } + /** * Verify that it's not possible to create a row label outside of the referenced area. */ @@ -314,6 +242,7 @@ public class TestXSSFPivotTable { /** * Verify that it's possible to create a new filter */ + @Test public void testAddReportFilter() { int columnIndex = 0; diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java index e695f385f..7fb1b507d 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java @@ -3091,4 +3091,41 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { assertEquals("09 Mar 2016", result); } + + // This bug is currently open. When this bug is fixed, it should not throw an AssertionError + @Test(expected=AssertionError.class) + public void test55076_collapseColumnGroups() throws Exception { + Workbook wb = new XSSFWorkbook(); + Sheet sheet = wb.createSheet(); + + // this column collapsing bug only occurs when the grouped columns are different widths + sheet.setColumnWidth(1, 400); + sheet.setColumnWidth(2, 600); + sheet.setColumnWidth(3, 800); + + assertEquals(400, sheet.getColumnWidth(1)); + assertEquals(600, sheet.getColumnWidth(2)); + assertEquals(800, sheet.getColumnWidth(3)); + + sheet.groupColumn(1, 3); + sheet.setColumnGroupCollapsed(1, true); + + assertEquals(0, sheet.getColumnOutlineLevel(0)); + assertEquals(1, sheet.getColumnOutlineLevel(1)); + assertEquals(1, sheet.getColumnOutlineLevel(2)); + assertEquals(1, sheet.getColumnOutlineLevel(3)); + assertEquals(0, sheet.getColumnOutlineLevel(4)); + + // none of the columns should be hidden + // column group collapsing is a different concept + for (int c=0; c<5; c++) { + assertFalse("Column " + c, sheet.isColumnHidden(c)); + } + + assertEquals(400, sheet.getColumnWidth(1)); + assertEquals(600, sheet.getColumnWidth(2)); + assertEquals(800, sheet.getColumnWidth(3)); + + wb.close(); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java index 84a4aa680..6dcaef960 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java @@ -682,15 +682,4 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { value = evaluator.evaluate(cell); assertEquals(1, value.getNumberValue(), 0.001); } - - @Test - public void evaluateInCellReturnsSameDataType() throws IOException { - XSSFWorkbook wb = new XSSFWorkbook(); - wb.createSheet().createRow(0).createCell(0); - XSSFFormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); - XSSFCell cell = wb.getSheetAt(0).getRow(0).getCell(0); - XSSFCell same = evaluator.evaluateInCell(cell); - assertSame(cell, same); - wb.close(); - } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableName.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableName.java new file mode 100644 index 000000000..8df2a663d --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableName.java @@ -0,0 +1,112 @@ +/* ==================================================================== + 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.xssf.usermodel; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.util.CellReference; +import org.junit.Before; + +/** + * Test pivot tables created by named range + */ +public class TestXSSFPivotTableName extends BaseTestXSSFPivotTable { + + @Override + @Before + public void setUp(){ + wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.createSheet(); + + Row row1 = sheet.createRow(0); + // Create a cell and put a value in it. + Cell cell = row1.createCell(0); + cell.setCellValue("Names"); + Cell cell2 = row1.createCell(1); + cell2.setCellValue("#"); + Cell cell7 = row1.createCell(2); + cell7.setCellValue("Data"); + Cell cell10 = row1.createCell(3); + cell10.setCellValue("Value"); + + Row row2 = sheet.createRow(1); + Cell cell3 = row2.createCell(0); + cell3.setCellValue("Jan"); + Cell cell4 = row2.createCell(1); + cell4.setCellValue(10); + Cell cell8 = row2.createCell(2); + cell8.setCellValue("Apa"); + Cell cell11 = row1.createCell(3); + cell11.setCellValue(11.11); + + Row row3 = sheet.createRow(2); + Cell cell5 = row3.createCell(0); + cell5.setCellValue("Ben"); + Cell cell6 = row3.createCell(1); + cell6.setCellValue(9); + Cell cell9 = row3.createCell(2); + cell9.setCellValue("Bepa"); + Cell cell12 = row1.createCell(3); + cell12.setCellValue(12.12); + + XSSFName namedRange = sheet.getWorkbook().createName(); + namedRange.setRefersToFormula(sheet.getSheetName() + "!" + "A1:C2"); + pivotTable = sheet.createPivotTable(namedRange, new CellReference("H5")); + + XSSFSheet offsetSheet = wb.createSheet(); + + Row tableRow_1 = offsetSheet.createRow(1); + offsetOuterCell = tableRow_1.createCell(1); + offsetOuterCell.setCellValue(-1); + Cell tableCell_1_1 = tableRow_1.createCell(2); + tableCell_1_1.setCellValue("Row #"); + Cell tableCell_1_2 = tableRow_1.createCell(3); + tableCell_1_2.setCellValue("Exponent"); + Cell tableCell_1_3 = tableRow_1.createCell(4); + tableCell_1_3.setCellValue("10^Exponent"); + + Row tableRow_2 = offsetSheet.createRow(2); + Cell tableCell_2_1 = tableRow_2.createCell(2); + tableCell_2_1.setCellValue(0); + Cell tableCell_2_2 = tableRow_2.createCell(3); + tableCell_2_2.setCellValue(0); + Cell tableCell_2_3 = tableRow_2.createCell(4); + tableCell_2_3.setCellValue(1); + + Row tableRow_3= offsetSheet.createRow(3); + Cell tableCell_3_1 = tableRow_3.createCell(2); + tableCell_3_1.setCellValue(1); + Cell tableCell_3_2 = tableRow_3.createCell(3); + tableCell_3_2.setCellValue(1); + Cell tableCell_3_3 = tableRow_3.createCell(4); + tableCell_3_3.setCellValue(10); + + Row tableRow_4 = offsetSheet.createRow(4); + Cell tableCell_4_1 = tableRow_4.createCell(2); + tableCell_4_1.setCellValue(2); + Cell tableCell_4_2 = tableRow_4.createCell(3); + tableCell_4_2.setCellValue(2); + Cell tableCell_4_3 = tableRow_4.createCell(4); + tableCell_4_3.setCellValue(100); + + namedRange = sheet.getWorkbook().createName(); + namedRange.setRefersToFormula("C2:E4"); + namedRange.setSheetIndex(sheet.getWorkbook().getSheetIndex(sheet)); + offsetPivotTable = offsetSheet.createPivotTable(namedRange, new CellReference("C6")); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableRef.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableRef.java new file mode 100644 index 000000000..ec0c5c6c1 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFPivotTableRef.java @@ -0,0 +1,111 @@ +/* ==================================================================== + 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.xssf.usermodel; + +import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.util.AreaReference; +import org.apache.poi.ss.util.CellReference; +import org.junit.Before; + +/** + * Test pivot tables created by area reference + */ +public class TestXSSFPivotTableRef extends BaseTestXSSFPivotTable { + + @Override + @Before + public void setUp(){ + wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.createSheet(); + + Row row1 = sheet.createRow(0); + // Create a cell and put a value in it. + Cell cell = row1.createCell(0); + cell.setCellValue("Names"); + Cell cell2 = row1.createCell(1); + cell2.setCellValue("#"); + Cell cell7 = row1.createCell(2); + cell7.setCellValue("Data"); + Cell cell10 = row1.createCell(3); + cell10.setCellValue("Value"); + + Row row2 = sheet.createRow(1); + Cell cell3 = row2.createCell(0); + cell3.setCellValue("Jan"); + Cell cell4 = row2.createCell(1); + cell4.setCellValue(10); + Cell cell8 = row2.createCell(2); + cell8.setCellValue("Apa"); + Cell cell11 = row1.createCell(3); + cell11.setCellValue(11.11); + + Row row3 = sheet.createRow(2); + Cell cell5 = row3.createCell(0); + cell5.setCellValue("Ben"); + Cell cell6 = row3.createCell(1); + cell6.setCellValue(9); + Cell cell9 = row3.createCell(2); + cell9.setCellValue("Bepa"); + Cell cell12 = row1.createCell(3); + cell12.setCellValue(12.12); + + AreaReference source = new AreaReference("A1:C2", SpreadsheetVersion.EXCEL2007); + pivotTable = sheet.createPivotTable(source, new CellReference("H5")); + + XSSFSheet offsetSheet = wb.createSheet(); + + Row tableRow_1 = offsetSheet.createRow(1); + offsetOuterCell = tableRow_1.createCell(1); + offsetOuterCell.setCellValue(-1); + Cell tableCell_1_1 = tableRow_1.createCell(2); + tableCell_1_1.setCellValue("Row #"); + Cell tableCell_1_2 = tableRow_1.createCell(3); + tableCell_1_2.setCellValue("Exponent"); + Cell tableCell_1_3 = tableRow_1.createCell(4); + tableCell_1_3.setCellValue("10^Exponent"); + + Row tableRow_2 = offsetSheet.createRow(2); + Cell tableCell_2_1 = tableRow_2.createCell(2); + tableCell_2_1.setCellValue(0); + Cell tableCell_2_2 = tableRow_2.createCell(3); + tableCell_2_2.setCellValue(0); + Cell tableCell_2_3 = tableRow_2.createCell(4); + tableCell_2_3.setCellValue(1); + + Row tableRow_3= offsetSheet.createRow(3); + Cell tableCell_3_1 = tableRow_3.createCell(2); + tableCell_3_1.setCellValue(1); + Cell tableCell_3_2 = tableRow_3.createCell(3); + tableCell_3_2.setCellValue(1); + Cell tableCell_3_3 = tableRow_3.createCell(4); + tableCell_3_3.setCellValue(10); + + Row tableRow_4 = offsetSheet.createRow(4); + Cell tableCell_4_1 = tableRow_4.createCell(2); + tableCell_4_1.setCellValue(2); + Cell tableCell_4_2 = tableRow_4.createCell(3); + tableCell_4_2.setCellValue(2); + Cell tableCell_4_3 = tableRow_4.createCell(4); + tableCell_4_3.setCellValue(100); + + AreaReference offsetSource = new AreaReference(new CellReference("C2"), new CellReference("E4")); + offsetPivotTable = offsetSheet.createPivotTable(offsetSource, new CellReference("C6")); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java index 3f7be066d..24c1d7237 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java @@ -1581,7 +1581,7 @@ public final class TestXSSFSheet extends BaseTestXSheet { System.out.println("Array formulas currently unsupported"); // FIXME: Array Formula set with Sheet.setArrayFormula() instead of cell.setFormula() /* - assertEquals("[Array Formula] N7 cell type", CellType.FORMULA, cell.getCellType()); + assertEquals("[Array Formula] N7 cell type", CellType.FORMULA, cell.getCellTypeEnum()); assertEquals("[Array Formula] N7 cell formula", "{SUM(H7:J7*{1,2,3})}", cell.getCellFormula()); */ @@ -1792,12 +1792,12 @@ public final class TestXSSFSheet extends BaseTestXSheet { // System.out.println("Array formulas currently unsupported"); /* // FIXME: Array Formula set with Sheet.setArrayFormula() instead of cell.setFormula() - assertEquals("[Array Formula] N10 cell type", CellType.FORMULA, cell.getCellType()); + assertEquals("[Array Formula] N10 cell type", CellType.FORMULA, cell.getCellTypeEnum()); assertEquals("[Array Formula] N10 cell formula", "{SUM(H10:J10*{1,2,3})}", cell.getCellFormula()); cell = CellUtil.getCell(destRow2, col); // FIXME: Array Formula set with Sheet.setArrayFormula() instead of cell.setFormula() - assertEquals("[Array Formula] N11 cell type", CellType.FORMULA, cell.getCellType()); + assertEquals("[Array Formula] N11 cell type", CellType.FORMULA, cell.getCellTypeEnum()); assertEquals("[Array Formula] N11 cell formula", "{SUM(H11:J11*{1,2,3})}", cell.getCellFormula()); */ @@ -2020,4 +2020,21 @@ public final class TestXSSFSheet extends BaseTestXSheet { } } + + // bug 59687: XSSFSheet.RemoveRow doesn't handle row gaps properly when removing row comments + @Test + public void testRemoveRowWithCommentAndGapAbove() throws IOException { + final Workbook wb = _testDataProvider.openSampleWorkbook("59687.xlsx"); + final Sheet sheet = wb.getSheetAt(0); + + // comment exists + CellAddress commentCellAddress = new CellAddress("A4"); + assertNotNull(sheet.getCellComment(commentCellAddress)); + + assertEquals("Wrong starting # of comments", 1, sheet.getCellComments().size()); + + sheet.removeRow(sheet.getRow(commentCellAddress.getRow())); + + assertEquals("There should not be any comments left!", 0, sheet.getCellComments().size()); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java index 3e2cd5403..fa4c77fbf 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java @@ -17,6 +17,8 @@ package org.apache.poi.xssf.usermodel; +import static org.apache.poi.POITestCase.skipTest; +import static org.apache.poi.POITestCase.testPassesNow; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -35,7 +37,7 @@ import org.apache.poi.ss.util.CellAddress; import org.apache.poi.ss.util.CellUtil; import org.apache.poi.xssf.XSSFITestDataProvider; import org.apache.poi.xssf.XSSFTestDataSamples; -import org.junit.Ignore; +import org.apache.xmlbeans.impl.values.XmlValueDisconnectedException; import org.junit.Test; public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { @@ -377,7 +379,9 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { wb.close(); } - @Ignore("Bug 59733 - shiftRows() causes org.apache.xmlbeans.impl.values.XmlValueDisconnectedException") + // This test is written as expected-to-fail and should be rewritten + // as expected-to-pass when the bug is fixed. + //@Ignore("Bug 59733 - shiftRows() causes org.apache.xmlbeans.impl.values.XmlValueDisconnectedException") @Test public void bug59733() throws IOException { Workbook workbook = new XSSFWorkbook(); @@ -399,9 +403,50 @@ public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows { at org.apache.poi.xssf.usermodel.XSSFRow.getRowNum(XSSFRow.java:363) at org.apache.poi.xssf.usermodel.TestXSSFSheetShiftRows.bug59733(TestXSSFSheetShiftRows.java:393) */ - sheet.removeRow(sheet.getRow(0)); - assertEquals(1, sheet.getRow(1).getRowNum()); + // FIXME: remove try, catch, and testPassesNow, skipTest when test passes + try { + sheet.removeRow(sheet.getRow(0)); + assertEquals(1, sheet.getRow(1).getRowNum()); + testPassesNow(59733); + } catch (XmlValueDisconnectedException e) { + skipTest(e); + } + workbook.close(); + } + + private static String getCellFormula(Sheet sheet, String address) { + CellAddress cellAddress = new CellAddress(address); + Row row = sheet.getRow(cellAddress.getRow()); + assertNotNull(row); + Cell cell = row.getCell(cellAddress.getColumn()); + assertNotNull(cell); + assertEquals(CellType.FORMULA, cell.getCellTypeEnum()); + return cell.getCellFormula(); + } + + // This test is written as expected-to-fail and should be rewritten + // as expected-to-pass when the bug is fixed. + @Test + public void testSharedFormulas() throws Exception { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("TestShiftRowSharedFormula.xlsx"); + XSSFSheet sheet = wb.getSheetAt(0); + assertEquals("SUM(C2:C4)", getCellFormula(sheet, "C5")); + assertEquals("SUM(D2:D4)", getCellFormula(sheet, "D5")); + assertEquals("SUM(E2:E4)", getCellFormula(sheet, "E5")); + + sheet.shiftRows(3, sheet.getLastRowNum(), 1); + // FIXME: remove try, catch, and testPassesNow, skipTest when test passes + try { + assertEquals("SUM(C2:C5)", getCellFormula(sheet, "C6")); + assertEquals("SUM(D2:D5)", getCellFormula(sheet, "D6")); + assertEquals("SUM(E2:E5)", getCellFormula(sheet, "E6")); + testPassesNow(59983); + } catch (AssertionError e) { + skipTest(e); + } + + wb.close(); } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java index 89b063c26..59c90a4dc 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java +++ b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java @@ -137,6 +137,8 @@ public final class Attachment { /** * Returns the contents of the attachment. + * + * @throws IllegalArgumentException if there is no AttachmentData available in this Attachment */ public byte[] getContents() { TNEFAttribute contents = getAttribute(TNEFProperty.ID_ATTACHDATA); diff --git a/src/scratchpad/src/org/apache/poi/hwpf/converter/WordToFoUtils.java b/src/scratchpad/src/org/apache/poi/hwpf/converter/WordToFoUtils.java index e901de821..32cf161a5 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/converter/WordToFoUtils.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/converter/WordToFoUtils.java @@ -154,37 +154,37 @@ public class WordToFoUtils extends AbstractWordUtils { block.setAttribute( "text-indent", - String.valueOf( paragraph.getFirstLineIndent() - / TWIPS_PER_PT ) + paragraph.getFirstLineIndent() + / TWIPS_PER_PT + "pt" ); } if ( paragraph.getIndentFromLeft() != 0 ) { block.setAttribute( "start-indent", - String.valueOf( paragraph.getIndentFromLeft() - / TWIPS_PER_PT ) + paragraph.getIndentFromLeft() + / TWIPS_PER_PT + "pt" ); } if ( paragraph.getIndentFromRight() != 0 ) { block.setAttribute( "end-indent", - String.valueOf( paragraph.getIndentFromRight() - / TWIPS_PER_PT ) + paragraph.getIndentFromRight() + / TWIPS_PER_PT + "pt" ); } if ( paragraph.getSpacingBefore() != 0 ) { block.setAttribute( "space-before", - String.valueOf( paragraph.getSpacingBefore() / TWIPS_PER_PT ) + paragraph.getSpacingBefore() / TWIPS_PER_PT + "pt" ); } if ( paragraph.getSpacingAfter() != 0 ) { block.setAttribute( "space-after", - String.valueOf( paragraph.getSpacingAfter() / TWIPS_PER_PT ) + paragraph.getSpacingAfter() / TWIPS_PER_PT + "pt" ); } } diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestBugs.java b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestBugs.java index 6a69aab8a..b4591ad45 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestBugs.java +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestBugs.java @@ -32,11 +32,13 @@ import org.apache.poi.hwpf.model.PlexOfField; import org.apache.poi.hwpf.model.SubdocumentType; import org.apache.poi.hwpf.model.io.HWPFOutputStream; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.util.IOUtils; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -901,4 +903,19 @@ public class TestBugs extends TestCase HWPFDocument hwpfDocument2 = HWPFTestDataSamples.writeOutAndReadBack(hwpfDocument); assertNotNull(hwpfDocument2); } + + public void test57843() throws IOException { + try { + File f = POIDataSamples.getDocumentInstance().getFile("57843.doc"); + boolean readOnly = true; + POIFSFileSystem fs = new POIFSFileSystem(f, readOnly); + HWPFOldDocument doc = new HWPFOldDocument(fs); + assertNotNull(doc); + doc.close(); + fs.close(); + fixed("57843"); + } catch (ArrayIndexOutOfBoundsException e) { + // expected until this bug is fixed + } + } } diff --git a/src/testcases/org/apache/poi/POITestCase.java b/src/testcases/org/apache/poi/POITestCase.java index 786c6e112..a8cc8e1db 100644 --- a/src/testcases/org/apache/poi/POITestCase.java +++ b/src/testcases/org/apache/poi/POITestCase.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; @@ -174,4 +175,62 @@ public final class POITestCase { } } } + + /** + * Rather than adding {@literal @}Ignore to known-failing tests, + * write the test so that it notifies us if it starts passing. + * This is useful for closing related or forgotten bugs. + * + * An Example: + *
+     * public static int add(int a, int b) {
+     *     // a known bug in behavior that has not been fixed yet
+     *     raise UnsupportedOperationException("add");
+     * }
+     * 
+     * {@literal @}Test
+     * public void knownFailingUnitTest() {
+     *     try {
+     *         assertEquals(2, add(1,1));
+     *         // this test fails because the assumption that this bug had not been fixed is false
+     *         testPassesNow(12345);
+     *     } catch (UnsupportedOperationException e) {
+     *         // test is skipped because the assumption that this bug had not been fixed is true
+     *         skipTest(e);
+     *     }
+     * }
+     * 
+     * Once passing, this unit test can be rewritten as:
+     * {@literal @}Test
+     * public void knownPassingUnitTest() {
+     *     assertEquals(2, add(1,1));
+     * }
+     * 
+     * If you have a better idea how to simplify test code while still notifying
+     * us when a previous known-failing test now passes, please improve these.
+     * As a bonus, a known-failing test that fails should not be counted as a
+     * passing test.
+     * 
+     * One possible alternative is to expect the known exception, but without
+     * a clear message that it is a good thing to no longer get the expected
+     * exception once the test passes.
+     * {@literal @}Test(expected=UnsupportedOperationException.class)
+     * public void knownFailingUnitTest() {
+     *     assertEquals(2, add(1,1));
+     * }
+     *
+     * @param e  the exception that was caught that will no longer
+     * be raised when the bug is fixed 
+     */
+    public static void skipTest(Throwable e) {
+        assumeTrue("This test currently fails with " + e, false);
+    }
+    /**
+     * @see #skipTest(Throwable)
+     *
+     * @param bug  the bug number corresponding to a known bug in bugzilla
+     */
+    public static void testPassesNow(int bug) {
+        fail("This test passes now. Please update the unit test and bug " + bug + ".");
+    }
 }
diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java
index 2ce6e1de8..65042127e 100644
--- a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java
+++ b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java
@@ -1885,12 +1885,12 @@ public final class TestBugs extends BaseTestBugzillaIssues {
 
        // TODO - Fix these so they work...
        /*row = s.getRow(4);
-       assertEquals(CellType.FORMULA, row.getCell(1).getCellType());
+       assertEquals(CellType.FORMULA, row.getCell(1).getCellTypeEnum());
        assertEquals("'[$http://gagravarr.org/FormulaRefs2.xls]Sheet1'!B2", row.getCell(1).getCellFormula());
        assertEquals(123.0, row.getCell(1).getNumericCellValue(), 0);
 
        row = s.getRow(5);
-       assertEquals(CellType.FORMULA, row.getCell(1).getCellType());
+       assertEquals(CellType.FORMULA, row.getCell(1).getCellTypeEnum());
        assertEquals("'[$http://example.com/FormulaRefs.xls]Sheet1'!B1", row.getCell(1).getCellFormula());
        assertEquals(234.0, row.getCell(1).getNumericCellValue(), 0);*/
        
diff --git a/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSDocumentPath.java b/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSDocumentPath.java
index fef200fdc..0ca030438 100644
--- a/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSDocumentPath.java
+++ b/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSDocumentPath.java
@@ -227,7 +227,7 @@ public final class TestPOIFSDocumentPath extends TestCase {
         {
             for (int k = 0; k < paths.length; k++)
             {
-                assertEquals(String.valueOf(j) + "<>" + String.valueOf(k),
+                assertEquals(j + "<>" + k,
                              paths[ j ], paths[ k ]);
             }
         }
@@ -274,13 +274,13 @@ public final class TestPOIFSDocumentPath extends TestCase {
             {
                 if (k == j)
                 {
-                    assertEquals(String.valueOf(j) + "<>"
-                                 + String.valueOf(k), fullPaths[ j ],
+                    assertEquals(j + "<>"
+                                 + k, fullPaths[ j ],
                                                       builtUpPaths[ k ]);
                 }
                 else
                 {
-                    assertTrue(String.valueOf(j) + "<>" + String.valueOf(k),
+                    assertTrue(j + "<>" + k,
                                !(fullPaths[ j ].equals(builtUpPaths[ k ])));
                 }
             }
@@ -306,7 +306,7 @@ public final class TestPOIFSDocumentPath extends TestCase {
         {
             for (int j = 0; j < badPaths.length; j++)
             {
-                assertTrue(String.valueOf(j) + "<>" + String.valueOf(k),
+                assertTrue(j + "<>" + k,
                            !(fullPaths[ k ].equals(badPaths[ j ])));
             }
         }
diff --git a/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java b/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java
index 2d98a4f38..614b4effa 100644
--- a/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java
+++ b/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java
@@ -18,10 +18,10 @@
 package org.apache.poi.poifs.macros;
 
 import static org.apache.poi.POITestCase.assertContains;
+import static org.apache.poi.POITestCase.skipTest;
+import static org.apache.poi.POITestCase.testPassesNow;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -268,16 +268,28 @@ public class TestVBAMacroReader {
     public void bug59858() throws IOException {
         try {
             fromFile(POIDataSamples.getSpreadSheetInstance(), "59858.xls");
-            fail("This test passes now. Please update the unit test and bug 59858.");
+            testPassesNow(59858);
         } catch (IOException e) {
             if (e.getMessage().matches("Module offset for '.+' was never read.")) {
                 //e.printStackTrace();
                 // NPE when reading module.offset in VBAMacroReader.readMacros (approx line 258)
-                assumeTrue("This test currently fails. See stdout.", false);
+                skipTest(e);
             } else {
                 // something unexpected failed
                 throw e;
             }
         }
     }
+    
+    // This test is written as expected-to-fail and should be rewritten
+    // as expected-to-pass when the bug is fixed.
+    @Test
+    public void bug60158() throws IOException {
+        try {
+            fromFile(POIDataSamples.getDocumentInstance(), "60158.docm");
+            testPassesNow(60158);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            skipTest(e);
+        }
+    }
 }
diff --git a/src/testcases/org/apache/poi/ss/formula/atp/TestIfError.java b/src/testcases/org/apache/poi/ss/formula/atp/TestIfError.java
index ebf6951f0..3a0a5b9b7 100644
--- a/src/testcases/org/apache/poi/ss/formula/atp/TestIfError.java
+++ b/src/testcases/org/apache/poi/ss/formula/atp/TestIfError.java
@@ -84,7 +84,7 @@ public class TestIfError extends TestCase {
         
         
         assertEquals("Checks that the cell is numeric",
-        		CellType.STRING, evaluator.evaluate(cell2).getCellTypeEnum());        
+        		CellType.STRING, evaluator.evaluate(cell2).getCellTypeEnum());
         assertEquals("Rounds -10 to a nearest multiple of -3 (-9)",
                 "Error in calculation", evaluator.evaluate(cell2).getStringValue());
         
diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java
index f9725ba8a..7e41827a7 100644
--- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java
+++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestBugzillaIssues.java
@@ -33,12 +33,9 @@ import java.awt.font.FontRenderContext;
 import java.awt.font.TextAttribute;
 import java.awt.font.TextLayout;
 import java.awt.geom.Rectangle2D;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.text.AttributedString;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import static org.junit.Assert.*;
diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java
index efae0d80c..556e89b43 100644
--- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java
+++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetShiftRows.java
@@ -17,6 +17,8 @@
 
 package org.apache.poi.ss.usermodel;
 
+import static org.apache.poi.POITestCase.skipTest;
+import static org.apache.poi.POITestCase.testPassesNow;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -295,7 +297,7 @@ public abstract class BaseTestSheetShiftRows {
         wb.close();
     }
 
-    @Ignore("bug 56454: Incorrectly handles merged regions that do not contain column 0")
+    //@Ignore("bug 56454: Incorrectly handles merged regions that do not contain column 0")
     @Test
     public final void shiftWithMergedRegions_bug56454() throws IOException {
         Workbook wb = _testDataProvider.createWorkbook();
@@ -328,7 +330,15 @@ public abstract class BaseTestSheetShiftRows {
         expectedMergedRegions.add(A4_B8);
         expectedMergedRegions.add(C4_D8);
         
-        assertEquals(expectedMergedRegions, sheet.getMergedRegions());
+        // This test is written as expected-to-fail and should be rewritten
+        // as expected-to-pass when the bug is fixed.
+        // FIXME: remove try, catch, and testPassesNow, skipTest when test passes
+        try {
+            assertEquals(expectedMergedRegions, sheet.getMergedRegions());
+            testPassesNow(56454);
+        } catch (AssertionError e) {
+            skipTest(e);
+        }
         wb.close();
     }
     
@@ -589,7 +599,7 @@ public abstract class BaseTestSheetShiftRows {
         read.close();
     }
     
-    @Ignore("bug 56454: Incorrectly handles merged regions that do not contain column 0")
+    //@Ignore("bug 56454: Incorrectly handles merged regions that do not contain column 0")
     @Test
     public void shiftRowsWithMergedRegionsThatDoNotContainColumnZero() throws IOException {
         Workbook wb = _testDataProvider.createWorkbook();
@@ -614,9 +624,17 @@ public abstract class BaseTestSheetShiftRows {
         // C5:D7 will be shifted down with same size
         sheet.shiftRows(4, sheet.getLastRowNum(), 1);
 
-        assertEquals(2, sheet.getNumMergedRegions());
-        assertEquals(CellRangeAddress.valueOf("A4:B8"), sheet.getMergedRegion(0));
-        assertEquals(CellRangeAddress.valueOf("C5:D8"), sheet.getMergedRegion(1));
+        // This test is written as expected-to-fail and should be rewritten
+        // as expected-to-pass when the bug is fixed.
+        // FIXME: remove try, catch, and testPassesNow, skipTest when test passes
+        try {
+            assertEquals(2, sheet.getNumMergedRegions());
+            assertEquals(CellRangeAddress.valueOf("A4:B8"), sheet.getMergedRegion(0));
+            assertEquals(CellRangeAddress.valueOf("C5:D8"), sheet.getMergedRegion(1));
+            testPassesNow(56454);
+        } catch (AssertionError e) {
+            skipTest(e);
+        }
         
         wb.close();
     }
diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetUpdateArrayFormulas.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetUpdateArrayFormulas.java
index 5e2ad4a8d..bbd95cccd 100644
--- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetUpdateArrayFormulas.java
+++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestSheetUpdateArrayFormulas.java
@@ -570,7 +570,7 @@ public abstract class BaseTestSheetUpdateArrayFormulas {
             assertEquals(cra.formatAsString(), mcell.getArrayFormulaRange().formatAsString());
             assertEquals("A2:A4*B2:B4", mcell.getCellFormula());
             assertTrue(mcell.isPartOfArrayFormulaGroup());
-            assertEquals(CellType.FORMULA, mcell.getCellType());
+            assertEquals(CellType.FORMULA, mcell.getCellTypeEnum());
         }
 
         */
diff --git a/src/testcases/org/apache/poi/util/DummyPOILogger.java b/src/testcases/org/apache/poi/util/DummyPOILogger.java
index c8566fe66..7da48ce26 100644
--- a/src/testcases/org/apache/poi/util/DummyPOILogger.java
+++ b/src/testcases/org/apache/poi/util/DummyPOILogger.java
@@ -23,6 +23,7 @@ import java.util.List;
  * POILogger which logs into an ArrayList, so that 
  *  tests can see what got logged
  */
+@Internal
 public class DummyPOILogger extends POILogger {
 	public Listlogged = new ArrayList(); 
 
@@ -39,12 +40,12 @@ public class DummyPOILogger extends POILogger {
 	public void initialize(String cat) {}
 
     @Override
-	public void log(int level, Object obj1) {
+	protected void _log(int level, Object obj1) {
 		logged.add(level + " - " + obj1);
 	}
 
     @Override
-	public void log(int level, Object obj1, Throwable exception) {
+	protected void _log(int level, Object obj1, Throwable exception) {
 		logged.add(level + " - " + obj1 + " - " + exception);
 	}
 }
diff --git a/src/testcases/org/apache/poi/util/TestPOILogger.java b/src/testcases/org/apache/poi/util/TestPOILogger.java
index 3914b7643..6650ed9e6 100644
--- a/src/testcases/org/apache/poi/util/TestPOILogger.java
+++ b/src/testcases/org/apache/poi/util/TestPOILogger.java
@@ -65,13 +65,13 @@ public final class TestPOILogger extends POILogger {
     }
 
     @Override
-    public void log(int level, Object obj1) {
+    protected void _log(int level, Object obj1) {
         lastLog = (obj1 == null) ? "" : obj1.toString();
         lastEx = null;
     }
 
     @Override
-    public void log(int level, Object obj1, Throwable exception) {
+    protected void _log(int level, Object obj1, Throwable exception) {
         lastLog = (obj1 == null) ? "" : obj1.toString();
         lastEx = exception;
     }
diff --git a/test-data/diagram/44501a.vsd b/test-data/diagram/44501a.vsd
old mode 100755
new mode 100644
diff --git a/test-data/diagram/44501b.vsd b/test-data/diagram/44501b.vsd
old mode 100755
new mode 100644
diff --git a/test-data/diagram/44501d.vsd b/test-data/diagram/44501d.vsd
old mode 100755
new mode 100644
diff --git a/test-data/document/57843.doc b/test-data/document/57843.doc
new file mode 100644
index 0000000000000000000000000000000000000000..57be6c6ed1ce6efb034dad41b5ad3638f5269d36
GIT binary patch
literal 8192
zcmeHM-)|g89UuQnW7=(#6igKHFlr@8(Dkrm;wB(bJ2nn!$ie*l6aUO@sPf%wsw&v$m$zPlX8srrxzul4op
z&X4cU&-XiXyLW$m{G+#je(LvZ==uhmVE1>X*^$2fG29dR>lnj~;`#p0&Q96OaC>+?
zMjrSWx{f1w6vK)r=1+i*f~G;A0X+$N3KW2z2GQKZ>+$!%HKy_B7-!#P8Ct`7i0;9!
z88)?>^HdM^z04n9|3Q3bY!Z{+DkqMyWynuin_XoM_Qt-;M-84}<753%7&T{9U!&~2
z4(WzPYz1w|a%dvGA4aVi)px3tPd1Srqvj0vO`+rDvqf)VwS;WP)}e!D$?)`jpJ&+E
z7}aFT;r}Lo@#Q~wzgKNP!zs4mtxlQnvA+!Ko8BkX%tpEG>iy?NO
z=<#9Ye}?mnK^$hSlgg}bTJ$V8Qs^b@%lo+=kWg{?1A6BBiRdgrfK`a&ewNgGrx}m
z-{;GHs&9ndr$gcYx=yfSH!s^a(9*SE`&o>Cr<`|?md>T$#e8aSmg}gU!}AB_^FdoI
z8MqF*Zq&R@aH{+^JYET@#;cZ2+(+^2YU}~cs})r{%yIa6*u&L$wZ3Yr{>P=Q^!{|)
zZwaH5h`*xSiS7n-bF=68s;l$mM3^nfFD9L+&Yz!Km^~L<5-O9nqmo3{`05ote|BMZ
zAvisM?(7*pKR^G%g%>Z(2Jx0>wAheIVs-A~dL~`4B14%rq~UWf*7(`k+1VgiGj;y5
zZ19V@jcfdp?nskq!*!FpSn>@goTP`m=}U=y9oqOF`xf=!VmA{N%lN|E^s7Xk)J+I)PEUqr+F&toUbZl9p)zBkx1SU;a1D&Jd0{UaG7_7#n@{Ka@MgF
zd^1eOjv}%azakpi2nREzBO7ZZDXX5v!
zb)!RRt%hg$s!kA31^zA;u7*X05*sp8VELdf%Q|i64hAp7a-BezZmg|@ttu6!hgL^<
z=scUR&du>i8F*1+1o|rLL>7`Gu1nq}B`Jta@07L#BG93sw)0GiQDRatB^|kKYaosm
zIz*@)>rC=2C)m^h7w`ERUlAP@@zZc8#FHCaMnri9mZfi>sc{8oXIy42z!mm%m5VuG
z08KY#7-u@sEpm0DLoAEP<=0e4NhA103C9E51?Eb3L!SsSYh{)j-Ox^jKtuyRp==$c##!9C4SX3+{%;Dnhimyn*zxsv~WElrE$)Jlau~U1QYk{B?Gs)O(@)fzL$Wo6EaDh!#d~O24krWhyH*p8T>&rLcF21IbT18#3(31oJoaGunv7MU`^?-Dx
zHATq7+&Y~iUz-jdkYwLi;Hntj@XzEonCfjR3j#?Cf+e2jurbwG6q0P&Y*N91Mo5g@
z7lN+NQ8XJK*2Kh%^(YpJks|6L;z$Z&PM##JCA{T}9Gx6tP7~FDRoICDid1C1ig`N`$VRV}#{uH7Tas%9G>ilQ$uv!|
zw4sqQvtK6_pkt~tY$PbCr+slC)%)k|VI)!(h?LP8axemJBt)Z%_bUTu0@b@L5fs{E
zP!IwCswN~=z$FC``-$_<+k=*pPuS
z!c1+z0L!Hd>!l|Mq%g2cB~~@5X1SL3d%aj06^iHP#fCP
z_86dk7=%Z3MbF2mtQLabs+?)D1)*x``nFGG%FU7s0%mWkQ44Vj_1)T)p{mv8g5s%{kWe{QDDu^)91rY{rgMI<}5cDh1Z$ZBU
z31mtG^e$dz-v+&hk4Eo0lkS2NCtEhnA!&Y81xg+PeH!~
zeE=f7{hPip{+XKBzqEhz&tl5R=5M3M?y+&=d9N7z6Pp}(nizQMpEbVC)kdK5%^Kkj`14|?+6Dk
zdUJ#~j)-&l#t|yf697}*9mU(5&&_-vs%>KLxBOKc3$AG#IM`F@9cLT8w4oFBCts<7^U4-#W<{EM+nJ2d@V@Fg!`}4ll<30}-)x{r~^~

literal 0
HcmV?d00001

diff --git a/test-data/document/60158.docm b/test-data/document/60158.docm
new file mode 100644
index 0000000000000000000000000000000000000000..2b7908fe3ced68a794f1dfda7153604d453296c0
GIT binary patch
literal 15617
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
bZ_|jJBt1dHSj(|3Bm8^HX0UO&C~4yGxx;eUic
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

literal 0
HcmV?d00001

diff --git a/test-data/spreadsheet/TestShiftRowSharedFormula.xlsx b/test-data/spreadsheet/TestShiftRowSharedFormula.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..d8ac0c6e2e650e7cbe896cb589a16f7d11507c59
GIT binary patch
literal 8431
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