File: MS\Internal\Documents\Application\TransactionalPackage.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationUI\PresentationUI_wgljbex5_wpftmp.csproj (PresentationUI)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
// Description:
//  This class represents a package which behaves similar to a Word document.
//
//   - original package is treated as read-only
//   - edits in the meantime are done lazily to a temporary package
//   - when the user Commits their changes the temporary package will be filled
//     and then the temporary file will be copied to the comparee location
//   - if the user discards the package, original will be untouched and the
//     temporary in theory, given the original could be recovered
//
//   - normalizedUri is used only for look ups, the original Uri is what we
//     should always store </description>
//
 
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
using System.Windows.TrustUI;
 
namespace MS.Internal.Documents.Application
{
    /// <summary>
    /// This class represents a Package which does not alter the original
    /// and writes the changes to a temporary package (when provided) as a
    /// type of change log; leaving the original untouched.
    /// </summary>
    /// <remarks>
    /// In the descriptions below the following terms are used:
    /// 
    ///   Proxy: This is the reference being given to callers that
    ///   contains underlying objects.
    ///   Active: The underlying object that the proxy should pass calls to.
    ///   Temp: This is the writeable object that contains changes.
    ///   Original: This is the read only object that has the source data.
    /// </remarks>
    internal class TransactionalPackage : Package, IDisposable
{
    #region Constructors
    //--------------------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------------------
 
    /// <summary>
    /// Requires an existing open Package; and returns a package which will
    /// capture changes with out applying them to the original.
    /// See the class description for details.
    /// </summary>
    /// <exception cref="System.ArgumentNullException" />
    /// <example>
    /// Package package = new TransactionalPackage(
    ///     Package.Open(source, FileMode.Open, FileAccess.Read));
    /// </example>
    /// <param name="originalPackage">An open package.</param>
    internal TransactionalPackage(Stream original)
        : base(FileAccess.ReadWrite)
    {
        ArgumentNullException.ThrowIfNull(original);
 
        Package originalPackage = Package.Open(original);
 
        _originalPackage = originalPackage;
        _tempPackage = null;
    }
    #endregion Constructors
 
    #region Internal Methods
    //-------------------------------------------------------------------------
    // Internal Methods
    //-------------------------------------------------------------------------
 
    /// <exception cref="System.ArgumentNullException" />
    internal void EnableEditMode(Stream workspace)
    {
        ArgumentNullException.ThrowIfNull(workspace);
 
        if (!workspace.CanWrite)
        {
            throw new ArgumentException(
                SR.PackagingWriteNotSupported,
                "workspace");
        }
 
        Package temporaryPackage = Package.Open(
            workspace, FileMode.Create, FileAccess.ReadWrite);
 
        _tempPackage = temporaryPackage;
    }
 
    /// <exception cref="System.ArgumentNullException" />
    /// <exception cref="System.ArgumentException" />
    internal virtual void MergeChanges(Stream target)
    {
        ArgumentNullException.ThrowIfNull(target);
 
        if (!target.CanWrite)
        {
            throw new InvalidOperationException();
        }
 
        if (_tempPackage != null)
        {
            Package destination = Package.Open(
                target, FileMode.Open, FileAccess.ReadWrite);
 
            foreach (PackagePart part in _tempPackage.GetParts())
            {
                if (destination.PartExists(part.Uri))
                {
                    Trace.SafeWrite(
                        Trace.Packaging,
                        "Over writing existing part {0}({1}).",
                        part.Uri,
                        part.ContentType);
 
                    CopyPackagePartStream(
                        part, destination.GetPart(part.Uri));
                }
                else
                {
                    Trace.SafeWrite(
                        Trace.Packaging,
                        "Creating new part from edited part {0}({1}).",
                        part.Uri,
                        part.ContentType);
 
                    CopyPackagePartStream(
                        part,
                        destination.CreatePart(
                            part.Uri, part.ContentType));
                }
            }
            destination.Flush();
            destination.Close();
            Trace.SafeWrite(Trace.Packaging, "Merge package closed.");
        }
    }
 
    internal void Rebind(Stream newOriginal)
    {
        ArgumentNullException.ThrowIfNull(newOriginal);
 
        // close this as we will open a new one
        _originalPackage.Close();
        _trashCan.Add(_originalPackage);
        _isDirty = false;
 
        Package newPackage = Package.Open(newOriginal, FileMode.Open, FileAccess.Read);
 
        // remap parts for people who keep references around after we rebind
        foreach (PackagePart part in newPackage.GetParts())
        {
            Uri normalizedPartUri = PackUriHelper.GetNormalizedPartUri(part.Uri);
            if (_activeParts.ContainsKey(normalizedPartUri))
            {
                _activeParts[normalizedPartUri].Target = newPackage.GetPart(part.Uri);
            }
        }
 
        _originalPackage = newPackage;
    }
 
