summaryrefslogtreecommitdiff
path: root/FireBase/Streaming/FirebaseCache.cs
blob: ba7990b36b9f401dcc302c76094872902705077d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
namespace Firebase.Database.Streaming
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    using Firebase.Database.Http;

    using Newtonsoft.Json;

    /// <summary>
    /// The firebase cache.
    /// </summary>
    /// <typeparam name="T"> Type of top-level entities in the cache. </typeparam>
    public class FirebaseCache<T> : IEnumerable<FirebaseObject<T>>
    {
        private readonly IDictionary<string, T> dictionary;
        private readonly bool isDictionaryType;
        private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings()
        {
            ObjectCreationHandling = ObjectCreationHandling.Replace
        };

        /// <summary>
        /// Initializes a new instance of the <see cref="FirebaseCache{T}"/> class.
        /// </summary>
        public FirebaseCache() 
            : this(new Dictionary<string, T>())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FirebaseCache{T}"/> class and populates it with existing data.
        /// </summary>
        /// <param name="existingItems"> The existing items. </param>
        public FirebaseCache(IDictionary<string, T> existingItems)
        {
            this.dictionary = existingItems;
            this.isDictionaryType = typeof(IDictionary).GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo());
        }

        /// <summary>
        /// The push data.
        /// </summary>
        /// <param name="path"> The path of incoming data, separated by slash. </param>  
        /// <param name="data"> The data in json format as returned by firebase. </param>  
        /// <returns> Collection of top-level entities which were affected by the push. </returns>
        public IEnumerable<FirebaseObject<T>> PushData(string path, string data, bool removeEmptyEntries = true)
        {
            object obj = this.dictionary;
            Action<object> primitiveObjSetter = null;
            Action objDeleter = null;

            var pathElements = path.Split(new[] { "/" }, removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);

            // first find where we should insert the data to
            foreach (var element in pathElements)
            {
                if (obj is IDictionary)
                {
                    // if it's a dictionary, then it's just a matter of inserting into it / accessing existing object by key
                    var dictionary = obj as IDictionary;
                    var valueType = obj.GetType().GenericTypeArguments[1];

                    primitiveObjSetter = (d) => dictionary[element] = d;
                    objDeleter = () => dictionary.Remove(element);

                    if (dictionary.Contains(element))
                    {
                        obj = dictionary[element];
                    }
                    else
                    {
                        dictionary[element] = this.CreateInstance(valueType);
                        obj = dictionary[element];
                    }
                }
                else
                {
                    // if it's not a dictionary, try to find the property of current object with the matching name
                    var objParent = obj;
                    var property = objParent
                        .GetType()
                        .GetRuntimeProperties()
                        .First(p => p.Name.Equals(element, StringComparison.OrdinalIgnoreCase) || element == p.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName);

                    objDeleter = () => property.SetValue(objParent, null);
                    primitiveObjSetter = (d) => property.SetValue(objParent, d);
                    obj = property.GetValue(obj);
                    if (obj == null)
                    {
                        obj = this.CreateInstance(property.PropertyType);
                        property.SetValue(objParent, obj);
                    }
                }
            }

            // if data is null (=empty string) delete it
            if (string.IsNullOrWhiteSpace(data) || data == "null")
            {
                var key = pathElements[0];
                var target = this.dictionary[key];

                objDeleter();

                yield return new FirebaseObject<T>(key, target);
                yield break;
            }

            // now insert the data
            if (obj is IDictionary && !this.isDictionaryType)
            {
                // insert data into dictionary and return it as a collection of FirebaseObject
                var dictionary = obj as IDictionary;
                var valueType = obj.GetType().GenericTypeArguments[1];
                var objectCollection = data.GetObjectCollection(valueType);

                foreach (var item in objectCollection)
                {
                    dictionary[item.Key] = item.Object;

                    // top level dictionary changed
                    if (!pathElements.Any())
                    {
                        yield return new FirebaseObject<T>(item.Key, (T)item.Object);
                    }
                }

                // nested dictionary changed
                if (pathElements.Any())
                {
                    this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]];
                    yield return new FirebaseObject<T>(pathElements[0], this.dictionary[pathElements[0]]);
                }
            }
            else
            {
                // set the data on a property of the given object
                var valueType = obj.GetType();

                // firebase sends strings without double quotes
                var targetObject = valueType == typeof(string) ? data.ToString() : JsonConvert.DeserializeObject(data, valueType);

                if ((valueType.GetTypeInfo().IsPrimitive || valueType == typeof(string)) && primitiveObjSetter != null)
                {
                    // handle primitive (value) types separately
                    primitiveObjSetter(targetObject);
                }
                else
                {
                    JsonConvert.PopulateObject(data, obj, this.serializerSettings);
                }

                this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]];
                yield return new FirebaseObject<T>(pathElements[0], this.dictionary[pathElements[0]]);
            }
        }

        public bool Contains(string key)
        {
            return this.dictionary.Keys.Contains(key);
        }

        private object CreateInstance(Type type)
        {
            if (type == typeof(string))
            {
                return string.Empty;
            }
            else
            {
                return Activator.CreateInstance(type);
            }
        }

        #region IEnumerable

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public IEnumerator<FirebaseObject<T>> GetEnumerator()
        {
            return this.dictionary.Select(p => new FirebaseObject<T>(p.Key, p.Value)).GetEnumerator();
        }

        #endregion
    }
}