PageRenderTime 24ms CodeModel.GetById 2ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 0ms

/Raven.Database/Json/JsonPatcher.cs

https://github.com/kairogyn/ravendb
C# | 300 lines | 263 code | 32 blank | 5 comment | 81 complexity | 80b1d097ad75339a79d8131e852aa6ec MD5 | raw file
  1//-----------------------------------------------------------------------
  2// <copyright file="JsonPatcher.cs" company="Hibernating Rhinos LTD">
  3//     Copyright (c) Hibernating Rhinos LTD. All rights reserved.
  4// </copyright>
  5//-----------------------------------------------------------------------
  6using System;
  7using System.Collections.Generic;
  8using Raven.Imports.Newtonsoft.Json.Linq;
  9using Raven.Abstractions.Data;
 10using Raven.Abstractions.Exceptions;
 11using Raven.Abstractions.Json;
 12using System.Linq;
 13using Raven.Json.Linq;
 14
 15namespace Raven.Database.Json
 16{
 17	public class JsonPatcher
 18	{
 19		private readonly RavenJObject document;
 20
 21		public JsonPatcher(RavenJObject document)
 22		{
 23			this.document = document;
 24		}
 25
 26		public RavenJObject Apply(PatchRequest[] patch)
 27		{
 28			foreach (var patchCmd in patch)
 29			{
 30				Apply(patchCmd);
 31			}
 32			return document;
 33		}
 34
 35		private void Apply(PatchRequest patchCmd)
 36		{
 37			if (patchCmd.Name == null)
 38				throw new InvalidOperationException("Patch property must have a name property");
 39			foreach (var result in document.SelectTokenWithRavenSyntaxReturningFlatStructure( patchCmd.Name, true))
 40			{
 41			    var token = result.Item1;
 42			    var parent = result.Item2;
 43				switch (patchCmd.Type)
 44				{
 45					case PatchCommandType.Set:
 46						SetProperty(patchCmd, patchCmd.Name, token);
 47						break;
 48					case PatchCommandType.Unset:
 49						RemoveProperty(patchCmd, patchCmd.Name, token, parent);
 50						break;
 51					case PatchCommandType.Add:
 52						AddValue(patchCmd, patchCmd.Name, token);
 53						break;
 54					case PatchCommandType.Insert:
 55						InsertValue(patchCmd, patchCmd.Name, token);
 56						break;
 57					case PatchCommandType.Remove:
 58						RemoveValue(patchCmd, patchCmd.Name, token);
 59						break;
 60					case PatchCommandType.Modify:
 61						ModifyValue(patchCmd, patchCmd.Name, token);
 62						break;
 63					case PatchCommandType.Inc:
 64						IncrementProperty(patchCmd, patchCmd.Name, token);
 65						break;
 66					case PatchCommandType.Copy:
 67						CopyProperty(patchCmd, token);
 68						break;
 69					case PatchCommandType.Rename:
 70						RenameProperty(patchCmd, patchCmd.Name, token);
 71						break;
 72					default:
 73						throw new ArgumentException(string.Format("Cannot understand command: '{0}'", patchCmd.Type));
 74				}
 75			}
 76		}
 77
 78		private void RenameProperty(PatchRequest patchCmd, string propName, RavenJToken property)
 79		{
 80			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
 81			if (property == null)
 82				return;
 83
 84			document[patchCmd.Value.Value<string>()] = property;
 85			document.Remove(propName);
 86		}
 87
 88		private void CopyProperty(PatchRequest patchCmd, RavenJToken property)
 89		{
 90			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
 91			if (property == null)
 92				return;
 93
 94			document[patchCmd.Value.Value<string>()] = property;
 95		}
 96
 97		private static void ModifyValue(PatchRequest patchCmd, string propName, RavenJToken property)
 98		{
 99			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
100			if (property == null)
101				throw new InvalidOperationException("Cannot modify value from '" + propName + "' because it was not found");
102
103			var nestedCommands = patchCmd.Nested;
104			if (nestedCommands == null)
105				throw new InvalidOperationException("Cannot understand modified value from '" + propName +
106													"' because could not find nested array of commands");
107
108			var arrayOrValue = TryGetArray(property) ?? property;
109			switch (arrayOrValue.Type)
110			{
111				case JTokenType.Object:
112					foreach (var cmd in nestedCommands)
113					{
114						var nestedDoc = property.Value<RavenJObject>();
115						new JsonPatcher(nestedDoc).Apply(cmd);
116					}
117					break;
118				case JTokenType.Array:
119					var position = patchCmd.Position;
120					var allPositionsIsSelected = patchCmd.AllPositions.HasValue ? patchCmd.AllPositions.Value : false;
121					if (position == null && !allPositionsIsSelected)
122						throw new InvalidOperationException("Cannot modify value from  '" + propName +
123						                                    "' because position element does not exists or not an integer and allPositions is not set");
124					var valueList = new List<RavenJToken>();
125					if (allPositionsIsSelected)
126					{
127						valueList.AddRange(arrayOrValue.Values<RavenJToken>());
128					}
129					else
130					{
131						valueList.Add(((RavenJArray)arrayOrValue)[position.Value]);
132					}
133
134					foreach (var value in valueList)
135					{
136						foreach (var cmd in nestedCommands)
137						{
138							new JsonPatcher(value.Value<RavenJObject>()).Apply(cmd);
139						}
140					}
141					break;
142				default:
143					throw new InvalidOperationException("Can't understand how to deal with: " + property.Type);
144			}
145		}
146
147		private void RemoveValue(PatchRequest patchCmd, string propName, RavenJToken token)
148		{
149			EnsurePreviousValueMatchCurrentValue(patchCmd, token);
150			if (token == null)
151			{
152				token = new RavenJArray();
153				document[propName] = token;
154			}
155			var array = GetArray(token, propName);
156			array = new RavenJArray(array);
157			document[propName] = array;
158
159			var position = patchCmd.Position;
160			var value = patchCmd.Value;
161			if (position == null && (value == null || value.Type == JTokenType.Null))
162				throw new InvalidOperationException("Cannot remove value from  '" + propName +
163				                                    "' because position element does not exists or not an integer and no value was present");
164			if (position != null && value != null && value.Type != JTokenType.Null)
165				throw new InvalidOperationException("Cannot remove value from  '" + propName +
166				                                    "' because both a position and a value are set");
167			if (position != null && (position.Value < 0 || position.Value >= array.Length))
168				throw new IndexOutOfRangeException("Cannot remove value from  '" + propName +
169				                                   "' because position element is out of bound bounds");
170
171			if (value != null && value.Type != JTokenType.Null)
172			{
173				foreach (var ravenJToken in array.Where(x => RavenJToken.DeepEquals(x, value)).ToList())
174				{
175					array.Remove(ravenJToken);
176				}
177
178				return;
179			}
180
181			if (position != null)
182				array.RemoveAt(position.Value);
183		}
184
185		private void InsertValue(PatchRequest patchCmd, string propName, RavenJToken property)
186		{
187			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
188			if (!(property is RavenJArray))
189			{
190				property = new RavenJArray();
191				document[propName] = property;
192			}
193			var array = property as RavenJArray;
194			if (array == null)
195				throw new InvalidOperationException("Cannot remove value from '" + propName + "' because it is not an array");
196			var position = patchCmd.Position;
197			if (position == null)
198				throw new InvalidOperationException("Cannot remove value from '" + propName + "' because position element does not exists or not an integer");
199			if (position < 0 || position >= array.Length)
200				throw new IndexOutOfRangeException("Cannot remove value from '" + propName +
201												   "' because position element is out of bound bounds");
202			array.Insert(position.Value, patchCmd.Value);
203		}
204
205		private void AddValue(PatchRequest patchCmd, string propName, RavenJToken token)
206		{
207			EnsurePreviousValueMatchCurrentValue(patchCmd, token);
208			if (token == null)
209			{
210				token = new RavenJArray();
211				document[propName] = token;
212			}
213			var array = GetArray(token, propName);
214			array = new RavenJArray(array);
215			document[propName] = array;
216			array.Add(patchCmd.Value);
217		}
218
219		private static RavenJArray GetArray(RavenJToken property, string propName)
220		{
221			var array = TryGetArray(property);
222			if(array == null)
223				throw new InvalidOperationException("Cannot modify '" + propName + "' because it is not an array");
224			return array;
225		}
226
227		private static RavenJArray TryGetArray(RavenJToken token)
228		{
229			if(token == null || token.Type == JTokenType.Null || token.Type == JTokenType.Undefined)
230				return new RavenJArray();
231
232			var array = token as RavenJArray;
233			if (array != null)
234				return array;
235
236			var jObject = token as RavenJObject;
237			if (jObject == null || !jObject.ContainsKey("$values"))
238				return null;
239			array = jObject.Value<RavenJArray>("$values");
240
241			return array;
242		}
243
244
245		private static void RemoveProperty(PatchRequest patchCmd, string propName, RavenJToken token, RavenJToken parent)
246		{
247			EnsurePreviousValueMatchCurrentValue(patchCmd, token);
248			var o = parent as RavenJObject;
249			if (o != null)
250				o.Remove(propName);
251		}
252
253		private void SetProperty(PatchRequest patchCmd, string propName, RavenJToken property)
254		{
255			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
256			document[propName] = patchCmd.Value;
257		}
258
259
260		private void IncrementProperty(PatchRequest patchCmd, string propName, RavenJToken property)
261		{
262			if (patchCmd.Value.Type != JTokenType.Integer)
263				throw new InvalidOperationException("Cannot increment when value is not an integer");
264
265			var valToSet = patchCmd.Value as RavenJValue; // never null since we made sure it's JTokenType.Integer
266
267			EnsurePreviousValueMatchCurrentValue(patchCmd, property);
268			var val = property as RavenJValue;
269			if (val == null)
270			{
271				document[propName] = valToSet.Value<int>();
272				return;
273			}
274			if (val.Value == null || val.Type == JTokenType.Null)
275				document[propName] = valToSet.Value<int>();
276			else
277				document[propName] = RavenJToken.FromObject(val.Value<int>() + valToSet.Value<int>()).Value<int>();
278		}
279
280		private static void EnsurePreviousValueMatchCurrentValue(PatchRequest patchCmd, RavenJToken property)
281		{
282			var prevVal = patchCmd.PrevVal;
283			if (prevVal == null)
284				return;
285			switch (prevVal.Type)
286			{
287				case JTokenType.Undefined:
288					if (property != null)
289						throw new ConcurrencyException();
290					break;
291				default:
292					if (property == null)
293						throw new ConcurrencyException();
294					if (RavenJToken.DeepEquals(property, prevVal) == false)
295						throw new ConcurrencyException();
296					break;
297			}
298		}
299	}
300}