comparison 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
comparison
equal deleted inserted replaced
4622:9086aeb9d9d2 4623:95ffe3b6ef7c
59 * This semaphore is used to limit the number of concurrent HTTP 59 * This semaphore is used to limit the number of concurrent HTTP
60 * requests on CPU-intensive routes of the REST API, in order to 60 * requests on CPU-intensive routes of the REST API, in order to
61 * prevent exhaustion of resources (new in Orthanc 1.7.0). 61 * prevent exhaustion of resources (new in Orthanc 1.7.0).
62 **/ 62 **/
63 static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER? 63 static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER?
64
65
66 static const std::string CHECK_REVISIONS = "CheckRevisions";
64 67
65 68
66 namespace Orthanc 69 namespace Orthanc
67 { 70 {
68 static std::string GetDocumentationSampleResource(ResourceType type) 71 static std::string GetDocumentationSampleResource(ResourceType type)
1445 1448
1446 call.GetOutput().AnswerJson(result); 1449 call.GetOutput().AnswerJson(result);
1447 } 1450 }
1448 1451
1449 1452
1453 static bool GetRevisionHeader(int64_t& revision /* out */,
1454 const RestApiCall& call,
1455 const std::string& header)
1456 {
1457 std::string lower;
1458 Toolbox::ToLowerCase(lower, header);
1459
1460 HttpToolbox::Arguments::const_iterator found = call.GetHttpHeaders().find(lower);
1461 if (found == call.GetHttpHeaders().end())
1462 {
1463 return false;
1464 }
1465 else
1466 {
1467 std::string value = Toolbox::StripSpaces(found->second);
1468 Toolbox::RemoveSurroundingQuotes(value);
1469
1470 try
1471 {
1472 revision = boost::lexical_cast<int64_t>(value);
1473 return true;
1474 }
1475 catch (boost::bad_lexical_cast&)
1476 {
1477 throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
1478 "\" HTTP header should contain the revision as an integer, but found: " + value);
1479 }
1480 }
1481 }
1482
1483
1450 static void GetMetadata(RestApiGetCall& call) 1484 static void GetMetadata(RestApiGetCall& call)
1451 { 1485 {
1452 if (call.IsDocumentation()) 1486 if (call.IsDocumentation())
1453 { 1487 {
1454 ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); 1488 ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
1457 .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) 1491 .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
1458 .SetSummary("Get metadata") 1492 .SetSummary("Get metadata")
1459 .SetDescription("Get the value of a metadata that is associated with the given " + r) 1493 .SetDescription("Get the value of a metadata that is associated with the given " + r)
1460 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") 1494 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
1461 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") 1495 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
1462 .AddAnswerType(MimeType_PlainText, "Value of the metadata"); 1496 .AddAnswerType(MimeType_PlainText, "Value of the metadata")
1497 .SetAnswerHeader("ETag", "Revision of the metadata, to be used in further `PUT` or `DELETE` operations")
1498 .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed");
1463 return; 1499 return;
1464 } 1500 }
1465 1501
1466 assert(!call.GetFullUri().empty()); 1502 assert(!call.GetFullUri().empty());
1467 const std::string publicId = call.GetUriComponent("id", ""); 1503 const std::string publicId = call.GetUriComponent("id", "");
1469 1505
1470 std::string name = call.GetUriComponent("name", ""); 1506 std::string name = call.GetUriComponent("name", "");
1471 MetadataType metadata = StringToMetadata(name); 1507 MetadataType metadata = StringToMetadata(name);
1472 1508
1473 std::string value; 1509 std::string value;
1474 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, level, metadata)) 1510 int64_t revision;
1475 { 1511 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata))
1476 call.GetOutput().AnswerBuffer(value, MimeType_PlainText); 1512 {
1513 call.GetOutput().GetLowLevelOutput().
1514 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2
1515
1516 int64_t userRevision;
1517 if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
1518 revision == userRevision)
1519 {
1520 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
1521 }
1522 else
1523 {
1524 call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
1525 }
1477 } 1526 }
1478 } 1527 }
1479 1528
1480 1529
1481 static void DeleteMetadata(RestApiDeleteCall& call) 1530 static void DeleteMetadata(RestApiDeleteCall& call)
1488 .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) 1537 .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
1489 .SetSummary("Delete metadata") 1538 .SetSummary("Delete metadata")
1490 .SetDescription("Delete some metadata associated with the given DICOM " + r + 1539 .SetDescription("Delete some metadata associated with the given DICOM " + r +
1491 ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).") 1540 ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).")
1492 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") 1541 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
1493 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)"); 1542 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
1543 .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can "
1544 "be deleted. This option is mandatory if `CheckRevision` option is `true`.");
1494 return; 1545 return;
1495 } 1546 }
1496 1547
1497 CheckValidResourceType(call); 1548 CheckValidResourceType(call);
1498 1549 const std::string publicId = call.GetUriComponent("id", "");
1499 std::string publicId = call.GetUriComponent("id", ""); 1550
1500 std::string name = call.GetUriComponent("name", ""); 1551 std::string name = call.GetUriComponent("name", "");
1501 MetadataType metadata = StringToMetadata(name); 1552 MetadataType metadata = StringToMetadata(name);
1502 1553
1503 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata 1554 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1504 { 1555 {
1505 OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); 1556 bool found;
1506 call.GetOutput().AnswerBuffer("", MimeType_PlainText); 1557 int64_t revision;
1558 if (GetRevisionHeader(revision, call, "if-match"))
1559 {
1560 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision);
1561 }
1562 else
1563 {
1564 OrthancConfiguration::ReaderLock lock;
1565 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
1566 {
1567 throw OrthancException(ErrorCode_Revision,
1568 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
1569 }
1570 else
1571 {
1572 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */);
1573 }
1574 }
1575
1576 if (found)
1577 {
1578 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1579 }
1580 else
1581 {
1582 throw OrthancException(ErrorCode_UnknownResource);
1583 }
1507 } 1584 }
1508 else 1585 else
1509 { 1586 {
1510 call.GetOutput().SignalError(HttpStatus_403_Forbidden); 1587 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1511 } 1588 }
1523 .SetSummary("Set metadata") 1600 .SetSummary("Set metadata")
1524 .SetDescription("Set the value of some metadata in the given DICOM " + r + 1601 .SetDescription("Set the value of some metadata in the given DICOM " + r +
1525 ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).") 1602 ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).")
1526 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") 1603 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
1527 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") 1604 .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
1528 .AddRequestType(MimeType_PlainText, "String value of the metadata"); 1605 .AddRequestType(MimeType_PlainText, "String value of the metadata")
1606 .SetHttpHeader("If-Match", "Revision of the metadata, if this is not the first time this metadata is set.");
1529 return; 1607 return;
1530 } 1608 }
1531 1609
1532 CheckValidResourceType(call); 1610 CheckValidResourceType(call);
1533 1611
1538 std::string value; 1616 std::string value;
1539 call.BodyToString(value); 1617 call.BodyToString(value);
1540 1618
1541 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata 1619 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1542 { 1620 {
1543 // It is forbidden to modify internal metadata 1621 int64_t oldRevision;
1544 OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); 1622 bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
1623
1624 if (!hasOldRevision)
1625 {
1626 OrthancConfiguration::ReaderLock lock;
1627 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
1628 {
1629 // "StatelessDatabaseOperations::SetMetadata()" will ignore
1630 // the actual value of "oldRevision" if the metadata is
1631 // inexistent as expected
1632 hasOldRevision = true;
1633 oldRevision = -1; // dummy value
1634 }
1635 }
1636
1637 int64_t newRevision;
1638 OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision);
1639
1640 call.GetOutput().GetLowLevelOutput().
1641 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\""); // New in Orthanc 1.9.2
1642
1545 call.GetOutput().AnswerBuffer("", MimeType_PlainText); 1643 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1546 } 1644 }
1547 else 1645 else
1548 { 1646 {
1549 call.GetOutput().SignalError(HttpStatus_403_Forbidden); 1647 call.GetOutput().SignalError(HttpStatus_403_Forbidden);