/* ====================================================================
   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.openxml4j.opc;

import static org.apache.poi.openxml4j.opc.ContentTypes.EXTENSION_XML;
import static org.apache.poi.openxml4j.opc.ContentTypes.PLAIN_OLD_XML;
import static org.apache.poi.openxml4j.opc.ContentTypes.RELATIONSHIPS_PART;
import static org.apache.poi.openxml4j.opc.PackagingURIHelper.RELATIONSHIP_PART_EXTENSION_NAME;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.exceptions.PartAlreadyExistsException;
import org.apache.poi.openxml4j.opc.internal.ContentType;
import org.apache.poi.openxml4j.opc.internal.ContentTypeManager;
import org.apache.poi.openxml4j.opc.internal.PackagePropertiesPart;
import org.apache.poi.openxml4j.opc.internal.PartMarshaller;
import org.apache.poi.openxml4j.opc.internal.PartUnmarshaller;
import org.apache.poi.openxml4j.opc.internal.ZipContentTypeManager;
import org.apache.poi.openxml4j.opc.internal.marshallers.DefaultMarshaller;
import org.apache.poi.openxml4j.opc.internal.marshallers.ZipPackagePropertiesMarshaller;
import org.apache.poi.openxml4j.opc.internal.unmarshallers.PackagePropertiesUnmarshaller;
import org.apache.poi.openxml4j.opc.internal.unmarshallers.UnmarshallContext;
import org.apache.poi.openxml4j.util.ZipEntrySource;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.NotImplemented;

/**
 * Represents a container that can store multiple data objects.
 */
public abstract class OPCPackage implements RelationshipSource, Closeable {

    /**
     * Logger.
     */
    private static final Logger LOG = LogManager.getLogger(OPCPackage.class);

    /**
     * Default package access.
     */
    protected static final PackageAccess defaultPackageAccess = PackageAccess.READ_WRITE;

    /**
     * Package access.
     */
    private final PackageAccess packageAccess;

    /**
     * Package parts collection.
     */
    private PackagePartCollection partList;

    /**
     * Package relationships.
     */
    protected PackageRelationshipCollection relationships;

    /**
     * Part marshallers by content type.
     */
    protected final Map<ContentType, PartMarshaller> partMarshallers = new HashMap<>(5);

    /**
     * Default part marshaller.
     */
    protected final PartMarshaller defaultPartMarshaller = new DefaultMarshaller();

    /**
     * Part unmarshallers by content type.
     */
    protected final Map<ContentType, PartUnmarshaller> partUnmarshallers = new HashMap<>(2);

    /**
     * Core package properties.
     */
    protected PackagePropertiesPart packageProperties;

    /**
     * Manage parts content types of this package.
     */
    protected ContentTypeManager contentTypeManager;

    /**
     * Flag if a modification is done to the document.
     */
    protected boolean isDirty;

    /**
     * File path of this package.
     */
    protected String originalPackagePath;

    /**
     * Output stream for writing this package.
     */
    protected OutputStream output;

    /**
     * Constructor.
     *
     * @param access
     *            Package access.
     */
    OPCPackage(PackageAccess access) {
        if (getClass() != ZipPackage.class) {
            throw new IllegalArgumentException("PackageBase may not be subclassed");
        }
        this.packageAccess = access;

        final ContentType contentType = newCorePropertiesPart();
        // TODO Delocalize specialized marshallers
        this.partUnmarshallers.put(contentType, new PackagePropertiesUnmarshaller());
        this.partMarshallers.put(contentType, new ZipPackagePropertiesMarshaller());
    }

    private static ContentType newCorePropertiesPart() {
        try {
            return new ContentType(ContentTypes.CORE_PROPERTIES_PART);
        } catch (InvalidFormatException e) {
            // Should never happen
            throw new OpenXML4JRuntimeException(
                "Package.init() : this exception should never happen, " +
                "if you read this message please send a mail to the developers team. : " +
                e.getMessage(), e
            );
        }
    }

    /**
     * Open a package with read/write permission.
     *
     * @param path
     *            The document path.
     * @return A Package object, else <b>null</b>.
     * @throws InvalidFormatException
     *             If the specified file doesn't exist, and a parsing error
     *             occur.
     */
    public static OPCPackage open(String path) throws InvalidFormatException {
        return open(path, defaultPackageAccess);
    }

   /**
    * Open a package with read/write permission.
    *
    * @param file
    *            The file to open.
    * @return A Package object, else <b>null</b>.
    * @throws InvalidFormatException
    *             If the specified file doesn't exist, and a parsing error
    *             occur.
    */
   public static OPCPackage open(File file) throws InvalidFormatException {
      return open(file, defaultPackageAccess);
   }

   /**
    * Open an user provided {@link ZipEntrySource} with read-only permission.
    * This method can be used to stream data into POI.
    * Opposed to other open variants, the data is read as-is, e.g. there aren't
    * any zip-bomb protection put in place.
    *
    * @param zipEntry the custom source
    * @return A Package object
    * @throws InvalidFormatException if a parsing error occur.
    */
   public static OPCPackage open(ZipEntrySource zipEntry)
   throws InvalidFormatException {
       OPCPackage pack = new ZipPackage(zipEntry, PackageAccess.READ);
       try {
           if (pack.partList == null) {
               pack.getParts();
           }
           // pack.originalPackagePath = file.getAbsolutePath();
           return pack;
       } catch (InvalidFormatException | RuntimeException e) {
           IOUtils.closeQuietly(pack);
           throw e;
       }
   }

