view OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents 7f4fab033c87
children ae6de7d857c5
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeadersServer.h"
#include "OrthancRestApi.h"

#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../../OrthancFramework/Sources/MetricsRegistry.h"
#include "../../Plugins/Engine/OrthancPlugins.h"
#include "../../Plugins/Engine/PluginsManager.h"
#include "../OrthancConfiguration.h"
#include "../OrthancInitialization.h"
#include "../ServerContext.h"

#include <boost/algorithm/string/predicate.hpp>


namespace Orthanc
{
  // System information -------------------------------------------------------

  static void ServeRoot(RestApiGetCall& call)
  {
    call.GetOutput().Redirect("app/explorer.html");
  }
 
  static void ServeFavicon(RestApiGetCall& call)
  {
    call.GetOutput().Redirect("app/images/favicon.ico");
  }

  static void GetMainDicomTagsConfiguration(Json::Value& result)
  {
      result["Patient"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient);
      result["Study"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Study);
      result["Series"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
      result["Instance"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
  }

  static void GetUserMetadataConfiguration(Json::Value& result)
  {
    std::map<std::string, int> userMetadata;
    Orthanc::GetRegisteredUserMetadata(userMetadata);

    for (std::map<std::string, int>::const_iterator it = userMetadata.begin(); it != userMetadata.end(); ++it)
    {
      result[it->first] = it->second;
    }
  }

  static void GetSystemInformation(RestApiGetCall& call)
  {
    static const char* const API_VERSION = "ApiVersion";
    static const char* const CHECK_REVISIONS = "CheckRevisions";
    static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin";
    static const char* const DATABASE_VERSION = "DatabaseVersion";
    static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
    static const char* const DICOM_AET = "DicomAet";
    static const char* const DICOM_PORT = "DicomPort";
    static const char* const HTTP_PORT = "HttpPort";
    static const char* const IS_HTTP_SERVER_SECURE = "IsHttpServerSecure";
    static const char* const NAME = "Name";
    static const char* const PLUGINS_ENABLED = "PluginsEnabled";
    static const char* const STORAGE_AREA_PLUGIN = "StorageAreaPlugin";
    static const char* const VERSION = "Version";
    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
    static const char* const STORAGE_COMPRESSION = "StorageCompression";
    static const char* const OVERWRITE_INSTANCES = "OverwriteInstances";
    static const char* const INGEST_TRANSCODING = "IngestTranscoding";
    static const char* const MAXIMUM_STORAGE_SIZE = "MaximumStorageSize";
    static const char* const MAXIMUM_PATIENT_COUNT = "MaximumPatientCount";
    static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode";
    static const char* const USER_METADATA = "UserMetadata";
    static const char* const HAS_LABELS = "HasLabels";

    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get system information")
        .SetDescription("Get system information about Orthanc")
        .SetAnswerField(API_VERSION, RestApiCallDocumentation::Type_Number, "Version of the REST API")
        .SetAnswerField(VERSION, RestApiCallDocumentation::Type_String, "Version of Orthanc")
        .SetAnswerField(DATABASE_VERSION, RestApiCallDocumentation::Type_Number,
                        "Version of the database: https://orthanc.uclouvain.be/book/developers/db-versioning.html")
        .SetAnswerField(DATABASE_SERVER_IDENTIFIER, RestApiCallDocumentation::Type_String,
                        "ID of the server in the database (when running multiple Orthanc on the same DB)")
        .SetAnswerField(IS_HTTP_SERVER_SECURE, RestApiCallDocumentation::Type_Boolean,
                        "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://orthanc.uclouvain.be/book/faq/security.html#securing-the-http-server")
        .SetAnswerField(STORAGE_AREA_PLUGIN, RestApiCallDocumentation::Type_String,
                        "Information about the installed storage area plugin (`null` if no such plugin is installed)")
        .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String,
                        "Information about the installed database index plugin (`null` if no such plugin is installed)")
        .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
        .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
        .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
        .SetAnswerField(NAME, RestApiCallDocumentation::Type_String,
                        "The name of the Orthanc server, cf. the `Name` configuration option")
        .SetAnswerField(PLUGINS_ENABLED, RestApiCallDocumentation::Type_Boolean,
                        "Whether Orthanc was built with support for plugins")
        .SetAnswerField(CHECK_REVISIONS, RestApiCallDocumentation::Type_Boolean,
                        "Whether Orthanc handle revisions of metadata and attachments to deal with multiple writers (new in Orthanc 1.9.2)")
        .SetAnswerField(MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_JsonObject,
                        "The list of MainDicomTags saved in DB for each resource level (new in Orthanc 1.11.0)")
        .SetAnswerField(STORAGE_COMPRESSION, RestApiCallDocumentation::Type_Boolean,
                        "Whether storage compression is enabled (new in Orthanc 1.11.0)")
        .SetAnswerField(OVERWRITE_INSTANCES, RestApiCallDocumentation::Type_Boolean,
                        "Whether instances are overwritten when re-ingested (new in Orthanc 1.11.0)")
        .SetAnswerField(INGEST_TRANSCODING, RestApiCallDocumentation::Type_String,
                        "Whether instances are transcoded when ingested into Orthanc (`""` if no transcoding is performed) (new in Orthanc 1.11.0)")
        .SetAnswerField(MAXIMUM_STORAGE_SIZE, RestApiCallDocumentation::Type_Number,
                        "The configured MaximumStorageSize in MB (new in Orthanc 1.11.3)")
        .SetAnswerField(MAXIMUM_PATIENT_COUNT, RestApiCallDocumentation::Type_Number,
                        "The configured MaximumPatientCount (new in Orthanc 1.12.4)")
        .SetAnswerField(MAXIMUM_STORAGE_MODE, RestApiCallDocumentation::Type_String,
                        "The configured MaximumStorageMode (new in Orthanc 1.11.3)")
        .SetAnswerField(USER_METADATA, RestApiCallDocumentation::Type_JsonObject,
                        "The configured UserMetadata (new in Orthanc 1.12.0)")
        .SetAnswerField(HAS_LABELS, RestApiCallDocumentation::Type_Boolean,
                        "Whether the database back-end supports labels (new in Orthanc 1.12.0)")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
      return;
    }

    ServerContext& context = OrthancRestApi::GetContext(call);

    Json::Value result = Json::objectValue;

    result[API_VERSION] = ORTHANC_API_VERSION;
    result[VERSION] = ORTHANC_VERSION;
    result[DATABASE_VERSION] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
    result[IS_HTTP_SERVER_SECURE] = context.IsHttpServerSecure();  // New in Orthanc 1.5.8

    {
      OrthancConfiguration::ReaderLock lock;
      result[DICOM_AET] = lock.GetConfiguration().GetOrthancAET();
      result[DICOM_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(DICOM_PORT, 4242);
      result[HTTP_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(HTTP_PORT, 8042);
      result[NAME] = lock.GetConfiguration().GetStringParameter(NAME, "");
      result[CHECK_REVISIONS] = lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false);  // New in Orthanc 1.9.2
      result[STORAGE_COMPRESSION] = lock.GetConfiguration().GetBooleanParameter(STORAGE_COMPRESSION, false); // New in Orthanc 1.11.0
      result[OVERWRITE_INSTANCES] = lock.GetConfiguration().GetBooleanParameter(OVERWRITE_INSTANCES, false); // New in Orthanc 1.11.0
      result[INGEST_TRANSCODING] = lock.GetConfiguration().GetStringParameter(INGEST_TRANSCODING, ""); // New in Orthanc 1.11.0
      result[DATABASE_SERVER_IDENTIFIER] = lock.GetConfiguration().GetDatabaseServerIdentifier();
      result[MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_STORAGE_SIZE, 0); // New in Orthanc 1.11.3
      result[MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_PATIENT_COUNT, 0); // New in Orthanc 1.12.4
      result[MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetStringParameter(MAXIMUM_STORAGE_MODE, "Recycle"); // New in Orthanc 1.11.3
    }

    result[STORAGE_AREA_PLUGIN] = Json::nullValue;
    result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;

#if ORTHANC_ENABLE_PLUGINS == 1
    result[PLUGINS_ENABLED] = true;
    const OrthancPlugins& plugins = context.GetPlugins();

    if (plugins.HasStorageArea())
    {
      std::string p = plugins.GetStorageAreaLibrary().GetPath();
      result[STORAGE_AREA_PLUGIN] = boost::filesystem::canonical(p).string();
    }

    if (plugins.HasDatabaseBackend())
    {
      std::string p = plugins.GetDatabaseBackendLibrary().GetPath();
      result[DATABASE_BACKEND_PLUGIN] = boost::filesystem::canonical(p).string();     
    }
#else
    result[PLUGINS_ENABLED] = false;
#endif

    result[MAIN_DICOM_TAGS] = Json::objectValue;
    GetMainDicomTagsConfiguration(result[MAIN_DICOM_TAGS]);

    result[USER_METADATA] = Json::objectValue;
    GetUserMetadataConfiguration(result[USER_METADATA]);

    result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport();
    
    call.GetOutput().AnswerJson(result);
  }

  static void GetStatistics(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get database statistics")
        .SetDescription("Get statistics related to the database of Orthanc")
        .SetAnswerField("CountInstances", RestApiCallDocumentation::Type_Number, "Number of DICOM instances stored in Orthanc")
        .SetAnswerField("CountSeries", RestApiCallDocumentation::Type_Number, "Number of DICOM series stored in Orthanc")
        .SetAnswerField("CountStudies", RestApiCallDocumentation::Type_Number, "Number of DICOM studies stored in Orthanc")
        .SetAnswerField("CountPatients", RestApiCallDocumentation::Type_Number, "Number of patients stored in Orthanc")
        .SetAnswerField("TotalDiskSize", RestApiCallDocumentation::Type_String, "Size of the storage area (in bytes)")
        .SetAnswerField("TotalDiskSizeMB", RestApiCallDocumentation::Type_Number, "Size of the storage area (in megabytes)")
        .SetAnswerField("TotalUncompressedSize", RestApiCallDocumentation::Type_String, "Total size of all the files once uncompressed (in bytes). This corresponds to `TotalDiskSize` if no compression is enabled, cf. `StorageCompression` configuration option")
        .SetAnswerField("TotalUncompressedSizeMB", RestApiCallDocumentation::Type_Number, "Total size of all the files once uncompressed (in megabytes)")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/statistics", true);
      return;
    }

    static const uint64_t MEGA_BYTES = 1024 * 1024;

    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
    OrthancRestApi::GetIndex(call).GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                                       countStudies, countSeries, countInstances);
    
    Json::Value result = Json::objectValue;
    result["TotalDiskSize"] = boost::lexical_cast<std::string>(diskSize);
    result["TotalUncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
    result["TotalDiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
    result["TotalUncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
    result["CountPatients"] = static_cast<unsigned int>(countPatients);
    result["CountStudies"] = static_cast<unsigned int>(countStudies);
    result["CountSeries"] = static_cast<unsigned int>(countSeries);
    result["CountInstances"] = static_cast<unsigned int>(countInstances);

    call.GetOutput().AnswerJson(result);
  }

  static void GenerateUid(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Generate an identifier")
        .SetDescription("Generate a random DICOM identifier")
        .SetHttpGetArgument("level", RestApiCallDocumentation::Type_String,
                            "Type of DICOM resource among: `patient`, `study`, `series` or `instance`", true)
        .AddAnswerType(MimeType_PlainText, "The generated identifier");
      return;
    }

    std::string level = call.GetArgument("level", "");
    if (level == "patient")
    {
      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), MimeType_PlainText);
    }
    else if (level == "study")
    {
      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), MimeType_PlainText);
    }
    else if (level == "series")
    {
      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), MimeType_PlainText);
    }
    else if (level == "instance")
    {
      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText);
    }
    else
    {
      LOG(ERROR) << "No 'level' or invalid GET argument specified in /tools/generate-uid";
    }
  }

  static void ExecuteScript(RestApiPostCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Execute Lua script")
        .SetDescription("Execute the provided Lua script by the Orthanc server. This is very insecure for "
                        "Orthanc servers that are remotely accessible.  Since Orthanc 1.5.8, this route "
                        "is disabled by default and can be enabled thanks to the `ExecuteLuaEnabled` configuration.")
        .AddRequestType(MimeType_PlainText, "The Lua script to be executed")
        .AddAnswerType(MimeType_PlainText, "Output of the Lua script");
      return;
    }

    ServerContext& context = OrthancRestApi::GetContext(call);

    if (!context.IsExecuteLuaEnabled())
    {
      LOG(ERROR) << "The URI /tools/execute-script is disallowed for security, "
                 << "check your configuration option `ExecuteLuaEnabled`";
      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
      return;
    }

    std::string result;
    std::string command;
    call.BodyToString(command);

    {
      LuaScripting::Lock lock(context.GetLuaScripting());
      lock.GetLua().Execute(result, command);
    }

    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
  }

  template <bool UTC>
  static void GetNowIsoString(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      std::string type(UTC ? "UTC" : "local");
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get " + type + " time")
        .AddAnswerType(MimeType_PlainText, "The " + type + " time")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo" + call.FlattenUri(), false);
      return;
    }

    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText);
  }


  static void GetDicomConformanceStatement(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get DICOM conformance")
        .SetDescription("Get the DICOM conformance statement of Orthanc")
        .AddAnswerType(MimeType_PlainText, "The DICOM conformance statement");
      return;
    }

    std::string statement;
    GetFileResource(statement, ServerResources::DICOM_CONFORMANCE_STATEMENT);
    call.GetOutput().AnswerBuffer(statement, MimeType_PlainText);
  }


  static void GetDefaultEncoding(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get default encoding")
        .SetDescription("Get the default encoding that is used by Orthanc if parsing "
                        "a DICOM instance without the `SpecificCharacterEncoding` tag, or during C-FIND. "
                        "This corresponds to the configuration option `DefaultEncoding`.")
        .AddAnswerType(MimeType_PlainText, "The name of the encoding");
      return;
    }

    Encoding encoding = GetDefaultDicomEncoding();
    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
  }


  static void SetDefaultEncoding(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Set default encoding")
        .SetDescription("Change the default encoding that is used by Orthanc if parsing "
                        "a DICOM instance without the `SpecificCharacterEncoding` tag, or during C-FIND. "
                        "This corresponds to the configuration option `DefaultEncoding`.")
        .AddRequestType(MimeType_PlainText, "The name of the encoding. Check out configuration "
                        "option `DefaultEncoding` for the allowed values.");
      return;
    }

    std::string body;
    call.BodyToString(body);

    Encoding encoding = StringToEncoding(body.c_str());

    {
      OrthancConfiguration::WriterLock lock;
      lock.GetConfiguration().SetDefaultEncoding(encoding);
    }

    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
  }


  static void AnswerAcceptedTransferSyntaxes(RestApiCall& call)
  {
    std::set<DicomTransferSyntax> syntaxes;
    OrthancRestApi::GetContext(call).GetAcceptedTransferSyntaxes(syntaxes);
    
    Json::Value json = Json::arrayValue;
    for (std::set<DicomTransferSyntax>::const_iterator
           syntax = syntaxes.begin(); syntax != syntaxes.end(); ++syntax)
    {
      json.append(GetTransferSyntaxUid(*syntax));
    }
    
    call.GetOutput().AnswerJson(json);
  }
  

  static void GetAcceptedTransferSyntaxes(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get accepted transfer syntaxes")
        .SetDescription("Get the list of UIDs of the DICOM transfer syntaxes that are accepted "
                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
                        "`AcceptedTransferSyntaxes` and `XXXTransferSyntaxAccepted`.")
        .AddAnswerType(MimeType_Json, "JSON array containing the transfer syntax UIDs");
      return;
    }

    AnswerAcceptedTransferSyntaxes(call);
  }


  static void SetAcceptedTransferSyntaxes(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Set accepted transfer syntaxes")
        .SetDescription("Set the DICOM transfer syntaxes that accepted by Orthanc C-STORE SCP")
        .AddRequestType(MimeType_PlainText, "UID of the transfer syntax to be accepted. "
                        "Wildcards `?` and `*` are accepted.")
        .AddRequestType(MimeType_Json, "JSON array containing a list of transfer syntax "
                        "UIDs to be accepted. Wildcards `?` and `*` are accepted.")
        .AddAnswerType(MimeType_Json, "JSON array containing the now-accepted transfer syntax UIDs");
      return;
    }

    std::set<DicomTransferSyntax> syntaxes;

    Json::Value json;
    if (call.ParseJsonRequest(json))
    {
      OrthancConfiguration::ParseAcceptedTransferSyntaxes(syntaxes, json);
    }
    else
    {
      std::string body;
      call.BodyToString(body);
      OrthancConfiguration::ParseAcceptedTransferSyntaxes(syntaxes, body);
    }

    OrthancRestApi::GetContext(call).SetAcceptedTransferSyntaxes(syntaxes);
    
    AnswerAcceptedTransferSyntaxes(call);
  }

  static void GetAcceptedSopClasses(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get accepted SOPClassUID")
        .SetDescription("Get the list of SOP Class UIDs that are accepted "
                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
                        "`AcceptedSopClasses` and `RejectedSopClasses`.")
        .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs");
      return;
    }

    std::set<std::string> sopClasses;
    OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
    
    Json::Value json = Json::arrayValue;
    for (std::set<std::string>::const_iterator
           sop = sopClasses.begin(); sop != sopClasses.end(); ++sop)
    {
      json.append(*sop);
    }
    
    call.GetOutput().AnswerJson(json);
  }


  static void GetUnknownSopClassAccepted(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Is unknown SOP class accepted?")
        .SetDescription("Shall Orthanc C-STORE SCP accept DICOM instances with an unknown SOP class UID?")
        .AddAnswerType(MimeType_PlainText, "`1` if accepted, `0` if not accepted");
      return;
    }

    const bool accepted = OrthancRestApi::GetContext(call).IsUnknownSopClassAccepted();
    call.GetOutput().AnswerBuffer(accepted ? "1" : "0", MimeType_PlainText);
  }


  static void SetUnknownSopClassAccepted(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Set unknown SOP class accepted")
        .SetDescription("Set whether Orthanc C-STORE SCP should accept DICOM instances with an unknown SOP class UID")
        .AddRequestType(MimeType_PlainText, "`1` if accepted, `0` if not accepted");
      return;
    }

    OrthancRestApi::GetContext(call).SetUnknownSopClassAccepted(call.ParseBooleanBody());
    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
  }


  
  // Plugins information ------------------------------------------------------

  static void ListPlugins(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("List plugins")
        .SetDescription("List all the installed plugins")
        .AddAnswerType(MimeType_Json, "JSON array containing the identifiers of the installed plugins")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/plugins", true);
      return;
    }

    Json::Value v = Json::arrayValue;

    v.append("explorer.js");

    if (OrthancRestApi::GetContext(call).HasPlugins())
    {
#if ORTHANC_ENABLE_PLUGINS == 1
      std::list<std::string> plugins;
      OrthancRestApi::GetContext(call).GetPlugins().GetManager().ListPlugins(plugins);

      for (std::list<std::string>::const_iterator 
             it = plugins.begin(); it != plugins.end(); ++it)
      {
        v.append(*it);
      }
#endif
    }

    call.GetOutput().AnswerJson(v);
  }


  static void GetPlugin(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get plugin")
        .SetDescription("Get system information about the plugin whose identifier is provided in the URL")
        .SetUriArgument("id", "Identifier of the job of interest")
        .AddAnswerType(MimeType_Json, "JSON object containing information about the plugin")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/plugins/dicom-web", true);
      return;
    }

    if (!OrthancRestApi::GetContext(call).HasPlugins())
    {
      return;
    }

