/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2021 Osimis S.A., Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero 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
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
#include "DicomWebClient.h"
#include "DicomWebServers.h"
#include "QidoRs.h"
#include "StowRs.h"
#include "WadoRs.h"
#include "WadoUri.h"
#include <Logging.h>
#include <SystemToolbox.h>
#include <Toolbox.h>
#include <EmbeddedResources.h>
#include <boost/algorithm/string/predicate.hpp>
static const char* const HAS_DELETE = "HasDelete";
bool RequestHasKey(const OrthancPluginHttpRequest* request, const char* key)
{
for (uint32_t i = 0; i < request->getCount; i++)
{
if (strcmp(key, request->getKeys[i]) == 0)
return true;
}
return false;
}
void ListServers(OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Get)
{
OrthancPluginSendMethodNotAllowed(context, output, "GET");
}
else
{
std::list<std::string> servers;
OrthancPlugins::DicomWebServers::GetInstance().ListServers(servers);
if (RequestHasKey(request, "expand"))
{
Json::Value result = Json::objectValue;
for (std::list<std::string>::const_iterator it = servers.begin(); it != servers.end(); ++it)
{
Orthanc::WebServiceParameters server = OrthancPlugins::DicomWebServers::GetInstance().GetServer(*it);
Json::Value jsonServer;
// only return the minimum information to identify the destination, do not include "security" information like passwords
server.FormatPublic(jsonServer);
result[*it] = jsonServer;
}
std::string answer = result.toStyledString();
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
}
else // if expand is not present, keep backward compatibility and return an array of server names
{
Json::Value json = Json::arrayValue;
for (std::list<std::string>::const_iterator it = servers.begin(); it != servers.end(); ++it)
{
json.append(*it);
}
std::string answer = json.toStyledString();
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
}
}
}
void ListServerOperations(OrthancPluginRestOutput* output,
const char* /*url*/,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
switch (request->method)
{
case OrthancPluginHttpMethod_Get:
{
// Make sure the server does exist
const Orthanc::WebServiceParameters& server =
OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0]);
Json::Value json = Json::arrayValue;
json.append("get");
json.append("retrieve");
json.append("stow");
json.append("wado");
json.append("qido");
if (server.GetBooleanUserProperty(HAS_DELETE, false))
{
json.append("delete");
}
std::string answer = json.toStyledString();
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
break;
}
case OrthancPluginHttpMethod_Delete:
{
OrthancPlugins::DicomWebServers::GetInstance().DeleteServer(request->groups[0]);
OrthancPlugins::Configuration::SaveDicomWebServers();
std::string answer = "{}";
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
break;
}
case OrthancPluginHttpMethod_Put:
{
Json::Value body;
OrthancPlugins::ParseJsonBody(body, request);
Orthanc::WebServiceParameters parameters(body);
OrthancPlugins::DicomWebServers::GetInstance().SetServer(request->groups[0], parameters);
OrthancPlugins::Configuration::SaveDicomWebServers();
std::string answer = "{}";
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
break;
}
default:
OrthancPluginSendMethodNotAllowed(context, output, "GET,PUT,DELETE");
break;
}
}
void GetClientInformation(OrthancPluginRestOutput* output,
const char* /*url*/,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Get)
{
OrthancPluginSendMethodNotAllowed(context, output, "GET");
}
else
{
Json::Value info = Json::objectValue;
info["DicomWebRoot"] = OrthancPlugins::Configuration::GetDicomWebRoot();
info["OrthancApiRoot"] = OrthancPlugins::Configuration::GetOrthancApiRoot();
std::string answer = info.toStyledString();
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
}
}
void QidoClient(OrthancPluginRestOutput* output,
const char* /*url*/,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Post)
{
OrthancPluginSendMethodNotAllowed(context, output, "POST");
}
else
{
Json::Value answer;
GetFromServer(answer, request);
if (answer.type() != Json::arrayValue)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
}
Json::Value result = Json::arrayValue;
for (Json::Value::ArrayIndex i = 0; i < answer.size(); i++)
{
if (answer[i].type() != Json::objectValue)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
}
Json::Value::Members tags = answer[i].getMemberNames();
Json::Value item = Json::objectValue;
for (size_t j = 0; j < tags.size(); j++)
{
Orthanc::DicomTag tag(0, 0);
if (Orthanc::DicomTag::ParseHexadecimal(tag, tags[j].c_str()))
{
Json::Value value = Json::objectValue;
value["Group"] = tag.GetGroup();
value["Element"] = tag.GetElement();
#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
OrthancPlugins::OrthancString name;
name.Assign(OrthancPluginGetTagName(context, tag.GetGroup(), tag.GetElement(), NULL));
if (name.GetContent() != NULL)
{
value["Name"] = std::string(name.GetContent());
}
#endif
const Json::Value& source = answer[i][tags[j]];
if (source.type() != Json::objectValue ||
!source.isMember("vr") ||
source["vr"].type() != Json::stringValue)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
}
value["vr"] = source["vr"].asString();
if (source.isMember("Value") &&
source["Value"].type() == Json::arrayValue &&
source["Value"].size() >= 1)
{
const Json::Value& content = source["Value"][0];
switch (content.type())
{
case Json::stringValue:
value["Value"] = content.asString();
break;
case Json::objectValue:
if (content.isMember("Alphabetic") &&
content["Alphabetic"].type() == Json::stringValue)
{
value["Value"] = content["Alphabetic"].asString();
}
break;
default:
break;
}
}
item[tags[j]] = value;
}
}
result.append(item);
}
std::string tmp = result.toStyledString();
OrthancPluginAnswerBuffer(context, output, tmp.c_str(), tmp.size(), "application/json");
}
}
void DeleteClient(OrthancPluginRestOutput* output,
const char* /*url*/,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Post)
{
OrthancPluginSendMethodNotAllowed(context, output, "POST");
}
else
{
static const char* const LEVEL = "Level";
static const char* const SERIES_INSTANCE_UID = "SeriesInstanceUID";
static const char* const STUDY_INSTANCE_UID = "StudyInstanceUID";
static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
const std::string serverName = request->groups[0];
const Orthanc::WebServiceParameters& server =
OrthancPlugins::DicomWebServers::GetInstance().GetServer(serverName);
if (!server.GetBooleanUserProperty(HAS_DELETE, false))
{
throw Orthanc::OrthancException(
Orthanc::ErrorCode_BadFileFormat,
"Cannot delete on DICOMweb server, check out property \"" + std::string(HAS_DELETE) + "\": " + serverName);
}
Json::Value body;
OrthancPlugins::ParseJsonBody(body, request);
if (body.type() != Json::objectValue ||
!body.isMember(LEVEL) ||
!body.isMember(STUDY_INSTANCE_UID) ||
body[LEVEL].type() != Json::stringValue ||
body[STUDY_INSTANCE_UID].type() != Json::stringValue)
{
throw Orthanc::OrthancException(
Orthanc::ErrorCode_BadFileFormat,
"The request body must contain a JSON object with fields \"Level\" and \"StudyInstanceUID\"");
}
Orthanc::ResourceType level = Orthanc::StringToResourceType(body[LEVEL].asCString());
const std::string study = body[STUDY_INSTANCE_UID].asString();
std::string series;
if (level == Orthanc::ResourceType_Series ||
level == Orthanc::ResourceType_Instance)
{
if (!body.isMember(SERIES_INSTANCE_UID) ||
body[SERIES_INSTANCE_UID].type() != Json::stringValue)
{
throw Orthanc::OrthancException(
Orthanc::ErrorCode_BadFileFormat,
"The request body must contain the field \"SeriesInstanceUID\"");
}
else
{
series = body[SERIES_INSTANCE_UID].asString();
}
}
std::string instance;
if (level == Orthanc::ResourceType_Instance)
{
if (!body.isMember(SOP_INSTANCE_UID) ||
body[SOP_INSTANCE_UID].type() != Json::stringValue)
{
throw Orthanc::OrthancException(
Orthanc::ErrorCode_BadFileFormat,
"The request body must contain the field \"SOPInstanceUID\"");
}
else
{
instance = body[SOP_INSTANCE_UID].asString();
}
}
std::string uri;
switch (level)
{
case Orthanc::ResourceType_Study:
uri = "/studies/" + study;
break;
case Orthanc::ResourceType_Series:
uri = "/studies/" + study + "/series/" + series;
break;
case Orthanc::ResourceType_Instance:
uri = "/studies/" + study + "/series/" + series + "/instances/" + instance;
break;
default:
throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
}
OrthancPlugins::HttpClient client;
std::map<std::string, std::string> userProperties;
OrthancPlugins::DicomWebServers::GetInstance().ConfigureHttpClient(client, userProperties, serverName, uri);
client.SetMethod(OrthancPluginHttpMethod_Delete);
client.Execute();
std::string tmp = "{}";
OrthancPluginAnswerBuffer(context, output, tmp.c_str(), tmp.size(), "application/json");
}
}
static bool DisplayPerformanceWarning(OrthancPluginContext* context)
{
(void) DisplayPerformanceWarning; // Disable warning about unused function
OrthancPluginLogWarning(context, "Performance warning in DICOMweb: "
"Non-release build, runtime debug assertions are turned on");
return true;
}
template <enum Orthanc::EmbeddedResources::DirectoryResourceId folder>
void ServeEmbeddedFolder(OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Get)
{
OrthancPluginSendMethodNotAllowed(context, output, "GET");
}
else
{
std::string path = "/" + std::string(request->groups[0]);
const char* mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path));
std::string s;
Orthanc::EmbeddedResources::GetDirectoryResource(s, folder, path.c_str());
const char* resource = s.size() ? s.c_str() : NULL;
OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
}
}
#if ORTHANC_STANDALONE == 0
void ServeDicomWebClient(OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request)
{
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
if (request->method != OrthancPluginHttpMethod_Get)
{
OrthancPluginSendMethodNotAllowed(context, output, "GET");
}
else
{
const std::string path = std::string(DICOMWEB_CLIENT_PATH) + std::string(request->groups[0]);
const char* mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path));
OrthancPlugins::MemoryBuffer f;
f.ReadFile(path);
OrthancPluginAnswerBuffer(context, output, f.GetData(), f.GetSize(), mime);
}
}
#endif
static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
OrthancPluginResourceType resourceType,
const char *resourceId)
{
try
{
switch (changeType)
{
case OrthancPluginChangeType_OrthancStarted:
OrthancPlugins::Configuration::LoadDicomWebServers();
break;
default:
break;
}
}
catch (Orthanc::OrthancException& e)
{
LOG(ERROR) << "Exception: " << e.What();
}
catch (...)
{
LOG(ERROR) << "Uncatched native exception";
}
return OrthancPluginErrorCode_Success;
}
extern "C"
{
ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
{
assert(DisplayPerformanceWarning(context));
OrthancPlugins::SetGlobalContext(context);
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
Orthanc::Logging::InitializePluginContext(context);
#else
Orthanc::Logging::Initialize(context);
#endif
Orthanc::Logging::EnableInfoLevel(true);
/* Check the version of the Orthanc core */
if (OrthancPluginCheckVersion(context) == 0)
{
char info[1024];
sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
context->orthancVersion,
ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
OrthancPluginLogError(context, info);
return -1;
}
#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 0
LOG(WARNING) << "Performance warning in DICOMweb: The plugin was compiled against "
<< "Orthanc SDK <= 1.5.6. STOW and WADO chunked transfers will be entirely stored in RAM.";
#endif
OrthancPluginSetDescription(context, "Implementation of DICOMweb (QIDO-RS, STOW-RS and WADO-RS) and WADO-URI.");
try
{
// Read the configuration
OrthancPlugins::Configuration::Initialize();
// Configure the DICOMweb callbacks
if (OrthancPlugins::Configuration::GetBooleanValue("Enable", true))
{
std::string root = OrthancPlugins::Configuration::GetDicomWebRoot();
assert(!root.empty() && root[root.size() - 1] == '/');
OrthancPlugins::LogWarning("URI to the DICOMweb REST API: " + root);
OrthancPlugins::ChunkedRestRegistration<
SearchForStudies /* TODO => Rename as QIDO-RS */,
OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies");
OrthancPlugins::ChunkedRestRegistration<
RetrieveDicomStudy /* TODO => Rename as WADO-RS */,
OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies/([^/]*)");
OrthancPlugins::RegisterRestCallback<SearchForInstances>(root + "instances", true);
OrthancPlugins::RegisterRestCallback<SearchForSeries>(root + "series", true);
OrthancPlugins::RegisterRestCallback<SearchForInstances>(root + "studies/([^/]*)/instances", true);
OrthancPlugins::RegisterRestCallback<RetrieveStudyMetadata>(root + "studies/([^/]*)/metadata", true);
OrthancPlugins::RegisterRestCallback<SearchForSeries>(root + "studies/([^/]*)/series", true);
OrthancPlugins::RegisterRestCallback<RetrieveDicomSeries>(root + "studies/([^/]*)/series/([^/]*)", true);
OrthancPlugins::RegisterRestCallback<SearchForInstances>(root + "studies/([^/]*)/series/([^/]*)/instances", true);
OrthancPlugins::RegisterRestCallback<RetrieveDicomInstance>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)", true);
OrthancPlugins::RegisterRestCallback<RetrieveBulkData>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/bulk/(.*)", true);
OrthancPlugins::RegisterRestCallback<RetrieveInstanceMetadata>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/metadata", true);
OrthancPlugins::RegisterRestCallback<RetrieveSeriesMetadata>(root + "studies/([^/]*)/series/([^/]*)/metadata", true);
OrthancPlugins::RegisterRestCallback<RetrieveAllFrames>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames", true);
OrthancPlugins::RegisterRestCallback<RetrieveSelectedFrames>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)", true);
OrthancPlugins::RegisterRestCallback<ListServers>(root + "servers", true);
OrthancPlugins::RegisterRestCallback<ListServerOperations>(root + "servers/([^/]*)", true);
OrthancPlugins::RegisterRestCallback<StowClient>(root + "servers/([^/]*)/stow", true);
OrthancPlugins::RegisterRestCallback<WadoRetrieveClient>(root + "servers/([^/]*)/wado", true);
OrthancPlugins::RegisterRestCallback<GetFromServer>(root + "servers/([^/]*)/get", true);
OrthancPlugins::RegisterRestCallback<RetrieveFromServer>(root + "servers/([^/]*)/retrieve", true);
OrthancPlugins::RegisterRestCallback<QidoClient>(root + "servers/([^/]*)/qido", true);
OrthancPlugins::RegisterRestCallback<DeleteClient>(root + "servers/([^/]*)/delete", true);
OrthancPlugins::RegisterRestCallback
<ServeEmbeddedFolder<Orthanc::EmbeddedResources::JAVASCRIPT_LIBS> >
(root + "app/libs/(.*)", true);
OrthancPlugins::RegisterRestCallback<GetClientInformation>(root + "info", true);
OrthancPlugins::RegisterRestCallback<RetrieveStudyRendered>(root + "studies/([^/]*)/rendered", true);
OrthancPlugins::RegisterRestCallback<RetrieveSeriesRendered>(root + "studies/([^/]*)/series/([^/]*)/rendered", true);
OrthancPlugins::RegisterRestCallback<RetrieveInstanceRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/rendered", true);
OrthancPlugins::RegisterRestCallback<RetrieveFrameRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)/rendered", true);
OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
// Extend the default Orthanc Explorer with custom JavaScript for STOW client
std::string explorer;
#if ORTHANC_STANDALONE == 1
Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
OrthancPlugins::RegisterRestCallback
<ServeEmbeddedFolder<Orthanc::EmbeddedResources::WEB_APPLICATION> >
(root + "app/client/(.*)", true);
#else
Orthanc::SystemToolbox::ReadFile(explorer, std::string(DICOMWEB_CLIENT_PATH) + "../Plugin/OrthancExplorer.js");
OrthancPlugins::RegisterRestCallback<ServeDicomWebClient>(root + "app/client/(.*)", true);
#endif
{
if (root.size() < 2 ||
root[0] != '/' ||
root[root.size() - 1] != '/')
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
std::map<std::string, std::string> dictionary;
dictionary["DICOMWEB_ROOT"] = root.substr(1, root.size() - 2); // Remove heading and trailing slashes
std::string configured = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), configured.c_str());
}
std::string uri = root + "app/client/index.html";
OrthancPluginSetRootUri(context, uri.c_str());
}
else
{
OrthancPlugins::LogWarning("DICOMweb support is disabled");
}
// Configure the WADO callback
if (OrthancPlugins::Configuration::GetBooleanValue("EnableWado", true))
{
std::string wado = OrthancPlugins::Configuration::GetWadoRoot();
OrthancPlugins::LogWarning("URI to the WADO-URI API: " + wado);
OrthancPlugins::RegisterRestCallback<WadoUriCallback>(wado, true);
}
else
{
OrthancPlugins::LogWarning("WADO-URI support is disabled");
}
}
catch (Orthanc::OrthancException& e)
{
OrthancPlugins::LogError("Exception while initializing the DICOMweb plugin: " +
std::string(e.What()));
return -1;
}
catch (...)
{
OrthancPlugins::LogError("Exception while initializing the DICOMweb plugin");
return -1;
}
return 0;
}
ORTHANC_PLUGINS_API void OrthancPluginFinalize()
{
}
ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
{
return "dicom-web";
}
ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
{
return ORTHANC_DICOM_WEB_VERSION;
}
}