    /**
     * Open a package.
     *
     * @param path
     *            The document path.
     * @param access
     *            PackageBase access.
     * @return A PackageBase object, else <b>null</b>.
     * @throws InvalidFormatException
     *             If the specified file doesn't exist, and a parsing error
     *             occur.
     * @throws InvalidOperationException If the zip file cannot be opened.
     * @throws InvalidFormatException if the package is not valid.
     */
    public static OPCPackage open(String path, PackageAccess access)
            throws InvalidFormatException, InvalidOperationException {
        if (path == null || path.trim().isEmpty()) {
            throw new IllegalArgumentException("'path' must be given");
        }

        File file = new File(path);
        if (file.exists() && file.isDirectory()) {
            throw new IllegalArgumentException("path must not be a directory");
        }

        OPCPackage pack = new ZipPackage(path, access); // NOSONAR
        boolean success = false;
        if (pack.partList == null && access != PackageAccess.WRITE) {
            try {
                pack.getParts();
                success = true;
            } finally {
                if (! success) {
                    IOUtils.closeQuietly(pack);
                }
            }
        }

        pack.originalPackagePath = new File(path).getAbsolutePath();
        return pack;
    }

   /**
    * Open a package.
    *
    * @param file
    *            The file to open.
    * @param access
    *            PackageBase access.
    * @return A PackageBase object, else <b>null</b>.
    * @throws InvalidFormatException
    *             If the specified file doesn't exist, and a parsing error
    *             occur.
    */
   public static OPCPackage open(File file, PackageAccess access)
         throws InvalidFormatException {
      if (file == null) {
          throw new IllegalArgumentException("'file' must be given");
      }
      if (file.exists() && file.isDirectory()) {
          throw new IllegalArgumentException("file must not be a directory");
      }

      OPCPackage pack = new ZipPackage(file, access);
       try {
           if (pack.partList == null && access != PackageAccess.WRITE) {
               pack.getParts();
           }
           pack.originalPackagePath = file.getAbsolutePath();
           return pack;
       } catch (InvalidFormatException | RuntimeException e) {
           IOUtils.closeQuietly(pack);
           throw e;
       }
   }

    /**
     * Open a package.
     *
     * Note - uses quite a bit more memory than {@link #open(String)}, which
     * doesn't need to hold the whole zip file in memory, and can take advantage
     * of native methods
     *
     * @param in
     *            The InputStream to read the package from
     * @return A PackageBase object
     *
     * @throws InvalidFormatException
     *              Throws if the specified file exist and is not valid.
     * @throws IOException If reading the stream fails
     */
    public static OPCPackage open(InputStream in) throws InvalidFormatException,
            IOException {
        OPCPackage pack = new ZipPackage(in, PackageAccess.READ_WRITE);
        try {
            if (pack.partList == null) {
                pack.getParts();
            }
        } catch (InvalidFormatException | RuntimeException e) {
            IOUtils.closeQuietly(pack);
            throw e;
        }
        return pack;
    }

    /**
     * Opens a package if it exists, else it creates one.
     *
     * @param file
     *            The file to open or to create.
     * @return A newly created package if the specified file does not exist,
     *         else the package extract from the file.
     * @throws InvalidFormatException
     *             Throws if the specified file exist and is not valid.
     */
    public static OPCPackage openOrCreate(File file) throws InvalidFormatException {
        if (file.exists()) {
            return open(file.getAbsolutePath());
        } else {
            return create(file);
        }
    }

    /**
     * Creates a new package.
     *
     * @param path
     *            Path of the document.
     * @return A newly created PackageBase ready to use.
     */
    public static OPCPackage create(String path) {
        return create(new File(path));
    }

    /**
     * Creates a new package.
     *
     * @param file
     *            Path of the document.
     * @return A newly created PackageBase ready to use.
     */
    public static OPCPackage create(File file) {
        if (file == null || (file.exists() && file.isDirectory())) {
            throw new IllegalArgumentException("file");
        }

        if (file.exists()) {
            throw new InvalidOperationException(
                    "This package (or file) already exists : use the open() method or delete the file.");
        }

        // Creates a new package
        OPCPackage pkg = new ZipPackage();
        pkg.originalPackagePath = file.getAbsolutePath();

        configurePackage(pkg);
        return pkg;
    }

    public static OPCPackage create(OutputStream output) {
        OPCPackage pkg = new ZipPackage();
        pkg.originalPackagePath = null;
        pkg.output = output;

        configurePackage(pkg);
        return pkg;
    }

    private static void configurePackage(OPCPackage pkg) {
        try {
            // Content type manager
            pkg.contentTypeManager = new ZipContentTypeManager(null, pkg);

            // Add default content types for .xml and .rels
            pkg.contentTypeManager.addContentType(
                    PackagingURIHelper.createPartName(
                            PackagingURIHelper.PACKAGE_RELATIONSHIPS_ROOT_URI),
                    RELATIONSHIPS_PART);
            pkg.contentTypeManager.addContentType(
                    PackagingURIHelper.createPartName("/default.xml"),
                    PLAIN_OLD_XML);

            // Initialise some PackageBase properties
            pkg.packageProperties = new PackagePropertiesPart(pkg,
                    PackagingURIHelper.CORE_PROPERTIES_PART_NAME);
            pkg.packageProperties.setCreatorProperty("Generated by Apache POI OpenXML4J");
            pkg.packageProperties.setCreatedProperty(Optional.of(new Date()));
        } catch (InvalidFormatException e) {
            // Should never happen
            throw new IllegalStateException(e);
        }
    }

    /**
     * Flush the package : save all.
     *
     * @see #close()
     */
    public void flush() {
        throwExceptionIfReadOnly();

        if (this.packageProperties != null) {
            this.packageProperties.flush();
        }

        this.flushImpl();
    }

    /**
     * Close the open, writable package and save its content.
     *
     * If your package is open read only, then you should call {@link #revert()}
     *  when finished with the package.
     *
     * This method is not thread-safe.
     *
     * @throws IOException
     *             If an IO exception occur during the saving process.
     */
    @Override
    public void close() throws IOException {
        if (isClosed()) {
            return;
        }

        if (this.packageAccess == PackageAccess.READ) {
            LOG.atWarn().log("The close() method is intended to SAVE a package. This package is open in READ ONLY mode, use the revert() method instead!");
            revert();
            return;
        }
        if (this.contentTypeManager == null) {
            LOG.atWarn().log("Unable to call close() on a package that hasn't been fully opened yet");
            revert();
            return;
        }

        if (this.originalPackagePath != null
                && !this.originalPackagePath.trim().isEmpty()) {
            File targetFile = new File(this.originalPackagePath);
            if (!targetFile.exists()
                    || !(this.originalPackagePath
                            .equalsIgnoreCase(targetFile.getAbsolutePath()))) {
                // Case of a package created from scratch
                save(targetFile);
            } else {
                closeImpl();
            }
        } else if (this.output != null) {
            try {
                save(this.output);
            } finally {
                output.close();
            }
        }

        // ensure all held resources are freed
        revert();

        // Clear
        this.contentTypeManager.clearAll();
    }