    #endregion Internal Methods
 
    //-------------------------------------------------------------------------
    // Internal Properties
    //-------------------------------------------------------------------------
    #region Internal Properties
 
    /// <summary>
    /// Indicates whether the TransactionalPackage has been dirtied.
    /// </summary>
    internal bool IsDirty
    {
        get { return _isDirty; }
    }
 
    #endregion Internal Properties
 
 
    #region Protected Methods - Package Overrides
    //-------------------------------------------------------------------------
    // Protected Methods - Package Overrides
    //-------------------------------------------------------------------------
 
    /// <summary>
    /// Creates a new PackagePart.
    /// </summary>
    /// <remarks>
    /// When creating a new PackagePart we must:
    ///    a) ensure the part does not exist in package
    ///    b) ensure there is a writable package
    ///    c) create a temp part
    ///    d) update active part reference to the temp part
    /// 
    /// What if a PackagePart with the same Uri already exists?
    ///   Package.CreatePart checks for this.
    ///
    /// Do we need to worry about updating relationships and other parts?
    ///   Relationships are a part and are thus intrinsically handled.
    /// </remarks>
    /// <param name="partUri">Uri for the part to create.</param>
    /// <param name="contentType">Content type string.</param>
    /// <param name="compressionOption">Compression options.</param>
    /// <returns>A new PackagePart.</returns>
    protected override PackagePart CreatePartCore(
        Uri partUri, string contentType, CompressionOption compressionOption)
    {
        // Skipping parameter validation as it is done by CreatePart.
 
        EnsureTempPackage();
 
        // the underlying temp package does all the physical work
        PackagePart result = _tempPackage.CreatePart(
            partUri, contentType, compressionOption);
 
        Uri normalizedPartUri = PackUriHelper.GetNormalizedPartUri(partUri);
 
        result = new WriteableOnDemandPackagePart(
            this, result, TempPackagePartFactory);
 
        _activeParts.Add(normalizedPartUri, (WriteableOnDemandPackagePart)result);
 
        Trace.SafeWrite(
            Trace.Packaging,
            "New part {0}({1})#{2} created.",
            result.Uri,
            result.ContentType,
            result.GetHashCode());
 
        return result;
    }
 
    /// <summary>
    /// Deletes a PackagePart.
    /// </summary>
    /// <remarks>
    /// When deleting a PackagePart we must:
    ///   a) ensure there is a writable package
    ///   b) remove the temp part
    /// 
    /// What if the part was already deleted?
    /// What if delete is the first operation?
    ///   Then a relationship part in the temorary package would be active;
    ///   and the case(s) would be handled by Package.DeletePart.
    /// 
    /// What if delete is called after a part is created / edited?
    ///   No different then the other cases.
    /// 
    /// What if we call DeletePart on temp Package?
    ///   Unsure why relationships are not updated twice; once by base
    ///   accessing relationship part, then a second when the underlying
    ///   implementor does for Package.DeletePart.
    /// 
    /// Note: We should explore only cleaning up the stream; as the rest is
    /// likely handed by base interacting with the relationship parts.
    /// </remarks>
    /// <param name="partUri">Uri for the part to delete.</param>
    protected override void DeletePartCore(Uri partUri)
    {
        // Skipping parameter validation as it is done by CreatePart.
        if (_tempPackage.PartExists(partUri))
        {
            _tempPackage.DeletePart(partUri);
 
            Trace.SafeWrite(Trace.Packaging, "Part {0} deleted.", partUri);
        }
 
        Uri normalizedPartUri = PackUriHelper.GetNormalizedPartUri(partUri);
        if (_activeParts.ContainsKey(normalizedPartUri))
        {
            _activeParts.Remove(normalizedPartUri);
        }
    }
 
    /// <summary>
    /// Release underlying resources; in this case our packages.
    /// </summary>
    /// <param name="disposing">Indicates if we are disposing.</param>
    protected override void Dispose(bool disposing)
    {
        Trace.SafeWrite(Trace.Packaging, "Dispose was called with {0}.", disposing);
 
        if (disposing)
        {
            if (_tempPackage != null)
            {
                ((IDisposable)_tempPackage).Dispose();
                _tempPackage = null;
            }
 
            if (_originalPackage != null)
            {
                ((IDisposable)_originalPackage).Dispose();
                _originalPackage = null;
            }
 
            _activeParts.Clear();
        }
 
        base.Dispose(disposing);
    }
 
