package org.apache.poi.openxml4j.opc;
import static org.apache.poi.openxml4j.opc.ContentTypes.RELATIONSHIPS_PART;
import static org.apache.poi.openxml4j.opc.internal.ContentTypeManager.CONTENT_TYPES_PART_NAME;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.poi.UnsupportedFileFormatException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.InvalidOperationException;
import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException;
import org.apache.poi.openxml4j.exceptions.ODFNotOfficeXmlFileException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException;
import org.apache.poi.openxml4j.opc.internal.ContentTypeManager;
import org.apache.poi.openxml4j.opc.internal.FileHelper;
import org.apache.poi.openxml4j.opc.internal.MemoryPackagePart;
import org.apache.poi.openxml4j.opc.internal.PartMarshaller;
import org.apache.poi.openxml4j.opc.internal.ZipContentTypeManager;
import org.apache.poi.openxml4j.opc.internal.ZipHelper;
import org.apache.poi.openxml4j.opc.internal.marshallers.ZipPartMarshaller;
import org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream;
import org.apache.poi.openxml4j.util.ZipEntrySource;
import org.apache.poi.openxml4j.util.ZipFileZipEntrySource;
import org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.poi.util.TempFile;
public final class ZipPackage extends OPCPackage {
private static final String MIMETYPE = "mimetype";
private static final String SETTINGS_XML = "settings.xml";
private static final POILogger LOG = POILogFactory.getLogger(ZipPackage.class);
private final ZipEntrySource zipArchive;
public ZipPackage() {
super(defaultPackageAccess);
this.zipArchive = null;
try {
this.contentTypeManager = new ZipContentTypeManager(null, this);
} catch (InvalidFormatException e) {
LOG.log(POILogger.WARN,"Could not parse ZipPackage", e);
}
}
ZipPackage(InputStream in, PackageAccess access) throws IOException {
super(access);
ZipArchiveThresholdInputStream zis = ZipHelper.openZipStream(in);
try {
this.zipArchive = new ZipInputStreamZipEntrySource(zis);
} catch (final IOException e) {
IOUtils.closeQuietly(zis);
throw e;
}
}
ZipPackage(String path, PackageAccess access) throws InvalidOperationException {
this(new File(path), access);
}
ZipPackage(File file, PackageAccess access) throws InvalidOperationException {
super(access);
ZipEntrySource ze;
try {
final ZipFile zipFile = ZipHelper.openZipFile(file);
ze = new ZipFileZipEntrySource(zipFile);
} catch (IOException e) {
if (access == PackageAccess.WRITE) {
throw new InvalidOperationException("Can't open the specified file: '" + file + "'", e);
}
if ("java.util.zip.ZipException: archive is not a ZIP archive".equals(e.getMessage())) {
throw new NotOfficeXmlFileException("archive is not a ZIP archive", e);
}
LOG.log(POILogger.ERROR, "Error in zip file "+file+" - falling back to stream processing (i.e. ignoring zip central directory)");
ze = openZipEntrySourceStream(file);
}
this.zipArchive = ze;
}
private static ZipEntrySource openZipEntrySourceStream(File file) throws InvalidOperationException {
final FileInputStream fis;
try {
fis = new FileInputStream(file);
} catch (final FileNotFoundException e) {
throw new InvalidOperationException("Can't open the specified file input stream from file: '" + file + "'", e);
}
try {
return openZipEntrySourceStream(fis);
} catch (final InvalidOperationException|UnsupportedFileFormatException e) {
IOUtils.closeQuietly(fis);
throw e;
} catch (final Exception e) {
IOUtils.closeQuietly(fis);
throw new InvalidOperationException("Failed to read the file input stream from file: '" + file + "'", e);
}
}
private static ZipEntrySource openZipEntrySourceStream(FileInputStream fis) throws InvalidOperationException {
final ZipArchiveThresholdInputStream zis;
try {
zis = ZipHelper.openZipStream(fis);
} catch (final IOException e) {
throw new InvalidOperationException("Could not open the file input stream", e);
}
try {
return openZipEntrySourceStream(zis);
} catch (final InvalidOperationException|UnsupportedFileFormatException e) {
IOUtils.closeQuietly(zis);
throw e;
} catch (final Exception e) {
IOUtils.closeQuietly(zis);
throw new InvalidOperationException("Failed to read the zip entry source stream", e);
}
}
private static ZipEntrySource openZipEntrySourceStream(ZipArchiveThresholdInputStream zis) throws InvalidOperationException {
try {
return new ZipInputStreamZipEntrySource(zis);
} catch (IOException e) {
throw new InvalidOperationException("Could not open the specified zip entry source stream", e);
}
}
ZipPackage(ZipEntrySource zipEntry, PackageAccess access) {
super(access);
this.zipArchive = zipEntry;
}
@Override
protected PackagePartCollection getPartsImpl() throws InvalidFormatException {
final PackagePartCollection newPartList = new PackagePartCollection();
if (zipArchive == null) {
return newPartList;
}
final ZipArchiveEntry contentTypeEntry =
zipArchive.getEntry(CONTENT_TYPES_PART_NAME);
if (contentTypeEntry != null) {
if (this.contentTypeManager != null) {
throw new InvalidFormatException("ContentTypeManager can only be created once. This must be a cyclic relation?");
}
try {
this.contentTypeManager = new ZipContentTypeManager(
zipArchive.getInputStream(contentTypeEntry), this);
} catch (IOException e) {
throw new InvalidFormatException(e.getMessage(), e);
}
} else {
final boolean hasMimetype = zipArchive.getEntry(MIMETYPE) != null;
final boolean hasSettingsXML = zipArchive.getEntry(SETTINGS_XML) != null;
if (hasMimetype && hasSettingsXML) {
throw new ODFNotOfficeXmlFileException(
"The supplied data appears to be in ODF (Open Document) Format. " +
"Formats like these (eg ODS, ODP) are not supported, try Apache ODFToolkit");
}
if (!zipArchive.getEntries().hasMoreElements()) {
throw new NotOfficeXmlFileException(
"No valid entries or contents found, this is not a valid OOXML " +
"(Office Open XML) file");
}
throw new InvalidFormatException(
"Package should contain a content type part [M1.13]");
}
final List<EntryTriple> entries =
Collections.list(zipArchive.getEntries()).stream()
.map(zae -> new EntryTriple(zae, contentTypeManager))
.filter(mm -> mm.partName != null)
.sorted()
.collect(Collectors.toList());
for (final EntryTriple et : entries) {
et.register(newPartList);
}
return newPartList;
}
private class EntryTriple implements Comparable<EntryTriple> {
final ZipArchiveEntry zipArchiveEntry;
final PackagePartName partName;
final String contentType;
EntryTriple(final ZipArchiveEntry zipArchiveEntry, final ContentTypeManager contentTypeManager) {
this.zipArchiveEntry = zipArchiveEntry;
final String entryName = zipArchiveEntry.getName();
PackagePartName ppn = null;
try {
ppn = (CONTENT_TYPES_PART_NAME.equalsIgnoreCase(entryName)) ? null
: PackagingURIHelper.createPartName(ZipHelper.getOPCNameFromZipItemName(entryName));
} catch (Exception e) {
LOG.log(POILogger.WARN,"Entry " + entryName + " is not valid, so this part won't be add to the package.", e);
}
this.partName = ppn;
this.contentType = (ppn == null) ? null : contentTypeManager.getContentType(partName);
}
void register(final PackagePartCollection partList) throws InvalidFormatException {
if (contentType == null) {
throw new InvalidFormatException("The part " + partName.getURI().getPath() + " does not have any " +
"content type ! Rule: Package require content types when retrieving a part from a package. [M.1.14]");
}
if (partList.containsKey(partName)) {
throw new InvalidFormatException(
"A part with the name '"+partName+"' already exist : Packages shall not contain equivalent part names " +
"and package implementers shall neither create nor recognize packages with equivalent part names. [M1.12]");
}
try {
partList.put(partName, new ZipPackagePart(ZipPackage.this, zipArchiveEntry, partName, contentType, false));
} catch (InvalidOperationException e) {
throw new InvalidFormatException(e.getMessage(), e);
}
}
@Override
public int compareTo(EntryTriple o) {
final int contentTypeOrder1 = RELATIONSHIPS_PART.equals(contentType) ? -1 : 1;
final int contentTypeOrder2 = RELATIONSHIPS_PART.equals(o.contentType) ? -1 : 1;
final int cmpCT = Integer.compare(contentTypeOrder1, contentTypeOrder2);
return cmpCT != 0 ? cmpCT : partName.compareTo(o.partName);
}
}
@Override
protected PackagePart createPartImpl(PackagePartName partName,
String contentType, boolean loadRelationships) {
if (contentType == null) {
throw new IllegalArgumentException("contentType");
}
if (partName == null) {
throw new IllegalArgumentException("partName");
}
try {
return new MemoryPackagePart(this, partName, contentType, loadRelationships);
} catch (InvalidFormatException e) {
LOG.log(POILogger.WARN, e);
return null;
}
}
@Override
protected void removePartImpl(PackagePartName partName) {
if (partName == null) {
throw new IllegalArgumentException("partUri");
}
}
@Override
protected void flushImpl() {
}
@Override
protected void closeImpl() throws IOException {
flush();
if (this.originalPackagePath == null || this.originalPackagePath.isEmpty()) {
return;
}
File targetFile = new File(this.originalPackagePath);
if (!targetFile.exists()) {
throw new InvalidOperationException(
"Can't close a package not previously open with the open() method !");
}
String tempFileName = generateTempFileName(FileHelper.getDirectory(targetFile));
File tempFile = TempFile.createTempFile(tempFileName, ".tmp");
boolean success = false;
try {
save(tempFile);
success = true;
} finally {
IOUtils.closeQuietly(this.zipArchive);
try {
if(success) {
FileHelper.copyFile(tempFile, targetFile);
}
} finally {
if (!tempFile.delete()) {
LOG.log(POILogger.WARN, "The temporary file: '"
+ targetFile.getAbsolutePath()
+ "' cannot be deleted ! Make sure that no other application use it.");
}
}
}
}
private synchronized String generateTempFileName(File directory) {
File tmpFilename;
do {
tmpFilename = new File(directory.getAbsoluteFile() + File.separator
+ "OpenXML4J" + System.nanoTime());
} while (tmpFilename.exists());
return FileHelper.getFilename(tmpFilename.getAbsoluteFile());
}
@Override
protected void revertImpl() {
try {
if (this.zipArchive != null) {
this.zipArchive.close();
}
} catch (IOException e) {
}
}
@Override
public void saveImpl(OutputStream outputStream) {
throwExceptionIfReadOnly();
final ZipArchiveOutputStream zos = (outputStream instanceof ZipArchiveOutputStream)
? (ZipArchiveOutputStream) outputStream : new ZipArchiveOutputStream(outputStream);
try {
if (this.getPartsByRelationshipType(PackageRelationshipTypes.CORE_PROPERTIES).size() == 0 &&
this.getPartsByRelationshipType(PackageRelationshipTypes.CORE_PROPERTIES_ECMA376).size() == 0 ) {
LOG.log(POILogger.DEBUG,"Save core properties part");
getPackageProperties();
addPackagePart(this.packageProperties);
this.relationships.addRelationship(this.packageProperties
.getPartName().getURI(), TargetMode.INTERNAL,
PackageRelationshipTypes.CORE_PROPERTIES, null);
if (!this.contentTypeManager
.isContentTypeRegister(ContentTypes.CORE_PROPERTIES_PART)) {
this.contentTypeManager.addContentType(
this.packageProperties.getPartName(),
ContentTypes.CORE_PROPERTIES_PART);
}
}
LOG.log(POILogger.DEBUG,"Save content types part");
this.contentTypeManager.save(zos);
LOG.log(POILogger.DEBUG,"Save package relationships");
ZipPartMarshaller.marshallRelationshipPart(this.getRelationships(),
PackagingURIHelper.PACKAGE_RELATIONSHIPS_ROOT_PART_NAME,
zos);
for (PackagePart part : getParts()) {
if (part.isRelationshipPart()) {
continue;
}
final PackagePartName ppn = part.getPartName();
LOG.log(POILogger.DEBUG,"Save part '" + ZipHelper.getZipItemNameFromOPCName(ppn.getName()) + "'");
final PartMarshaller marshaller = partMarshallers.get(part._contentType);
final PartMarshaller pm = (marshaller != null) ? marshaller : defaultPartMarshaller;
if (!pm.marshall(part, zos)) {
String errMsg = "The part " + ppn.getURI() + " failed to be saved in the stream with marshaller ";
throw new OpenXML4JException(errMsg + pm);
}
}
zos.finish();
} catch (OpenXML4JRuntimeException e) {
throw e;
} catch (Exception e) {
throw new OpenXML4JRuntimeException(
"Fail to save: an error occurs while saving the package : "
+ e.getMessage(), e);
}
}
public ZipEntrySource getZipArchive() {
return zipArchive;
}
}