1#region Copyright notice and license
2// Protocol Buffers - Google's data interchange format
3// Copyright 2015 Google Inc.  All rights reserved.
4// https://developers.google.com/protocol-buffers/
5//
6// Redistribution and use in source and binary forms, with or without
7// modification, are permitted provided that the following conditions are
8// met:
9//
10//     * Redistributions of source code must retain the above copyright
11// notice, this list of conditions and the following disclaimer.
12//     * Redistributions in binary form must reproduce the above
13// copyright notice, this list of conditions and the following disclaimer
14// in the documentation and/or other materials provided with the
15// distribution.
16//     * Neither the name of Google Inc. nor the names of its
17// contributors may be used to endorse or promote products derived from
18// this software without specific prior written permission.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#endregion
32
33using System.Collections;
34using System.Collections.Generic;
35using System.Diagnostics;
36using Google.Protobuf.Reflection;
37using Google.Protobuf.WellKnownTypes;
38
39namespace Google.Protobuf
40{
41    /// <summary>
42    /// <para>A tree representation of a FieldMask. Each leaf node in this tree represent
43    /// a field path in the FieldMask.</para>
44    ///
45    /// <para>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:</para>
46    /// <code>
47    ///   [root] -+- foo -+- bar
48    ///           |       |
49    ///           |       +- baz
50    ///           |
51    ///           +- bar --- baz
52    /// </code>
53    ///
54    /// <para>By representing FieldMasks with this tree structure we can easily convert
55    /// a FieldMask to a canonical form, merge two FieldMasks, calculate the
56    /// intersection to two FieldMasks and traverse all fields specified by the
57    /// FieldMask in a message tree.</para>
58    /// </summary>
59    internal sealed class FieldMaskTree
60    {
61        private const char FIELD_PATH_SEPARATOR = '.';
62
63        internal sealed class Node
64        {
65            public Dictionary<string, Node> Children { get; } = new Dictionary<string, Node>();
66        }
67
68        private readonly Node root = new Node();
69
70        /// <summary>
71        /// Creates an empty FieldMaskTree.
72        /// </summary>
73        public FieldMaskTree()
74        {
75        }
76
77        /// <summary>
78        /// Creates a FieldMaskTree for a given FieldMask.
79        /// </summary>
80        public FieldMaskTree(FieldMask mask)
81        {
82            MergeFromFieldMask(mask);
83        }
84
85        public override string ToString()
86        {
87            return ToFieldMask().ToString();
88        }
89
90        /// <summary>
91        /// Adds a field path to the tree. In a FieldMask, every field path matches the
92        /// specified field as well as all its sub-fields. For example, a field path
93        /// "foo.bar" matches field "foo.bar" and also "foo.bar.baz", etc. When adding
94        /// a field path to the tree, redundant sub-paths will be removed. That is,
95        /// after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
96        /// exists, which will turn the tree node for "foo.bar" to a leaf node.
97        /// Likewise, if the field path to add is a sub-path of an existing leaf node,
98        /// nothing will be changed in the tree.
99        /// </summary>
100        public FieldMaskTree AddFieldPath(string path)
101        {
102            var parts = path.Split(FIELD_PATH_SEPARATOR);
103            if (parts.Length == 0)
104            {
105                return this;
106            }
107
108            var node = root;
109            var createNewBranch = false;
110
111            // Find the matching node in the tree.
112            foreach (var part in parts)
113            {
114                // Check whether the path matches an existing leaf node.
115                if (!createNewBranch
116                    && node != root
117                    && node.Children.Count == 0)
118                {
119                    // The path to add is a sub-path of an existing leaf node.
120                    return this;
121                }
122
123                Node childNode;
124                if (!node.Children.TryGetValue(part, out childNode))
125                {
126                    createNewBranch = true;
127                    childNode = new Node();
128                    node.Children.Add(part, childNode);
129                }
130                node = childNode;
131            }
132
133            // Turn the matching node into a leaf node (i.e., remove sub-paths).
134            node.Children.Clear();
135            return this;
136        }
137
138        /// <summary>
139        /// Merges all field paths in a FieldMask into this tree.
140        /// </summary>
141        public FieldMaskTree MergeFromFieldMask(FieldMask mask)
142        {
143            foreach (var path in mask.Paths)
144            {
145                AddFieldPath(path);
146            }
147
148            return this;
149        }
150
151        /// <summary>
152        /// Converts this tree to a FieldMask.
153        /// </summary>
154        public FieldMask ToFieldMask()
155        {
156            var mask = new FieldMask();
157            if (root.Children.Count != 0)
158            {
159                var paths = new List<string>();
160                GetFieldPaths(root, "", paths);
161                mask.Paths.AddRange(paths);
162            }
163
164            return mask;
165        }
166
167        /// <summary>
168        /// Gathers all field paths in a sub-tree.
169        /// </summary>
170        private void GetFieldPaths(Node node, string path, List<string> paths)
171        {
172            if (node.Children.Count == 0)
173            {
174                paths.Add(path);
175                return;
176            }
177
178            foreach (var entry in node.Children)
179            {
180                var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
181                GetFieldPaths(entry.Value, childPath, paths);
182            }
183        }
184
185        /// <summary>
186        /// Adds the intersection of this tree with the given <paramref name="path"/> to <paramref name="output"/>.
187        /// </summary>
188        public void IntersectFieldPath(string path, FieldMaskTree output)
189        {
190            if (root.Children.Count == 0)
191            {
192                return;
193            }
194
195            var parts = path.Split(FIELD_PATH_SEPARATOR);
196            if (parts.Length == 0)
197            {
198                return;
199            }
200
201            var node = root;
202            foreach (var part in parts)
203            {
204                if (node != root
205                    && node.Children.Count == 0)
206                {
207                    // The given path is a sub-path of an existing leaf node in the tree.
208                    output.AddFieldPath(path);
209                    return;
210                }
211
212                if (!node.Children.TryGetValue(part, out node))
213                {
214                    return;
215                }
216            }
217
218            // We found a matching node for the path. All leaf children of this matching
219            // node is in the intersection.
220            var paths = new List<string>();
221            GetFieldPaths(node, path, paths);
222            foreach (var value in paths)
223            {
224                output.AddFieldPath(value);
225            }
226        }
227
228        /// <summary>
229        /// Merges all fields specified by this FieldMaskTree from <paramref name="source"/> to <paramref name="destination"/>.
230        /// </summary>
231        public void Merge(IMessage source, IMessage destination, FieldMask.MergeOptions options)
232        {
233            if (source.Descriptor != destination.Descriptor)
234            {
235                throw new InvalidProtocolBufferException("Cannot merge messages of different types.");
236            }
237
238            if (root.Children.Count == 0)
239            {
240                return;
241            }
242
243            Merge(root, "", source, destination, options);
244        }
245
246        /// <summary>
247        /// Merges all fields specified by a sub-tree from <paramref name="source"/> to <paramref name="destination"/>.
248        /// </summary>
249        private void Merge(
250            Node node,
251            string path,
252            IMessage source,
253            IMessage destination,
254            FieldMask.MergeOptions options)
255        {
256            if (source.Descriptor != destination.Descriptor)
257            {
258                throw new InvalidProtocolBufferException($"source ({source.Descriptor}) and destination ({destination.Descriptor}) descriptor must be equal");
259            }
260
261            var descriptor = source.Descriptor;
262            foreach (var entry in node.Children)
263            {
264                var field = descriptor.FindFieldByName(entry.Key);
265                if (field == null)
266                {
267                    Debug.WriteLine($"Cannot find field \"{entry.Key}\" in message type \"{descriptor.FullName}\"");
268                    continue;
269                }
270
271                if (entry.Value.Children.Count != 0)
272                {
273                    if (field.IsRepeated
274                        || field.FieldType != FieldType.Message)
275                    {
276                        Debug.WriteLine($"Field \"{field.FullName}\" is not a singular message field and cannot have sub-fields.");
277                        continue;
278                    }
279
280                    var sourceField = field.Accessor.GetValue(source);
281                    var destinationField = field.Accessor.GetValue(destination);
282                    if (sourceField == null
283                        && destinationField == null)
284                    {
285                        // If the message field is not present in both source and destination, skip recursing
286                        // so we don't create unnecessary empty messages.
287                        continue;
288                    }
289
290                    if (destinationField == null)
291                    {
292                        // If we have to merge but the destination does not contain the field, create it.
293                        destinationField = field.MessageType.Parser.CreateTemplate();
294                        field.Accessor.SetValue(destination, destinationField);
295                    }
296
297                    var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
298                    Merge(entry.Value, childPath, (IMessage)sourceField, (IMessage)destinationField, options);
299                    continue;
300                }
301
302                if (field.IsRepeated)
303                {
304                    if (options.ReplaceRepeatedFields)
305                    {
306                        field.Accessor.Clear(destination);
307                    }
308
309                    var sourceField = (IList)field.Accessor.GetValue(source);
310                    var destinationField = (IList)field.Accessor.GetValue(destination);
311                    foreach (var element in sourceField)
312                    {
313                        destinationField.Add(element);
314                    }
315                }
316                else
317                {
318                    var sourceField = field.Accessor.GetValue(source);
319                    if (field.FieldType == FieldType.Message)
320                    {
321                        if (options.ReplaceMessageFields)
322                        {
323                            if (sourceField == null)
324                            {
325                                field.Accessor.Clear(destination);
326                            }
327                            else
328                            {
329                                field.Accessor.SetValue(destination, sourceField);
330                            }
331                        }
332                        else
333                        {
334                            if (sourceField != null)
335                            {
336                                var sourceByteString = ((IMessage)sourceField).ToByteString();
337                                var destinationValue = (IMessage)field.Accessor.GetValue(destination);
338                                if (destinationValue != null)
339                                {
340                                    destinationValue.MergeFrom(sourceByteString);
341                                }
342                                else
343                                {
344                                    field.Accessor.SetValue(destination, field.MessageType.Parser.ParseFrom(sourceByteString));
345                                }
346                            }
347                        }
348                    }
349                    else
350                    {
351                        if (sourceField != null
352                            || !options.ReplacePrimitiveFields)
353                        {
354                            field.Accessor.SetValue(destination, sourceField);
355                        }
356                        else
357                        {
358                            field.Accessor.Clear(destination);
359                        }
360                    }
361                }
362            }
363        }
364    }
365}
366