/**
* 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-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 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 .
**/
#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
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 userMetadata;
Orthanc::GetRegisteredUserMetadata(userMetadata);
for (std::map::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 DEFAULT_RETRIEVE_METHOD = "DefaultRetrieveMethod";
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";
static const char* const CAPABILITIES = "Capabilities";
static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores";
static const char* const HAS_QUEUES = "HasQueues";
static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
static const char* const READ_ONLY = "ReadOnly";
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(DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DefaultRetrieveMethod configuration")
.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)")
.SetAnswerField(CAPABILITIES, RestApiCallDocumentation::Type_JsonObject,
"Whether the back-end supports optional features like 'HasExtendedChanges', 'HasExtendedFind' (new in Orthanc 1.12.5) ")
.SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean,
"Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
.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[DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DEFAULT_RETRIEVE_METHOD, "C-MOVE");
}
result[STORAGE_AREA_PLUGIN] = Json::nullValue;
result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
result[READ_ONLY] = context.IsReadOnly();
#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();
result[CAPABILITIES] = Json::objectValue;
result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport();
result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport();
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(diskSize);
result["TotalUncompressedSize"] = boost::lexical_cast(uncompressedSize);
result["TotalDiskSizeMB"] = static_cast(diskSize / MEGA_BYTES);
result["TotalUncompressedSizeMB"] = static_cast(uncompressedSize / MEGA_BYTES);
result["CountPatients"] = static_cast(countPatients);
result["CountStudies"] = static_cast(countStudies);
result["CountSeries"] = static_cast(countSeries);
result["CountInstances"] = static_cast(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
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 syntaxes;
OrthancRestApi::GetContext(call).GetAcceptedTransferSyntaxes(syntaxes);
Json::Value json = Json::arrayValue;
for (std::set::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 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 sopClasses;
OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
Json::Value json = Json::arrayValue;
for (std::set::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 plugins;
OrthancRestApi::GetContext(call).GetPlugins().GetManager().ListPlugins(plugins);
for (std::list::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 lst;
manager.ListPlugins(lst);
for (std::list::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 jobs;
OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs);
for (std::set::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
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(diskSize) / MEGA_BYTES);
registry.SetFloatValue("orthanc_uncompressed_size_mb", static_cast(diskSize) / MEGA_BYTES);
registry.SetIntegerValue("orthanc_count_patients", static_cast(countPatients));
registry.SetIntegerValue("orthanc_count_studies", static_cast(countStudies));
registry.SetIntegerValue("orthanc_count_series", static_cast(countSeries));
registry.SetIntegerValue("orthanc_count_instances", static_cast(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 labels;
OrthancRestApi::GetIndex(call).ListAllLabels(labels);
Json::Value json = Json::arrayValue;
for (std::set::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);
Register("/tools/now-local", GetNowIsoString);
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);
Register("/jobs/{id}/pause", ApplyJobAction);
Register("/jobs/{id}/resubmit", ApplyJobAction);
Register("/jobs/{id}/resume", ApplyJobAction);
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);
}
}