    /**
     * Close the package WITHOUT saving its content. Reinitialize this package
     * and cancel all changes done to it.
     */
    public void revert() {
        revertImpl();
    }

    /**
     * Add a thumbnail to the package. This method is provided to make easier
     * the addition of a thumbnail in a package. You can do the same work by
     * using the traditionnal relationship and part mechanism.
     *
     * @param path The full path to the image file.
     */
    public void addThumbnail(String path) throws IOException {
        // Check parameter
        if (path == null || path.isEmpty()) {
            throw new IllegalArgumentException("path");
        }
        String name = path.substring(path.lastIndexOf(File.separatorChar) + 1);

        try (FileInputStream is = new FileInputStream(path)) {
            addThumbnail(name, is);
        }
    }
    /**
     * Add a thumbnail to the package. This method is provided to make easier
     * the addition of a thumbnail in a package. You can do the same work by
     * using the traditionnal relationship and part mechanism.
     *
     * @param filename The full path to the image file.
     * @param data the image data
     */
    public void addThumbnail(String filename, InputStream data) throws IOException {
        // Check parameter
        if (filename == null || filename.isEmpty()) {
            throw new IllegalArgumentException("filename");
        }

        // Create the thumbnail part name
        String contentType = ContentTypes
                .getContentTypeFromFileExtension(filename);
        PackagePartName thumbnailPartName;
        try {
            thumbnailPartName = PackagingURIHelper.createPartName("/docProps/"
                    + filename);
        } catch (InvalidFormatException e) {
            String partName = "/docProps/thumbnail" +
                    filename.substring(filename.lastIndexOf('.') + 1);
            try {
                thumbnailPartName = PackagingURIHelper.createPartName(partName);
            } catch (InvalidFormatException e2) {
                throw new InvalidOperationException(
                        "Can't add a thumbnail file named '" + filename + "'", e2);
            }
        }

        // Check if part already exist
        if (this.getPart(thumbnailPartName) != null) {
            throw new InvalidOperationException(
                    "You already add a thumbnail named '" + filename + "'");
        }

        // Add the thumbnail part to this package.
        PackagePart thumbnailPart = this.createPart(thumbnailPartName,
                contentType, false);

        // Add the relationship between the package and the thumbnail part
        this.addRelationship(thumbnailPartName, TargetMode.INTERNAL,
                PackageRelationshipTypes.THUMBNAIL);

        // Copy file data to the newly created part
        StreamHelper.copyStream(data, thumbnailPart.getOutputStream());
    }

    /**
     * Throws an exception if the package access mode is in read only mode
     * (PackageAccess.Read).
     *
     * @throws InvalidOperationException
     *             Throws if a writing operation is done on a read only package.
     * @see PackageAccess
     */
    void throwExceptionIfReadOnly() throws InvalidOperationException {
        if (packageAccess == PackageAccess.READ) {
            throw new InvalidOperationException(
                    "Operation not allowed, document open in read only mode!");
        }
    }

    /**
     * Throws an exception if the package access mode is in write only mode
     * (PackageAccess.Write). This method is call when other methods need write
     * right.
     *
     * @throws InvalidOperationException if a read operation is done on a write only package.
     * @see PackageAccess
     */
    void throwExceptionIfWriteOnly() throws InvalidOperationException {
        if (packageAccess == PackageAccess.WRITE) {
            throw new InvalidOperationException(
                    "Operation not allowed, document open in write only mode!");
        }
    }

    /**
     * Retrieves or creates if none exists, core package property part.
     *
     * @return The PackageProperties part of this package.
     */
    public PackageProperties getPackageProperties()
            throws InvalidFormatException {
        this.throwExceptionIfWriteOnly();
        // If no properties part has been found then we create one
        if (this.packageProperties == null) {
            this.packageProperties = new PackagePropertiesPart(this,
                    PackagingURIHelper.CORE_PROPERTIES_PART_NAME);
        }
        return this.packageProperties;
    }

    /**
     * Retrieve a part identified by its name.
     *
     * @param partName
     *            Part name of the part to retrieve.
     * @return The part with the specified name, else {@code null}.
     */
    public PackagePart getPart(PackagePartName partName) {
        throwExceptionIfWriteOnly();

        if (partName == null) {
            throw new IllegalArgumentException("partName");
        }

        // If the partlist is null, then we parse the package.
        if (partList == null) {
            try {
                getParts();
            } catch (InvalidFormatException e) {
                return null;
            }
        }

        return partList.get(partName);
    }

    /**
     * Retrieve parts by content type.
     *
     * @param contentType
     *            The content type criteria.
     * @return All part associated to the specified content type.
     */
    public ArrayList<PackagePart> getPartsByContentType(String contentType) {
        ArrayList<PackagePart> retArr = new ArrayList<>();
        for (PackagePart part : partList.sortedValues()) {
            if (part.getContentType().equals(contentType)) {
                retArr.add(part);
            }
        }
        return retArr;
    }

