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