From b36776efb3d7953fff6682d2ca6eeb9077af14cc Mon Sep 17 00:00:00 2001 From: Jan Steemann Date: Mon, 20 Aug 2012 19:16:45 +0200 Subject: [PATCH] issue #110: add PATCH method for documents --- Doxygen/Examples.ArangoDB/rest-patch-document | 67 +++++++++++++ arangod/RestHandler/RestDocumentHandler.cpp | 89 +++++++++++++++--- arangod/RestHandler/RestDocumentHandler.h | 2 +- lib/BasicsC/json-utilities.c | 75 +++++++++++++++ lib/BasicsC/json-utilities.h | 11 ++- lib/BasicsC/json.c | 94 ++++++++++++++++++- lib/BasicsC/json.h | 18 +++- lib/HttpServer/HttpCommTask.h | 1 + 8 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 Doxygen/Examples.ArangoDB/rest-patch-document diff --git a/Doxygen/Examples.ArangoDB/rest-patch-document b/Doxygen/Examples.ArangoDB/rest-patch-document new file mode 100644 index 0000000000..db980c4dab --- /dev/null +++ b/Doxygen/Examples.ArangoDB/rest-patch-document @@ -0,0 +1,67 @@ +> curl --data @- -X PATCH --dump - http://localhost:8529/_api/document/73482/7229681 +{ "hello": "world" } + +HTTP/1.1 200 OK +content-type: application/json; charset=utf-8 + +{ + "_rev": 11916057, + "_id": "73482/7229681", + "error": false +} + +> curl --data @- -X PATCH --dump - http://localhost:8529/_api/document/73482/7229681 +{ "numbers": { "one": 1, "two": 2, "three": 3, "empty": null } } + +HTTP/1.1 200 OK +content-type: application/json; charset=utf-8 + +{ + "_rev": 12309273, + "_id": "73482/7229681", + "error": false +} + +> curl -X GET --dump - http://localhost:8529/_api/document/73482/7229681 +{ "numbers": { "one": 1, "two": 2, "three": 3, "empty": null } } + +HTTP/1.1 200 OK +content-type: application/json; charset=utf-8 + +{ + "hello": "world", + "numbers": { + "empty": null, + "one": 1, + "two": 2, + "three": 3 + }, + "_rev": 12309273, + "_id": "73482/7229681" +} + +> curl --data @- -X PATCH --dump - http://localhost:8529/_api/document/73482/7229681?keepNull=false +{ "hello": null, "numbers": { "four": 4 } } + +{ + "_rev": 12571417, + "_id": "73482/7229681", + "error": false +} + +> curl -X GET --dump - http://localhost:8529/_api/document/73482/7229681 + +HTTP/1.1 200 OK +content-type: application/json; charset=utf-8 + +{ + "numbers": { + "empty": null, + "one": 1, + "two": 2, + "three": 3, + "four": 4, + }, + "_rev": 12571417, + "_id": "73482/7229681" +} diff --git a/arangod/RestHandler/RestDocumentHandler.cpp b/arangod/RestHandler/RestDocumentHandler.cpp index 8763cde6c3..ac08156c2d 100644 --- a/arangod/RestHandler/RestDocumentHandler.cpp +++ b/arangod/RestHandler/RestDocumentHandler.cpp @@ -29,6 +29,8 @@ #include "Basics/StringUtils.h" #include "BasicsC/conversions.h" +#include "BasicsC/json.h" +#include "BasicsC/json-utilities.h" #include "BasicsC/string-buffer.h" #include "Rest/HttpRequest.h" #include "Rest/JsonContainer.h" @@ -129,8 +131,8 @@ HttpHandler::status_e RestDocumentHandler::execute () { case HttpRequest::HTTP_REQUEST_GET: res = readDocument(); break; case HttpRequest::HTTP_REQUEST_HEAD: res = checkDocument(); break; case HttpRequest::HTTP_REQUEST_POST: res = createDocument(); break; - case HttpRequest::HTTP_REQUEST_PUT: res = updateDocument(); break; - case HttpRequest::HTTP_REQUEST_PATCH: res = updateDocument(); break; + case HttpRequest::HTTP_REQUEST_PUT: res = updateDocument(false); break; + case HttpRequest::HTTP_REQUEST_PATCH: res = updateDocument(true); break; case HttpRequest::HTTP_REQUEST_ILLEGAL: res = false; @@ -619,15 +621,14 @@ bool RestDocumentHandler::checkDocument () { /// /// @REST{PUT /_api/document/@FA{document-handle}} /// -/// Updates the document identified by @FA{document-handle}. If the document exists -/// and can be updated, then a @LIT{HTTP 201} is returned and the "ETag" header -/// field contains the new revision of the document. +/// Completely updates (i.e. replaces) the document identified by @FA{document-handle}. +/// If the document exists and can be updated, then a @LIT{HTTP 201} is returned +/// and the "ETag" header field contains the new revision of the document. /// -/// Note the updated document passed in the body of the request normally also -/// contains the @FA{document-handle} in the attribute @LIT{_id} and the -/// revision in @LIT{_rev}. These attributes, however, are ignored. Only the URI -/// and the "ETag" header are relevant in order to avoid confusion when using -/// proxies. +/// If the new document passed in the body of the request contains the +/// @FA{document-handle} in the attribute @LIT{_id} and the revision in @LIT{_rev}, +/// these attributes will be ignored. Only the URI and the "ETag" header are +/// relevant in order to avoid confusion when using proxies. /// /// The body of the response contains a JSON object with the information about /// the handle and the revision. The attribute @LIT{_id} contains the known @@ -674,9 +675,39 @@ bool RestDocumentHandler::checkDocument () { /// Alternative to header field: /// /// @verbinclude rest-update-document-rev-other +/// +/// @RESTHEADER{PATCH /_api/document,patches a document} +/// +/// @REST{PATCH /_api/document/@FA{document-handle}} +/// +/// Partially updates the document identified by @FA{document-handle}. +/// The body of the request must contain a JSON document with the attributes +/// to patch (the patch document). All attributes from the patch document will +/// be added to the existing document if they do not yet exist, and overwritten +/// in the existing document if they do exist there. +/// +/// Setting an attribute value to @LIT{null} in the patch document will cause a +/// value of @LIT{null} be saved for the attribute by default. If the intention +/// is to delete existing attributes with the patch command, the URL parameter +/// @LIT{keepNull} can be used with a value of @LIT{false}. +/// This will modify the behavior of the patch command to remove any attributes +/// from the existing document that are contained in the patch document with an +/// attribute value of @LIT{null}. +/// +/// The body of the response contains a JSON object with the information about +/// the handle and the revision. The attribute @LIT{_id} contains the known +/// @FA{document-handle} of the updated document, the attribute @LIT{_rev} +/// contains the new document revision. +/// +/// If the document does not exist, then a @LIT{HTTP 404} is returned and the +/// body of the response contains an error document. +/// +/// @EXAMPLES +/// +/// @verbinclude rest-patch-document //////////////////////////////////////////////////////////////////////////////// -bool RestDocumentHandler::updateDocument () { +bool RestDocumentHandler::updateDocument (bool isPatch) { vector const& suffix = _request->suffix(); if (suffix.size() != 2) { @@ -724,7 +755,41 @@ bool RestDocumentHandler::updateDocument () { TRI_voc_rid_t rid = 0; TRI_voc_cid_t cid = _documentCollection->base._cid; - TRI_doc_mptr_t const mptr = _documentCollection->updateJson(_documentCollection, json, did, revision, &rid, policy, true); + // init target document + TRI_doc_mptr_t mptr; + + if (isPatch) { + bool nullMeansRemove; + bool found; + + char const* valueStr = _request->value("keepNull", found); + if (!found || StringUtils::boolean(valueStr)) { + // keep null values + nullMeansRemove = false; + } + else { + // delete null attributes + nullMeansRemove = true; + } + + // read the existing document + TRI_doc_mptr_t document = _documentCollection->read(_documentCollection, did); + TRI_json_t* old = TRI_JsonShapedJson(_documentCollection->_shaper, &document._document); + + if (old != 0) { + TRI_json_t* patchedJson = TRI_MergeJson(TRI_UNKNOWN_MEM_ZONE, old, json, nullMeansRemove); + TRI_FreeJson(_documentCollection->_shaper->_memoryZone, old); + + if (patchedJson != 0) { + mptr = _documentCollection->updateJson(_documentCollection, patchedJson, did, revision, &rid, policy, true); + + TRI_FreeJson(TRI_UNKNOWN_MEM_ZONE, patchedJson); + } + } + } + else { + mptr = _documentCollection->updateJson(_documentCollection, json, did, revision, &rid, policy, true); + } res = mptr._did == 0 ? TRI_errno() : 0; // ............................................................................. diff --git a/arangod/RestHandler/RestDocumentHandler.h b/arangod/RestHandler/RestDocumentHandler.h index 05761d3f4d..2752318df0 100644 --- a/arangod/RestHandler/RestDocumentHandler.h +++ b/arangod/RestHandler/RestDocumentHandler.h @@ -145,7 +145,7 @@ namespace triagens { /// @brief updates a document //////////////////////////////////////////////////////////////////////////////// - virtual bool updateDocument (); + virtual bool updateDocument (bool); //////////////////////////////////////////////////////////////////////////////// /// @brief deletes a document diff --git a/lib/BasicsC/json-utilities.c b/lib/BasicsC/json-utilities.c index 92960b702b..3522fb4b67 100644 --- a/lib/BasicsC/json-utilities.c +++ b/lib/BasicsC/json-utilities.c @@ -37,6 +37,63 @@ /// @{ //////////////////////////////////////////////////////////////////////////////// +static TRI_json_t* MergeRecursive (TRI_memory_zone_t* zone, + const TRI_json_t* const lhs, + const TRI_json_t* const rhs, + const bool nullMeansRemove) { + size_t i, n; + + TRI_json_t* result = TRI_CopyJson(zone, lhs); + + if (result == NULL) { + return NULL; + } + + n = rhs->_value._objects._length; + for (i = 0; i < n; i += 2) { + // enumerate all the replacement values + TRI_json_t* key = TRI_AtVector(&rhs->_value._objects, i); + TRI_json_t* value = TRI_AtVector(&rhs->_value._objects, i + 1); + + if (value->_type == TRI_JSON_NULL && nullMeansRemove) { + // replacement value is a null and we don't want to store nulls => delete attribute from the result + TRI_DeleteArrayJson(zone, result, key->_value._string.data); + } + else { + // replacement value is not a null or we want to store nulls + TRI_json_t* lhsValue = TRI_LookupArrayJson(lhs, key->_value._string.data); + + if (lhsValue == NULL) { + // existing array does not have the attribute => append new attribute + if (value->_type == TRI_JSON_ARRAY) { + TRI_json_t* empty = TRI_CreateArrayJson(zone); + TRI_json_t* merged = MergeRecursive(zone, empty, value, nullMeansRemove); + TRI_Insert3ArrayJson(zone, result, key->_value._string.data, merged); + + TRI_FreeJson(zone, empty); + } + else { + TRI_Insert3ArrayJson(zone, result, key->_value._string.data, TRI_CopyJson(zone, value)); + } + } + else { + // existing array already has the attribute => replace attribute + if (lhsValue->_type == TRI_JSON_ARRAY && value->_type == TRI_JSON_ARRAY) { + TRI_json_t* merged = MergeRecursive(zone, lhsValue, value, nullMeansRemove); + TRI_ReplaceArrayJson(zone, result, key->_value._string.data, merged); + TRI_FreeJson(zone, merged); + } + else { + TRI_ReplaceArrayJson(zone, result, key->_value._string.data, value); + } + } + } + + } + + return result; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief fruitsort initialisation parameters /// @@ -663,6 +720,24 @@ bool TRI_HasDuplicateKeyJson (const TRI_json_t* const object) { return false; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief merge two JSON documents into one +//////////////////////////////////////////////////////////////////////////////// + +TRI_json_t* TRI_MergeJson (TRI_memory_zone_t* zone, + const TRI_json_t* const lhs, + const TRI_json_t* const rhs, + const bool nullMeansRemove) { + TRI_json_t* result; + + assert(lhs->_type == TRI_JSON_ARRAY); + assert(rhs->_type == TRI_JSON_ARRAY); + + result = MergeRecursive(zone, lhs, rhs, nullMeansRemove); + + return result; +} + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// diff --git a/lib/BasicsC/json-utilities.h b/lib/BasicsC/json-utilities.h index dfa5d1ac4f..b75a1d55ab 100644 --- a/lib/BasicsC/json-utilities.h +++ b/lib/BasicsC/json-utilities.h @@ -120,7 +120,7 @@ TRI_json_t* TRI_IntersectListsJson (const TRI_json_t* const, /// @brief sorts a json list in place //////////////////////////////////////////////////////////////////////////////// -TRI_json_t* TRI_SortListJson (TRI_json_t* const list); +TRI_json_t* TRI_SortListJson (TRI_json_t* const); //////////////////////////////////////////////////////////////////////////////// /// @brief checks if a JSON struct has duplicate attribute names @@ -128,6 +128,15 @@ TRI_json_t* TRI_SortListJson (TRI_json_t* const list); bool TRI_HasDuplicateKeyJson (const TRI_json_t* const); +//////////////////////////////////////////////////////////////////////////////// +/// @brief merge two JSON documents into one +//////////////////////////////////////////////////////////////////////////////// + +TRI_json_t* TRI_MergeJson (TRI_memory_zone_t*, + const TRI_json_t* const, + const TRI_json_t* const, + const bool); + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// diff --git a/lib/BasicsC/json.c b/lib/BasicsC/json.c index 673a1eae95..5ff7fd88cd 100755 --- a/lib/BasicsC/json.c +++ b/lib/BasicsC/json.c @@ -31,7 +31,6 @@ #include "BasicsC/logging.h" #include "BasicsC/string-buffer.h" #include "BasicsC/strings.h" -#include "BasicsC/strings.h" // ----------------------------------------------------------------------------- // --SECTION-- JSON @@ -651,7 +650,7 @@ void TRI_Insert4ArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char* na /// @brief looks up an attribute in an json array //////////////////////////////////////////////////////////////////////////////// -TRI_json_t* TRI_LookupArrayJson (TRI_json_t* object, char const* name) { +TRI_json_t* TRI_LookupArrayJson (const TRI_json_t* const object, char const* name) { size_t n; size_t i; @@ -677,6 +676,95 @@ TRI_json_t* TRI_LookupArrayJson (TRI_json_t* object, char const* name) { return NULL; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief deletes an element from a json array +//////////////////////////////////////////////////////////////////////////////// + +bool TRI_DeleteArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char const* name) { + size_t n; + size_t i; + + assert(object->_type == TRI_JSON_ARRAY); + assert(name); + + n = object->_value._objects._length; + + for (i = 0; i < n; i += 2) { + TRI_json_t* key; + + key = TRI_AtVector(&object->_value._objects, i); + + if (key->_type != TRI_JSON_STRING) { + continue; + } + + if (TRI_EqualString(key->_value._string.data, name)) { + TRI_json_t* old; + + // remove key + old = TRI_AtVector(&object->_value._objects, i); + if (old != NULL) { + TRI_DestroyJson(zone, old); + } + TRI_RemoveVector(&object->_value._objects, i); + + // remove value + old = TRI_AtVector(&object->_value._objects, i); + if (old != NULL) { + TRI_DestroyJson(zone, old); + } + TRI_RemoveVector(&object->_value._objects, i); + + return true; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief replaces an element in a json array +//////////////////////////////////////////////////////////////////////////////// + +bool TRI_ReplaceArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char const* name, TRI_json_t* replacement) { + size_t n; + size_t i; + + assert(object->_type == TRI_JSON_ARRAY); + assert(name); + + n = object->_value._objects._length; + + for (i = 0; i < n; i += 2) { + TRI_json_t* key; + + key = TRI_AtVector(&object->_value._objects, i); + + if (key->_type != TRI_JSON_STRING) { + continue; + } + + if (TRI_EqualString(key->_value._string.data, name)) { + TRI_json_t copy; + + // retrieve the old element + TRI_json_t* old = TRI_AtVector(&object->_value._objects, i + 1); + if (old != NULL) { + TRI_DestroyJson(zone, old); + } + + TRI_CopyToJson(zone, ©, replacement); + TRI_SetVector(&object->_value._objects, i + 1, ©); + return true; + } + } + + // object not found in array, now simply add it + TRI_Insert2ArrayJson(zone, object, name, replacement); + + return false; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief stringifies a json object //////////////////////////////////////////////////////////////////////////////// @@ -865,7 +953,7 @@ int TRI_CopyToJson (TRI_memory_zone_t* zone, /// @brief copies a json object //////////////////////////////////////////////////////////////////////////////// -TRI_json_t* TRI_CopyJson (TRI_memory_zone_t* zone, TRI_json_t* src) { +TRI_json_t* TRI_CopyJson (TRI_memory_zone_t* zone, const TRI_json_t* const src) { TRI_json_t* dst; int res; diff --git a/lib/BasicsC/json.h b/lib/BasicsC/json.h index 07bfa253ea..c74c385a40 100755 --- a/lib/BasicsC/json.h +++ b/lib/BasicsC/json.h @@ -249,10 +249,22 @@ void TRI_Insert3ArrayJson (TRI_memory_zone_t*, TRI_json_t* object, char const* n void TRI_Insert4ArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char* name, size_t nameLength, TRI_json_t* subobject); //////////////////////////////////////////////////////////////////////////////// -/// @brief looks up an attribute in an json array +/// @brief looks up an attribute in a json array //////////////////////////////////////////////////////////////////////////////// -TRI_json_t* TRI_LookupArrayJson (TRI_json_t* object, char const* name); +TRI_json_t* TRI_LookupArrayJson (const TRI_json_t* const object, char const* name); + +//////////////////////////////////////////////////////////////////////////////// +/// @brief deletes an element from a json array +//////////////////////////////////////////////////////////////////////////////// + +bool TRI_DeleteArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char const* name); + +//////////////////////////////////////////////////////////////////////////////// +/// @brief replaces an element in a json array +//////////////////////////////////////////////////////////////////////////////// + +bool TRI_ReplaceArrayJson (TRI_memory_zone_t* zone, TRI_json_t* object, char const* name, TRI_json_t* replacement); //////////////////////////////////////////////////////////////////////////////// /// @brief stringifies a json object @@ -288,7 +300,7 @@ int TRI_CopyToJson (TRI_memory_zone_t*, TRI_json_t* dst, TRI_json_t const* src); /// @brief copies a json object //////////////////////////////////////////////////////////////////////////////// -TRI_json_t* TRI_CopyJson (TRI_memory_zone_t*, TRI_json_t*); +TRI_json_t* TRI_CopyJson (TRI_memory_zone_t*, const TRI_json_t* const); //////////////////////////////////////////////////////////////////////////////// /// @brief parses a json string diff --git a/lib/HttpServer/HttpCommTask.h b/lib/HttpServer/HttpCommTask.h index d244d75add..67b611c67a 100644 --- a/lib/HttpServer/HttpCommTask.h +++ b/lib/HttpServer/HttpCommTask.h @@ -198,6 +198,7 @@ namespace triagens { case HttpRequest::HTTP_REQUEST_POST: case HttpRequest::HTTP_REQUEST_PUT: + case HttpRequest::HTTP_REQUEST_PATCH: this->_bodyLength = this->_request->contentLength(); if (this->_bodyLength > 0) {