#if ORTHANC_ENABLE_PLUGINS == 1
    const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPlugins().GetManager();
    std::string id = call.GetUriComponent("id", "");

    if (manager.HasPlugin(id))
    {
      Json::Value v = Json::objectValue;
      v["ID"] = id;
      v["Version"] = manager.GetPluginVersion(id);

      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
      const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri);
      if (c != NULL)
      {
        std::string root = c;
        if (!root.empty())
        {
          // Turn the root URI into a URI relative to "/app/explorer.js"
          if (root[0] == '/')
          {
            root = ".." + root;
          }

          v["RootUri"] = root;
        }
      }

      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description);
      if (c != NULL)
      {
        v["Description"] = c;
      }

      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer);
      v["ExtendsOrthancExplorer"] = (c != NULL);

      call.GetOutput().AnswerJson(v);
    }
#endif
  }


  static void GetOrthancExplorerPlugins(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("JavaScript extensions to Orthanc Explorer")
        .SetDescription("Get the JavaScript extensions that are installed by all the plugins using the "
                        "`OrthancPluginExtendOrthancExplorer()` function of the plugin SDK. "
                        "This route is for internal use of Orthanc Explorer.")
        .AddAnswerType(MimeType_JavaScript, "The JavaScript extensions");
      return;
    }

    std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n";

    if (OrthancRestApi::GetContext(call).HasPlugins())
    {
#if ORTHANC_ENABLE_PLUGINS == 1
      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
      const PluginsManager& manager = plugins.GetManager();

      std::list<std::string> lst;
      manager.ListPlugins(lst);

      for (std::list<std::string>::const_iterator
             it = lst.begin(); it != lst.end(); ++it)
      {
        const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer);
        if (tmp != NULL)
        {
          s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n";
          s += std::string(tmp) + "\n\n";
        }
      }