    /// <summary>
    /// Flushes our underlying packages.
    /// </summary>
    /// <remarks>
    /// Only the original package will have data integrity at this point.
    /// This is by design as the scope of the class is to be a change log.
    /// </remarks>
    protected override void FlushCore()
    {
        if (_tempPackage != null)
        {
            _tempPackage.Flush();
        }
    }
 
    /// <summary>
    /// Returns an existing PackagePart.
    /// </summary>
    /// <remarks>
    /// When getting an existing PackagePart we must:
    ///   a) Create a proxy (WriteableOnDemandPackagePart) if there is not 
    ///      already one; if there is we just return it.
    ///   b) We must return the same instance of the proxy for any request
    ///      for that part, as new instances will not have any way of 
    ///      knowing which internal part is 'active' (original/temp) within
    ///      the proxy.
    /// 
    /// What if the part does not exist?
    /// What if the part has been deleted?
    ///   These cases are handled by Package.GetPart.
    /// 
    /// What if the part ends up being edited?
    ///   That is the reason for the proxy, on edit the internal reference 
    ///   will be updated to the temp object and it will service the call.
    /// 
    /// What if a part has already been edited?
    ///   That is why we must return the active part.
    /// </remarks>
    /// <param name="partUri">The Uri of the part to return.</param>
    /// <returns>An existing PackagePart.</returns>
    protected override PackagePart GetPartCore(Uri partUri)
    {
        // Skipping parameter validation as it is done by CreatePart.
 
        PackagePart result = null;
 
        Uri normalizedPartUri = PackUriHelper.GetNormalizedPartUri(partUri);
#if DEBUG
        if (_activeParts.ContainsKey(normalizedPartUri))
        {
            Trace.SafeWrite(
                Trace.Packaging,
                "WARNING: GetPartCore called multiple times for {0}.",
                partUri);
        }
#endif
 
        // We can get the part from three places, which we check in this order:
        // 1) Our saved list of active parts. It's important to get it from
        //    here if possible because all references should point to a single
        //    instance of our WriteableOnDemandPackagePart class.
        // 2) The temp package. If there is a change to a part it will be here,
        //    and we want to return the user's changes.
        // 3) The original package.
 
        // Even if the part exists in our list of active parts, as part of our
        // contract with the Packaging team we still must use PartExists to
        // check if the part is actually present in either the temporary or the
        // original package. If the part does not exist in either the original
        // or the temporary package, this method will return null.
 
        bool canGetFromTempPackage =
            (_tempPackage != null) && (_tempPackage.PartExists(partUri));
        bool canGetFromOriginalPackage =
            canGetFromTempPackage ? false : _originalPackage.PartExists(partUri);
 
        if (_activeParts.ContainsKey(normalizedPartUri)
            && (canGetFromTempPackage || canGetFromOriginalPackage))
        {
            result = _activeParts[normalizedPartUri];
        }
        else if (canGetFromTempPackage)
        {
            result = _tempPackage.GetPart(partUri);
 
            result = new WriteableOnDemandPackagePart(
                this, result, TempPackagePartFactory);
 
            _activeParts.Add(normalizedPartUri, (WriteableOnDemandPackagePart)result);
 
            Trace.SafeWrite(
                Trace.Packaging,
                "GetPartCore returned {0}({1})#{2} a temp part.",
                partUri,
                result.ContentType,
                result.GetHashCode());
        }
        else if (canGetFromOriginalPackage)
        {
            PackagePart original = _originalPackage.GetPart(partUri);
            result = new WriteableOnDemandPackagePart(
                this, original, TempPackagePartFactory);
 
            _activeParts.Add(normalizedPartUri, (WriteableOnDemandPackagePart)result);
 
            Trace.SafeWrite(
                Trace.Packaging,
                "GetPartCore returned {0}({1})#{2} a new proxy.",
                partUri,
                result.ContentType,
                result.GetHashCode());
        }
 
        return result;
    }
 
