/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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. */ using System; using System.Collections.Generic; using System.IO; using System.Linq; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using Amazon.Runtime.Internal.Util; using Amazon.Util; namespace Amazon.DynamoDBv2.DocumentModel { /// /// A collection of attribute key-value pairs that defines /// an item in DynamoDB. /// public class Document : DynamoDBEntry, IDictionary { #region Private/internal members private Dictionary originalValues; private Dictionary currentValues; #endregion #region Constructors /// /// Constructs an empty Document. /// public Document() { originalValues = new Dictionary(); currentValues = new Dictionary(); } /// /// Constructs a Document with the passed-in values as its attribute values. /// /// public Document(Dictionary values) { originalValues = new Dictionary(); currentValues = new Dictionary(values); } internal Document(Document source) { originalValues = new Dictionary(source.originalValues); currentValues = new Dictionary(source.currentValues); } #endregion #region Properties/accessors /// /// Attribute accessor, allows getting or setting of an individual attribute. /// /// Name of the attribute. /// Current value of the attribute. public DynamoDBEntry this[string key] { get { return currentValues[key]; } set { if (value == null) { currentValues[key] = new Primitive(); } else { currentValues[key] = value; } } } /// /// Returns true if the attribute has been changed. /// /// Name of the attribute. /// True if the attribute has been changed. public bool IsAttributeChanged(string attributeName) { DynamoDBEntry original; this.originalValues.TryGetValue(attributeName, out original); DynamoDBEntry current; this.currentValues.TryGetValue(attributeName, out current); if ((original != null && current == null) || (original == null && current != null)) return true; if (original == null && current == null) return false; return !original.Equals(current); } /// /// Returns true if the document contains attributes that have not been saved. /// /// True if the document contains attributes that have not been saved. public bool IsDirty() { Dictionary keys = new Dictionary(); foreach (string key in currentValues.Keys) { keys.Add(key, key); } foreach (string key in originalValues.Keys) { if (!keys.ContainsKey(key)) { keys.Add(key, key); } } foreach (string key in keys.Keys) { if (this.IsAttributeChanged(key)) { return true; } } return false; } /// /// Gets the value associated with the specified attribute value. /// /// Attribute name /// /// If the specified attribute value is found, returns the value associated with the /// attribute; otherwise, null. /// /// True if attribute is found; false otherwise public bool TryGetValue(string attributeName, out DynamoDBEntry entry) { return this.currentValues.TryGetValue(attributeName, out entry); } /// /// Determines if a specific attribute is set on the Document. /// /// Attribute name /// Returns true if the specified attribute is found; false otherwise. public bool Contains(string attributeName) { return this.currentValues.ContainsKey(attributeName); } /// /// Returns a new instance of Document where all unconverted .NET types /// are converted to DynamoDBEntry types using a specific conversion. /// /// /// public Document ForceConversion(DynamoDBEntryConversion conversion) { Document newDocument = new Document(); foreach(var kvp in this) { string name = kvp.Key; DynamoDBEntry entry = kvp.Value; var unconvertedEntry = entry as UnconvertedDynamoDBEntry; if (unconvertedEntry != null) entry = unconvertedEntry.Convert(conversion); var doc = entry as Document; if (doc != null) entry = doc.ForceConversion(conversion); var list = entry as DynamoDBList; if (list != null) entry = list.ForceConversion(conversion); newDocument[name] = entry; } return newDocument; } #endregion #region Private/internal methods internal void CommitChanges() { this.originalValues.Clear(); foreach (var kvp in currentValues) { this.originalValues[kvp.Key] = kvp.Value.Clone() as DynamoDBEntry; } } // Converts a Numeric Primitive value from a service attribute to a DynamoDBEntry that can // be converted to a DateTime. internal static DynamoDBEntry EpochSecondsToDateTime(DynamoDBEntry entry, string attributeName) { var primitive = entry.AsPrimitive(); // only try to convert N types to epoch time if (primitive != null && primitive.Type == DynamoDBEntryType.Numeric) { DateTime? dateTime = null; try { var epochSeconds = primitive.AsInt(); dateTime = AWSSDKUtils.ConvertFromUnixEpochSeconds(epochSeconds); } catch (Exception e) { var logger = Logger.GetLogger(typeof(Document)); logger.InfoFormat( "Encountered error attempting to convert attribute '{0}' with value '{1}' to DateTime: {2}", attributeName, entry, e); } if (dateTime.HasValue) { entry = (Primitive)(dateTime.Value); } } return entry; } // Converts a user-supplied DateTime-convertible DynamoDBEntry to epoch seconds stored in a Numeric Primitive. internal static DynamoDBEntry DateTimeToEpochSeconds(DynamoDBEntry entry, string attributeName) { int? epochSeconds = null; try { var dateTime = entry.AsDateTime(); epochSeconds = AWSSDKUtils.ConvertToUnixEpochSeconds(dateTime); } catch (Exception e) { var logger = Logger.GetLogger(typeof(Document)); logger.InfoFormat( "Encountered error attempting to convert '{0}' with value '{1}' to epoch seconds: {1}", attributeName, entry, e); } if (epochSeconds.HasValue) { entry = (Primitive)(epochSeconds.Value); } return entry; } internal static Document FromAttributeMap(Dictionary data, IEnumerable epochAttributes) { Document doc = new Document(); if (data != null) { // Add Primitives and PrimitiveLists foreach (var attribute in data) { string wholeKey = attribute.Key; AttributeValue value = attribute.Value; DynamoDBEntry convertedValue = AttributeValueToDynamoDBEntry(value); if (convertedValue != null) doc.currentValues[wholeKey] = convertedValue; } } if (epochAttributes != null) { foreach (var epochAttribute in epochAttributes) { DynamoDBEntry epochEntry; if (doc.currentValues.TryGetValue(epochAttribute, out epochEntry)) { doc.currentValues[epochAttribute] = EpochSecondsToDateTime(epochEntry, epochAttribute); } } } doc.CommitChanges(); return doc; } internal Dictionary ToAttributeMap(DynamoDBEntryConversion conversion, IEnumerable epochAttributes, bool isEmptyStringValueEnabled) { if (conversion == null) throw new ArgumentNullException("conversion"); Dictionary ret = new Dictionary(); foreach (var kvp in currentValues) { var attributeName = kvp.Key; var entry = kvp.Value; ApplyEpochRules(epochAttributes, attributeName, ref entry); var attributeConversionConfig = new AttributeConversionConfig(conversion, isEmptyStringValueEnabled); var value = entry.ConvertToAttributeValue(attributeConversionConfig); if (value != null) { ret.Add(attributeName, value); } } return ret; } internal Dictionary ToExpectedAttributeMap(DynamoDBEntryConversion conversion, IEnumerable epochAttributes, bool isEmptyStringValueEnabled) { if (conversion == null) throw new ArgumentNullException("conversion"); Dictionary ret = new Dictionary(); foreach (var kvp in currentValues) { var attributeName = kvp.Key; var entry = kvp.Value; ApplyEpochRules(epochAttributes, attributeName, ref entry); var attributeConversionConfig = new AttributeConversionConfig(conversion, isEmptyStringValueEnabled); ret.Add(attributeName, entry.ConvertToExpectedAttributeValue(attributeConversionConfig)); } return ret; } internal Dictionary ToAttributeUpdateMap(DynamoDBEntryConversion conversion, bool changedAttributesOnly, IEnumerable epochAttributes, bool isEmptyStringValueEnabled) { if (conversion == null) throw new ArgumentNullException("conversion"); Dictionary ret = new Dictionary(); foreach (var kvp in currentValues) { string attributeName = kvp.Key; DynamoDBEntry entry = kvp.Value; ApplyEpochRules(epochAttributes, attributeName, ref entry); if (!changedAttributesOnly || this.IsAttributeChanged(attributeName)) { var attributeConversionConfig = new AttributeConversionConfig(conversion, isEmptyStringValueEnabled); ret.Add(attributeName, entry.ConvertToAttributeUpdateValue(attributeConversionConfig)); } } return ret; } private static void ApplyEpochRules(IEnumerable epochAttributes, string attributeName, ref DynamoDBEntry entry) { if (epochAttributes != null) { foreach (var epochAttribute in epochAttributes) { if (string.Equals(epochAttribute, attributeName, StringComparison.Ordinal)) { entry = DateTimeToEpochSeconds(entry, attributeName); } } } } #endregion #region DynamoDB conversion /// /// /// Converts the current Document into a matching JSON string. /// /// /// DynamoDB types are a superset of JSON types, thus the following DynamoDB cannot /// be properly represented as JSON data: /// PrimitiveList (SS, NS, BS types) - these sets will be converted to JSON arrays /// Binary Primitive (B type) - binary data will be converted to Base64 strings /// /// /// If the resultant JSON is passed to Document.FromJson, the binary values will be /// treated as Base64 strings. Invoke Document.DecodeBase64Attributes to decode these /// strings into binary data. /// /// /// JSON string corresponding to the current Document. public string ToJson() { return JsonUtils.ToJson(this, prettyPrint: false); } /// /// /// Converts the current Document into a matching pretty JSON string. /// /// /// DynamoDB types are a superset of JSON types, thus the following DynamoDB cannot /// be properly represented as JSON data: /// PrimitiveList (SS, NS, BS types) - these sets will be converted to JSON arrays /// Binary Primitive (B type) - binary data will be converted to Base64 strings /// /// /// If the resultant JSON is passed to Document.FromJson, the binary values will be /// treated as Base64 strings. Invoke Document.DecodeBase64Attributes to decode these /// strings into binary data. /// /// /// JSON string corresponding to the current Document. public string ToJsonPretty() { return JsonUtils.ToJson(this, prettyPrint: true); } /// /// Creates a map of attribute names mapped to AttributeValue objects. /// Converts .NET types using the conversion specified by AWSConfigs.DynamoDBConfig.ConversionSchema /// /// public Dictionary ToAttributeMap() { return ToAttributeMap(DynamoDBEntryConversion.CurrentConversion); } /// /// Creates a map of attribute names mapped to AttributeValue objects. /// /// Conversion to use for converting .NET values to DynamoDB values. /// public Dictionary ToAttributeMap(DynamoDBEntryConversion conversion) { return ToAttributeMap(conversion, epochAttributes: null, isEmptyStringValueEnabled: false); } /// /// Creates a map of attribute names mapped to AttributeValue objects. /// /// Conversion to use for converting .NET values to DynamoDB values. /// If the property is false, empty string values will be interpreted as null values. /// public Dictionary ToAttributeMap(DynamoDBEntryConversion conversion, bool isEmptyStringValueEnabled) { return ToAttributeMap(conversion, epochAttributes: null, isEmptyStringValueEnabled: isEmptyStringValueEnabled); } /// /// Creates a map of attribute names mapped to ExpectedAttributeValue objects. /// /// public Dictionary ToExpectedAttributeMap() { return ToExpectedAttributeMap(DynamoDBEntryConversion.CurrentConversion); } /// /// Creates a map of attribute names mapped to ExpectedAttributeValue objects. /// /// Conversion to use for converting .NET values to DynamoDB values. /// public Dictionary ToExpectedAttributeMap(DynamoDBEntryConversion conversion) { return ToExpectedAttributeMap(conversion, false); } /// /// Creates a map of attribute names mapped to ExpectedAttributeValue objects. /// /// Conversion to use for converting .NET values to DynamoDB values. /// If the property is false, empty string values will be interpreted as null values. /// public Dictionary ToExpectedAttributeMap(DynamoDBEntryConversion conversion, bool isEmptyStringValueEnabled) { return ToExpectedAttributeMap(conversion, epochAttributes: null, isEmptyStringValueEnabled: isEmptyStringValueEnabled); } /// /// Creates a map of attribute names mapped to AttributeValueUpdate objects. /// /// If true, only attributes that have been changed will be in the map. /// public Dictionary ToAttributeUpdateMap(bool changedAttributesOnly) { return ToAttributeUpdateMap(DynamoDBEntryConversion.CurrentConversion, changedAttributesOnly, false); } /// /// Creates a map of attribute names mapped to AttributeValueUpdate objects. /// /// If true, only attributes that have been changed will be in the map. /// Conversion to use for converting .NET values to DynamoDB values. /// public Dictionary ToAttributeUpdateMap(DynamoDBEntryConversion conversion, bool changedAttributesOnly) { return ToAttributeUpdateMap(conversion, changedAttributesOnly, epochAttributes: null, isEmptyStringValueEnabled: false); } /// /// Creates a map of attribute names mapped to AttributeValueUpdate objects. /// /// If true, only attributes that have been changed will be in the map. /// Conversion to use for converting .NET values to DynamoDB values. /// If the property is false, empty string values will be interpreted as null values. /// public Dictionary ToAttributeUpdateMap(DynamoDBEntryConversion conversion, bool changedAttributesOnly, bool isEmptyStringValueEnabled) { return ToAttributeUpdateMap(conversion, changedAttributesOnly, epochAttributes: null, isEmptyStringValueEnabled: isEmptyStringValueEnabled); } /// /// Returns the names of all the attributes. /// /// List of attribute names. public List GetAttributeNames() { return new List(this.currentValues.Keys); } /// /// /// Decodes root-level Base64-encoded strings to their binary representations. /// Use this method if the Document was constructed from JSON that contains /// base64-encoded binary values, which result from calling ToJson on a Document /// with binary data. /// /// /// Individual strings become binary data. /// List and sets of Base64-encoded strings become lists and sets of binary data. /// /// /// Names of root-level attributes to decode. public void DecodeBase64Attributes(params string[] attributeNames) { JsonUtils.DecodeBase64Attributes(this, attributeNames); } #endregion #region Attribute to DynamoDBEntry conversion internal static DynamoDBEntry AttributeValueToDynamoDBEntry(AttributeValue attributeValue) { Primitive primitive; if (TryToPrimitive(attributeValue, out primitive)) return primitive; PrimitiveList primitiveList; if (TryToPrimitiveList(attributeValue, out primitiveList)) return primitiveList; DynamoDBBool ddbBool; if (TryToDynamoDBBool(attributeValue, out ddbBool)) return ddbBool; DynamoDBList ddbList; if (TryToDynamoDBList(attributeValue, out ddbList)) return ddbList; Document document; if (TryToDocument(attributeValue, out document)) return document; DynamoDBNull ddbNull; if (TryToDynamoDBNull(attributeValue, out ddbNull)) return ddbNull; return null; } private static bool TryToPrimitiveList(AttributeValue attributeValue, out PrimitiveList primitiveList) { primitiveList = null; Primitive primitive; if (attributeValue.IsSetSS()) { primitiveList = new PrimitiveList(DynamoDBEntryType.String); foreach (string item in attributeValue.SS) { primitive = new Primitive(item); primitiveList.Add(primitive); } } else if (attributeValue.IsSetNS()) { primitiveList = new PrimitiveList(DynamoDBEntryType.Numeric); foreach (string item in attributeValue.NS) { primitive = new Primitive(item, true); primitiveList.Add(primitive); } } else if (attributeValue.IsSetBS()) { primitiveList = new PrimitiveList(DynamoDBEntryType.Binary); foreach (MemoryStream item in attributeValue.BS) { primitive = new Primitive(item); primitiveList.Add(primitive); } } return (primitiveList != null); } private static bool TryToPrimitive(AttributeValue attributeValue, out Primitive primitive) { primitive = null; if (attributeValue.IsSetS()) { primitive = new Primitive(attributeValue.S); } else if (attributeValue.IsSetN()) { primitive = new Primitive(attributeValue.N, true); } else if (attributeValue.IsSetB()) { primitive = new Primitive(attributeValue.B); } return (primitive != null); } private static bool TryToDynamoDBBool(AttributeValue attributeValue, out DynamoDBBool ddbBool) { ddbBool = null; if (attributeValue.IsSetBOOL()) { ddbBool = new DynamoDBBool(attributeValue.BOOL); } return (ddbBool != null); } private static bool TryToDynamoDBNull(AttributeValue attributeValue, out DynamoDBNull ddbNull) { ddbNull = null; if (attributeValue.IsSetNULL()) { ddbNull = new DynamoDBNull(); } return (ddbNull != null); } private static bool TryToDynamoDBList(AttributeValue attributeValue, out DynamoDBList list) { list = null; if (attributeValue.IsSetL()) { var items = attributeValue.L; var entries = items.Select(AttributeValueToDynamoDBEntry).Where(item => item != null); list = new DynamoDBList(entries); } return (list != null); } private static bool TryToDocument(AttributeValue attributeValue, out Document document) { document = null; if (attributeValue.IsSetM()) { document = new Document(); var items = attributeValue.M; foreach(var kvp in items) { var name = kvp.Key; var value = kvp.Value; var entry = AttributeValueToDynamoDBEntry(value); document[name] = entry; } } return (document != null); } #endregion #region Static methods /// /// Creates a Document from an attribute map. /// /// Map of attribute names to attribute values. /// Document representing the data. public static Document FromAttributeMap(Dictionary data) { return FromAttributeMap(data, epochAttributes: null); } /// /// Creates a document from a JSON string. /// The conversion is as follows: /// Objects are converted to DynamoDB M types. /// Arrays are converted to DynamoDB L types. /// Boolean types are converted to DynamoDB BOOL types. /// Null values are converted to DynamoDB NULL types. /// Numerics are converted to DynamoDB N types. /// Strings are converted to DynamoDB S types. /// /// JSON string. /// Document representing the JSON data. public static Document FromJson(string json) { return JsonUtils.FromJson(json); } /// /// Parses JSON text to produce an array of . /// /// /// An of type . public static IEnumerable FromJsonArray(string jsonText) { return JsonUtils.FromJsonArray(jsonText); } #endregion #region IDictionary Members /// /// Add value to Doucment. /// /// /// public void Add(string key, DynamoDBEntry value) { currentValues.Add(key, value); } /// /// Check to see if the value is set on the document. /// /// /// public bool ContainsKey(string key) { return currentValues.ContainsKey(key); } /// /// This list of attribute keys for the document. /// public ICollection Keys { get { return currentValues.Keys; } } /// /// Remove the attribute from the Document. /// /// /// public bool Remove(string key) { return currentValues.Remove(key); } /// /// Get a list of all the values in the Document. /// public ICollection Values { get { return currentValues.Values; } } #endregion #region ICollection> Members /// /// Add attributes to Document. /// /// public void Add(KeyValuePair item) { currentValues.Add(item.Key, item.Value); } /// /// Clear attributes from document. /// public void Clear() { currentValues.Clear(); } /// /// Check to see if the attributes are in the Document. /// /// /// public bool Contains(KeyValuePair item) { //DynamoDBEntry value; //if (!this.TryGetValue(item.Key, out value)) // return false; //return value.Equals(item.Value); var icollection = (ICollection>)currentValues; return icollection.Contains(item); } /// /// Copies the attributes to the array. /// /// /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { var icollection = (ICollection>)currentValues; icollection.CopyTo(array, arrayIndex); } /// /// Gets the count of attributes. /// public int Count { get { return currentValues.Count; } } /// /// Returns true if the document is read only. /// public bool IsReadOnly { get { return false; } } /// /// Removes the attributes from the document. /// /// /// public bool Remove(KeyValuePair item) { var icollection = (ICollection>)currentValues; return icollection.Remove(item); } #endregion #region IEnumerable> Members /// /// Gets the enumerator for the attributes. /// /// public IEnumerator> GetEnumerator() { return currentValues.GetEnumerator(); } #endregion #region IEnumerable Members /// /// Gets the enumerator for the attributes. /// /// System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return currentValues.GetEnumerator(); } #endregion #region DynamoDBEntry overrides internal override AttributeValue ConvertToAttributeValue(AttributeConversionConfig conversionConfig) { var map = new Dictionary(StringComparer.Ordinal); foreach(var item in currentValues) { var key = item.Key; var entry = item.Value; AttributeValue entryAttributeValue; using (conversionConfig.CRT.Track(entry)) { entryAttributeValue = entry.ConvertToAttributeValue(conversionConfig); } if (entryAttributeValue != null) { map[key] = entryAttributeValue; } } var attributeValue = new AttributeValue(); attributeValue.M = map; attributeValue.IsMSet = true; return attributeValue; } /// /// Clones the Document /// /// public override object Clone() { var doc = new Document(this); return doc; } #endregion #region Overrides /// /// Compare the document to see if it is equal. /// /// /// public override bool Equals(object obj) { var otherDocument = obj as Document; if (otherDocument == null) return false; if (Keys.Count != otherDocument.Keys.Count) return false; foreach(var key in Keys) { if (!otherDocument.ContainsKey(key)) return false; var a = this[key]; var b = otherDocument[key]; if (!a.Equals(b)) return false; } return true; } /// /// Implements the GetHashCode. /// /// public override int GetHashCode() { var hashCode = 0; foreach(var kvp in this) { string key = kvp.Key; DynamoDBEntry entry = kvp.Value; hashCode = Hashing.CombineHashes(hashCode, key.GetHashCode(), entry.GetHashCode()); } return hashCode; } #endregion } }