| /* ==================================================================== |
| 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 java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.security.GeneralSecurityException; |
| import java.util.List; |
| |
| import org.apache.poi.hpsf.DocumentSummaryInformation; |
| 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.Encryptor; |
| import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; |
| import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor; |
| import org.apache.poi.poifs.filesystem.DirectoryNode; |
| import org.apache.poi.poifs.filesystem.DocumentInputStream; |
| 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.Internal; |
| import org.apache.poi.util.POILogFactory; |
| import org.apache.poi.util.POILogger; |
| |
| /** |
| * This holds the common functionality for all POI |
| * Document classes. |
| * Currently, this relates to Document Information Properties |
| */ |
| public abstract class POIDocument implements Closeable { |
| /** Holds metadata on our document */ |
| private SummaryInformation sInf; |
| /** Holds further metadata on our document */ |
| private DocumentSummaryInformation dsInf; |
| /** The directory that our document lives in */ |
| private DirectoryNode directory; |
| |
| /** For our own logging use */ |
| private static final POILogger logger = POILogFactory.getLogger(POIDocument.class); |
| |
| /* Have the property streams been read yet? (Only done on-demand) */ |
| private boolean initialized; |
| |
| /** |
| * Constructs a POIDocument with the given directory node. |
| * |
| * @param dir The {@link DirectoryNode} where information is read from. |
| */ |
| protected POIDocument(DirectoryNode dir) { |
| this.directory = dir; |
| } |
| |
| /** |
| * Constructs from an old-style OPOIFS |
| * |
| * @param fs the filesystem the document is read from |
| */ |
| protected POIDocument(OPOIFSFileSystem fs) { |
| this(fs.getRoot()); |
| } |
| /** |
| * Constructs from an old-style OPOIFS |
| * |
| * @param fs the filesystem the document is read from |
| */ |
| protected POIDocument(NPOIFSFileSystem fs) { |
| this(fs.getRoot()); |
| } |
| /** |
| * Constructs from the default POIFS |
| * |
| * @param fs the filesystem the document is read from |
| */ |
| protected POIDocument(POIFSFileSystem fs) { |
| this(fs.getRoot()); |
| } |
| |
| /** |
| * Fetch the Document Summary Information of the document |
| * |
| * @return The Document Summary Information or null |
| * if it could not be read for this document. |
| */ |
| public DocumentSummaryInformation getDocumentSummaryInformation() { |
| if(!initialized) { |
| readProperties(); |
| } |
| return dsInf; |
| } |
| |
| /** |
| * Fetch the Summary Information of the document |
| * |
| * @return The Summary information for the document or null |
| * if it could not be read for this document. |
| */ |
| public SummaryInformation getSummaryInformation() { |
| if(!initialized) { |
| readProperties(); |
| } |
| return sInf; |
| } |
| |
| /** |
| * Will create whichever of SummaryInformation |
| * and DocumentSummaryInformation (HPSF) properties |
| * are not already part of your document. |
| * This is normally useful when creating a new |
| * document from scratch. |
| * If the information properties are already there, |
| * then nothing will happen. |
| */ |
| public void createInformationProperties() { |
| if (!initialized) { |
| readProperties(); |
| } |
| if (sInf == null) { |
| sInf = PropertySetFactory.newSummaryInformation(); |
| } |
| if (dsInf == null) { |
| dsInf = PropertySetFactory.newDocumentSummaryInformation(); |
| } |
| } |
| |
| /** |
| * Find, and create objects for, the standard |
| * Document Information Properties (HPSF). |
| * If a given property set is missing or corrupt, |
| * it will remain null; |
| */ |
| protected void readProperties() { |
| if (initialized) { |
| return; |
| } |
| DocumentSummaryInformation dsi = readPropertySet(DocumentSummaryInformation.class, DocumentSummaryInformation.DEFAULT_STREAM_NAME); |
| if (dsi != null) { |
| dsInf = dsi; |
| } |
| SummaryInformation si = readPropertySet(SummaryInformation.class, SummaryInformation.DEFAULT_STREAM_NAME); |
| if (si != null) { |
| sInf = si; |
| } |
| |
| // Mark the fact that we've now loaded up the properties |
| initialized = true; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private <T> T readPropertySet(Class<T> clazz, String name) { |
| String localName = clazz.getName().substring(clazz.getName().lastIndexOf('.')+1); |
| try { |
| PropertySet ps = getPropertySet(name); |
| if (clazz.isInstance(ps)) { |
| return (T)ps; |
| } else if (ps != null) { |
| logger.log(POILogger.WARN, localName+" property set came back with wrong class - "+ps.getClass().getName()); |
| } else { |
| logger.log(POILogger.WARN, localName+" property set came back as null"); |
| } |
| } catch (IOException e) { |
| logger.log(POILogger.ERROR, "can't retrieve property set", e); |
| } |
| return null; |
| } |
| |
| /** |
| * For a given named property entry, either return it or null if |
| * if it wasn't found |
| * |
| * @param setName The property to read |
| * @return The value of the given property or null if it wasn't found. |
| */ |
| protected PropertySet getPropertySet(String setName) throws IOException { |
| return getPropertySet(setName, getEncryptionInfo()); |
| } |
| |
| /** |
| * For a given named property entry, either return it or null if |
| * if it wasn't found |
| * |
| * @param setName The property to read |
| * @param encryptionInfo the encryption descriptor in case of cryptoAPI encryption |
| * @return The value of the given property or null if it wasn't found. |
| */ |
| protected PropertySet getPropertySet(String setName, EncryptionInfo encryptionInfo) throws IOException { |
| DirectoryNode dirNode = directory; |
| |
| NPOIFSFileSystem encPoifs = null; |
| String step = "getting"; |
| try { |
| if (encryptionInfo != null && encryptionInfo.isDocPropsEncrypted()) { |
| step = "getting encrypted"; |
| String encryptedStream = getEncryptedPropertyStreamName(); |
| if (!dirNode.hasEntry(encryptedStream)) { |
| throw new EncryptedDocumentException("can't find encrypted property stream '"+encryptedStream+"'"); |
| } |
| CryptoAPIDecryptor dec = (CryptoAPIDecryptor)encryptionInfo.getDecryptor(); |
| encPoifs = dec.getSummaryEntries(dirNode, encryptedStream); |
| dirNode = encPoifs.getRoot(); |
| } |
| |
| //directory can be null when creating new documents |
| if (dirNode == null || !dirNode.hasEntry(setName)) { |
| return null; |
| } |
| |
| // Find the entry, and get an input stream for it |
| step = "getting"; |
| DocumentInputStream dis = dirNode.createDocumentInputStream( dirNode.getEntry(setName) ); |
| try { |
| // Create the Property Set |
| step = "creating"; |
| return PropertySetFactory.create(dis); |
| } finally { |
| dis.close(); |
| } |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Error "+step+" property set with name " + setName, e); |
| } finally { |
| IOUtils.closeQuietly(encPoifs); |
| } |
| } |
| |
| /** |
| * Writes out the updated standard Document Information Properties (HPSF) |
| * into the currently open NPOIFSFileSystem |
| * |
| * @throws IOException if an error when writing to the open |
| * {@link NPOIFSFileSystem} occurs |
| */ |
| protected void writeProperties() throws IOException { |
| validateInPlaceWritePossible(); |
| writeProperties(directory.getFileSystem(), null); |
| } |
| |
| /** |
| * Writes out the standard Document Information Properties (HPSF) |
| * @param outFS the POIFSFileSystem to write the properties into |
| * |
| * @throws IOException if an error when writing to the |
| * {@link NPOIFSFileSystem} occurs |
| */ |
| protected void writeProperties(NPOIFSFileSystem outFS) throws IOException { |
| writeProperties(outFS, null); |
| } |
| /** |
| * Writes out the standard Document Information Properties (HPSF) |
| * @param outFS the NPOIFSFileSystem to write the properties into |
| * @param writtenEntries a list of POIFS entries to add the property names too |
| * |
| * @throws IOException if an error when writing to the |
| * {@link NPOIFSFileSystem} occurs |
| */ |
| protected void writeProperties(NPOIFSFileSystem outFS, List<String> writtenEntries) throws IOException { |
| EncryptionInfo ei = getEncryptionInfo(); |
| final boolean encryptProps = (ei != null && ei.isDocPropsEncrypted()); |
| NPOIFSFileSystem fs = (encryptProps) ? new NPOIFSFileSystem() : outFS; |
| |
| SummaryInformation si = getSummaryInformation(); |
| if (si != null) { |
| writePropertySet(SummaryInformation.DEFAULT_STREAM_NAME, si, fs); |
| if(writtenEntries != null) { |
| writtenEntries.add(SummaryInformation.DEFAULT_STREAM_NAME); |
| } |
| } |
| DocumentSummaryInformation dsi = getDocumentSummaryInformation(); |
| if (dsi != null) { |
| writePropertySet(DocumentSummaryInformation.DEFAULT_STREAM_NAME, dsi, fs); |
| if(writtenEntries != null) { |
| writtenEntries.add(DocumentSummaryInformation.DEFAULT_STREAM_NAME); |
| } |
| } |
| |
| if (!encryptProps) { |
| return; |
| } |
| |
| // create empty document summary |
| dsi = PropertySetFactory.newDocumentSummaryInformation(); |
| writePropertySet(DocumentSummaryInformation.DEFAULT_STREAM_NAME, dsi, outFS); |
| // remove summary, if previously available |
| if (outFS.getRoot().hasEntry(SummaryInformation.DEFAULT_STREAM_NAME)) { |
| outFS.getRoot().getEntry(SummaryInformation.DEFAULT_STREAM_NAME).delete(); |
| } |
| Encryptor encGen = ei.getEncryptor(); |
| if (!(encGen instanceof CryptoAPIEncryptor)) { |
| throw new EncryptedDocumentException("Using "+ei.getEncryptionMode()+" encryption. Only CryptoAPI encryption supports encrypted property sets!"); |
| } |
| CryptoAPIEncryptor enc = (CryptoAPIEncryptor)encGen; |
| try { |
| enc.setSummaryEntries(outFS.getRoot(), getEncryptedPropertyStreamName(), fs); |
| } catch (GeneralSecurityException e) { |
| throw new IOException(e); |
| } finally { |
| fs.close(); |
| } |
| } |
| |
| /** |
| * Writes out a given ProperySet |
| * @param name the (POIFS Level) name of the property to write |
| * @param set the PropertySet to write out |
| * @param outFS the NPOIFSFileSystem to write the property into |
| * |
| * @throws IOException if an error when writing to the |
| * {@link NPOIFSFileSystem} occurs |
| */ |
| protected void writePropertySet(String name, PropertySet set, NPOIFSFileSystem outFS) throws IOException { |
| try { |
| PropertySet mSet = new PropertySet(set); |
| ByteArrayOutputStream bOut = new ByteArrayOutputStream(); |
| |
| mSet.write(bOut); |
| byte[] data = bOut.toByteArray(); |
| ByteArrayInputStream bIn = new ByteArrayInputStream(data); |
| |
| // Create or Update the Property Set stream in the POIFS |
| outFS.createOrUpdateDocument(bIn, name); |
| |
| logger.log(POILogger.INFO, "Wrote property set " + name + " of size " + data.length); |
| } catch(org.apache.poi.hpsf.WritingNotSupportedException wnse) { |
| logger.log( POILogger.ERROR, "Couldn't write property set with name " + name + " as not supported by HPSF yet"); |
| } |
| } |
| |
| /** |
| * Called during a {@link #write()} to ensure that the Document (and |
| * associated {@link POIFSFileSystem}) was opened in a way compatible |
| * with an in-place write. |
| * |
| * @throws IllegalStateException if the document was opened suitably |
| */ |
| protected void validateInPlaceWritePossible() throws IllegalStateException { |
| if (directory == null) { |
| throw new IllegalStateException("Newly created Document, cannot save in-place"); |
| } |
| if (directory.getParent() != null) { |
| throw new IllegalStateException("This is not the root Document, cannot save embedded resource in-place"); |
| } |
| if (directory.getFileSystem() == null || |
| !directory.getFileSystem().isInPlaceWriteable()) { |
| throw new IllegalStateException("Opened read-only or via an InputStream, a Writeable File is required"); |
| } |
| } |
| |
| /** |
| * Writes the document out to the currently open {@link File}, via the |
| * writeable {@link POIFSFileSystem} it was opened from. |
| * |
| * <p>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 POI 3.15 beta 3 |
| * |
| * @throws IOException thrown on errors writing to the file |
| * @throws IllegalStateException if this isn't from a writable File |
| */ |
| public abstract void write() throws IOException; |
| |
| /** |
| * Writes the document out to the specified new {@link File}. If the file |
| * exists, it will be replaced, otherwise a new one will be created |
| * |
| * @since POI 3.15 beta 3 |
| * |
| * @param newFile The new File to write to. |
| * |
| * @throws IOException thrown on errors writing to the file |
| */ |
| public abstract void write(File newFile) throws IOException; |
| |
| /** |
| * Writes the document out to the specified output stream. The |
| * stream is not closed as part of this operation. |
| * |
| * Note - if the Document was opened from a {@link File} rather |
| * than an {@link InputStream}, you <b>must</b> write out using |
| * {@link #write()} or to a different File. Overwriting the currently |
| * open file via an OutputStream isn't possible. |
| * |
| * If {@code stream} is a {@link java.io.FileOutputStream} on a networked drive |
| * or has a high cost/latency associated with each written byte, |
| * consider wrapping the OutputStream in a {@link java.io.BufferedOutputStream} |
| * to improve write performance, or use {@link #write()} / {@link #write(File)} |
| * if possible. |
| * |
| * @param out The stream to write to. |
| * |
| * @throws IOException thrown on errors writing to the stream |
| */ |
| public abstract void write(OutputStream out) throws IOException; |
| |
| /** |
| * Closes the underlying {@link NPOIFSFileSystem} from which |
| * the document was read, if any. Has no effect on documents |
| * opened from an InputStream, or newly created ones. |
| * <p>Once {@link #close()} has been called, no further operations |
| * should be called on the document. |
| */ |
| @Override |
| public void close() throws IOException { |
| if (directory != null) { |
| if (directory.getNFileSystem() != null) { |
| directory.getNFileSystem().close(); |
| clearDirectory(); |
| } |
| } |
| } |
| |
| @Internal |
| public DirectoryNode getDirectory() { |
| return directory; |
| } |
| |
| /** |
| * Clear/unlink the attached directory entry |
| */ |
| @Internal |
| protected void clearDirectory() { |
| directory = null; |
| } |
| |
| /** |
| * check if we were created by POIFS otherwise create a new dummy POIFS |
| * for storing the package data |
| * |
| * @return {@code true} if dummy directory was created, {@code false} otherwise |
| */ |
| @SuppressWarnings("resource") |
| @Internal |
| protected boolean initDirectory() { |
| if (directory == null) { |
| directory = new NPOIFSFileSystem().getRoot(); // NOSONAR |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Replaces the attached directory, e.g. if this document is written |
| * to a new POIFSFileSystem |
| * |
| * @param newDirectory the new directory |
| * @return the old/previous directory |
| */ |
| @Internal |
| protected DirectoryNode replaceDirectory(DirectoryNode newDirectory) { |
| DirectoryNode dn = directory; |
| directory = newDirectory; |
| return dn; |
| } |
| |
| /** |
| * @return the stream name of the property set collection, if the document is encrypted |
| */ |
| protected String getEncryptedPropertyStreamName() { |
| return "encryption"; |
| } |
| |
| /** |
| * @return the encryption info if the document is encrypted, otherwise {@code null} |
| */ |
| public EncryptionInfo getEncryptionInfo() throws IOException { |
| return null; |
| } |
| } |