PageRenderTime 54ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/Raven.Database/Json/ScriptedJsonPatcher.cs

https://github.com/kairogyn/ravendb
C# | 512 lines | 457 code | 47 blank | 8 comment | 62 complexity | 9cd6768419d6141d4416352d5b2dd2c0 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-SA-3.0
  1. //-----------------------------------------------------------------------
  2. // <copyright file="ScriptedJsonPatcher.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 System.IO;
  9. using System.Linq;
  10. using System.Reflection;
  11. using System.Runtime.Serialization;
  12. using System.Text;
  13. using Jint;
  14. using Jint.Native;
  15. using Raven.Abstractions.Data;
  16. using Raven.Abstractions.Exceptions;
  17. using Raven.Imports.Newtonsoft.Json.Linq;
  18. using Raven.Json.Linq;
  19. namespace Raven.Database.Json
  20. {
  21. public class ScriptedJsonPatcher
  22. {
  23. private static readonly ScriptsCache scriptsCache = new ScriptsCache();
  24. private readonly Func<string, RavenJObject> loadDocument;
  25. [ThreadStatic]
  26. private static Func<string, RavenJObject> loadDocumentStatic;
  27. public List<string> Debug = new List<string>();
  28. public IList<JsonDocument> CreatedDocs = new List<JsonDocument>();
  29. private readonly int maxSteps;
  30. private readonly int additionalStepsPerSize;
  31. private readonly Dictionary<JsInstance, KeyValuePair<RavenJValue, object>> propertiesByValue = new Dictionary<JsInstance, KeyValuePair<RavenJValue, object>>();
  32. public ScriptedJsonPatcher(DocumentDatabase database = null)
  33. {
  34. if (database == null)
  35. {
  36. maxSteps = 10 * 1000;
  37. additionalStepsPerSize = 5;
  38. loadDocument = (s =>
  39. {
  40. throw new InvalidOperationException(
  41. "Cannot load by id without database context");
  42. });
  43. }
  44. else
  45. {
  46. maxSteps = database.Configuration.MaxStepsForScript;
  47. additionalStepsPerSize = database.Configuration.AdditionalStepsForScriptBasedOnDocumentSize;
  48. loadDocument = id =>
  49. {
  50. var jsonDocument = database.Get(id, null);
  51. return jsonDocument == null ? null : jsonDocument.ToJson();
  52. };
  53. }
  54. }
  55. public RavenJObject Apply(RavenJObject document, ScriptedPatchRequest patch, int size = 0, string docId = null)
  56. {
  57. if (document == null)
  58. return null;
  59. if (String.IsNullOrEmpty(patch.Script))
  60. throw new InvalidOperationException("Patch script must be non-null and not empty");
  61. var resultDocument = ApplySingleScript(document, patch, size, docId);
  62. if (resultDocument != null)
  63. document = resultDocument;
  64. return document;
  65. }
  66. private RavenJObject ApplySingleScript(RavenJObject doc, ScriptedPatchRequest patch, int size, string docId)
  67. {
  68. JintEngine jintEngine;
  69. try
  70. {
  71. jintEngine = scriptsCache.CheckoutScript(CreateEngine, patch);
  72. }
  73. catch (NotSupportedException e)
  74. {
  75. throw new ParseException("Could not parse script", e);
  76. }
  77. catch (JintException e)
  78. {
  79. throw new ParseException("Could not parse script", e);
  80. }
  81. catch (Exception e)
  82. {
  83. throw new ParseException("Could not parse: " + Environment.NewLine + patch.Script, e);
  84. }
  85. loadDocumentStatic = loadDocument;
  86. try
  87. {
  88. CustomizeEngine(jintEngine);
  89. jintEngine.SetFunction("PutDocument", ((Action<string, JsObject, JsObject>) (PutDocument)));
  90. jintEngine.SetParameter("__document_id", docId);
  91. foreach (var kvp in patch.Values)
  92. {
  93. var token = kvp.Value as RavenJToken;
  94. if (token != null)
  95. {
  96. jintEngine.SetParameter(kvp.Key, ToJsInstance(jintEngine.Global, token));
  97. }
  98. else
  99. {
  100. var rjt = RavenJToken.FromObject(kvp.Value);
  101. var jsInstance = ToJsInstance(jintEngine.Global, rjt);
  102. jintEngine.SetParameter(kvp.Key, jsInstance);
  103. }
  104. }
  105. var jsObject = ToJsObject(jintEngine.Global, doc);
  106. jintEngine.ResetSteps();
  107. if (size != 0)
  108. {
  109. jintEngine.SetMaxSteps(maxSteps + (size*additionalStepsPerSize));
  110. }
  111. jintEngine.CallFunction("ExecutePatchScript", jsObject);
  112. foreach (var kvp in patch.Values)
  113. {
  114. jintEngine.RemoveParameter(kvp.Key);
  115. }
  116. jintEngine.RemoveParameter("__document_id");
  117. RemoveEngineCustomizations(jintEngine);
  118. OutputLog(jintEngine);
  119. scriptsCache.CheckinScript(patch, jintEngine);
  120. return ConvertReturnValue(jsObject);
  121. }
  122. catch (ConcurrencyException)
  123. {
  124. throw;
  125. }
  126. catch (Exception errorEx)
  127. {
  128. OutputLog(jintEngine);
  129. var errorMsg = "Unable to execute JavaScript: " + Environment.NewLine + patch.Script;
  130. var error = errorEx as JsException;
  131. if (error != null)
  132. errorMsg += Environment.NewLine + "Error: " + Environment.NewLine + string.Join(Environment.NewLine, error.Value);
  133. if (Debug.Count != 0)
  134. errorMsg += Environment.NewLine + "Debug information: " + Environment.NewLine +
  135. string.Join(Environment.NewLine, Debug);
  136. throw new InvalidOperationException(errorMsg, errorEx);
  137. }
  138. finally
  139. {
  140. loadDocumentStatic = null;
  141. }
  142. }
  143. protected virtual void RemoveEngineCustomizations(JintEngine jintEngine)
  144. {
  145. }
  146. protected virtual RavenJObject ConvertReturnValue(JsObject jsObject)
  147. {
  148. return ToRavenJObject(jsObject);
  149. }
  150. public RavenJObject ToRavenJObject(JsObject jsObject)
  151. {
  152. var rjo = new RavenJObject();
  153. foreach (var key in jsObject.GetKeys())
  154. {
  155. if (key == Constants.ReduceKeyFieldName || key == Constants.DocumentIdFieldName)
  156. continue;
  157. var jsInstance = jsObject[key];
  158. switch (jsInstance.Type)
  159. {
  160. case JsInstance.CLASS_REGEXP:
  161. case JsInstance.CLASS_ERROR:
  162. case JsInstance.CLASS_ARGUMENTS:
  163. case JsInstance.CLASS_DESCRIPTOR:
  164. case JsInstance.CLASS_FUNCTION:
  165. continue;
  166. }
  167. rjo[key] = ToRavenJToken(jsInstance);
  168. }
  169. return rjo;
  170. }
  171. private RavenJToken ToRavenJToken(JsInstance v)
  172. {
  173. switch (v.Class)
  174. {
  175. case JsInstance.TYPE_OBJECT:
  176. case JsInstance.CLASS_OBJECT:
  177. return ToRavenJObject((JsObject) v);
  178. case JsInstance.CLASS_DATE:
  179. var dt = (DateTime) v.Value;
  180. return new RavenJValue(dt);
  181. case JsInstance.TYPE_NUMBER:
  182. case JsInstance.CLASS_NUMBER:
  183. var num = (double) v.Value;
  184. KeyValuePair<RavenJValue, object> property;
  185. if (propertiesByValue.TryGetValue(v, out property))
  186. {
  187. var originalValue = property.Key;
  188. if (originalValue.Type == JTokenType.Float)
  189. return new RavenJValue(num);
  190. if (originalValue.Type == JTokenType.Integer)
  191. {
  192. // If the current value is exactly as the original value, we can return the original value before we made the JS conversion,
  193. // which will convert a Int64 to jsFloat.
  194. var originalJsValue = property.Value;
  195. if (originalJsValue is double && Math.Abs(num - (double)originalJsValue) < double.Epsilon)
  196. return originalValue;
  197. return new RavenJValue((long) num);
  198. }
  199. }
  200. // If we don't have the type, assume that if the number ending with ".0" it actually an integer.
  201. var integer = Math.Truncate(num);
  202. if (Math.Abs(num - integer) < double.Epsilon)
  203. return new RavenJValue((long) integer);
  204. return new RavenJValue(num);
  205. case JsInstance.TYPE_STRING:
  206. case JsInstance.CLASS_STRING:
  207. {
  208. const string ravenDataByteArrayToBase64 = "raven-data:byte[];base64,";
  209. var value = v.Value as string;
  210. if (value != null && value.StartsWith(ravenDataByteArrayToBase64))
  211. {
  212. value = value.Remove(0, ravenDataByteArrayToBase64.Length);
  213. var byteArray = Convert.FromBase64String(value);
  214. return new RavenJValue(byteArray);
  215. }
  216. return new RavenJValue(v.Value);
  217. }
  218. case JsInstance.TYPE_BOOLEAN:
  219. case JsInstance.CLASS_BOOLEAN:
  220. return new RavenJValue(v.Value);
  221. case JsInstance.CLASS_NULL:
  222. case JsInstance.TYPE_NULL:
  223. return RavenJValue.Null;
  224. case JsInstance.CLASS_UNDEFINED:
  225. case JsInstance.TYPE_UNDEFINED:
  226. return RavenJValue.Null;
  227. case JsInstance.CLASS_ARRAY:
  228. var jsArray = ((JsArray) v);
  229. var rja = new RavenJArray();
  230. for (int i = 0; i < jsArray.Length; i++)
  231. {
  232. var jsInstance = jsArray.get(i);
  233. var ravenJToken = ToRavenJToken(jsInstance);
  234. if (ravenJToken == null)
  235. continue;
  236. rja.Add(ravenJToken);
  237. }
  238. return rja;
  239. case JsInstance.CLASS_REGEXP:
  240. case JsInstance.CLASS_ERROR:
  241. case JsInstance.CLASS_ARGUMENTS:
  242. case JsInstance.CLASS_DESCRIPTOR:
  243. case JsInstance.CLASS_FUNCTION:
  244. return null;
  245. default:
  246. throw new NotSupportedException(v.Class);
  247. }
  248. }
  249. protected JsObject ToJsObject(IGlobal global, RavenJObject doc)
  250. {
  251. var jsObject = global.ObjectClass.New();
  252. foreach (var prop in doc)
  253. {
  254. var jsValue = ToJsInstance(global, prop.Value);
  255. var value = prop.Value as RavenJValue;
  256. if (value != null)
  257. propertiesByValue[jsValue] = new KeyValuePair<RavenJValue, object>(value, jsValue.Value);
  258. jsObject.DefineOwnProperty(prop.Key, jsValue);
  259. }
  260. return jsObject;
  261. }
  262. private JsInstance ToJsInstance(IGlobal global, RavenJToken value)
  263. {
  264. switch (value.Type)
  265. {
  266. case JTokenType.Array:
  267. return ToJsArray(global, (RavenJArray)value);
  268. case JTokenType.Object:
  269. return ToJsObject(global, (RavenJObject)value);
  270. case JTokenType.Null:
  271. return JsNull.Instance;
  272. case JTokenType.Boolean:
  273. var boolVal = ((RavenJValue)value);
  274. return global.BooleanClass.New((bool)boolVal.Value);
  275. case JTokenType.Float:
  276. var fltVal = ((RavenJValue)value);
  277. if (fltVal.Value is float)
  278. return new JsNumber((float)fltVal.Value, global.NumberClass);
  279. if (fltVal.Value is decimal)
  280. return global.NumberClass.New((double)(decimal)fltVal.Value);
  281. return global.NumberClass.New((double)fltVal.Value);
  282. case JTokenType.Integer:
  283. var intVal = ((RavenJValue)value);
  284. if (intVal.Value is int)
  285. {
  286. return global.NumberClass.New((int)intVal.Value);
  287. }
  288. return global.NumberClass.New((long)intVal.Value);
  289. case JTokenType.Date:
  290. var dtVal = ((RavenJValue)value);
  291. return global.DateClass.New((DateTime)dtVal.Value);
  292. case JTokenType.String:
  293. var strVal = ((RavenJValue)value);
  294. return global.StringClass.New((string)strVal.Value);
  295. case JTokenType.Bytes:
  296. var byteValue = (RavenJValue)value;
  297. var base64 = Convert.ToBase64String((byte[])byteValue.Value);
  298. return global.StringClass.New("raven-data:byte[];base64," + base64);
  299. default:
  300. throw new NotSupportedException(value.Type.ToString());
  301. }
  302. }
  303. private JsArray ToJsArray(IGlobal global, RavenJArray array)
  304. {
  305. var jsArr = global.ArrayClass.New();
  306. for (int i = 0; i < array.Length; i++)
  307. {
  308. jsArr.put(i, ToJsInstance(global, array[i]));
  309. }
  310. return jsArr;
  311. }
  312. private JintEngine CreateEngine(ScriptedPatchRequest patch)
  313. {
  314. var scriptWithProperLines = NormalizeLineEnding(patch.Script);
  315. var wrapperScript = String.Format(@"
  316. function ExecutePatchScript(docInner){{
  317. (function(doc){{
  318. {0}
  319. }}).apply(docInner);
  320. }};
  321. ", scriptWithProperLines);
  322. var jintEngine = new JintEngine()
  323. .AllowClr(false)
  324. #if DEBUG
  325. .SetDebugMode(true)
  326. #else
  327. .SetDebugMode(false)
  328. #endif
  329. .SetMaxRecursions(50)
  330. .SetMaxSteps(maxSteps);
  331. AddScript(jintEngine, "Raven.Database.Json.lodash.js");
  332. AddScript(jintEngine, "Raven.Database.Json.ToJson.js");
  333. AddScript(jintEngine, "Raven.Database.Json.RavenDB.js");
  334. jintEngine.SetFunction("LoadDocument", ((Func<string, object>)(value =>
  335. {
  336. var loadedDoc = loadDocumentStatic(value);
  337. if (loadedDoc == null)
  338. return null;
  339. loadedDoc[Constants.DocumentIdFieldName] = value;
  340. return ToJsObject(jintEngine.Global, loadedDoc);
  341. })));
  342. jintEngine.Run(wrapperScript);
  343. return jintEngine;
  344. }
  345. private static readonly string[] EtagKeyNames = new[]
  346. {
  347. "etag",
  348. "@etag",
  349. "Etag",
  350. "ETag",
  351. };
  352. private void PutDocument(string key, JsObject doc, JsObject meta)
  353. {
  354. if (doc == null)
  355. {
  356. throw new InvalidOperationException(
  357. string.Format("Created document cannot be null or empty. Document key: '{0}'", key));
  358. }
  359. var newDocument = new JsonDocument
  360. {
  361. Key = key,
  362. DataAsJson = ToRavenJObject(doc)
  363. };
  364. if (meta == null)
  365. {
  366. RavenJToken value;
  367. if (newDocument.DataAsJson.TryGetValue("@metadata", out value))
  368. {
  369. newDocument.DataAsJson.Remove("@metadata");
  370. newDocument.Metadata = (RavenJObject) value;
  371. }
  372. }
  373. else
  374. {
  375. foreach (var etagKeyName in EtagKeyNames)
  376. {
  377. JsInstance result;
  378. if (!meta.TryGetProperty(etagKeyName, out result))
  379. continue;
  380. string etag = result.ToString();
  381. meta.Delete(etagKeyName);
  382. if (string.IsNullOrEmpty(etag))
  383. continue;
  384. Etag newDocumentEtag;
  385. if (Etag.TryParse(etag, out newDocumentEtag) == false)
  386. {
  387. throw new InvalidOperationException(string.Format("Invalid ETag value '{0}' for document '{1}'",
  388. etag, key));
  389. }
  390. newDocument.Etag = newDocumentEtag;
  391. }
  392. newDocument.Metadata = ToRavenJObject(meta);
  393. }
  394. ValidateDocument(newDocument);
  395. CreatedDocs.Add(newDocument);
  396. }
  397. protected virtual void ValidateDocument(JsonDocument newDocument)
  398. {
  399. }
  400. private static string NormalizeLineEnding(string script)
  401. {
  402. var sb = new StringBuilder();
  403. using (var reader = new StringReader(script))
  404. {
  405. while (true)
  406. {
  407. var line = reader.ReadLine();
  408. if (line == null)
  409. return sb.ToString();
  410. sb.AppendLine(line);
  411. }
  412. }
  413. }
  414. private static void AddScript(JintEngine jintEngine, string ravenDatabaseJsonMapJs)
  415. {
  416. jintEngine.Run(GetFromResources(ravenDatabaseJsonMapJs));
  417. }
  418. protected virtual void CustomizeEngine(JintEngine jintEngine)
  419. {
  420. }
  421. private void OutputLog(JintEngine engine)
  422. {
  423. var arr = engine.GetParameter("debug_outputs") as JsArray;
  424. if (arr == null)
  425. return;
  426. for (int i = 0; i < arr.Length; i++)
  427. {
  428. var o = arr.get(i);
  429. if (o == null)
  430. continue;
  431. Debug.Add(o.ToString());
  432. }
  433. engine.SetParameter("debug_outputs", engine.Global.ArrayClass.New());
  434. }
  435. private static string GetFromResources(string resourceName)
  436. {
  437. Assembly assem = typeof(ScriptedJsonPatcher).Assembly;
  438. using (Stream stream = assem.GetManifestResourceStream(resourceName))
  439. {
  440. using (var reader = new StreamReader(stream))
  441. {
  442. return reader.ReadToEnd();
  443. }
  444. }
  445. }
  446. }
  447. [Serializable]
  448. public class ParseException : Exception
  449. {
  450. public ParseException()
  451. {
  452. }
  453. public ParseException(string message) : base(message)
  454. {
  455. }
  456. public ParseException(string message, Exception inner) : base(message, inner)
  457. {
  458. }
  459. protected ParseException(
  460. SerializationInfo info,
  461. StreamingContext context) : base(info, context)
  462. {
  463. }
  464. }
  465. }