diff OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp @ 4623:95ffe3b6ef7c db-changes

handling of revisions for metadata
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 16 Apr 2021 17:13:03 +0200
parents bec74e29f86b
children f7d5372b59b3
line wrap: on
line diff
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 16 10:48:57 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 16 17:13:03 2021 +0200
@@ -63,6 +63,9 @@
 static Orthanc::Semaphore throttlingSemaphore_(4);  // TODO => PARAMETER?
 
 
+static const std::string CHECK_REVISIONS = "CheckRevisions";
+
+
 namespace Orthanc
 {
   static std::string GetDocumentationSampleResource(ResourceType type)
@@ -1447,6 +1450,37 @@
   }
 
 
+  static bool GetRevisionHeader(int64_t& revision /* out */,
+                                const RestApiCall& call,
+                                const std::string& header)
+  {
+    std::string lower;
+    Toolbox::ToLowerCase(lower, header);
+    
+    HttpToolbox::Arguments::const_iterator found = call.GetHttpHeaders().find(lower);
+    if (found == call.GetHttpHeaders().end())
+    {
+      return false;
+    }
+    else
+    {
+      std::string value = Toolbox::StripSpaces(found->second);
+      Toolbox::RemoveSurroundingQuotes(value);
+
+      try
+      {
+        revision = boost::lexical_cast<int64_t>(value);
+        return true;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
+                               "\" HTTP header should contain the revision as an integer, but found: " + value);
+      }
+    }
+  }
+
+
   static void GetMetadata(RestApiGetCall& call)
   {
     if (call.IsDocumentation())
@@ -1459,7 +1493,9 @@
         .SetDescription("Get the value of a metadata that is associated with the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
-        .AddAnswerType(MimeType_PlainText, "Value of the metadata");
+        .AddAnswerType(MimeType_PlainText, "Value of the metadata")
+        .SetAnswerHeader("ETag", "Revision of the metadata, to be used in further `PUT` or `DELETE` operations")
+        .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed");
       return;
     }
 
@@ -1471,9 +1507,22 @@
     MetadataType metadata = StringToMetadata(name);
 
     std::string value;
-    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, level, metadata))
+    int64_t revision;
+    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata))
     {
-      call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+
+      int64_t userRevision;
+      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
+          revision == userRevision)
+      {
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+      }
+      else
+      {
+        call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
+      }
     }
   }
 
@@ -1490,20 +1539,48 @@
         .SetDescription("Delete some metadata associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)");
+        .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
+        .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can "
+                       "be deleted. This option is mandatory if `CheckRevision` option is `true`.");
       return;
     }
 
     CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
+    const std::string publicId = call.GetUriComponent("id", "");
+
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
 
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
-    {      
-      OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    {
+      bool found;
+      int64_t revision;
+      if (GetRevisionHeader(revision, call, "if-match"))
+      {
+        found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision);
+      }
+      else
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          throw OrthancException(ErrorCode_Revision,
+                                 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
+        }
+        else
+        {
+          found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */);
+        }
+      }
+
+      if (found)
+      {
+        call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
     else
     {
@@ -1525,7 +1602,8 @@
                         ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
-        .AddRequestType(MimeType_PlainText, "String value of the metadata");
+        .AddRequestType(MimeType_PlainText, "String value of the metadata")
+        .SetHttpHeader("If-Match", "Revision of the metadata, if this is not the first time this metadata is set.");
       return;
     }
 
@@ -1540,8 +1618,28 @@
 
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
     {
-      // It is forbidden to modify internal metadata
-      OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
+      int64_t oldRevision;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+
+      if (!hasOldRevision)
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          // "StatelessDatabaseOperations::SetMetadata()" will ignore
+          // the actual value of "oldRevision" if the metadata is
+          // inexistent as expected
+          hasOldRevision = true;
+          oldRevision = -1;  // dummy value
+        }
+      }
+
+      int64_t newRevision;
+      OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision);
+
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\"");  // New in Orthanc 1.9.2
+      
       call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else