#endif
    }

    call.GetOutput().AnswerBuffer(s, MimeType_JavaScript);
  }




  // Jobs information ------------------------------------------------------

  static void ListJobs(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary("List jobs")
        .SetDescription("List all the available jobs")
        .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
                            "If present, retrieve detailed information about the individual jobs", false)
        .AddAnswerType(MimeType_Json, "JSON array containing either the jobs identifiers, or detailed information "
                       "about the reported jobs (if `expand` argument is provided)")
        .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/jobs", 3);
      return;
    }

    bool expand = call.HasArgument("expand") && call.GetBooleanArgument("expand", true);

    Json::Value v = Json::arrayValue;

    std::set<std::string> jobs;
    OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs);

    for (std::set<std::string>::const_iterator it = jobs.begin();
         it != jobs.end(); ++it)
    {
      if (expand)
      {
        JobInfo info;
        if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, *it))
        {
          Json::Value tmp;
          info.Format(tmp);
          v.append(tmp);
        }
      }
      else
      {
        v.append(*it);
      }
    }
    
    call.GetOutput().AnswerJson(v);
  }

  static void GetJobInfo(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      Json::Value sample;
      sample["CompletionTime"] = "20201227T161842.520129";
      sample["Content"]["ArchiveSizeMB"] = 22;
      sample["Content"]["Description"] = "REST API";
      sample["Content"]["InstancesCount"] = 232;
      sample["Content"]["UncompressedSizeMB"] = 64;
      sample["CreationTime"] = "20201227T161836.428311";
      sample["EffectiveRuntime"] = 6.0810000000000004;
      sample["ErrorCode"] = 0;
      sample["ErrorDescription"] = "Success";
      sample["ID"] = "645ecb02-7c0e-4465-b767-df873222dcfb";
      sample["Priority"] = 0;
      sample["Progress"] = 100;
      sample["State"] = "Success";
      sample["Timestamp"] = "20201228T160340.253201";
      sample["Type"] = "Media";
      
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary("Get job")
        .SetDescription("Retrieve detailed information about the job whose identifier is provided in the URL: "
                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
        .SetUriArgument("id", "Identifier of the job of interest")
        .AddAnswerType(MimeType_Json, "JSON object detailing the job")
        .SetSample(sample);
      return;
    }

    std::string id = call.GetUriComponent("id", "");

    JobInfo info;
    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id))
    {
      Json::Value json;
      info.Format(json);
      call.GetOutput().AnswerJson(json);
    }
  }

  static void DeleteJobInfo(RestApiDeleteCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary("Delete a job from history")
        .SetDescription("Delete the job from the jobs history.  Only a completed job can be deleted. "
                        "If the job has not run or not completed yet, you must cancel it first. "
                        "If the job has outputs, all outputs will be deleted as well. ")
        .SetUriArgument("id", "Identifier of the job of interest");
      return;
    }

    std::string job = call.GetUriComponent("id", "");

    if (OrthancRestApi::GetContext(call).GetJobsEngine().
        GetRegistry().DeleteJobInfo(job))
    {
      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
    }
    else
    {
      throw OrthancException(ErrorCode_InexistentItem,
                             "No job found with this id: " + job);
    }
  }


  static void GetJobOutput(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary("Get job output")
        .SetDescription("Retrieve some output produced by a job. As of Orthanc 1.8.2, only the jobs that generate a "
                        "DICOMDIR media or a ZIP archive provide such an output (with `key` equals to `archive`).")
        .SetUriArgument("id", "Identifier of the job of interest")
        .SetUriArgument("key", "Name of the output of interest")
        .AddAnswerType(MimeType_Binary, "Content of the output of the job");
      return;
    }

    std::string job = call.GetUriComponent("id", "");
    std::string key = call.GetUriComponent("key", "");

    std::string value;
    MimeType mime;
    std::string filename;
    
    if (OrthancRestApi::GetContext(call).GetJobsEngine().
        GetRegistry().GetJobOutput(value, mime, filename, job, key))
    {
      if (!filename.empty())
      {
        call.GetOutput().SetContentFilename(filename.c_str());
      }

      call.GetOutput().AnswerBuffer(value, mime);
    }
    else
    {
      throw OrthancException(ErrorCode_InexistentItem,
                             "Job has no such output: " + key);
    }
  }


  static void DeleteJobOutput(RestApiDeleteCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary("Delete a job output")
        .SetDescription("Delete the output produced by a job. As of Orthanc 1.12.1, only the jobs that generate a "
                        "DICOMDIR media or a ZIP archive provide such an output (with `key` equals to `archive`).")
        .SetUriArgument("id", "Identifier of the job of interest")
        .SetUriArgument("key", "Name of the output of interest");
      return;
    }

    std::string job = call.GetUriComponent("id", "");
    std::string key = call.GetUriComponent("key", "");

    if (OrthancRestApi::GetContext(call).GetJobsEngine().
        GetRegistry().DeleteJobOutput(job, key))
    {
      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
    }
    else
    {
      throw OrthancException(ErrorCode_InexistentItem,
                             "Job has no such output: " + key);
    }
  }


  enum JobAction
  {
    JobAction_Cancel,
    JobAction_Pause,
    JobAction_Resubmit,
    JobAction_Resume
  };

  template <JobAction action>
  static void ApplyJobAction(RestApiPostCall& call)
  {
    if (call.IsDocumentation())
    {
      std::string verb;
      switch (action)
      {
        case JobAction_Cancel:
          verb = "Cancel";
          break;

        case JobAction_Pause:
          verb = "Pause";
          break;
 
        case JobAction_Resubmit:
          verb = "Resubmit";
          break;

        case JobAction_Resume:
          verb = "Resume";
          break;

        default:
          throw OrthancException(ErrorCode_InternalError);
      }      
      
      call.GetDocumentation()
        .SetTag("Jobs")
        .SetSummary(verb + " job")
        .SetDescription(verb + " the job whose identifier is provided in the URL. Check out the "
                        "Orthanc Book for more information about the state machine applicable to jobs: "
                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
        .SetUriArgument("id", "Identifier of the job of interest")
        .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success");
      return;
    }

    std::string id = call.GetUriComponent("id", "");

    bool ok = false;

    switch (action)
    {
      case JobAction_Cancel:
        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Cancel(id);
        break;

      case JobAction_Pause:
        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id);
        break;
 
      case JobAction_Resubmit:
        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resubmit(id);
        break;

      case JobAction_Resume:
        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resume(id);
        break;

      default:
        throw OrthancException(ErrorCode_InternalError);
    }
    
    if (ok)
    {
      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
    }
  }

  
  static void GetMetricsPrometheus(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get usage metrics")
        .SetDescription("Get usage metrics of Orthanc in the Prometheus file format (OpenMetrics): "
                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#instrumentation-with-prometheus")
        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/tools/metrics-prometheus", false);
      return;
    }

#if ORTHANC_ENABLE_PLUGINS == 1
    OrthancRestApi::GetContext(call).GetPlugins().RefreshMetrics();
#endif

    static const float MEGA_BYTES = 1024 * 1024;

    ServerContext& context = OrthancRestApi::GetContext(call);

    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                           countStudies, countSeries, countInstances);

    unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);

    int64_t serverUpTime = context.GetServerUpTime();
    Json::Value lastChange;
    context.GetIndex().GetLastChange(lastChange);

    MetricsRegistry& registry = context.GetMetricsRegistry();
    registry.SetFloatValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
    registry.SetFloatValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
    registry.SetIntegerValue("orthanc_count_patients", static_cast<int64_t>(countPatients));
    registry.SetIntegerValue("orthanc_count_studies", static_cast<int64_t>(countStudies));
    registry.SetIntegerValue("orthanc_count_series", static_cast<int64_t>(countSeries));
    registry.SetIntegerValue("orthanc_count_instances", static_cast<int64_t>(countInstances));
    registry.SetIntegerValue("orthanc_jobs_pending", jobsPending);
    registry.SetIntegerValue("orthanc_jobs_running", jobsRunning);
    registry.SetIntegerValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
    registry.SetIntegerValue("orthanc_jobs_success", jobsSuccess);
    registry.SetIntegerValue("orthanc_jobs_failed", jobsFailed);
    registry.SetIntegerValue("orthanc_up_time_s", serverUpTime);
    registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64());

    context.PublishCacheMetrics();

    std::string s;
    registry.ExportPrometheusText(s);

    call.GetOutput().AnswerBuffer(s, MimeType_PrometheusText);
  }


  static void GetMetricsEnabled(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Are metrics collected?")
        .SetDescription("Returns a Boolean specifying whether Prometheus metrics "
                        "are collected and exposed at `/tools/metrics-prometheus`")
        .AddAnswerType(MimeType_PlainText, "`1` if metrics are collected, `0` if metrics are disabled");
      return;
    }

    bool enabled = OrthancRestApi::GetContext(call).GetMetricsRegistry().IsEnabled();
    call.GetOutput().AnswerBuffer(enabled ? "1" : "0", MimeType_PlainText);
  }


  static void PutMetricsEnabled(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Enable collection of metrics")
        .SetDescription("Enable or disable the collection and publication of metrics at `/tools/metrics-prometheus`")
        .AddRequestType(MimeType_PlainText, "`1` if metrics are collected, `0` if metrics are disabled");
      return;
    }

    const bool enabled = call.ParseBooleanBody();

    // Success
    OrthancRestApi::GetContext(call).GetMetricsRegistry().SetEnabled(enabled);
    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
  }


  static void GetLogLevel(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Logs")
        .SetSummary("Get main log level")
        .SetDescription("Get the main log level of Orthanc")
        .AddAnswerType(MimeType_PlainText, "Possible values: `default`, `verbose` or `trace`");
      return;
    }

    const std::string s = EnumerationToString(GetGlobalVerbosity());
    call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
  }


  static void PutLogLevel(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("Logs")
        .SetSummary("Set main log level")
        .SetDescription("Set the main log level of Orthanc")
        .AddRequestType(MimeType_PlainText, "Possible values: `default`, `verbose` or `trace`");
      return;
    }

    std::string body;
    call.BodyToString(body);

    SetGlobalVerbosity(StringToVerbosity(body));
    
    // Success
    LOG(WARNING) << "REST API call has switched the log level to: " << body;
    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
  }


  static Logging::LogCategory GetCategory(const RestApiCall& call)
  {
    static const std::string PREFIX = "log-level-";

    if (call.GetFullUri().size() == 2 &&
        call.GetFullUri() [0] == "tools" &&
        boost::starts_with(call.GetFullUri() [1], PREFIX))
    {
      Logging::LogCategory category;
      if (Logging::LookupCategory(category, call.GetFullUri() [1].substr(PREFIX.size())))
      {
        return category;
      }
    }

    throw OrthancException(ErrorCode_InternalError);
  }
  

  static void GetLogLevelCategory(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      std::string category = Logging::GetCategoryName(GetCategory(call));
      call.GetDocumentation()
        .SetTag("Logs")
        .SetSummary("Get log level for `" + category + "`")
        .SetDescription("Get the log level of the log category `" + category + "`")
        .AddAnswerType(MimeType_PlainText, "Possible values: `default`, `verbose` or `trace`");
      return;
    }

    const std::string s = EnumerationToString(GetCategoryVerbosity(GetCategory(call)));
    call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
  }


  static void PutLogLevelCategory(RestApiPutCall& call)
  {
    if (call.IsDocumentation())
    {
      std::string category = Logging::GetCategoryName(GetCategory(call));
      call.GetDocumentation()
        .SetTag("Logs")
        .SetSummary("Set log level for `" + category + "`")
        .SetDescription("Set the log level of the log category `" + category + "`")
        .AddRequestType(MimeType_PlainText, "Possible values: `default`, `verbose` or `trace`");
      return;
    }

    std::string body;
    call.BodyToString(body);

    Verbosity verbosity = StringToVerbosity(body);
    Logging::LogCategory category = GetCategory(call);
    SetCategoryVerbosity(category, verbosity);
    
    // Success
    LOG(WARNING) << "REST API call has switched the log level of category \""
                 << Logging::GetCategoryName(category) << "\" to \""
                 << EnumerationToString(verbosity) << "\"";
    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
  }


  static void ListAllLabels(RestApiGetCall& call)
  {
    if (call.IsDocumentation())
    {
      call.GetDocumentation()
        .SetTag("System")
        .SetSummary("Get all the used labels")
        .SetDescription("List all the labels that are associated with any resource of the Orthanc database")
        .AddAnswerType(MimeType_Json, "JSON array containing the labels");
      return;
    }

    std::set<std::string> labels;
    OrthancRestApi::GetIndex(call).ListAllLabels(labels);

    Json::Value json = Json::arrayValue;
    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
    {
      json.append(*it);
    }
    
    call.GetOutput().AnswerJson(json);
   }


  void OrthancRestApi::RegisterSystem(bool orthancExplorerEnabled)
  {
    if (orthancExplorerEnabled)
    {
      Register("/", ServeRoot);
      Register("/favicon.ico", ServeFavicon);  // New in Orthanc 1.9.0
    }
    
    Register("/system", GetSystemInformation);
    Register("/statistics", GetStatistics);
    Register("/tools/generate-uid", GenerateUid);
    Register("/tools/execute-script", ExecuteScript);
    Register("/tools/now", GetNowIsoString<true>);
    Register("/tools/now-local", GetNowIsoString<false>);
    Register("/tools/dicom-conformance", GetDicomConformanceStatement);
    Register("/tools/default-encoding", GetDefaultEncoding);
    Register("/tools/default-encoding", SetDefaultEncoding);
    Register("/tools/metrics", GetMetricsEnabled);
    Register("/tools/metrics", PutMetricsEnabled);
    Register("/tools/metrics-prometheus", GetMetricsPrometheus);
    Register("/tools/log-level", GetLogLevel);
    Register("/tools/log-level", PutLogLevel);

    for (size_t i = 0; i < Logging::GetCategoriesCount(); i++)
    {
      const std::string name(Logging::GetCategoryName(i));
      Register("/tools/log-level-" + name, GetLogLevelCategory);
      Register("/tools/log-level-" + name, PutLogLevelCategory);
    }

    Register("/plugins", ListPlugins);
    Register("/plugins/{id}", GetPlugin);
    Register("/plugins/explorer.js", GetOrthancExplorerPlugins);

    Register("/jobs", ListJobs);
    Register("/jobs/{id}", GetJobInfo);
    Register("/jobs/{id}", DeleteJobInfo);
    Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>);
    Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
    Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
    Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>);
    Register("/jobs/{id}/{key}", GetJobOutput);
    Register("/jobs/{id}/{key}", DeleteJobOutput);

    // New in Orthanc 1.9.0
    Register("/tools/accepted-transfer-syntaxes", GetAcceptedTransferSyntaxes);
    Register("/tools/accepted-transfer-syntaxes", SetAcceptedTransferSyntaxes);
    Register("/tools/unknown-sop-class-accepted", GetUnknownSopClassAccepted);
    Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);

    Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0

    Register("/tools/accepted-sop-classes", GetAcceptedSopClasses);

  }
}