    /// <summary>
    /// Will return all the PackageParts in the Package.
    /// </summary>
    /// <remarks>
    /// WARNING: This implementation is based on GetParts implementations
    /// current behavior.  We only expect to be called once on open and then
    /// our base implementation should handle things.
    /// </remarks>
    /// <returns>All PackageParts.</returns>
    protected override PackagePart[] GetPartsCore()
    {
        // need to call get parts from the underlying reading package
        PackagePartCollection parts = _originalPackage.GetParts();
        // // a temporary list of proxied package parts which will be use to fill return value
        List<PackagePart> _proxiedParts = new List<PackagePart>();
 
        // for all parts not in the active list create a proxy for them
        // and add to active table
        foreach (PackagePart part in parts)
        {
            _proxiedParts.Add(GetPartCore(part.Uri));
        }
 
        // return the active table
        PackagePart[] result = new PackagePart[_proxiedParts.Count];
        _proxiedParts.CopyTo(result, 0);
 
        return result;
    }
    #endregion Protected Methods - Package Overrides
 
    #region Protected Properties
    //-------------------------------------------------------------------------
    // Protected Properties
    //-------------------------------------------------------------------------
 
    protected Package TempPackage
    {
        get
        {
            return _tempPackage;
        }
    }
    #endregion Protected Properties
 
    #region Private Methods
    //-------------------------------------------------------------------------
    // Private Methods
    //-------------------------------------------------------------------------
 
    /// <summary>
    /// A simple stream copy from one PackagePart to another.
    /// </summary>
    /// <param name="original">The source PackagePart.</param>
    /// <param name="copy">The comparee PackagePart.</param>
    private static void CopyPackagePartStream(PackagePart original, PackagePart copy)
    {
        Stream source = original.GetStream(FileMode.Open, FileAccess.Read);
        Stream target = copy.GetStream(FileMode.Create, FileAccess.ReadWrite);
 
        StreamHelper.CopyStream(source, target);
 
        source.Close();
        target.Close();
    }
 
    /// <exception cref="System.InvalidOperationException" />
    private void EnsureTempPackage()
    {
        // if we can not edit ask for it
        if (_tempPackage == null)
        {
            DocumentManager.CreateDefault().EnableEdit(null);
        }
 
        // if we still don't have it fail
        if (_tempPackage == null)
        {
            throw new InvalidOperationException(
                SR.PackagingWriteNotSupported);
        }
    }
 
    /// <summary>
    /// Will create the temporary package part.
    /// </summary>
    /// <exception cref="System.ArgumentNullException" />
    /// <remarks>
    /// Designed for use as a call back from WriteableOnDemandPackagePart.
    /// On being called we must:
    ///   a) ensuring there is a writable package
    ///   b) create the 'temp' part
    ///   c) copy the original part
    /// </remarks>
    /// <param name="packagePart">The original PackagePart to copy.</param>
    /// <returns>A writeable PackagePart.</returns>
    private PackagePart TempPackagePartFactory(PackagePart packagePart)
    {
        ArgumentNullException.ThrowIfNull(packagePart);
 
        EnsureTempPackage();
 
        Uri partUri = packagePart.Uri;
 
        PackagePart temp = null;
 
        if (!_tempPackage.PartExists(partUri))
        {
            Trace.SafeWrite(
                Trace.Packaging,
                "Temporary part {0} does not exist.",
                partUri);
 
            _isDirty = true;
 
            temp = _tempPackage.CreatePart(
                partUri,
                packagePart.ContentType,
                packagePart.CompressionOption);
 
            Trace.SafeWrite(
                Trace.Packaging,
                "Temporary part {0}({1}) created.",
                temp.Uri,
                temp.ContentType);
 
            CopyPackagePartStream(packagePart, temp);
        }
        else
        {
            temp = _tempPackage.GetPart(partUri);
 
            Trace.SafeWrite(
                Trace.Packaging,
                "Temporary part {0}({1}) existed.",
                temp.Uri,
                temp.ContentType);
        }
 
        return temp;
    }
    #endregion Private Methods
 
    #region Private Fields
    //-------------------------------------------------------------------------
    // Private Fields
    //-------------------------------------------------------------------------
 
    /// <summary>
    /// Parts that have proxies constructed; this occurs when the are
    /// referenced.
    /// </summary>
    private Dictionary<Uri, WriteableOnDemandPackagePart> _activeParts =
        new Dictionary<Uri, WriteableOnDemandPackagePart>();
 
    /// <summary>
    /// The original Package; this one is to be treated as read-only.
    /// </summary>
    private Package _originalPackage;
    /// <summary>
    /// The temporary Package; this is the one we work in.
    /// </summary>
    private Package _tempPackage;
 
    private List<Package> _trashCan = new List<Package>();
 
    private bool _isDirty;
 
    #endregion Private Fields
}
}