/*
* 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
}
}