    /**
     * Retrieve parts by relationship type.
     *
     * @param relationshipType
     *            Relationship type. Must not be {@code null}.
     * @return All parts which are the target of a relationship with the
     *         specified type. If no such parts are found, the list is empty.
     * @throws InvalidOperationException If called on a write-only package.
     * @throws IllegalArgumentException if relationshipType input param is null.
     */
    public ArrayList<PackagePart> getPartsByRelationshipType(
            String relationshipType) {
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }
        ArrayList<PackagePart> retArr = new ArrayList<>();
        for (PackageRelationship rel : getRelationshipsByType(relationshipType)) {
            PackagePart part = getPart(rel);
            if (part != null) {
                retArr.add(part);
            }
        }
        Collections.sort(retArr);
        return retArr;
    }

    /**
     * Retrieve parts by name
     *
     * @param namePattern
     *            The pattern for matching the names
     * @return All parts associated to the specified content type, sorted
     * in alphanumerically by the part-name
     */
    public List<PackagePart> getPartsByName(final Pattern namePattern) {
        if (namePattern == null) {
            throw new IllegalArgumentException("name pattern must not be null");
        }
        Matcher matcher = namePattern.matcher("");
        ArrayList<PackagePart> result = new ArrayList<>();
        for (PackagePart part : partList.sortedValues()) {
            PackagePartName partName = part.getPartName();
            if (matcher.reset(partName.getName()).matches()) {
                result.add(part);
            }
        }
        return result;
    }

    /**
     * Get the target part from the specified relationship.
     *
     * @param partRel
     *            The part relationship uses to retrieve the part.
     */
    public PackagePart getPart(PackageRelationship partRel) {
        PackagePart retPart = null;
        ensureRelationships();
        for (PackageRelationship rel : relationships) {
            if (rel.getRelationshipType().equals(partRel.getRelationshipType())) {
                try {
                    retPart = getPart(PackagingURIHelper.createPartName(rel
                            .getTargetURI()));
                } catch (InvalidFormatException e) {
                    continue;
                }
                break;
            }
        }
        return retPart;
    }

    /**
     * Load the parts of the archive if it has not been done yet. The
     * relationships of each part are not loaded.
     *
     * Note - Rule M4.1 states that there may only ever be one Core
     *  Properties Part, but Office produced files will sometimes
     *  have multiple! As Office ignores all but the first, we relax
     *  Compliance with Rule M4.1, and ignore all others silently too.
     *
     * @return All this package's parts.
     * @throws InvalidFormatException if the package is not valid.
     */
    public ArrayList<PackagePart> getParts() throws InvalidFormatException {
        throwExceptionIfWriteOnly();

        // If the part list is null, we parse the package to retrieve all parts.
        if (partList == null) {
            /* Variables use to validate OPC Compliance */

            // Check rule M4.1 -> A format consumer shall consider more than
            // one core properties relationship for a package to be an error
            // (We just log it and move on, as real files break this!)
            boolean hasCorePropertiesPart = false;
            boolean needCorePropertiesPart = true;

            partList = getPartsImpl();
            for (PackagePart part : new ArrayList<>(partList.sortedValues())) {
                part.loadRelationships();

                // Check OPC compliance rule M4.1
                if (ContentTypes.CORE_PROPERTIES_PART.equals(part.getContentType())) {
                    if (!hasCorePropertiesPart) {
                        hasCorePropertiesPart = true;
                    } else {
                       LOG.atWarn().log("OPC Compliance error [M4.1]: " +
                            "there is more than one core properties relationship in the package! " +
                            "POI will use only the first, but other software may reject this file.");
                    }
                }

                PartUnmarshaller partUnmarshaller = partUnmarshallers.get(part._contentType);

                if (partUnmarshaller != null) {
                    UnmarshallContext context = new UnmarshallContext(this, part._partName);
                    try (InputStream partStream = part.getInputStream()) {
                        PackagePart unmarshallPart = partUnmarshaller.unmarshall(context, partStream);
                        partList.remove(part.getPartName());
                        partList.put(unmarshallPart._partName, unmarshallPart);

                        // Core properties case-- use first CoreProperties part we come across
                        // and ignore any subsequent ones
                        if (unmarshallPart instanceof PackagePropertiesPart &&
                                hasCorePropertiesPart &&
                                needCorePropertiesPart) {
                            this.packageProperties = (PackagePropertiesPart) unmarshallPart;
                            needCorePropertiesPart = false;
                        }
                    } catch (IOException ioe) {
                        LOG.atWarn().log("Unmarshall operation : IOException for {}", part._partName);
                    } catch (InvalidOperationException invoe) {
                        throw new InvalidFormatException(invoe.getMessage(), invoe);
                    }
                }
            }
        }
        return new ArrayList<>(partList.sortedValues());
    }

    /**
     * Create and add a part, with the specified name and content type, to the
     * package.
     *
     * @param partName
     *            Part name.
     * @param contentType
     *            Part content type.
     * @return The newly created part.
     * @throws PartAlreadyExistsException
     *             If rule M1.12 is not verified : Packages shall not contain
     *             equivalent part names and package implementers shall neither
     *             create nor recognize packages with equivalent part names.
     * @see #createPartImpl(PackagePartName, String, boolean)
     */
    public PackagePart createPart(PackagePartName partName, String contentType) {
        return this.createPart(partName, contentType, true);
    }

    /**
     * Create and add a part, with the specified name and content type, to the
     * package. For general purpose, prefer the overload version of this method
     * without the 'loadRelationships' parameter.
     *
     * @param partName
     *            Part name.
     * @param contentType
     *            Part content type.
     * @param loadRelationships
     *            Specify if the existing relationship part, if any, logically
     *            associated to the newly created part will be loaded.
     * @return The newly created part.
     * @throws PartAlreadyExistsException
     *             If rule M1.12 is not verified : Packages shall not contain
     *             equivalent part names and package implementers shall neither
     *             create nor recognize packages with equivalent part names.
     * @see #createPartImpl(PackagePartName, String, boolean)
     */
    PackagePart createPart(PackagePartName partName, String contentType,
            boolean loadRelationships) {
        throwExceptionIfReadOnly();
        if (partName == null) {
            throw new IllegalArgumentException("partName");
        }

        if (contentType == null || contentType.isEmpty()) {
            throw new IllegalArgumentException("contentType");
        }

        // Check if the specified part name already exists
        if (partList.containsKey(partName)
                && !partList.get(partName).isDeleted()) {
            throw new PartAlreadyExistsException(
                    "A part with the name '" + partName.getName() + "'" +
                    " already exists : Packages shall not contain equivalent part names and package" +
                    " implementers shall neither create nor recognize packages with equivalent part names. [M1.12]");
        }

        /* Check OPC compliance */

        // Rule [M4.1]: The format designer shall specify and the format producer
        // shall create at most one core properties relationship for a package.
        // A format consumer shall consider more than one core properties
        // relationship for a package to be an error. If present, the
        // relationship shall target the Core Properties part.
        // Note - POI will read files with more than one Core Properties, which
        //  Office sometimes produces, but is strict on generation
        if (contentType.equals(ContentTypes.CORE_PROPERTIES_PART)) {
            if (this.packageProperties != null) {
                throw new InvalidOperationException(
                        "OPC Compliance error [M4.1]: you try to add more than one core properties relationship in the package !");
            }
        }

        /* End check OPC compliance */

        PackagePart part = this.createPartImpl(partName, contentType,
                loadRelationships);

        /* check/create default entries - for bug54803 */
        try {
            PackagePartName ppn = PackagingURIHelper.createPartName("/."+EXTENSION_XML);
            contentTypeManager.addContentType(ppn, PLAIN_OLD_XML);
            ppn = PackagingURIHelper.createPartName("/"+RELATIONSHIP_PART_EXTENSION_NAME);
            contentTypeManager.addContentType(ppn, RELATIONSHIPS_PART);
        } catch (InvalidFormatException e) {
            throw new InvalidOperationException("unable to create default content-type entries.", e);
        }

        this.contentTypeManager.addContentType(partName, contentType);
        this.partList.put(partName, part);
        this.isDirty = true;
        return part;
    }

    /**
     * Add a part to the package.
     *
     * @param partName
     *            Part name of the part to create.
     * @param contentType
     *            type associated with the file
     * @param content
     *            the contents to add. In order to have faster operation in
     *            document merge, the data are stored in memory not on a hard
     *            disk
     *
     * @return The new part.
     * @see #createPart(PackagePartName, String)
     */
    public PackagePart createPart(PackagePartName partName, String contentType,
            ByteArrayOutputStream content) {
        PackagePart addedPart = this.createPart(partName, contentType);
        if (addedPart == null) {
            return null;
        }
        // Extract the zip entry content to put it in the part content
        if (content != null) {
            try (OutputStream partOutput = addedPart.getOutputStream()) {
                if (partOutput == null) {
                    return null;
                }
                content.writeTo(partOutput);
            } catch (IOException ignored) {
                return null;
            }
        } else {
            return null;
        }
        return addedPart;
    }

    /**
     * Add the specified part to the package. If a part already exists in the
     * package with the same name as the one specified, then we replace the old
     * part by the specified part.
     *
     * @param part
     *            The part to add (or replace).
     * @return The part added to the package, the same as the one specified.
     * @throws InvalidOperationException
     *             If rule M1.12 is not verified : Packages shall not contain
     *             equivalent part names and package implementers shall neither
     *             create nor recognize packages with equivalent part names.
     */
    protected PackagePart addPackagePart(PackagePart part) {
        throwExceptionIfReadOnly();
        if (part == null) {
            throw new IllegalArgumentException("part");
        }

        if (partList.containsKey(part._partName)) {
            if (!partList.get(part._partName).isDeleted()) {
                throw new InvalidOperationException(
                        "A part with the name '"
                                + part._partName.getName()
                                + "' already exists : Packages shall not contain equivalent part names and package implementers shall neither create nor recognize packages with equivalent part names. [M1.12]");
            }
            // If the specified partis flagged as deleted, we make it
            // available
            part.setDeleted(false);
            // and delete the old part to replace it thereafeter
            this.partList.remove(part._partName);
        }
        this.partList.put(part._partName, part);
        this.isDirty = true;
        return part;
    }

    /**
     * Remove the specified part in this package. If this part is relationship
     * part, then delete all relationships in the source part.
     *
     * @param part
     *            The part to remove. If {@code null}, skip the action.
     * @see #removePart(PackagePartName)
     */
    public void removePart(PackagePart part) {
        if (part != null) {
            removePart(part.getPartName());
        }
    }

    /**
     * Remove a part in this package. If this part is relationship part, then
     * delete all relationships in the source part.
     *
     * @param partName
     *            The part name of the part to remove.
     */
    public void removePart(PackagePartName partName) {
        throwExceptionIfReadOnly();
        if (partName == null || !this.containPart(partName)) {
            throw new IllegalArgumentException("partName");
        }

        // Delete the specified part from the package.
        if (this.partList.containsKey(partName)) {
            this.partList.get(partName).setDeleted(true);
            this.removePartImpl(partName);
            this.partList.remove(partName);
        } else {
            this.removePartImpl(partName);
        }

        // Delete content type
        this.contentTypeManager.removeContentType(partName);

        // If this part is a relationship part, then delete all relationships of
        // the source part.
        if (partName.isRelationshipPartURI()) {
            URI sourceURI = PackagingURIHelper
                    .getSourcePartUriFromRelationshipPartUri(partName.getURI());
            PackagePartName sourcePartName;
            try {
                sourcePartName = PackagingURIHelper.createPartName(sourceURI);
            } catch (InvalidFormatException e) {
                LOG.atError().log("Part name URI '{}' is not valid! This message is not intended to be displayed!", sourceURI);
                return;
            }
            if (sourcePartName.getURI().equals(
                    PackagingURIHelper.PACKAGE_ROOT_URI)) {
                clearRelationships();
            } else if (containPart(sourcePartName)) {
                PackagePart part = getPart(sourcePartName);
                if (part != null) {
                    part.clearRelationships();
                }
            }
        }

        this.isDirty = true;
    }

    /**
     * Remove a part from this package as well as its relationship part, if one
     * exists, and all parts listed in the relationship part. Be aware that this
     * do not delete relationships which target the specified part.
     *
     * @param partName
     *            The name of the part to delete.
     * @throws InvalidFormatException
     *             Throws if the associated relationship part of the specified
     *             part is not valid.
     */
    public void removePartRecursive(PackagePartName partName)
            throws InvalidFormatException {
        // Retrieves relationship part, if one exists
        PackagePart relPart = this.partList.get(PackagingURIHelper
                .getRelationshipPartName(partName));
        // Retrieves PackagePart object from the package
        PackagePart partToRemove = this.partList.get(partName);

        if (relPart != null) {
            PackageRelationshipCollection partRels = new PackageRelationshipCollection(
                    partToRemove);
            for (PackageRelationship rel : partRels) {
                PackagePartName partNameToRemove = PackagingURIHelper
                        .createPartName(PackagingURIHelper.resolvePartUri(rel
                                .getSourceURI(), rel.getTargetURI()));
                removePart(partNameToRemove);
            }

            // Finally delete its relationship part if one exists
            this.removePart(relPart._partName);
        }

        // Delete the specified part
        this.removePart(partToRemove._partName);
    }

    /**
     * Delete the part with the specified name and its associated relationships
     * part if one exists. Prefer the use of this method to delete a part in the
     * package, compare to the remove() methods that don't remove associated
     * relationships part.
     *
     * @param partName
     *            Name of the part to delete
     */
    public void deletePart(PackagePartName partName) {
        if (partName == null) {
            throw new IllegalArgumentException("partName");
        }

        // Remove the part
        this.removePart(partName);
        // Remove the relationships part
        this.removePart(PackagingURIHelper.getRelationshipPartName(partName));
    }

    /**
     * Delete the part with the specified name and all part listed in its
     * associated relationships part if one exists. This process is recursively
     * apply to all parts in the relationships part of the specified part.
     * Prefer the use of this method to delete a part in the package, compare to
     * the remove() methods that don't remove associated relationships part.
     *
     * @param partName
     *            Name of the part to delete
     */
    public void deletePartRecursive(PackagePartName partName) {
        if (partName == null || !this.containPart(partName)) {
            throw new IllegalArgumentException("partName");
        }

        PackagePart partToDelete = this.getPart(partName);
        // Remove the part
        this.removePart(partName);
        // Remove all relationship parts associated
        try {
            for (PackageRelationship relationship : partToDelete
                    .getRelationships()) {
                PackagePartName targetPartName = PackagingURIHelper
                        .createPartName(PackagingURIHelper.resolvePartUri(
                                partName.getURI(), relationship.getTargetURI()));
                this.deletePartRecursive(targetPartName);
            }
        } catch (InvalidFormatException e) {
            LOG.atWarn().withThrowable(e).log("An exception occurs while deleting part '{}'. Some parts may remain in the package.", partName.getName());
            return;
        }
        // Remove the relationships part
        PackagePartName relationshipPartName = PackagingURIHelper
                .getRelationshipPartName(partName);
        if (relationshipPartName != null && containPart(relationshipPartName)) {
            this.removePart(relationshipPartName);
        }
    }

    /**
     * Check if a part already exists in this package from its name.
     *
     * @param partName
     *            Part name to check.
     * @return <i>true</i> if the part is logically added to this package, else
     *         <i>false</i>.
     */
    public boolean containPart(PackagePartName partName) {
        return (this.getPart(partName) != null);
    }

    /**
     * Add a relationship to the package (except relationships part).
     *
     * Check rule M4.1 : The format designer shall specify and the format
     * producer shall create at most one core properties relationship for a
     * package. A format consumer shall consider more than one core properties
     * relationship for a package to be an error. If present, the relationship
     * shall target the Core Properties part.
     *
     * Check rule M1.25: The Relationships part shall not have relationships to
     * any other part. Package implementers shall enforce this requirement upon
     * the attempt to create such a relationship and shall treat any such
     * relationship as invalid.
     *
     * @param targetPartName
     *            Target part name.
     * @param targetMode
     *            Target mode, either Internal or External.
     * @param relationshipType
     *            Relationship type.
     * @param relID
     *            ID of the relationship.
     * @see PackageRelationshipTypes
     */
    @Override
    public PackageRelationship addRelationship(PackagePartName targetPartName,
            TargetMode targetMode, String relationshipType, String relID) {
        /* Check OPC compliance */

        // Check rule M4.1 : The format designer shall specify and the format
        // producer
        // shall create at most one core properties relationship for a package.
        // A format consumer shall consider more than one core properties
        // relationship for a package to be an error. If present, the
        // relationship shall target the Core Properties part.
        if (relationshipType.equals(PackageRelationshipTypes.CORE_PROPERTIES)
                && this.packageProperties != null) {
            throw new InvalidOperationException(
                    "OPC Compliance error [M4.1]: can't add another core properties part ! Use the built-in package method instead.");
        }

        /*
         * Check rule M1.25: The Relationships part shall not have relationships
         * to any other part. Package implementers shall enforce this
         * requirement upon the attempt to create such a relationship and shall
         * treat any such relationship as invalid.
         */
        if (targetPartName.isRelationshipPartURI()) {
            throw new InvalidOperationException(
                    "Rule M1.25: The Relationships part shall not have relationships to any other part.");
        }

        /* End OPC compliance */

        ensureRelationships();
        PackageRelationship retRel = relationships.addRelationship(
                targetPartName.getURI(), targetMode, relationshipType, relID);
        this.isDirty = true;
        return retRel;
    }

    /**
     * Add a package relationship.
     *
     * @param targetPartName
     *            Target part name.
     * @param targetMode
     *            Target mode, either Internal or External.
     * @param relationshipType
     *            Relationship type.
     * @see PackageRelationshipTypes
     */
    @Override
    public PackageRelationship addRelationship(PackagePartName targetPartName,
            TargetMode targetMode, String relationshipType) {
        return this.addRelationship(targetPartName, targetMode,
                relationshipType, null);
    }

    /**
     * Adds an external relationship to a part (except relationships part).
     *
     * The targets of external relationships are not subject to the same
     * validity checks that internal ones are, as the contents is potentially
     * any file, URL or similar.
     *
     * @param target
     *            External target of the relationship
     * @param relationshipType
     *            Type of relationship.
     * @return The newly created and added relationship
     */
    @Override
    public PackageRelationship addExternalRelationship(String target,
            String relationshipType) {
        return addExternalRelationship(target, relationshipType, null);
    }

    /**
     * Adds an external relationship to a part (except relationships part).
     *
     * The targets of external relationships are not subject to the same
     * validity checks that internal ones are, as the contents is potentially
     * any file, URL or similar.
     *
     * @param target
     *            External target of the relationship
     * @param relationshipType
     *            Type of relationship.
     * @param id
     *            Relationship unique id.
     * @return The newly created and added relationship
     * @see RelationshipSource#addExternalRelationship(String,
     *      String)
     */
    @Override
    public PackageRelationship addExternalRelationship(String target,
            String relationshipType, String id) {
        if (target == null) {
            throw new IllegalArgumentException("target");
        }
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }

        URI targetURI;
        try {
            targetURI = new URI(target);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Invalid target - " + e);
        }

        ensureRelationships();
        PackageRelationship retRel = relationships.addRelationship(targetURI,
                TargetMode.EXTERNAL, relationshipType, id);
        this.isDirty = true;
        return retRel;
    }

    /**
     * Delete a relationship from this package.
     *
     * @param id
     *            Id of the relationship to delete.
     */
    @Override
    public void removeRelationship(String id) {
        if (relationships != null) {
            relationships.removeRelationship(id);
            this.isDirty = true;
        }
    }

    /**
     * Retrieves all package relationships.
     *
     * @return All package relationships of this package.
     * @throws InvalidOperationException if a read operation is done on a write only package.
     * @see #getRelationshipsHelper(String)
     */
    @Override
    public PackageRelationshipCollection getRelationships() {
        return getRelationshipsHelper(null);
    }

    /**
     * Retrieves all relationships with the specified type.
     *
     * @param relationshipType
     *            The filter specifying the relationship type.
     * @return All relationships with the specified relationship type.
     */
    @Override
    public PackageRelationshipCollection getRelationshipsByType(
            String relationshipType) {
        throwExceptionIfWriteOnly();
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }
        return getRelationshipsHelper(relationshipType);
    }

    /**
     * Retrieves all relationships with specified id (normally just ine because
     * a relationship id is supposed to be unique).
     *
     * @param id
     *            Id of the wanted relationship.
     */
    private PackageRelationshipCollection getRelationshipsHelper(String id) {
        throwExceptionIfWriteOnly();
        ensureRelationships();
        return this.relationships.getRelationships(id);
    }

    /**
     * Clear package relationships.
     */
    @Override
    public void clearRelationships() {
        if (relationships != null) {
            relationships.clear();
            this.isDirty = true;
        }
    }

    /**
     * Ensure that the relationships collection is not null.
     */
    public void ensureRelationships() {
        if (this.relationships == null) {
            try {
                this.relationships = new PackageRelationshipCollection(this);
            } catch (InvalidFormatException e) {
                this.relationships = new PackageRelationshipCollection();
            }
        }
    }

    @Override
    public PackageRelationship getRelationship(String id) {
        return this.relationships.getRelationshipByID(id);
    }

    @Override
    public boolean hasRelationships() {
        return !relationships.isEmpty();
    }

    @Override
    public boolean isRelationshipExists(PackageRelationship rel) {
        for (PackageRelationship r : relationships) {
            if (r == rel) {
                return true;
            }
        }
        return false;
    }

    /**
     * Add a marshaller.
     *
     * @param contentType
     *            The content type to bind to the specified marshaller.
     * @param marshaller
     *            The marshaller to register with the specified content type.
     */
    public void addMarshaller(String contentType, PartMarshaller marshaller) {
        try {
            partMarshallers.put(new ContentType(contentType), marshaller);
        } catch (InvalidFormatException e) {
            LOG.atWarn().log("The specified content type is not valid: '{}'. The marshaller will not be added !", e.getMessage());
        }
    }

    /**
     * Add an unmarshaller.
     *
     * @param contentType
     *            The content type to bind to the specified unmarshaller.
     * @param unmarshaller
     *            The unmarshaller to register with the specified content type.
     */
    public void addUnmarshaller(String contentType,
            PartUnmarshaller unmarshaller) {
        try {
            partUnmarshallers.put(new ContentType(contentType), unmarshaller);
        } catch (InvalidFormatException e) {
            LOG.atWarn().log("The specified content type is not valid: '{}'. The unmarshaller will not be added !", e.getMessage());
        }
    }

    /**
     * Remove a marshaller by its content type.
     *
     * @param contentType
     *            The content type associated with the marshaller to remove.
     */
    public void removeMarshaller(String contentType) {
        try {
            partMarshallers.remove(new ContentType(contentType));
        } catch (InvalidFormatException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Remove an unmarshaller by its content type.
     *
     * @param contentType
     *            The content type associated with the unmarshaller to remove.
     */
    public void removeUnmarshaller(String contentType) {
        try {
            partUnmarshallers.remove(new ContentType(contentType));
        } catch (InvalidFormatException e) {
            throw new RuntimeException(e);
        }
    }


    /* Accesseurs */

    /**
     * Get the package access mode.
     *
     * @return the packageAccess The current package access.
     */
    public PackageAccess getPackageAccess() {
        return packageAccess;
    }

    /**
     * Validates the package compliance with the OPC specifications.
     *
     * @return <b>true</b> if the package is valid else <b>false</b>
     */
    @NotImplemented
    public boolean validatePackage(OPCPackage pkg) throws InvalidFormatException {
        throw new InvalidOperationException("Not implemented yet !!!");
    }

    /**
     * Save the document in the specified file.
     *
     * @param targetFile
     *            Destination file.
     * @throws IOException
     *             Throws if an IO exception occur.
     * @see #save(OutputStream)
     */
    public void save(File targetFile) throws IOException {
        if (targetFile == null) {
            throw new IllegalArgumentException("targetFile");
        }

        this.throwExceptionIfReadOnly();

        // You shouldn't save the the same file, do a close instead
        if(targetFile.exists() &&
                targetFile.getAbsolutePath().equals(this.originalPackagePath)) {
            throw new InvalidOperationException(
                    "You can't call save(File) to save to the currently open " +
                    "file. To save to the current file, please just call close()"
            );
        }

        // Do the save
        try (FileOutputStream fos = new FileOutputStream(targetFile)) {
            this.save(fos);
        }
    }

    /**
     * Save the document in the specified output stream.
     *
     * @param outputStream
     *            The stream to save the package.
     * @see #saveImpl(OutputStream)
     */
    public void save(OutputStream outputStream) throws IOException {
        throwExceptionIfReadOnly();
        this.saveImpl(outputStream);
    }

    /**
     * Core method to create a package part. This method must be implemented by
     * the subclass.
     *
     * @param partName
     *            URI of the part to create.
     * @param contentType
     *            Content type of the part to create.
     * @return The newly created package part.
     */
    protected abstract PackagePart createPartImpl(PackagePartName partName,
            String contentType, boolean loadRelationships);

    /**
     * Core method to delete a package part. This method must be implemented by
     * the subclass.
     *
     * @param partName
     *            The URI of the part to delete.
     */
    protected abstract void removePartImpl(PackagePartName partName);

    /**
     * Flush the package but not save.
     */
    protected abstract void flushImpl();

    /**
     * Close the package and cause a save of the package.
     *
     */
    protected abstract void closeImpl() throws IOException;

    /**
     * Close the package without saving the document. Discard all changes made
     * to this package.
     */
    protected abstract void revertImpl();

    /**
     * Save the package into the specified output stream.
     *
     * @param outputStream
     *            The output stream use to save this package.
     */
    protected abstract void saveImpl(OutputStream outputStream)
            throws IOException;

    /**
     * Get all parts link to the package.
     *
     * @return A list of the part owned by the package.
     */
    protected abstract PackagePartCollection getPartsImpl()
            throws InvalidFormatException;

    /**
     * Replace a content type in this package.<p>
     * A typical scneario to call this method is to rename a template file to the main format, e.g.
     * <ul>
     *     <li>".dotx" to ".docx"</li>
     *     <li>".dotm" to ".docm"</li>
     *     <li>".xltx" to ".xlsx"</li>
     *     <li>".xltm" to ".xlsm"</li>
     *     <li>".potx" to ".pptx"</li>
     *     <li>".potm" to ".pptm"</li>
     * </ul>
     * For example, a code converting  a .xlsm macro workbook to .xlsx would look as follows:
     * <pre>{@code
     *
     *  OPCPackage pkg = OPCPackage.open(new FileInputStream("macro-workbook.xlsm"));
     *  pkg.replaceContentType(
     *      "application/vnd.ms-excel.sheet.macroEnabled.main+xml",
     *      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
     *
     *  try (FileOutputStream out = new FileOutputStream("workbook.xlsx")) {
     *    pkg.save(out);
     *  }
     *
     * }</pre>
     *
     * @param oldContentType  the content type to be replaced
     * @param newContentType  the replacement
     * @return whether replacement was successful
     * @since POI-3.8
     */
    public boolean replaceContentType(String oldContentType, String newContentType){
        boolean success = false;
        ArrayList<PackagePart> list = getPartsByContentType(oldContentType);
        for (PackagePart packagePart : list) {
            if (packagePart.getContentType().equals(oldContentType)) {
                PackagePartName partName = packagePart.getPartName();
                contentTypeManager.addContentType(partName, newContentType);
                try {
                    packagePart.setContentType(newContentType);
                } catch (InvalidFormatException e) {
                    throw new OpenXML4JRuntimeException("invalid content type - "+newContentType, e);
                }
                success = true;
                this.isDirty = true;
            }
        }
        return success;
    }

    /**
    * Add the specified part, and register its content type with the content
    * type manager.
    *
    * @param part
    *            The part to add.
    */
    public void registerPartAndContentType(PackagePart part) {
        addPackagePart(part);
        this.contentTypeManager.addContentType(part.getPartName(), part.getContentType());
        this.isDirty = true;
    }

    /**
     * Remove the specified part, and clear its content type from the content
     * type manager.
     *
     * @param partName
     *            The part name of the part to remove.
     */
    public void unregisterPartAndContentType(PackagePartName partName) {
        removePart(partName);
        this.contentTypeManager.removeContentType(partName);
        this.isDirty = true;
    }


    /**
     * Get an unused part index based on the namePattern, which doesn't exist yet
     * and has the lowest positive index
     *
     * @param nameTemplate
     *      The template for new part names containing a {@code '#'} for the index,
     *      e.g. "/ppt/slides/slide#.xml"
     * @return the next available part name index
     * @throws InvalidFormatException if the nameTemplate is null or doesn't contain
     *      the index char (#) or results in an invalid part name
     */
    public int getUnusedPartIndex(final String nameTemplate) throws InvalidFormatException {
        return partList.getUnusedPartIndex(nameTemplate);
    }

    /**
     * @return true if the package is in Strict OOXML format
     * @since POI 5.1.0
     */
    public boolean isStrictOoxmlFormat() {
        PackageRelationshipCollection coreDocRelationships = getRelationshipsByType(
                PackageRelationshipTypes.STRICT_CORE_DOCUMENT);
        return !coreDocRelationships.isEmpty();
    }

    /**
     * Has close been called already?
     */
    public abstract boolean isClosed();

    @Override
    public String toString() {
        return "OPCPackage{" +
                "packageAccess=" + packageAccess +
                ", relationships=" + relationships +
                ", packageProperties=" + packageProperties +
                ", isDirty=" + isDirty +
                //", originalPackagePath='" + originalPackagePath + '\'' +
                '}';
    }
}
