|
//-----------------------------------------------------------------------------
//
// <copyright file="PartBasedPackageProperties.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved.
// </copyright>
//
// Description:
// The package properties are a subset of the standard OLE property sets
// SummaryInformation and DocumentSummaryInformation, and include such properties
// as Title and Subject.
//
// History:
// 06/23/2005: JohnLarc: Initial implementation.
// 09/23/2005: JohnLarc: Removed from public surface.
//
//-----------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Packaging;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Windows; // for ExceptionStringTable
using System.Globalization; // For CultureInfo
using MS.Internal; // for Invariant.Assert
using MS.Internal.IO.Packaging; // for IgnoreFlushAndCloseStream, PackageXmlEnum, and PackageXmlStringTable
using MS.Internal.WindowsBase;
namespace MS.Internal.IO.Packaging
{
/// <summary>
/// The package properties are a subset of the standard OLE property sets
/// SummaryInformation and DocumentSummaryInformation, and include such properties
/// as Title and Subject.
/// </summary>
/// <remarks>
/// <para>No specific API is provided to interleave the properties part in streaming creation.
/// However, Package.Flush is expected to persist package-wide data, in particular metadata.
/// Therefore, in streaming creation, a write-only write-once discipline has to be enforced.</para>
/// <para>Setting a property to null deletes this property. 'null' is never strictly speaking
/// a property value, but an absence indicator.</para>
/// </remarks>
internal class PartBasedPackageProperties : PackageProperties
{
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
#region Constructors
internal PartBasedPackageProperties(Package package)
{
_package = package;
// Initialize literals as Xml Atomic strings.
// We use a clone of the name table so that different threads don't
// change the same table concurrently (Dev11 880952)
_nameTable = PackageXmlStringTable.CloneNameTable();
ReadPropertyValuesFromPackage();
// No matter what happens during initialization, the dirty flag should not be set.
_dirty = false;
}
#endregion Constructors
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
//------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------
#region Public Properties
/// <value>
/// The primary creator. The identification is environment-specific and
/// can consist of a name, email address, employee ID, etc. It is
/// recommended that this value be only as verbose as necessary to
/// identify the individual.
/// </value>
public override string Creator
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Creator);
}
set
{
RecordNewBinding(PackageXmlEnum.Creator, value);
}
}
/// <value>
/// The title.
/// </value>
public override string Title
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Title);
}
set
{
RecordNewBinding(PackageXmlEnum.Title, value);
}
}
/// <value>
/// The topic of the contents.
/// </value>
public override string Subject
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Subject);
}
set
{
RecordNewBinding(PackageXmlEnum.Subject, value);
}
}
/// <value>
/// The category. This value is typically used by UI applications to create navigation
/// controls.
/// </value>
public override string Category
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Category);
}
set
{
RecordNewBinding(PackageXmlEnum.Category, value);
}
}
/// <value>
/// A delimited set of keywords to support searching and indexing. This
/// is typically a list of terms that are not available elsewhere in the
/// properties.
/// </value>
public override string Keywords
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Keywords);
}
set
{
RecordNewBinding(PackageXmlEnum.Keywords, value);
}
}
/// <value>
/// The description or abstract of the contents.
/// </value>
public override string Description
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Description);
}
set
{
RecordNewBinding(PackageXmlEnum.Description, value);
}
}
/// <value>
/// The type of content represented, generally defined by a specific
/// use and intended audience. Example values include "Whitepaper",
/// "Security Bulletin", and "Exam". (This property is distinct from
/// MIME content types as defined in RFC 2616.)
/// </value>
public override string ContentType
{
get
{
string contentType = GetPropertyValue(PackageXmlEnum.ContentType) as string;
return contentType;
}
set
{
RecordNewBinding(PackageXmlEnum.ContentType, value);
}
}
/// <value>
/// The status of the content. Example values include "Draft",
/// "Reviewed", and "Final".
/// </value>
public override string ContentStatus
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.ContentStatus);
}
set
{
RecordNewBinding(PackageXmlEnum.ContentStatus, value);
}
}
/// <value>
/// The version number. This value is set by the user or by the application.
/// </value>
public override string Version
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Version);
}
set
{
RecordNewBinding(PackageXmlEnum.Version, value);
}
}
/// <value>
/// The revision number. This value indicates the number of saves or
/// revisions. The application is responsible for updating this value
/// after each revision.
/// </value>
public override string Revision
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Revision);
}
set
{
RecordNewBinding(PackageXmlEnum.Revision, value);
}
}
/// <value>
/// The creation date and time.
/// </value>
public override Nullable<DateTime> Created
{
get
{
return GetDateTimePropertyValue(PackageXmlEnum.Created);
}
set
{
RecordNewBinding(PackageXmlEnum.Created, value);
}
}
/// <value>
/// The date and time of the last modification.
/// </value>
public override Nullable<DateTime> Modified
{
get
{
return GetDateTimePropertyValue(PackageXmlEnum.Modified);
}
set
{
RecordNewBinding(PackageXmlEnum.Modified, value);
}
}
/// <value>
/// The user who performed the last modification. The identification is
/// environment-specific and can consist of a name, email address,
/// employee ID, etc. It is recommended that this value be only as
/// verbose as necessary to identify the individual.
/// </value>
public override string LastModifiedBy
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.LastModifiedBy);
}
set
{
RecordNewBinding(PackageXmlEnum.LastModifiedBy, value);
}
}
/// <value>
/// The date and time of the last printing.
/// </value>
public override Nullable<DateTime> LastPrinted
{
get
{
return GetDateTimePropertyValue(PackageXmlEnum.LastPrinted);
}
set
{
RecordNewBinding(PackageXmlEnum.LastPrinted, value);
}
}
/// <value>
/// A language of the intellectual content of the resource
/// </value>
public override string Language
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Language);
}
set
{
RecordNewBinding(PackageXmlEnum.Language, value);
}
}
/// <value>
/// A unique identifier.
/// </value>
public override string Identifier
{
get
{
return (string)GetPropertyValue(PackageXmlEnum.Identifier);
}
set
{
RecordNewBinding(PackageXmlEnum.Identifier, value);
}
}
#endregion Public Properties
//------------------------------------------------------
//
// Internal Methods
//
//------------------------------------------------------
#region Internal Methods
// Invoked from Package.Flush.
// The expectation is that whatever is currently dirty will get flushed.
// In streaming production, this requires flushing to a piece.
// Outside of streaming creation, the whole thing gets rewritten at every flush.
internal void Flush()
{
if (!_dirty)
return;
// Make sure there is a part to write to and that it contains
// the expected start markup.
EnsureXmlWriter();
// Write the property elements and clear _dirty.
// The whole property table gets cleared in streaming production.
SerializeDirtyProperties();
// In streaming mode, keep the writer around until we close.
// Do a simple flush on the writer to make sure the content is written to a piece.
if (_package.InStreamingCreation && _xmlWriter != null)
{
_xmlWriter.Flush();
}
// Else, add closing markup and close the writer.
else
{
CloseXmlWriter();
}
}
// Invoked from Package.Close.
// Carries out the same operations as Flush, except that, in streaming mode,
// the writer has to be closed.
internal void Close()
{
Flush();
// The following has to be done even if all properties have been saved (_dirty == false).
if (_package.InStreamingCreation && _xmlWriter != null)
CloseXmlWriter();
}
#endregion Internal Methods
//------------------------------------------------------
//
// Internal Properties
//
//------------------------------------------------------
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// The property store is implemented as a hash table of objects.
// Keys are taken from the set of string constants defined in this
// class and compared by their references rather than their values.
private object GetPropertyValue(PackageXmlEnum propertyName)
{
_package.ThrowIfWriteOnly();
if (!_propertyDictionary.ContainsKey(propertyName))
return null;
return _propertyDictionary[propertyName];
}
// Shim function to adequately cast the result of GetPropertyValue.
private Nullable<DateTime> GetDateTimePropertyValue(PackageXmlEnum propertyName)
{
object valueObject = GetPropertyValue(propertyName);
if (valueObject == null)
return null;
// If an object is there, it will be a DateTime (not a Nullable<DateTime>).
return (Nullable<DateTime>) valueObject;
}
// Set new property value.
// Override that sets the initializing flag to false to reflect the default
// situation: recording a binding to implement a value assignment.
private void RecordNewBinding(PackageXmlEnum propertyenum, object value)
{
RecordNewBinding(propertyenum, value, false /* not invoked at construction */, null);
}
// Set new property value.
// Null value is passed for deleting a property.
// While initializing, we are not assigning new values, and so the dirty flag should
// stay untouched.
private void RecordNewBinding(PackageXmlEnum propertyenum, object value, bool initializing, XmlTextReader reader)
{
// If we are reading values from the package, reader cannot be null
Debug.Assert(!initializing || reader != null);
if (!initializing)
_package.ThrowIfReadOnly();
// Case of an existing property.
if (_propertyDictionary.ContainsKey(propertyenum))
{
// Parsing should detect redundant entries.
if (initializing)
{
throw new XmlException(SR.Get(SRID.DuplicateCorePropertyName, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
// No edit of existing properties in streaming production.
if (_package.InStreamingCreation)
throw new InvalidOperationException(SR.Get(SRID.OperationViolatesWriteOnceSemantics));
// Nullable<DateTime> values can be checked against null
if (value == null) // a deletion
{
_propertyDictionary.Remove(propertyenum);
}
else // an update
{
_propertyDictionary[propertyenum] = value;
}
// If the binding is an assignment rather than an initialization, set the dirty flag.
_dirty = !initializing;
}
// Case of a null value in the absence of an existing property.
else if (value == null)
{
// Even thinking about setting a property to null in streaming production is a no-no.
if (_package.InStreamingCreation)
throw new InvalidOperationException(SR.Get(SRID.OperationViolatesWriteOnceSemantics));
// Outside of streaming production, deleting a non-existing property is a no-op.
}
// Case of an initial value being set for a property.
else
{
_propertyDictionary.Add(propertyenum, value);
// If the binding is an assignment rather than an initialization, set the dirty flag.
_dirty = !initializing;
}
}
// Initialize object from property values found in package.
// All values will remain null if the package is not enabled for reading.
private void ReadPropertyValuesFromPackage()
{
Invariant.Assert(_propertyPart == null); // This gets called exclusively from constructor.
// Don't try to read properties from the package it does not have read access
if (_package.FileOpenAccess == FileAccess.Write)
return;
_propertyPart = GetPropertyPart();
if (_propertyPart == null)
return;
ParseCorePropertyPart(_propertyPart);
}
// Locate core properties part using the package relationship that points to it.
private PackagePart GetPropertyPart()
{
// Find a package-wide relationship of type CoreDocumentPropertiesRelationshipType.
PackageRelationship corePropertiesRelationship = GetCorePropertiesRelationship();
if (corePropertiesRelationship == null)
return null;
// Retrieve the part referenced by its target URI.
if (corePropertiesRelationship.TargetMode != TargetMode.Internal)
throw new FileFormatException(SR.Get(SRID.NoExternalTargetForMetadataRelationship));
PackagePart propertiesPart = null;
Uri propertiesPartUri = PackUriHelper.ResolvePartUri(
PackUriHelper.PackageRootUri,
corePropertiesRelationship.TargetUri);
if (!_package.PartExists(propertiesPartUri))
throw new FileFormatException(SR.Get(SRID.DanglingMetadataRelationship));
propertiesPart = _package.GetPart(propertiesPartUri);
if (!propertiesPart.ValidatedContentType.AreTypeAndSubTypeEqual(_coreDocumentPropertiesContentType))
{
throw new FileFormatException(SR.Get(SRID.WrongContentTypeForPropertyPart));
}
return propertiesPart;
}
// Find a package-wide relationship of type CoreDocumentPropertiesRelationshipType.
private PackageRelationship GetCorePropertiesRelationship()
{
PackageRelationship propertiesPartRelationship = null;
foreach (PackageRelationship rel
in _package.GetRelationshipsByType(_coreDocumentPropertiesRelationshipType))
{
if (propertiesPartRelationship != null)
{
throw new FileFormatException(SR.Get(SRID.MoreThanOneMetadataRelationships));
}
propertiesPartRelationship = rel;
}
return propertiesPartRelationship;
}
// Deserialize properties part.
private void ParseCorePropertyPart(PackagePart part)
{
Stream stream = part.GetStream(FileMode.Open, FileAccess.Read);
// Create a reader that uses _nameTable so as to use the set of tag literals
// in effect as a set of atomic identifiers.
XmlTextReader reader = new XmlTextReader(stream, _nameTable);
//Prohibit DTD from the markup as per the OPC spec
reader.ProhibitDtd = true;
//This method expects the reader to be in ReadState.Initial.
//It will make the first read call.
PackagingUtilities.PerformInitailReadAndVerifyEncoding(reader);
//Note: After the previous method call the reader should be at the first tag in the markup.
//MoveToContent - Skips over the following - ProcessingInstruction, DocumentType, Comment, Whitespace, or SignificantWhitespace
//If the reader is currently at a content node then this function call is a no-op
if ( reader.MoveToContent() != XmlNodeType.Element
|| (object)reader.NamespaceURI != PackageXmlStringTable.GetXmlStringAsObject(PackageXmlEnum.PackageCorePropertiesNamespace)
|| (object)reader.LocalName != PackageXmlStringTable.GetXmlStringAsObject(PackageXmlEnum.CoreProperties))
{
throw new XmlException(SR.Get(SRID.CorePropertiesElementExpected),
null, reader.LineNumber, reader.LinePosition);
}
// The schema is closed and defines no attributes on the root element.
if (PackagingUtilities.GetNonXmlnsAttributeCount(reader) != 0)
{
throw new XmlException(SR.Get(SRID.PropertyWrongNumbOfAttribsDefinedOn, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
// Iterate through property elements until EOF. Note the proper closing of all
// open tags is checked by the reader itself.
// This loop deals only with depth-1 start tags. Handling of element content
// is delegated to dedicated functions.
int attributesCount;
while (reader.Read() && reader.MoveToContent() != XmlNodeType.None)
{
// Ignore end-tags. We check element errors on opening tags.
if (reader.NodeType == XmlNodeType.EndElement)
continue;
// Any content markup that is not an element here is unexpected.
if (reader.NodeType != XmlNodeType.Element)
throw new XmlException(SR.Get(SRID.PropertyStartTagExpected),
null, reader.LineNumber, reader.LinePosition);
// Any element below the root should open at level 1 exclusively.
if (reader.Depth != 1)
throw new XmlException(SR.Get(SRID.NoStructuredContentInsideProperties),
null, reader.LineNumber, reader.LinePosition);
attributesCount = PackagingUtilities.GetNonXmlnsAttributeCount(reader);
// Property elements can occur in any order (xsd:all).
object localName = reader.LocalName;
PackageXmlEnum xmlStringIndex = PackageXmlStringTable.GetEnumOf(localName);
String valueType = PackageXmlStringTable.GetValueType(xmlStringIndex);
if (Array.IndexOf(_validProperties, xmlStringIndex) == -1) // An unexpected element is an error.
{
throw new XmlException(
SR.Get(SRID.InvalidPropertyNameInCorePropertiesPart, reader.LocalName),
null, reader.LineNumber, reader.LinePosition);
}
// Any element not in the valid core properties namespace is unexpected.
// The following is an object comparison, not a string comparison.
if ((object)reader.NamespaceURI != PackageXmlStringTable.GetXmlStringAsObject(PackageXmlStringTable.GetXmlNamespace(xmlStringIndex)))
throw new XmlException(SR.Get(SRID.UnknownNamespaceInCorePropertiesPart),
null, reader.LineNumber, reader.LinePosition);
if (String.CompareOrdinal(valueType, "String") == 0)
{
// The schema is closed and defines no attributes on this type of element.
if (attributesCount != 0)
{
throw new XmlException(SR.Get(SRID.PropertyWrongNumbOfAttribsDefinedOn, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
RecordNewBinding(xmlStringIndex, GetStringData(reader), true /*initializing*/, reader);
}
else if (String.CompareOrdinal(valueType, "DateTime") == 0)
{
int allowedAttributeCount = (object) reader.NamespaceURI ==
PackageXmlStringTable.GetXmlStringAsObject(PackageXmlEnum.DublinCoreTermsNamespace)
? 1 : 0;
// The schema is closed and defines no attributes on this type of element.
if (attributesCount != allowedAttributeCount)
{
throw new XmlException(SR.Get(SRID.PropertyWrongNumbOfAttribsDefinedOn, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
if (allowedAttributeCount != 0)
{
ValidateXsiType(reader,
PackageXmlStringTable.GetXmlStringAsObject(PackageXmlEnum.DublinCoreTermsNamespace),
_w3cdtf);
}
RecordNewBinding(xmlStringIndex, GetDateData(reader), true /*initializing*/, reader);
}
else // An unexpected element is an error.
{
Invariant.Assert(false, "Unknown value type for properties");
}
}
}
// This method validates xsi:type="dcterms:W3CDTF"
// The valude of xsi:type is a qualified name. It should have a prefix that matches
// the xml namespace (ns) within the scope and the name that matches name
// The comparisons should be case-sensitive comparisons
internal static void ValidateXsiType(XmlTextReader reader, Object ns, string name)
{
// Get the value of xsi;type
String typeValue = reader.GetAttribute(PackageXmlStringTable.GetXmlString(PackageXmlEnum.Type),
PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlSchemaInstanceNamespace));
// Missing xsi:type
if (typeValue == null)
{
throw new XmlException(SR.Get(SRID.UnknownDCDateTimeXsiType, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
int index = typeValue.IndexOf(':');
// The valude of xsi:type is not a qualified name
if (index == -1)
{
throw new XmlException(SR.Get(SRID.UnknownDCDateTimeXsiType, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
// Check the following conditions
// The namespace of the prefix (string before ":") matches "ns"
// The name (string after ":") matches "name"
if (!Object.ReferenceEquals(ns, reader.LookupNamespace(typeValue.Substring(0, index)))
|| String.CompareOrdinal(name, typeValue.Substring(index + 1, typeValue.Length - index - 1)) != 0)
{
throw new XmlException(SR.Get(SRID.UnknownDCDateTimeXsiType, reader.Name),
null, reader.LineNumber, reader.LinePosition);
}
}
// Expect to find text data and return its value.
private string GetStringData(XmlTextReader reader)
{
if (reader.IsEmptyElement)
return string.Empty;
reader.Read();
if (reader.MoveToContent() == XmlNodeType.EndElement)
return string.Empty;
// If there is any content in the element, it should be text content and nothing else.
if (reader.NodeType != XmlNodeType.Text)
throw new XmlException(SR.Get(SRID.NoStructuredContentInsideProperties),
null, reader.LineNumber, reader.LinePosition);
return reader.Value;
}
// Expect to find text data and return its value as DateTime.
private Nullable<DateTime> GetDateData(XmlTextReader reader)
{
string data = GetStringData(reader);
DateTime dateTime;
try
{
// Note: No more than 7 second decimals are accepted by the
// list of formats given. There currently is no method that
// would perform XSD-compliant parsing.
dateTime = XmlConvert.ToDateTime(data, _dateTimeFormats);
}
catch (FormatException exc)
{
throw new XmlException(SR.Get(SRID.XsdDateTimeExpected),
exc, reader.LineNumber, reader.LinePosition);
}
return dateTime;
}
// Make sure there is a part to write to and that it contains
// the expected start markup.
private void EnsureXmlWriter()
{
// It does not make sense to reuse a writer outside of streaming creation.
if (!_package.InStreamingCreation)
Invariant.Assert(_xmlWriter == null);
if (_xmlWriter != null)
return;
EnsurePropertyPart(); // Should succeed or throw an exception.
Stream writerStream;
if(_package.InStreamingCreation)
writerStream = _propertyPart.GetStream(FileMode.Create, FileAccess.Write);
else
writerStream = new IgnoreFlushAndCloseStream(_propertyPart.GetStream(FileMode.Create, FileAccess.Write));
_xmlWriter = new XmlTextWriter(writerStream, System.Text.Encoding.UTF8);
WriteXmlStartTagsForPackageProperties();
}
// Create a property part if none exists yet.
private void EnsurePropertyPart()
{
if (_propertyPart != null)
return;
// If _propertyPart is null, no property part existed when this object was created,
// and this function is being called for the first time.
// However, when read access is available, we can afford the luxury of checking whether
// a property part and its referring relationship got correctly created in the meantime
// outside of this class.
// In write-only mode, it is impossible to perform this check, and the external creation
// scenario will result in an exception being thrown.
if (_package.FileOpenAccess == FileAccess.Read || _package.FileOpenAccess == FileAccess.ReadWrite)
{
_propertyPart = GetPropertyPart();
if (_propertyPart != null)
return;
}
CreatePropertyPart();
}
// Create a new property relationship pointing to a new property part.
// If only this class is used for manipulating property relationships, there cannot be a
// pre-existing dangling property relationship.
// No check is performed here for other classes getting misused insofar as this function
// has to work in write-only mode.
private void CreatePropertyPart()
{
_propertyPart = _package.CreatePart(GeneratePropertyPartUri(), _coreDocumentPropertiesContentType.ToString());
_package.CreateRelationship(_propertyPart.Uri, TargetMode.Internal,
_coreDocumentPropertiesRelationshipType);
}
private Uri GeneratePropertyPartUri()
{
string propertyPartName = _defaultPropertyPartNamePrefix
+ Guid.NewGuid().ToString(_guidStorageFormatString, (IFormatProvider)null)
+ _defaultPropertyPartNameExtension;
return PackUriHelper.CreatePartUri(new Uri(propertyPartName, UriKind.Relative));
}
private void WriteXmlStartTagsForPackageProperties()
{
_xmlWriter.WriteStartDocument();
// <coreProperties
_xmlWriter.WriteStartElement(PackageXmlStringTable.GetXmlString(PackageXmlEnum.CoreProperties), // local name
PackageXmlStringTable.GetXmlString(PackageXmlEnum.PackageCorePropertiesNamespace)); // namespace
// xmlns:dc
_xmlWriter.WriteAttributeString(PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlNamespacePrefix),
PackageXmlStringTable.GetXmlString(PackageXmlEnum.DublinCorePropertiesNamespacePrefix),
null,
PackageXmlStringTable.GetXmlString(PackageXmlEnum.DublinCorePropertiesNamespace));
// xmlns:dcterms
_xmlWriter.WriteAttributeString(PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlNamespacePrefix),
PackageXmlStringTable.GetXmlString(PackageXmlEnum.DublincCoreTermsNamespacePrefix),
null,
PackageXmlStringTable.GetXmlString(PackageXmlEnum.DublinCoreTermsNamespace));
// xmlns:xsi
_xmlWriter.WriteAttributeString(PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlNamespacePrefix),
PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlSchemaInstanceNamespacePrefix),
null,
PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlSchemaInstanceNamespace));
}
// Write the property elements and clear _dirty.
private void SerializeDirtyProperties()
{
// In streaming mode, nullify dictionary values.
// As no property can be set to null through the API, this makes it possible to keep
// track of all properties that have been set since the CoreProperties object was created.
KeyValuePair<PackageXmlEnum, Object>[] entriesToNullify = null;
int numEntriesToNullify = 0;
if (_package.InStreamingCreation)
entriesToNullify = new KeyValuePair<PackageXmlEnum, Object>[_propertyDictionary.Count];
// Create a property element for each non-null entry.
foreach (KeyValuePair<PackageXmlEnum, Object> entry in _propertyDictionary)
{
// If we are NOT in streaming mode, the property value should NOT be null
Debug.Assert(entry.Value != null || _package.InStreamingCreation);
if (_package.InStreamingCreation
&& entry.Value == null) // already saved
{
continue;
}
PackageXmlEnum propertyNamespace = PackageXmlStringTable.GetXmlNamespace(entry.Key);
_xmlWriter.WriteStartElement(PackageXmlStringTable.GetXmlString(entry.Key),
PackageXmlStringTable.GetXmlString(propertyNamespace));
if (entry.Value is Nullable<DateTime>)
{
if (propertyNamespace == PackageXmlEnum.DublinCoreTermsNamespace)
{
// xsi:type=
_xmlWriter.WriteStartAttribute(PackageXmlStringTable.GetXmlString(PackageXmlEnum.Type),
PackageXmlStringTable.GetXmlString(PackageXmlEnum.XmlSchemaInstanceNamespace));
// "dcterms:W3CDTF"
_xmlWriter.WriteQualifiedName(_w3cdtf,
PackageXmlStringTable.GetXmlString(PackageXmlEnum.DublinCoreTermsNamespace));
_xmlWriter.WriteEndAttribute();
}
// Use sortable ISO 8601 date/time pattern. Include second fractions down to the 100-nanosecond interval,
// which is the definition of a "tick" for the DateTime type.
_xmlWriter.WriteString(XmlConvert.ToString(((Nullable<DateTime>)entry.Value).Value.ToUniversalTime(), "yyyy-MM-ddTHH:mm:ss.fffffffZ"));
}
else
{
// The following uses the fact that ToString is virtual.
_xmlWriter.WriteString(entry.Value.ToString());
}
_xmlWriter.WriteEndElement();
if (_package.InStreamingCreation)
entriesToNullify[numEntriesToNullify++] = entry;
}
// Mark properties as saved.
_dirty = false;
// Detailed marking of saved properties for the streaming mode.
if (_package.InStreamingCreation)
{
for (int i = 0; i < numEntriesToNullify; ++i)
{
_propertyDictionary[entriesToNullify[i].Key] = null;
}
}
}
// Add end markup and close the writer.
private void CloseXmlWriter()
{
// Close the root element.
_xmlWriter.WriteEndElement();
// Close the writer itself.
_xmlWriter.Close();
// Make sure we know it's closed.
_xmlWriter = null;
}
#endregion Private Methods
//------------------------------------------------------
//
// Private fields
//
//------------------------------------------------------
#region Private Fields
private Package _package;
private PackagePart _propertyPart;
private XmlTextWriter _xmlWriter;
// Table of objects from the closed set of literals defined below.
// (Uses object comparison rather than string comparison.)
private const int _numCoreProperties = 16;
private Dictionary<PackageXmlEnum, Object> _propertyDictionary = new Dictionary<PackageXmlEnum, Object>(_numCoreProperties);
private bool _dirty = false;
// This System.Xml.NameTable makes sure that we use the same references to strings
// throughout (including when parsing Xml) and so can perform reference comparisons
// rather than value comparisons.
private NameTable _nameTable;
// Literals.
private static readonly ContentType _coreDocumentPropertiesContentType
= new ContentType("application/vnd.openxmlformats-package.core-properties+xml");
private const string _coreDocumentPropertiesRelationshipType
= "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
private const string _defaultPropertyPartNamePrefix =
"/package/services/metadata/core-properties/";
private const string _w3cdtf = "W3CDTF";
private const string _defaultPropertyPartNameExtension = ".psmdcp";
private const string _guidStorageFormatString = @"N"; // N - simple format without adornments
private static PackageXmlEnum[] _validProperties = new PackageXmlEnum[] {
PackageXmlEnum.Creator,
PackageXmlEnum.Identifier,
PackageXmlEnum.Title,
PackageXmlEnum.Subject,
PackageXmlEnum.Description,
PackageXmlEnum.Language,
PackageXmlEnum.Created,
PackageXmlEnum.Modified,
PackageXmlEnum.ContentType,
PackageXmlEnum.Keywords,
PackageXmlEnum.Category,
PackageXmlEnum.Version,
PackageXmlEnum.LastModifiedBy,
PackageXmlEnum.ContentStatus,
PackageXmlEnum.Revision,
PackageXmlEnum.LastPrinted
};
// Array of formats to supply to XmlConvert.ToDateTime or DateTime.ParseExact.
// xsd:DateTime requires full date time in sortable (ISO 8601) format.
// It can be expressed in local time, universal time (Z), or relative to universal time (zzz).
// Negative years are accepted.
// IMPORTANT: Second fractions are recognized only down to 1 tenth of a microsecond because this is the resolution
// of the DateTime type. The Xml standard, however, allows any number of decimals; but XmlConvert only offers
// this very awkward API with an explicit pattern enumeration.
private static readonly string[] _dateTimeFormats = new string[] {
"yyyy-MM-ddTHH:mm:ss",
"yyyy-MM-ddTHH:mm:ssZ",
"yyyy-MM-ddTHH:mm:sszzz",
@"\-yyyy-MM-ddTHH:mm:ss",
@"\-yyyy-MM-ddTHH:mm:ssZ",
@"\-yyyy-MM-ddTHH:mm:sszzz",
"yyyy-MM-ddTHH:mm:ss.ff",
"yyyy-MM-ddTHH:mm:ss.fZ",
"yyyy-MM-ddTHH:mm:ss.fzzz",
@"\-yyyy-MM-ddTHH:mm:ss.f",
@"\-yyyy-MM-ddTHH:mm:ss.fZ",
@"\-yyyy-MM-ddTHH:mm:ss.fzzz",
"yyyy-MM-ddTHH:mm:ss.ff",
"yyyy-MM-ddTHH:mm:ss.ffZ",
"yyyy-MM-ddTHH:mm:ss.ffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.ff",
@"\-yyyy-MM-ddTHH:mm:ss.ffZ",
@"\-yyyy-MM-ddTHH:mm:ss.ffzzz",
"yyyy-MM-ddTHH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ss.fffZ",
"yyyy-MM-ddTHH:mm:ss.fffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.fff",
@"\-yyyy-MM-ddTHH:mm:ss.fffZ",
@"\-yyyy-MM-ddTHH:mm:ss.fffzzz",
"yyyy-MM-ddTHH:mm:ss.ffff",
"yyyy-MM-ddTHH:mm:ss.ffffZ",
"yyyy-MM-ddTHH:mm:ss.ffffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.ffff",
@"\-yyyy-MM-ddTHH:mm:ss.ffffZ",
@"\-yyyy-MM-ddTHH:mm:ss.ffffzzz",
"yyyy-MM-ddTHH:mm:ss.fffff",
"yyyy-MM-ddTHH:mm:ss.fffffZ",
"yyyy-MM-ddTHH:mm:ss.fffffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.fffff",
@"\-yyyy-MM-ddTHH:mm:ss.fffffZ",
@"\-yyyy-MM-ddTHH:mm:ss.fffffzzz",
"yyyy-MM-ddTHH:mm:ss.ffffff",
"yyyy-MM-ddTHH:mm:ss.ffffffZ",
"yyyy-MM-ddTHH:mm:ss.ffffffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.ffffff",
@"\-yyyy-MM-ddTHH:mm:ss.ffffffZ",
@"\-yyyy-MM-ddTHH:mm:ss.ffffffzzz",
"yyyy-MM-ddTHH:mm:ss.fffffff",
"yyyy-MM-ddTHH:mm:ss.fffffffZ",
"yyyy-MM-ddTHH:mm:ss.fffffffzzz",
@"\-yyyy-MM-ddTHH:mm:ss.fffffff",
@"\-yyyy-MM-ddTHH:mm:ss.fffffffZ",
@"\-yyyy-MM-ddTHH:mm:ss.fffffffzzz",
};
#endregion Private Fields
}
}
|