2688 lines
101 KiB
C++
2688 lines
101 KiB
C++
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
|
|
#include "../PrecompiledHeadersServer.h"
|
|
#include "OrthancRestApi.h"
|
|
|
|
#include "../../../OrthancFramework/Sources/Cache/SharedArchive.h"
|
|
#include "../../../OrthancFramework/Sources/DicomNetworking/DicomAssociation.h"
|
|
#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
|
|
#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
|
|
#include "../../../OrthancFramework/Sources/Logging.h"
|
|
#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
|
|
|
|
#include "../OrthancConfiguration.h"
|
|
#include "../QueryRetrieveHandler.h"
|
|
#include "../ServerContext.h"
|
|
#include "../ServerJobs/DicomModalityStoreJob.h"
|
|
#include "../ServerJobs/DicomMoveScuJob.h"
|
|
#include "../ServerJobs/DicomGetScuJob.h"
|
|
#include "../ServerJobs/OrthancPeerStoreJob.h"
|
|
#include "../ServerToolbox.h"
|
|
#include "../StorageCommitmentReports.h"
|
|
|
|
|
|
namespace Orthanc
|
|
{
|
|
static const char* const KEY_LEVEL = "Level";
|
|
static const char* const KEY_LOCAL_AET = "LocalAet";
|
|
static const char* const KEY_CALLED_AET = "CalledAet";
|
|
static const char* const KEY_HOST = "Host";
|
|
static const char* const KEY_PORT = "Port";
|
|
static const char* const KEY_NORMALIZE = "Normalize";
|
|
static const char* const KEY_QUERY = "Query";
|
|
static const char* const KEY_RESOURCES = "Resources";
|
|
static const char* const KEY_TARGET_AET = "TargetAet";
|
|
static const char* const KEY_TIMEOUT = "Timeout";
|
|
static const char* const KEY_CHECK_FIND = "CheckFind";
|
|
static const char* const SOP_CLASS_UID = "SOPClassUID";
|
|
static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
|
|
static const char* const KEY_RETRIEVE_METHOD = "RetrieveMethod";
|
|
|
|
static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
return lock.GetConfiguration().GetModalityUsingSymbolicName(name);
|
|
}
|
|
|
|
|
|
static void InjectAssociationTimeout(DicomAssociationParameters& params,
|
|
const Json::Value& body)
|
|
{
|
|
if (body.type() == Json::objectValue &&
|
|
body.isMember(KEY_TIMEOUT))
|
|
{
|
|
// New in Orthanc 1.7.0
|
|
params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT));
|
|
}
|
|
}
|
|
|
|
static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call,
|
|
const Json::Value& body)
|
|
{
|
|
const std::string& localAet =
|
|
OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle();
|
|
const RemoteModalityParameters remote =
|
|
MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
|
|
|
|
DicomAssociationParameters params(localAet, remote);
|
|
InjectAssociationTimeout(params, body);
|
|
|
|
return params;
|
|
}
|
|
|
|
|
|
static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call)
|
|
{
|
|
Json::Value body;
|
|
|
|
if (!call.ParseJsonRequest(body))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse the JSON body");
|
|
}
|
|
|
|
return GetAssociationParameters(call, body);
|
|
}
|
|
|
|
|
|
static void DocumentModalityParametersShared(RestApiCall& call,
|
|
bool includePermissions)
|
|
{
|
|
call.GetDocumentation()
|
|
.SetRequestField("AET", RestApiCallDocumentation::Type_String,
|
|
"AET of the remote DICOM modality", true)
|
|
.SetRequestField("Host", RestApiCallDocumentation::Type_String,
|
|
"Host address of the remote DICOM modality (typically, an IP address)", true)
|
|
.SetRequestField("Port", RestApiCallDocumentation::Type_Number,
|
|
"TCP port of the remote DICOM modality", true)
|
|
.SetRequestField("Manufacturer", RestApiCallDocumentation::Type_String, "Manufacturer of the remote DICOM "
|
|
"modality (check configuration option `DicomModalities` for possible values", false)
|
|
.SetRequestField("UseDicomTls", RestApiCallDocumentation::Type_Boolean, "Whether to use DICOM TLS "
|
|
"in the SCU connection initiated by Orthanc (new in Orthanc 1.9.0)", false)
|
|
.SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String, "Whether to override the default DicomAet "
|
|
"in the SCU connection initiated by Orthanc to this modality", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, "Whether to override the default DicomScuTimeout "
|
|
"in the SCU connection initiated by Orthanc to this modality", false);
|
|
|
|
if (includePermissions)
|
|
{
|
|
call.GetDocumentation()
|
|
.SetRequestField("AllowEcho", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-ECHO SCU commands issued by the remote modality", false)
|
|
.SetRequestField("AllowStore", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-STORE SCU commands issued by the remote modality", false)
|
|
.SetRequestField("AllowFind", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-FIND SCU commands issued by the remote modality", false)
|
|
.SetRequestField("AllowFindWorklist", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-FIND SCU commands for worklists issued by the remote modality", false)
|
|
.SetRequestField("AllowMove", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-MOVE SCU commands issued by the remote modality", false)
|
|
.SetRequestField("AllowGet", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept C-GET SCU commands issued by the remote modality", false)
|
|
.SetRequestField("AllowStorageCommitment", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to accept storage commitment requests issued by the remote modality", false)
|
|
.SetRequestField("AllowTranscoding", RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to allow transcoding for operations initiated by this modality. "
|
|
"This option applies to Orthanc C-GET SCP and to Orthanc C-STORE SCU. "
|
|
"It only has an effect if the global option `EnableTranscoding` is set to `true`.", false);
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Echo SCU
|
|
***************************************************************************/
|
|
|
|
static void ExecuteEcho(RestApiOutput& output,
|
|
const DicomAssociationParameters& parameters,
|
|
const Json::Value& body)
|
|
{
|
|
bool checkFind = false;
|
|
|
|
if (body.type() == Json::objectValue &&
|
|
body.isMember(KEY_CHECK_FIND))
|
|
{
|
|
checkFind = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
|
|
}
|
|
else
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
|
|
}
|
|
|
|
ScuOperationFlags operations = ScuOperationFlags_Echo;
|
|
|
|
if (checkFind)
|
|
{
|
|
operations = static_cast<ScuOperationFlags>(operations | ScuOperationFlags_Find);
|
|
}
|
|
|
|
DicomControlUserConnection connection(parameters, operations);
|
|
if (connection.Echo())
|
|
{
|
|
|
|
if (checkFind)
|
|
{
|
|
// Issue a C-FIND request at the study level about a random Study Instance UID
|
|
const std::string studyInstanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study);
|
|
|
|
DicomMap query;
|
|
query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
|
|
|
|
DicomFindAnswers answers(false /* not a worklist */);
|
|
|
|
// The following line throws an exception if the remote modality doesn't support C-FIND
|
|
connection.Find(answers, ResourceType_Study, query, false /* normalize */);
|
|
}
|
|
|
|
// Echo has succeeded
|
|
output.AnswerBuffer("{}", MimeType_Json);
|
|
}
|
|
else
|
|
{
|
|
// Echo has failed
|
|
output.SignalError(HttpStatus_500_InternalServerError);
|
|
}
|
|
}
|
|
|
|
|
|
static void DocumentEchoShared(RestApiPostCall& call)
|
|
{
|
|
call.GetDocumentation()
|
|
.SetRequestField(KEY_CHECK_FIND, RestApiCallDocumentation::Type_Boolean,
|
|
"Issue a dummy C-FIND command after the C-GET SCU, in order to check whether the remote "
|
|
"modality knows about Orthanc. This field defaults to the value of the `DicomEchoChecksFind` "
|
|
"configuration option. New in Orthanc 1.8.1.", false);
|
|
}
|
|
|
|
|
|
static void DicomEcho(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
DocumentEchoShared(call);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger C-ECHO SCU")
|
|
.SetDescription("Trigger C-ECHO SCU command against the DICOM modality whose identifier is provided in URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-c-echo")
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-ECHO command, in seconds", false)
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
Json::Value body = Json::objectValue;
|
|
|
|
if (call.GetBodySize() == 0 /* allow empty body, was disallowed in Orthanc 1.7.0->1.8.1 */ ||
|
|
call.ParseJsonRequest(body))
|
|
{
|
|
const DicomAssociationParameters parameters = GetAssociationParameters(call, body);
|
|
ExecuteEcho(call.GetOutput(), parameters, body);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse the JSON body");
|
|
}
|
|
}
|
|
|
|
|
|
static void DicomEchoTool(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
DocumentEchoShared(call);
|
|
DocumentModalityParametersShared(call, false);
|
|
call.GetDocumentation()
|
|
.SetTag("System")
|
|
.SetSummary("Trigger C-ECHO SCU")
|
|
.SetDescription("Trigger C-ECHO SCU command against a DICOM modality described in the POST body, "
|
|
"without having to register the modality in some `/modalities/{id}` (new in Orthanc 1.8.1)");
|
|
return;
|
|
}
|
|
|
|
Json::Value body;
|
|
if (call.ParseJsonRequest(body))
|
|
{
|
|
RemoteModalityParameters modality;
|
|
modality.Unserialize(body);
|
|
|
|
const std::string& localAet =
|
|
OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle();
|
|
|
|
DicomAssociationParameters params(localAet, modality);
|
|
InjectAssociationTimeout(params, body);
|
|
|
|
ExecuteEcho(call.GetOutput(), params, body);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse the JSON body");
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Find SCU => DEPRECATED!
|
|
***************************************************************************/
|
|
|
|
static bool MergeQueryAndTemplate(DicomMap& result,
|
|
const RestApiCall& call)
|
|
{
|
|
Json::Value query;
|
|
|
|
if (!call.ParseJsonRequest(query) ||
|
|
query.type() != Json::objectValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Json::Value::Members members = query.getMemberNames();
|
|
for (size_t i = 0; i < members.size(); i++)
|
|
{
|
|
DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
|
|
result.SetValue(t, query[members[i]].asString(), false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void FindPatient(DicomFindAnswers& result,
|
|
DicomControlUserConnection& connection,
|
|
const DicomMap& fields)
|
|
{
|
|
// Only keep the filters from "fields" that are related to the patient
|
|
DicomMap s;
|
|
fields.ExtractPatientInformation(s);
|
|
connection.Find(result, ResourceType_Patient, s, true /* normalize */);
|
|
}
|
|
|
|
|
|
static void FindStudy(DicomFindAnswers& result,
|
|
DicomControlUserConnection& connection,
|
|
const DicomMap& fields)
|
|
{
|
|
// Only keep the filters from "fields" that are related to the study
|
|
DicomMap s;
|
|
fields.ExtractStudyInformation(s);
|
|
|
|
s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
|
|
|
|
connection.Find(result, ResourceType_Study, s, true /* normalize */);
|
|
}
|
|
|
|
static void FindSeries(DicomFindAnswers& result,
|
|
DicomControlUserConnection& connection,
|
|
const DicomMap& fields)
|
|
{
|
|
// Only keep the filters from "fields" that are related to the series
|
|
DicomMap s;
|
|
fields.ExtractSeriesInformation(s);
|
|
|
|
s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
|
|
connection.Find(result, ResourceType_Series, s, true /* normalize */);
|
|
}
|
|
|
|
static void FindInstance(DicomFindAnswers& result,
|
|
DicomControlUserConnection& connection,
|
|
const DicomMap& fields)
|
|
{
|
|
// Only keep the filters from "fields" that are related to the instance
|
|
DicomMap s;
|
|
fields.ExtractInstanceInformation(s);
|
|
|
|
s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
|
|
|
|
connection.Find(result, ResourceType_Instance, s, true /* normalize */);
|
|
}
|
|
|
|
|
|
static void DicomFindPatient(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetDeprecated()
|
|
.SetTag("Networking")
|
|
.SetSummary("C-FIND SCU for patients")
|
|
.SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL, "
|
|
"in order to find a patient. Deprecated in favor of `/modalities/{id}/query`.")
|
|
.AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching patients")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
|
|
|
|
DicomMap fields;
|
|
DicomMap::SetupFindPatientTemplate(fields);
|
|
if (!MergeQueryAndTemplate(fields, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
DicomFindAnswers answers(false);
|
|
|
|
{
|
|
DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindPatient);
|
|
FindPatient(answers, connection, fields);
|
|
}
|
|
|
|
Json::Value result;
|
|
answers.ToJson(result, DicomToJsonFormat_Human);
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
static void DicomFindStudy(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetDeprecated()
|
|
.SetTag("Networking")
|
|
.SetSummary("C-FIND SCU for studies")
|
|
.SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL, "
|
|
"in order to find a study. Deprecated in favor of `/modalities/{id}/query`.")
|
|
.AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching studies")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
|
|
|
|
DicomMap fields;
|
|
DicomMap::SetupFindStudyTemplate(fields);
|
|
if (!MergeQueryAndTemplate(fields, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
|
|
fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DicomFindAnswers answers(false);
|
|
|
|
{
|
|
DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
|
|
FindStudy(answers, connection, fields);
|
|
}
|
|
|
|
Json::Value result;
|
|
answers.ToJson(result, DicomToJsonFormat_Human);
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
static void DicomFindSeries(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetDeprecated()
|
|
.SetTag("Networking")
|
|
.SetSummary("C-FIND SCU for series")
|
|
.SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL, "
|
|
"in order to find a series. Deprecated in favor of `/modalities/{id}/query`.")
|
|
.AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching series")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
|
|
|
|
DicomMap fields;
|
|
DicomMap::SetupFindSeriesTemplate(fields);
|
|
if (!MergeQueryAndTemplate(fields, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
|
|
fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
|
|
fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DicomFindAnswers answers(false);
|
|
|
|
{
|
|
DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
|
|
FindSeries(answers, connection, fields);
|
|
}
|
|
|
|
Json::Value result;
|
|
answers.ToJson(result, DicomToJsonFormat_Human);
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
static void DicomFindInstance(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetDeprecated()
|
|
.SetTag("Networking")
|
|
.SetSummary("C-FIND SCU for instances")
|
|
.SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL, "
|
|
"in order to find an instance. Deprecated in favor of `/modalities/{id}/query`.")
|
|
.AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching instances")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
|
|
|
|
DicomMap fields;
|
|
DicomMap::SetupFindInstanceTemplate(fields);
|
|
if (!MergeQueryAndTemplate(fields, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
|
|
fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
|
|
fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2 ||
|
|
fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent().size() <= 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DicomFindAnswers answers(false);
|
|
|
|
{
|
|
DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
|
|
FindInstance(answers, connection, fields);
|
|
}
|
|
|
|
Json::Value result;
|
|
answers.ToJson(result, DicomToJsonFormat_Human);
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
|
|
static void CopyTagIfExists(DicomMap& target,
|
|
const ParsedDicomFile& source,
|
|
const DicomTag& tag)
|
|
{
|
|
std::string tmp;
|
|
if (source.GetTagValue(tmp, tag))
|
|
{
|
|
target.SetValue(tag, tmp, false);
|
|
}
|
|
}
|
|
|
|
|
|
static void DicomFind(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetDeprecated()
|
|
.SetTag("Networking")
|
|
.SetSummary("Hierarchical C-FIND SCU")
|
|
.SetDescription("Trigger a sequence of C-FIND SCU commands against the DICOM modality whose identifier is provided in URL, "
|
|
"in order to discover a hierarchy of matching patients/studies/series. "
|
|
"Deprecated in favor of `/modalities/{id}/query`.")
|
|
.AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching patients, embedding the "
|
|
"matching studies, then the matching series.")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
|
|
|
|
DicomMap m;
|
|
DicomMap::SetupFindPatientTemplate(m);
|
|
if (!MergeQueryAndTemplate(m, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_Find);
|
|
|
|
DicomFindAnswers patients(false);
|
|
FindPatient(patients, connection, m);
|
|
|
|
// Loop over the found patients
|
|
Json::Value result = Json::arrayValue;
|
|
for (size_t i = 0; i < patients.GetSize(); i++)
|
|
{
|
|
Json::Value patient;
|
|
patients.ToJson(patient, i, DicomToJsonFormat_Human);
|
|
|
|
DicomMap::SetupFindStudyTemplate(m);
|
|
if (!MergeQueryAndTemplate(m, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
|
|
|
|
DicomFindAnswers studies(false);
|
|
FindStudy(studies, connection, m);
|
|
|
|
patient["Studies"] = Json::arrayValue;
|
|
|
|
// Loop over the found studies
|
|
for (size_t j = 0; j < studies.GetSize(); j++)
|
|
{
|
|
Json::Value study;
|
|
studies.ToJson(study, j, DicomToJsonFormat_Human);
|
|
|
|
DicomMap::SetupFindSeriesTemplate(m);
|
|
if (!MergeQueryAndTemplate(m, call))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
|
|
CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
|
|
|
|
DicomFindAnswers series(false);
|
|
FindSeries(series, connection, m);
|
|
|
|
// Loop over the found series
|
|
study["Series"] = Json::arrayValue;
|
|
for (size_t k = 0; k < series.GetSize(); k++)
|
|
{
|
|
Json::Value series2;
|
|
series.ToJson(series2, k, DicomToJsonFormat_Human);
|
|
study["Series"].append(series2);
|
|
}
|
|
|
|
patient["Studies"].append(study);
|
|
}
|
|
|
|
result.append(patient);
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0
|
|
***************************************************************************/
|
|
|
|
static void AnswerQueryHandler(RestApiPostCall& call,
|
|
std::unique_ptr<QueryRetrieveHandler>& handler)
|
|
{
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
if (handler.get() == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_NullPointer);
|
|
}
|
|
|
|
handler->Run();
|
|
|
|
std::string s = context.GetQueryRetrieveArchive().Add(handler.release());
|
|
Json::Value result = Json::objectValue;
|
|
result["ID"] = s;
|
|
result["Path"] = "/queries/" + s;
|
|
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
|
|
static void DicomQuery(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger C-FIND SCU")
|
|
.SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
|
|
.SetUriArgument("id", "Identifier of the modality of interest")
|
|
.SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
|
|
"Associative array containing the filter on the values of the DICOM tags", true)
|
|
.SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
|
|
"Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true)
|
|
.SetRequestField(KEY_NORMALIZE, RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to normalize the query, i.e. whether to wipe out from the query, the DICOM tags "
|
|
"that are not applicable for the query-retrieve level of interest", false)
|
|
.SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
|
|
"Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
|
|
"Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-FIND command and subsequent C-MOVE retrievals, in seconds (new in Orthanc 1.9.1)", false)
|
|
.SetAnswerField("ID", RestApiCallDocumentation::Type_JsonObject,
|
|
"Identifier of the query, to be used with `/queries/{id}`")
|
|
.SetAnswerField("Path", RestApiCallDocumentation::Type_JsonObject,
|
|
"Root path to the query in the REST API");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
Json::Value request;
|
|
|
|
if (!call.ParseJsonRequest(request) ||
|
|
request.type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
|
|
}
|
|
else if (!request.isMember(KEY_LEVEL) ||
|
|
request[KEY_LEVEL].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The JSON body must contain field " + std::string(KEY_LEVEL));
|
|
}
|
|
else if (request.isMember(KEY_NORMALIZE) &&
|
|
request[KEY_NORMALIZE].type() != Json::booleanValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The field " + std::string(KEY_NORMALIZE) + " must contain a Boolean");
|
|
}
|
|
else if (request.isMember(KEY_QUERY) &&
|
|
request[KEY_QUERY].type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The field " + std::string(KEY_QUERY) + " must contain a JSON object");
|
|
}
|
|
else if (request.isMember(KEY_LOCAL_AET) &&
|
|
request[KEY_LOCAL_AET].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The field " + std::string(KEY_LOCAL_AET) + " must contain a string");
|
|
}
|
|
else
|
|
{
|
|
std::unique_ptr<QueryRetrieveHandler> handler(new QueryRetrieveHandler(context));
|
|
|
|
handler->SetModality(call.GetUriComponent("id", ""));
|
|
handler->SetLevel(StringToResourceType(request[KEY_LEVEL].asCString()));
|
|
|
|
if (request.isMember(KEY_QUERY))
|
|
{
|
|
std::map<DicomTag, std::string> query;
|
|
SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
|
|
|
|
for (std::map<DicomTag, std::string>::const_iterator
|
|
it = query.begin(); it != query.end(); ++it)
|
|
{
|
|
handler->SetQuery(it->first, it->second);
|
|
}
|
|
}
|
|
|
|
if (request.isMember(KEY_NORMALIZE))
|
|
{
|
|
handler->SetFindNormalized(request[KEY_NORMALIZE].asBool());
|
|
}
|
|
|
|
if (request.isMember(KEY_LOCAL_AET))
|
|
{
|
|
handler->SetLocalAet(request[KEY_LOCAL_AET].asString());
|
|
}
|
|
|
|
if (request.isMember(KEY_TIMEOUT))
|
|
{
|
|
// New in Orthanc 1.9.1
|
|
handler->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
|
|
}
|
|
|
|
AnswerQueryHandler(call, handler);
|
|
}
|
|
}
|
|
|
|
|
|
static void ListQueries(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List query/retrieve operations")
|
|
.SetDescription("List the identifiers of all the query/retrieve operations on DICOM modalities, "
|
|
"as initiated by calls to `/modalities/{id}/query`. The length of this list is bounded "
|
|
"by the `QueryRetrieveSize` configuration option of Orthanc. "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
|
|
.AddAnswerType(MimeType_Json, "JSON array containing the identifiers");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
std::list<std::string> queries;
|
|
context.GetQueryRetrieveArchive().List(queries);
|
|
|
|
Json::Value result = Json::arrayValue;
|
|
for (std::list<std::string>::const_iterator
|
|
it = queries.begin(); it != queries.end(); ++it)
|
|
{
|
|
result.append(*it);
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
class QueryAccessor
|
|
{
|
|
private:
|
|
ServerContext& context_;
|
|
SharedArchive::Accessor accessor_;
|
|
QueryRetrieveHandler* handler_;
|
|
|
|
public:
|
|
explicit QueryAccessor(RestApiCall& call) :
|
|
context_(OrthancRestApi::GetContext(call)),
|
|
accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")),
|
|
handler_(NULL)
|
|
{
|
|
if (accessor_.IsValid())
|
|
{
|
|
handler_ = &dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem());
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource);
|
|
}
|
|
}
|
|
|
|
QueryRetrieveHandler& GetHandler() const
|
|
{
|
|
assert(handler_ != NULL);
|
|
return *handler_;
|
|
}
|
|
};
|
|
|
|
static void AnswerDicomMap(RestApiGetCall& call,
|
|
const DicomMap& value,
|
|
DicomToJsonFormat format)
|
|
{
|
|
Json::Value full = Json::objectValue;
|
|
FromDcmtkBridge::ToJson(full, value, format);
|
|
call.GetOutput().AnswerJson(full);
|
|
}
|
|
}
|
|
|
|
|
|
static void ListQueryAnswers(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
|
|
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List answers to a query")
|
|
.SetDescription("List the indices of all the available answers resulting from a query/retrieve operation "
|
|
"on some DICOM modality, whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
|
|
"If present, retrieve detailed information about the individual answers", false)
|
|
.AddAnswerType(MimeType_Json, "JSON array containing the indices of the answers, or detailed information "
|
|
"about the reported answers (if `expand` argument is provided)");
|
|
return;
|
|
}
|
|
|
|
const bool expand = call.HasArgument("expand") && call.GetBooleanArgument("expand", true);
|
|
const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full);
|
|
|
|
QueryAccessor query(call);
|
|
size_t count = query.GetHandler().GetAnswersCount();
|
|
|
|
Json::Value result = Json::arrayValue;
|
|
for (size_t i = 0; i < count; i++)
|
|
{
|
|
if (expand)
|
|
{
|
|
// New in Orthanc 1.5.0
|
|
DicomMap value;
|
|
query.GetHandler().GetAnswer(value, i);
|
|
|
|
Json::Value json = Json::objectValue;
|
|
FromDcmtkBridge::ToJson(json, value, format);
|
|
|
|
result.append(json);
|
|
}
|
|
else
|
|
{
|
|
result.append(boost::lexical_cast<std::string>(i));
|
|
}
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
|
|
|
|
static void GetQueryOneAnswer(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
|
|
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get one answer")
|
|
.SetDescription("Get the content (DICOM tags) of one answer associated with the "
|
|
"query/retrieve operation whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.SetUriArgument("index", "Index of the answer")
|
|
.AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags of the answer");
|
|
return;
|
|
}
|
|
|
|
size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
|
|
|
|
QueryAccessor query(call);
|
|
|
|
DicomMap map;
|
|
query.GetHandler().GetAnswer(map, index);
|
|
|
|
AnswerDicomMap(call, map, OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full));
|
|
}
|
|
|
|
|
|
static void SubmitRetrieveJob(RestApiPostCall& call,
|
|
bool allAnswers,
|
|
size_t index)
|
|
{
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
std::string targetAet;
|
|
int timeout = -1;
|
|
|
|
QueryAccessor query(call);
|
|
|
|
RetrieveMethod retrieveMethod = query.GetHandler().GetRemoteModality().GetRetrieveMethod();
|
|
|
|
Json::Value body;
|
|
if (call.ParseJsonRequest(body))
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
|
|
timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
|
|
|
|
std::string strRetrieveMethod = SerializationToolbox::ReadString(body, KEY_RETRIEVE_METHOD, "");
|
|
|
|
if (!strRetrieveMethod.empty())
|
|
{
|
|
retrieveMethod = StringToRetrieveMethod(strRetrieveMethod);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
body = Json::objectValue;
|
|
if (call.GetBodySize() > 0)
|
|
{
|
|
call.BodyToString(targetAet);
|
|
}
|
|
else
|
|
{
|
|
targetAet = context.GetDefaultLocalApplicationEntityTitle();
|
|
}
|
|
}
|
|
|
|
if (retrieveMethod == RetrieveMethod_SystemDefault)
|
|
{
|
|
retrieveMethod = context.GetDefaultDicomRetrieveMethod();
|
|
}
|
|
|
|
std::unique_ptr<DicomRetrieveScuBaseJob> job;
|
|
|
|
|
|
switch (retrieveMethod)
|
|
{
|
|
case RetrieveMethod_Move:
|
|
{
|
|
job.reset(new DicomMoveScuJob(context));
|
|
(dynamic_cast<DicomMoveScuJob*>(job.get()))->SetTargetAet(targetAet);
|
|
|
|
LOG(WARNING) << "Driving C-Move SCU on remote modality "
|
|
<< query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
|
|
<< " to target modality " << targetAet;
|
|
}; break;
|
|
case RetrieveMethod_Get:
|
|
{
|
|
job.reset(new DicomGetScuJob(context));
|
|
|
|
LOG(WARNING) << "Driving C-Get SCU on remote modality "
|
|
<< query.GetHandler().GetRemoteModality().GetApplicationEntityTitle();
|
|
}; break;
|
|
default:
|
|
throw OrthancException(ErrorCode_NotImplemented);
|
|
}
|
|
|
|
job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
|
|
|
|
job->SetLocalAet(query.GetHandler().GetLocalAet());
|
|
job->SetRemoteModality(query.GetHandler().GetRemoteModality());
|
|
|
|
if (timeout >= 0)
|
|
{
|
|
// New in Orthanc 1.7.0
|
|
job->SetTimeout(static_cast<uint32_t>(timeout));
|
|
}
|
|
else if (query.GetHandler().HasTimeout())
|
|
{
|
|
// New in Orthanc 1.9.1
|
|
job->SetTimeout(query.GetHandler().GetTimeout());
|
|
}
|
|
|
|
if (allAnswers)
|
|
{
|
|
for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
|
|
{
|
|
job->AddFindAnswer(query.GetHandler(), i);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
job->AddFindAnswer(query.GetHandler(), index);
|
|
}
|
|
|
|
OrthancRestApi::GetApi(call).SubmitCommandsJob
|
|
(call, job.release(), true /* synchronous by default */, body);
|
|
|
|
}
|
|
|
|
|
|
static void DocumentRetrieveShared(RestApiPostCall& call)
|
|
{
|
|
OrthancRestApi::DocumentSubmitCommandsJob(call);
|
|
OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Short);
|
|
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.SetRequestField(KEY_TARGET_AET, RestApiCallDocumentation::Type_String,
|
|
"AET of the target modality. By default, the AET of Orthanc is used, as defined in the "
|
|
"`DicomAet` configuration option.", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-MOVE command, in seconds", false)
|
|
.SetRequestField(KEY_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String,
|
|
"Force usage of C-MOVE or C-GET to retrieve the resource. If note defined in the payload, "
|
|
"the retrieve method is defined in the DicomDefaultRetrieveMethod configuration or in "
|
|
"DicomModalities->..->RetrieveMethod", false)
|
|
.AddRequestType(MimeType_PlainText, "AET of the target modality");
|
|
}
|
|
|
|
|
|
static void RetrieveOneAnswer(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
DocumentRetrieveShared(call);
|
|
call.GetDocumentation()
|
|
.SetSummary("Retrieve one answer with a C-MOVE or a C-GET SCU")
|
|
.SetDescription("Start a C-MOVE or a C-GET SCU command as a job, in order to retrieve one answer associated with the "
|
|
"query/retrieve operation whose identifiers are provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
|
|
.SetUriArgument("index", "Index of the answer");
|
|
return;
|
|
}
|
|
|
|
size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
|
|
SubmitRetrieveJob(call, false, index);
|
|
}
|
|
|
|
|
|
|
|
|
|
static void RetrieveAllAnswers(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
DocumentRetrieveShared(call);
|
|
call.GetDocumentation()
|
|
.SetSummary("Retrieve all answers with C-MOVE SCU")
|
|
.SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the "
|
|
"query/retrieve operation whose identifier is provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
|
|
return;
|
|
}
|
|
|
|
SubmitRetrieveJob(call, true, 0);
|
|
}
|
|
|
|
|
|
static void GetQueryArguments(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
|
|
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get original query arguments")
|
|
.SetDescription("Get the original DICOM filter associated with the query/retrieve operation "
|
|
"whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.AddAnswerType(MimeType_Json, "Content of the original query");
|
|
return;
|
|
}
|
|
|
|
QueryAccessor query(call);
|
|
AnswerDicomMap(call, query.GetHandler().GetQuery(), OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full));
|
|
}
|
|
|
|
|
|
static void GetQueryLevel(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get level of original query")
|
|
.SetDescription("Get the query level (value of the `QueryRetrieveLevel` tag) of the query/retrieve operation "
|
|
"whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.AddAnswerType(MimeType_PlainText, "The level");
|
|
return;
|
|
}
|
|
|
|
QueryAccessor query(call);
|
|
call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText);
|
|
}
|
|
|
|
|
|
static void GetQueryModality(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get modality of original query")
|
|
.SetDescription("Get the identifier of the DICOM modality that was targeted by the query/retrieve operation "
|
|
"whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.AddAnswerType(MimeType_PlainText, "The identifier of the DICOM modality");
|
|
return;
|
|
}
|
|
|
|
QueryAccessor query(call);
|
|
call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText);
|
|
}
|
|
|
|
|
|
static void DeleteQuery(RestApiDeleteCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Delete a query")
|
|
.SetDescription("Delete the query/retrieve operation whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
|
|
call.GetOutput().AnswerBuffer("", MimeType_PlainText);
|
|
}
|
|
|
|
|
|
static void ListQueryOperations(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List operations on a query")
|
|
.SetDescription("List the available operations for the query/retrieve operation whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.AddAnswerType(MimeType_Json, "JSON array containing the list of operations");
|
|
return;
|
|
}
|
|
|
|
// Ensure that the query of interest does exist
|
|
QueryAccessor query(call);
|
|
|
|
RestApi::AutoListChildren(call);
|
|
}
|
|
|
|
|
|
static void ListQueryAnswerOperations(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List operations on an answer")
|
|
.SetDescription("List the available operations on an answer associated with the "
|
|
"query/retrieve operation whose identifier is provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.SetUriArgument("index", "Index of the answer")
|
|
.AddAnswerType(MimeType_Json, "JSON array containing the list of operations");
|
|
return;
|
|
}
|
|
|
|
// Ensure that the query of interest does exist
|
|
QueryAccessor query(call);
|
|
|
|
// Ensure that the answer of interest does exist
|
|
size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
|
|
|
|
DicomMap map;
|
|
query.GetHandler().GetAnswer(map, index);
|
|
|
|
Json::Value answer = Json::arrayValue;
|
|
answer.append("content");
|
|
answer.append("retrieve");
|
|
|
|
switch (query.GetHandler().GetLevel())
|
|
{
|
|
case ResourceType_Patient:
|
|
answer.append("query-study");
|
|
|
|
case ResourceType_Study:
|
|
answer.append("query-series");
|
|
|
|
case ResourceType_Series:
|
|
answer.append("query-instances");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(answer);
|
|
}
|
|
|
|
|
|
template <ResourceType CHILDREN_LEVEL>
|
|
static void QueryAnswerChildren(RestApiPostCall& call)
|
|
{
|
|
// New in Orthanc 1.5.0
|
|
assert(CHILDREN_LEVEL == ResourceType_Study ||
|
|
CHILDREN_LEVEL == ResourceType_Series ||
|
|
CHILDREN_LEVEL == ResourceType_Instance);
|
|
|
|
if (call.IsDocumentation())
|
|
{
|
|
const std::string resources = GetResourceTypeText(CHILDREN_LEVEL, true /* plural */, false /* lower case */);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Query the child " + resources + " of an answer")
|
|
.SetDescription("Issue a second DICOM C-FIND operation, in order to query the child " + resources +
|
|
" associated with one answer to some query/retrieve operation whose identifiers are provided in the URL")
|
|
.SetUriArgument("id", "Identifier of the query of interest")
|
|
.SetUriArgument("index", "Index of the answer")
|
|
.SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
|
|
"Associative array containing the filter on the values of the DICOM tags", true)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-FIND command, in seconds (new in Orthanc 1.9.1)", false)
|
|
.SetAnswerField("ID", RestApiCallDocumentation::Type_JsonObject,
|
|
"Identifier of the query, to be used with `/queries/{id}`")
|
|
.SetAnswerField("Path", RestApiCallDocumentation::Type_JsonObject,
|
|
"Root path to the query in the REST API");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
std::unique_ptr<QueryRetrieveHandler> handler(new QueryRetrieveHandler(context));
|
|
|
|
{
|
|
const QueryAccessor parent(call);
|
|
const ResourceType level = parent.GetHandler().GetLevel();
|
|
|
|
const size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
|
|
|
|
Json::Value request;
|
|
|
|
if (index >= parent.GetHandler().GetAnswersCount())
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
else if (CHILDREN_LEVEL == ResourceType_Study &&
|
|
level != ResourceType_Patient)
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource);
|
|
}
|
|
else if (CHILDREN_LEVEL == ResourceType_Series &&
|
|
level != ResourceType_Patient &&
|
|
level != ResourceType_Study)
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource);
|
|
}
|
|
else if (CHILDREN_LEVEL == ResourceType_Instance &&
|
|
level != ResourceType_Patient &&
|
|
level != ResourceType_Study &&
|
|
level != ResourceType_Series)
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource);
|
|
}
|
|
else if (!call.ParseJsonRequest(request))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
|
|
}
|
|
else
|
|
{
|
|
handler->SetFindNormalized(parent.GetHandler().IsFindNormalized());
|
|
handler->SetModality(parent.GetHandler().GetModalitySymbolicName());
|
|
handler->SetLevel(CHILDREN_LEVEL);
|
|
|
|
// New in Orthanc 1.9.1
|
|
if (request.isMember(KEY_TIMEOUT))
|
|
{
|
|
handler->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
|
|
}
|
|
else if (parent.GetHandler().HasTimeout())
|
|
{
|
|
handler->SetTimeout(parent.GetHandler().GetTimeout());
|
|
}
|
|
|
|
if (request.isMember(KEY_QUERY))
|
|
{
|
|
std::map<DicomTag, std::string> query;
|
|
SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
|
|
|
|
for (std::map<DicomTag, std::string>::const_iterator
|
|
it = query.begin(); it != query.end(); ++it)
|
|
{
|
|
handler->SetQuery(it->first, it->second);
|
|
}
|
|
}
|
|
|
|
DicomMap answer;
|
|
parent.GetHandler().GetAnswer(answer, index);
|
|
|
|
// This switch-case mimics "DicomControlUserConnection::Move()"
|
|
switch (parent.GetHandler().GetLevel())
|
|
{
|
|
case ResourceType_Patient:
|
|
handler->CopyStringTag(answer, DICOM_TAG_PATIENT_ID);
|
|
break;
|
|
|
|
case ResourceType_Study:
|
|
handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
break;
|
|
|
|
case ResourceType_Series:
|
|
handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
|
|
break;
|
|
|
|
case ResourceType_Instance:
|
|
handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
|
|
handler->CopyStringTag(answer, DICOM_TAG_SOP_INSTANCE_UID);
|
|
break;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
|
|
AnswerQueryHandler(call, handler);
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Store SCU
|
|
***************************************************************************/
|
|
|
|
static void GetInstancesToExport(Json::Value& otherArguments,
|
|
SetOfInstancesJob& job,
|
|
const std::string& remote,
|
|
RestApiPostCall& call)
|
|
{
|
|
otherArguments = Json::objectValue;
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
Json::Value request;
|
|
if (Toolbox::IsSHA1(call.GetBodyData(), call.GetBodySize()))
|
|
{
|
|
std::string s;
|
|
call.BodyToString(s);
|
|
|
|
// This is for compatibility with Orthanc <= 0.5.1.
|
|
request = Json::arrayValue;
|
|
request.append(Toolbox::StripSpaces(s));
|
|
}
|
|
else if (!call.ParseJsonRequest(request))
|
|
{
|
|
// Bad JSON request
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON value");
|
|
}
|
|
|
|
if (request.isString())
|
|
{
|
|
std::string item = request.asString();
|
|
request = Json::arrayValue;
|
|
request.append(item);
|
|
}
|
|
else if (!request.isArray() &&
|
|
!request.isObject())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object, or a JSON array of strings");
|
|
}
|
|
|
|
const Json::Value* resources;
|
|
if (request.isArray())
|
|
{
|
|
resources = &request;
|
|
}
|
|
else
|
|
{
|
|
if (request.type() != Json::objectValue ||
|
|
!request.isMember(KEY_RESOURCES))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Missing field in JSON: \"" + std::string(KEY_RESOURCES) + "\"");
|
|
}
|
|
|
|
resources = &request[KEY_RESOURCES];
|
|
if (!resources->isArray())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"JSON field \"" + std::string(KEY_RESOURCES) + "\" must contain an array");
|
|
}
|
|
|
|
// Copy the remaining arguments
|
|
Json::Value::Members members = request.getMemberNames();
|
|
for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
|
|
{
|
|
otherArguments[members[i]] = request[members[i]];
|
|
}
|
|
}
|
|
|
|
bool logExportedResources;
|
|
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
|
|
}
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
|
|
{
|
|
if (!(*resources) [i].isString())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Resources to be exported must be specified as a JSON array of strings");
|
|
}
|
|
|
|
std::string stripped = Toolbox::StripSpaces((*resources) [i].asString());
|
|
if (!Toolbox::IsSHA1(stripped))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"This string is not a valid Orthanc identifier: " + stripped);
|
|
}
|
|
|
|
job.AddParentResource(stripped); // New in Orthanc 1.5.7
|
|
|
|
context.AddChildInstances(job, stripped);
|
|
|
|
if (logExportedResources)
|
|
{
|
|
context.GetIndex().LogExportedResource(stripped, remote);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void DicomStore(RestApiPostCall& call)
|
|
{
|
|
static const char* KEY_MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
|
|
static const char* KEY_MOVE_ORIGINATOR_ID = "MoveOriginatorID";
|
|
static const char* KEY_STORAGE_COMMITMENT = "StorageCommitment";
|
|
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentSubmitCommandsJob(call);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger C-STORE SCU")
|
|
.SetDescription("Start a C-STORE SCU command as a job, in order to send DICOM resources stored locally "
|
|
"to some remote DICOM modality whose identifier is provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#rest-store-scu")
|
|
.AddRequestType(MimeType_PlainText, "The Orthanc identifier of one resource to be sent")
|
|
.SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
|
|
"List of the Orthanc identifiers of all the DICOM resources to be sent", true)
|
|
.SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
|
|
"Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
|
|
"Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
|
|
.SetRequestField(KEY_CALLED_AET, RestApiCallDocumentation::Type_String,
|
|
"Called AET that is used for this commands, defaults to `AET` configuration option. "
|
|
"Allows you to overwrite the destination AET for a specific operation.", false)
|
|
.SetRequestField(KEY_HOST, RestApiCallDocumentation::Type_String,
|
|
"Host that is used for this commands, defaults to `Host` configuration option. "
|
|
"Allows you to overwrite the destination host for a specific operation.", false)
|
|
.SetRequestField(KEY_PORT, RestApiCallDocumentation::Type_Number,
|
|
"Port that is used for this command, defaults to `Port` configuration option. "
|
|
"Allows you to overwrite the destination port for a specific operation.", false)
|
|
.SetRequestField(KEY_MOVE_ORIGINATOR_AET, RestApiCallDocumentation::Type_String,
|
|
"Move originator AET that is used for this commands, in order to fake a C-MOVE SCU", false)
|
|
.SetRequestField(KEY_MOVE_ORIGINATOR_ID, RestApiCallDocumentation::Type_Number,
|
|
"Move originator ID that is used for this commands, in order to fake a C-MOVE SCU", false)
|
|
.SetRequestField(KEY_STORAGE_COMMITMENT, RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to chain C-STORE with DICOM storage commitment to validate the success of the transmission: "
|
|
"https://orthanc.uclouvain.be/book/users/storage-commitment.html#chaining-c-store-with-storage-commitment", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-STORE command, in seconds", false)
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
std::string remote = call.GetUriComponent("id", "");
|
|
|
|
Json::Value request;
|
|
std::unique_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
|
|
|
|
GetInstancesToExport(request, *job, remote, call);
|
|
|
|
std::string localAet = Toolbox::GetJsonStringField
|
|
(request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
|
|
std::string moveOriginatorAET = Toolbox::GetJsonStringField
|
|
(request, KEY_MOVE_ORIGINATOR_AET, context.GetDefaultLocalApplicationEntityTitle());
|
|
int moveOriginatorID = Toolbox::GetJsonIntegerField
|
|
(request, KEY_MOVE_ORIGINATOR_ID, 0 /* By default, not a C-MOVE */);
|
|
|
|
RemoteModalityParameters remoteModality = MyGetModalityUsingSymbolicName(remote);
|
|
|
|
remoteModality.SetApplicationEntityTitle(Toolbox::GetJsonStringField
|
|
(request, KEY_CALLED_AET, remoteModality.GetApplicationEntityTitle()));
|
|
remoteModality.SetHost(Toolbox::GetJsonStringField
|
|
(request, KEY_HOST, remoteModality.GetHost()));
|
|
remoteModality.SetPortNumber(static_cast<uint16_t>(Toolbox::GetJsonUnsignedIntegerField
|
|
(request, KEY_PORT, remoteModality.GetPortNumber())));
|
|
|
|
job->SetLocalAet(localAet);
|
|
job->SetRemoteModality(remoteModality);
|
|
|
|
if (moveOriginatorID != 0)
|
|
{
|
|
job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
|
|
}
|
|
|
|
// New in Orthanc 1.6.0
|
|
if (Toolbox::GetJsonBooleanField(request, KEY_STORAGE_COMMITMENT, false))
|
|
{
|
|
job->EnableStorageCommitment(true);
|
|
}
|
|
|
|
// New in Orthanc 1.7.0
|
|
if (request.isMember(KEY_TIMEOUT))
|
|
{
|
|
job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
|
|
}
|
|
|
|
OrthancRestApi::GetApi(call).SubmitCommandsJob
|
|
(call, job.release(), true /* synchronous by default */, request);
|
|
}
|
|
|
|
|
|
static void DicomStoreStraight(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Straight C-STORE SCU")
|
|
.SetDescription("Synchronously send the DICOM instance in the POST body to the remote DICOM modality "
|
|
"whose identifier is provided in URL, without having to first store it locally within Orthanc. "
|
|
"This is an alternative to command-line tools such as `storescu` from DCMTK or dcm4che.")
|
|
.SetUriArgument("id", "Identifier of the modality of interest")
|
|
.AddRequestType(MimeType_Dicom, "DICOM instance to be sent")
|
|
.SetAnswerField(SOP_CLASS_UID, RestApiCallDocumentation::Type_String,
|
|
"SOP class UID of the DICOM instance, if the C-STORE SCU has succeeded")
|
|
.SetAnswerField(SOP_INSTANCE_UID, RestApiCallDocumentation::Type_String,
|
|
"SOP instance UID of the DICOM instance, if the C-STORE SCU has succeeded");
|
|
return;
|
|
}
|
|
|
|
Json::Value body = Json::objectValue; // No body
|
|
DicomStoreUserConnection connection(GetAssociationParameters(call, body));
|
|
|
|
std::string sopClassUid, sopInstanceUid;
|
|
connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(),
|
|
call.GetBodySize(),
|
|
false /* Not a C-MOVE */,
|
|
"", 0);
|
|
|
|
Json::Value answer = Json::objectValue;
|
|
answer[SOP_CLASS_UID] = sopClassUid;
|
|
answer[SOP_INSTANCE_UID] = sopInstanceUid;
|
|
|
|
call.GetOutput().AnswerJson(answer);
|
|
}
|
|
|
|
void ParseMoveGetJob(DicomRetrieveScuBaseJob& job, Json::Value& request, RestApiPostCall& call)
|
|
{
|
|
const ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
if (!call.ParseJsonRequest(request) ||
|
|
request.type() != Json::objectValue ||
|
|
!request.isMember(KEY_RESOURCES) ||
|
|
!request.isMember(KEY_LEVEL) ||
|
|
request[KEY_RESOURCES].type() != Json::arrayValue ||
|
|
request[KEY_LEVEL].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
|
|
std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
|
|
}
|
|
|
|
ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
|
|
|
|
std::string localAet = Toolbox::GetJsonStringField
|
|
(request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
|
|
|
|
const RemoteModalityParameters source =
|
|
MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
|
|
|
|
job.SetQueryFormat(DicomToJsonFormat_Short);
|
|
job.SetLocalAet(localAet);
|
|
job.SetRemoteModality(source);
|
|
|
|
if (request.isMember(KEY_TIMEOUT))
|
|
{
|
|
job.SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
|
|
}
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
|
|
{
|
|
DicomMap resource;
|
|
FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
|
|
|
|
resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
|
|
|
|
job.AddQuery(resource);
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Move SCU
|
|
***************************************************************************/
|
|
|
|
static void DicomMove(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentSubmitCommandsJob(call);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger C-MOVE SCU")
|
|
.SetDescription("Start a C-MOVE SCU command as a job, in order to drive the execution of a sequence of "
|
|
"C-STORE commands by some remote DICOM modality whose identifier is provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move")
|
|
.SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
|
|
"List of queries identifying all the DICOM resources to be sent", true)
|
|
.SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
|
|
"Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true)
|
|
.SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
|
|
"Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
|
|
"Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
|
|
.SetRequestField(KEY_TARGET_AET, RestApiCallDocumentation::Type_String,
|
|
"Target AET that will be used by the remote DICOM modality as a target for its C-STORE SCU "
|
|
"commands, defaults to `DicomAet` configuration option in order to do a simple query/retrieve", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-MOVE command, in seconds", false)
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
Json::Value request;
|
|
|
|
std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
|
|
|
|
ParseMoveGetJob(*job, request, call);
|
|
|
|
std::string targetAet = Toolbox::GetJsonStringField
|
|
(request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
|
|
job->SetTargetAet(targetAet);
|
|
|
|
OrthancRestApi::GetApi(call).SubmitCommandsJob
|
|
(call, job.release(), true /* synchronous by default */, request);
|
|
return;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
* DICOM C-Get SCU
|
|
***************************************************************************/
|
|
|
|
static void DicomGet(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentSubmitCommandsJob(call);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger C-GET SCU")
|
|
.SetDescription("Start a C-GET SCU command as a job, in order to retrieve DICOM resources "
|
|
"from a remote DICOM modality whose identifier is provided in the URL: ")
|
|
// "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move") // TODO-GET
|
|
.SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
|
|
"List of queries identifying all the DICOM resources to be sent. "
|
|
"Usage of wildcards is prohibited and the query shall only contain DICOM ID tags. "
|
|
"Additionally, you may provide SOPClassesInStudy to limit the scope of the DICOM "
|
|
"negotiation to certain SOPClassUID or to present uncommon SOPClassUID during "
|
|
"the DICOM negotiation. By default, "
|
|
"Orhanc will propose the most 120 common SOPClassUIDs.", true)
|
|
.SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
|
|
"Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true)
|
|
.SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
|
|
"Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
|
|
"Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the C-GET command, in seconds", false)
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
Json::Value request;
|
|
|
|
std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context));
|
|
|
|
ParseMoveGetJob(*job, request, call);
|
|
|
|
OrthancRestApi::GetApi(call).SubmitCommandsJob
|
|
(call, job.release(), true /* synchronous by default */, request);
|
|
return;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
* Orthanc Peers => Store client
|
|
***************************************************************************/
|
|
|
|
static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
|
|
const std::string& id)
|
|
{
|
|
return peers.find(id) != peers.end();
|
|
}
|
|
|
|
static void ListPeers(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List Orthanc peers")
|
|
.SetDescription("List all the Orthanc peers that are known to Orthanc. This corresponds either to the content of the "
|
|
"`OrthancPeers` configuration option, or to the information stored in the database if "
|
|
"`OrthancPeersInDatabase` is `true`.")
|
|
.SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
|
|
"If present, retrieve detailed information about the individual Orthanc peers", false)
|
|
.AddAnswerType(MimeType_Json, "JSON array containing either the identifiers of the peers, or detailed information "
|
|
"about the peers (if `expand` argument is provided)");
|
|
return;
|
|
}
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
OrthancRestApi::SetOfStrings peers;
|
|
lock.GetConfiguration().GetListOfOrthancPeers(peers);
|
|
|
|
if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true))
|
|
{
|
|
Json::Value result = Json::objectValue;
|
|
for (OrthancRestApi::SetOfStrings::const_iterator
|
|
it = peers.begin(); it != peers.end(); ++it)
|
|
{
|
|
WebServiceParameters peer;
|
|
|
|
if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
|
|
{
|
|
Json::Value info;
|
|
peer.FormatPublic(info);
|
|
result[*it] = info;
|
|
}
|
|
}
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
else // if expand is not present, keep backward compatibility and return an array of peers
|
|
{
|
|
Json::Value result = Json::arrayValue;
|
|
for (OrthancRestApi::SetOfStrings::const_iterator
|
|
it = peers.begin(); it != peers.end(); ++it)
|
|
{
|
|
result.append(*it);
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
}
|
|
|
|
static void ListPeerOperations(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List operations on peer")
|
|
.SetDescription("List the operations that are available for an Orthanc peer.")
|
|
.SetUriArgument("id", "Identifier of the peer of interest")
|
|
.AddAnswerType(MimeType_Json, "List of the available operations");
|
|
return;
|
|
}
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
OrthancRestApi::SetOfStrings peers;
|
|
lock.GetConfiguration().GetListOfOrthancPeers(peers);
|
|
|
|
std::string id = call.GetUriComponent("id", "");
|
|
if (IsExistingPeer(peers, id))
|
|
{
|
|
RestApi::AutoListChildren(call);
|
|
}
|
|
}
|
|
|
|
static void PeerStore(RestApiPostCall& call)
|
|
{
|
|
static const char* KEY_TRANSCODE = "Transcode";
|
|
static const char* KEY_COMPRESS = "Compress";
|
|
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentSubmitCommandsJob(call);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Send to Orthanc peer")
|
|
.SetDescription("Send DICOM resources stored locally to some remote Orthanc peer whose identifier is provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/rest.html#sending-one-resource")
|
|
.AddRequestType(MimeType_PlainText, "The Orthanc identifier of one resource to be sent")
|
|
.SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
|
|
"List of the Orthanc identifiers of all the DICOM resources to be sent", true)
|
|
.SetRequestField(KEY_TRANSCODE, RestApiCallDocumentation::Type_String,
|
|
"Transcode to the provided DICOM transfer syntax before the actual sending", false)
|
|
.SetRequestField(KEY_COMPRESS, RestApiCallDocumentation::Type_Boolean,
|
|
"Whether to compress the DICOM instances using gzip before the actual sending", false)
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
std::string remote = call.GetUriComponent("id", "");
|
|
|
|
Json::Value request;
|
|
std::unique_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
|
|
|
|
GetInstancesToExport(request, *job, remote, call);
|
|
|
|
if (request.type() == Json::objectValue &&
|
|
request.isMember(KEY_TRANSCODE))
|
|
{
|
|
job->SetTranscode(SerializationToolbox::ReadString(request, KEY_TRANSCODE));
|
|
}
|
|
|
|
if (request.type() == Json::objectValue &&
|
|
request.isMember(KEY_COMPRESS))
|
|
{
|
|
job->SetCompress(SerializationToolbox::ReadBoolean(request, KEY_COMPRESS));
|
|
}
|
|
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
WebServiceParameters peer;
|
|
if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
|
|
{
|
|
job->SetPeer(peer);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource,
|
|
"No peer with symbolic name: " + remote);
|
|
}
|
|
}
|
|
|
|
OrthancRestApi::GetApi(call).SubmitCommandsJob
|
|
(call, job.release(), true /* synchronous by default */, request);
|
|
}
|
|
|
|
static void PeerSystem(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get peer system information")
|
|
.SetDescription("Get system information about some Orthanc peer. This corresponds to doing a `GET` request "
|
|
"against the `/system` URI of the remote peer. This route can be used to test connectivity.")
|
|
.SetUriArgument("id", "Identifier of the peer of interest")
|
|
.AddAnswerType(MimeType_Json, "System information about the peer");
|
|
return;
|
|
}
|
|
|
|
std::string remote = call.GetUriComponent("id", "");
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
WebServiceParameters peer;
|
|
if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
|
|
{
|
|
HttpClient client(peer, "system");
|
|
std::string answer;
|
|
|
|
client.SetMethod(HttpMethod_Get);
|
|
|
|
if (!client.Apply(answer))
|
|
{
|
|
LOG(ERROR) << "Unable to get the system info from remote Orthanc peer: " << peer.GetUrl();
|
|
call.GetOutput().SignalError(client.GetLastStatus());
|
|
return;
|
|
}
|
|
|
|
call.GetOutput().AnswerBuffer(answer, MimeType_Json);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource,
|
|
"No peer with symbolic name: " + remote);
|
|
}
|
|
}
|
|
|
|
static void GetPeerConfiguration(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
Json::Value sample;
|
|
sample["HttpHeaders"] = Json::objectValue;
|
|
sample["Password"] = Json::nullValue;
|
|
sample["Pkcs11"] = false;
|
|
sample["Url"] = "http://127.0.1.1:5000/";
|
|
sample["Username"] = "alice";
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get peer configuration")
|
|
.SetDescription("Get detailed information about the configuration of some Orthanc peer")
|
|
.SetUriArgument("id", "Identifier of the peer of interest")
|
|
.AddAnswerType(MimeType_Json, "Configuration of the peer")
|
|
.SetSample(sample);
|
|
return;
|
|
}
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
const std::string peer = call.GetUriComponent("id", "");
|
|
|
|
WebServiceParameters info;
|
|
if (lock.GetConfiguration().LookupOrthancPeer(info, peer))
|
|
{
|
|
Json::Value answer;
|
|
info.FormatPublic(answer);
|
|
call.GetOutput().AnswerJson(answer);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource,
|
|
"No peer with symbolic name: " + peer);
|
|
}
|
|
}
|
|
|
|
static void PeerStoreStraight(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Straight store to peer")
|
|
.SetDescription("Synchronously send the DICOM instance in the POST body to the Orthanc peer "
|
|
"whose identifier is provided in URL, without having to first store it locally within Orthanc. "
|
|
"This is an alternative to command-line tools such as `curl`.")
|
|
.SetUriArgument("id", "Identifier of the modality of interest")
|
|
.AddRequestType(MimeType_Dicom, "DICOM instance to be sent")
|
|
.SetAnswerField("ID", RestApiCallDocumentation::Type_String,
|
|
"Orthanc identifier of the DICOM instance in the remote Orthanc peer")
|
|
.SetAnswerField("ParentPatient", RestApiCallDocumentation::Type_String,
|
|
"Orthanc identifier of the parent patient in the remote Orthanc peer")
|
|
.SetAnswerField("ParentStudy", RestApiCallDocumentation::Type_String,
|
|
"Orthanc identifier of the parent study in the remote Orthanc peer")
|
|
.SetAnswerField("ParentSeries", RestApiCallDocumentation::Type_String,
|
|
"Orthanc identifier of the parent series in the remote Orthanc peer")
|
|
.SetAnswerField("Path", RestApiCallDocumentation::Type_String,
|
|
"Path to the DICOM instance in the remote Orthanc server")
|
|
.SetAnswerField("Status", RestApiCallDocumentation::Type_String,
|
|
"Status of the store operation");
|
|
return;
|
|
}
|
|
|
|
const std::string peer = call.GetUriComponent("id", "");
|
|
|
|
WebServiceParameters info;
|
|
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
if (!lock.GetConfiguration().LookupOrthancPeer(info, peer))
|
|
{
|
|
throw OrthancException(ErrorCode_UnknownResource, "No peer with symbolic name: " + peer);
|
|
}
|
|
}
|
|
|
|
HttpClient client(info, "instances");
|
|
client.SetMethod(HttpMethod_Post);
|
|
client.AddHeader("Expect", "");
|
|
client.SetExternalBody(call.GetBodyData(), call.GetBodySize());
|
|
|
|
Json::Value answer;
|
|
if (client.Apply(answer))
|
|
{
|
|
call.GetOutput().AnswerJson(answer);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Cannot send DICOM to remote peer: " + peer);
|
|
}
|
|
}
|
|
|
|
|
|
// DICOM bridge -------------------------------------------------------------
|
|
|
|
static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities,
|
|
const std::string& id)
|
|
{
|
|
return modalities.find(id) != modalities.end();
|
|
}
|
|
|
|
static void ListModalities(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List DICOM modalities")
|
|
.SetDescription("List all the DICOM modalities that are known to Orthanc. This corresponds either to the content of the "
|
|
"`DicomModalities` configuration option, or to the information stored in the database if "
|
|
"`DicomModalitiesInDatabase` is `true`.")
|
|
.SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
|
|
"If present, retrieve detailed information about the individual DICOM modalities", false)
|
|
.AddAnswerType(MimeType_Json, "JSON array containing either the identifiers of the modalities, or detailed information "
|
|
"about the modalities (if `expand` argument is provided)");
|
|
return;
|
|
}
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
OrthancRestApi::SetOfStrings modalities;
|
|
lock.GetConfiguration().GetListOfDicomModalities(modalities);
|
|
|
|
if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true))
|
|
{
|
|
Json::Value result = Json::objectValue;
|
|
for (OrthancRestApi::SetOfStrings::const_iterator
|
|
it = modalities.begin(); it != modalities.end(); ++it)
|
|
{
|
|
const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it);
|
|
|
|
Json::Value info;
|
|
remote.Serialize(info, true /* force advanced format */);
|
|
result[*it] = info;
|
|
}
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
else // if expand is not present, keep backward compatibility and return an array of modalities ids
|
|
{
|
|
Json::Value result = Json::arrayValue;
|
|
for (OrthancRestApi::SetOfStrings::const_iterator
|
|
it = modalities.begin(); it != modalities.end(); ++it)
|
|
{
|
|
result.append(*it);
|
|
}
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
}
|
|
|
|
|
|
static void ListModalityOperations(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("List operations on modality")
|
|
.SetDescription("List the operations that are available for a DICOM modality.")
|
|
.SetUriArgument("id", "Identifier of the DICOM modality of interest")
|
|
.AddAnswerType(MimeType_Json, "List of the available operations");
|
|
return;
|
|
}
|
|
|
|
OrthancConfiguration::ReaderLock lock;
|
|
|
|
OrthancRestApi::SetOfStrings modalities;
|
|
lock.GetConfiguration().GetListOfDicomModalities(modalities);
|
|
|
|
std::string id = call.GetUriComponent("id", "");
|
|
if (IsExistingModality(modalities, id))
|
|
{
|
|
RestApi::AutoListChildren(call);
|
|
}
|
|
}
|
|
|
|
|
|
static void UpdateModality(RestApiPutCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
DocumentModalityParametersShared(call, true);
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Update DICOM modality")
|
|
.SetDescription("Define a new DICOM modality, or update an existing one. This change is permanent iff. "
|
|
"`DicomModalitiesInDatabase` is `true`, otherwise it is lost at the next restart of Orthanc.")
|
|
.SetUriArgument("id", "Identifier of the new/updated DICOM modality");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
Json::Value json;
|
|
if (call.ParseJsonRequest(json))
|
|
{
|
|
RemoteModalityParameters modality;
|
|
modality.Unserialize(json);
|
|
|
|
{
|
|
OrthancConfiguration::WriterLock lock;
|
|
lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality);
|
|
}
|
|
|
|
context.SignalUpdatedModalities();
|
|
|
|
call.GetOutput().AnswerBuffer("", MimeType_PlainText);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
}
|
|
|
|
|
|
static void DeleteModality(RestApiDeleteCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Delete DICOM modality")
|
|
.SetDescription("Delete one DICOM modality. This change is permanent iff. `DicomModalitiesInDatabase` is `true`, "
|
|
"otherwise it is lost at the next restart of Orthanc.")
|
|
.SetUriArgument("id", "Identifier of the DICOM modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
{
|
|
OrthancConfiguration::WriterLock lock;
|
|
lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", ""));
|
|
}
|
|
|
|
context.SignalUpdatedModalities();
|
|
|
|
call.GetOutput().AnswerBuffer("", MimeType_PlainText);
|
|
}
|
|
|
|
|
|
static void GetModalityConfiguration(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
Json::Value sample;
|
|
sample["AET"] = "ORTHANCTEST";
|
|
sample["AllowEcho"] = true;
|
|
sample["AllowEventReport"] = true;
|
|
sample["AllowFind"] = true;
|
|
sample["AllowFindWorklist"] = true;
|
|
sample["AllowGet"] = true;
|
|
sample["AllowMove"] = true;
|
|
sample["AllowNAction"] = true;
|
|
sample["AllowStore"] = true;
|
|
sample["AllowTranscoding"] = true;
|
|
sample["Host"] = "127.0.1.1";
|
|
sample["Manufacturer"] = "Generic";
|
|
sample["Port"] = 5001;
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get modality configuration")
|
|
.SetDescription("Get detailed information about the configuration of some DICOM modality")
|
|
.SetUriArgument("id", "Identifier of the modality of interest")
|
|
.AddAnswerType(MimeType_Json, "Configuration of the modality")
|
|
.SetSample(sample);
|
|
return;
|
|
}
|
|
|
|
const std::string modality = call.GetUriComponent("id", "");
|
|
|
|
Json::Value answer;
|
|
|
|
{
|
|
OrthancConfiguration::ReaderLock lock;
|
|
lock.GetConfiguration().GetModalityUsingSymbolicName(modality).Serialize(answer, true /* force advanced format */);
|
|
}
|
|
|
|
call.GetOutput().AnswerJson(answer);
|
|
}
|
|
|
|
|
|
static void UpdatePeer(RestApiPutCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Update Orthanc peer")
|
|
.SetDescription("Define a new Orthanc peer, or update an existing one. This change is permanent iff. "
|
|
"`OrthancPeersInDatabase` is `true`, otherwise it is lost at the next restart of Orthanc.")
|
|
.SetUriArgument("id", "Identifier of the new/updated Orthanc peer")
|
|
.SetRequestField("URL", RestApiCallDocumentation::Type_String,
|
|
"URL of the root of the REST API of the remote Orthanc peer, for instance `http://localhost:8042/`", true)
|
|
.SetRequestField("Username", RestApiCallDocumentation::Type_String,
|
|
"Username for the credentials", false)
|
|
.SetRequestField("Password", RestApiCallDocumentation::Type_String,
|
|
"Password for the credentials", false)
|
|
.SetRequestField("CertificateFile", RestApiCallDocumentation::Type_String,
|
|
"SSL certificate for the HTTPS connections", false)
|
|
.SetRequestField("CertificateKeyFile", RestApiCallDocumentation::Type_String,
|
|
"Key file for the SSL certificate for the HTTPS connections", false)
|
|
.SetRequestField("CertificateKeyPassword", RestApiCallDocumentation::Type_String,
|
|
"Key password for the SSL certificate for the HTTPS connections", false)
|
|
.SetRequestField("HttpHeaders", RestApiCallDocumentation::Type_JsonObject,
|
|
"HTTP headers to be used for the connections to the remote peer", false);
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
Json::Value json;
|
|
if (call.ParseJsonRequest(json))
|
|
{
|
|
WebServiceParameters peer;
|
|
peer.Unserialize(json);
|
|
|
|
{
|
|
OrthancConfiguration::WriterLock lock;
|
|
lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer);
|
|
}
|
|
|
|
context.SignalUpdatedPeers();
|
|
|
|
call.GetOutput().AnswerBuffer("", MimeType_PlainText);
|
|
}
|
|
}
|
|
|
|
|
|
static void DeletePeer(RestApiDeleteCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Delete Orthanc peer")
|
|
.SetDescription("Delete one Orthanc peer. This change is permanent iff. `OrthancPeersInDatabase` is `true`, "
|
|
"otherwise it is lost at the next restart of Orthanc.")
|
|
.SetUriArgument("id", "Identifier of the Orthanc peer of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
{
|
|
OrthancConfiguration::WriterLock lock;
|
|
lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", ""));
|
|
}
|
|
|
|
context.SignalUpdatedPeers();
|
|
|
|
call.GetOutput().AnswerBuffer("", MimeType_PlainText);
|
|
}
|
|
|
|
|
|
static void DicomFindWorklist(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
|
|
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("C-FIND SCU for worklist")
|
|
.SetDescription("Trigger C-FIND SCU command against the remote worklists of the DICOM modality "
|
|
"whose identifier is provided in URL")
|
|
.SetUriArgument("id", "Identifier of the modality of interest")
|
|
.SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
|
|
"Associative array containing the filter on the values of the DICOM tags", true)
|
|
.AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching worklists");
|
|
return;
|
|
}
|
|
|
|
Json::Value json;
|
|
if (call.ParseJsonRequest(json))
|
|
{
|
|
std::unique_ptr<ParsedDicomFile> query;
|
|
DicomToJsonFormat format;
|
|
|
|
if (json.isMember(KEY_QUERY))
|
|
{
|
|
// New in Orthanc 1.9.5
|
|
query.reset(ParsedDicomFile::CreateFromJson(json[KEY_QUERY], static_cast<DicomFromJsonFlags>(0),
|
|
"" /* no private creator */));
|
|
format = OrthancRestApi::GetDicomFormat(json, DicomToJsonFormat_Human);
|
|
}
|
|
else
|
|
{
|
|
// Compatibility with Orthanc <= 1.9.4
|
|
query.reset(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
|
|
"" /* no private creator */));
|
|
format = DicomToJsonFormat_Human;
|
|
}
|
|
|
|
DicomFindAnswers answers(true);
|
|
|
|
{
|
|
DicomControlUserConnection connection(GetAssociationParameters(call, json), ScuOperationFlags_FindWorklist);
|
|
connection.FindWorklist(answers, *query);
|
|
}
|
|
|
|
Json::Value result;
|
|
answers.ToJson(result, format);
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
|
|
}
|
|
}
|
|
|
|
|
|
// Storage commitment SCU ---------------------------------------------------
|
|
|
|
static void StorageCommitmentScu(RestApiPostCall& call)
|
|
{
|
|
static const char* const ORTHANC_RESOURCES = "Resources";
|
|
static const char* const DICOM_INSTANCES = "DicomInstances";
|
|
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Trigger storage commitment request")
|
|
.SetDescription("Trigger a storage commitment request to some remote DICOM modality whose identifier is provided "
|
|
"in the URL: https://orthanc.uclouvain.be/book/users/storage-commitment.html#storage-commitment-scu")
|
|
.SetRequestField(ORTHANC_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
|
|
"List of the Orthanc identifiers of the DICOM resources to be checked by storage commitment", true)
|
|
.SetRequestField(DICOM_INSTANCES, RestApiCallDocumentation::Type_JsonListOfObjects,
|
|
"List of DICOM resources that are not necessarily stored within Orthanc, but that must "
|
|
"be checked by storage commitment. This is a list of JSON objects that must contain the "
|
|
"`SOPClassUID` and `SOPInstanceUID` fields.", true)
|
|
.SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
|
|
"Timeout for the storage commitment command (new in Orthanc 1.9.1)", false)
|
|
.SetAnswerField("ID", RestApiCallDocumentation::Type_JsonObject,
|
|
"Identifier of the storage commitment report, to be used with `/storage-commitment/{id}`")
|
|
.SetAnswerField("Path", RestApiCallDocumentation::Type_JsonObject,
|
|
"Root path to the storage commitment report in the REST API")
|
|
.SetUriArgument("id", "Identifier of the modality of interest");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
Json::Value json;
|
|
if (!call.ParseJsonRequest(json) ||
|
|
json.type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Must provide a JSON object with a list of resources");
|
|
}
|
|
else if (!json.isMember(ORTHANC_RESOURCES) &&
|
|
!json.isMember(DICOM_INSTANCES))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Empty storage commitment request, one of these fields is mandatory: \"" +
|
|
std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\"");
|
|
}
|
|
else
|
|
{
|
|
std::list<std::string> sopClassUids, sopInstanceUids;
|
|
|
|
if (json.isMember(ORTHANC_RESOURCES))
|
|
{
|
|
const Json::Value& resources = json[ORTHANC_RESOURCES];
|
|
|
|
if (resources.type() != Json::arrayValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The \"" + std::string(ORTHANC_RESOURCES) +
|
|
"\" field must provide an array of Orthanc resources");
|
|
}
|
|
else
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
|
|
{
|
|
if (resources[i].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The \"" + std::string(ORTHANC_RESOURCES) +
|
|
"\" field must provide an array of strings, found: " + resources[i].toStyledString());
|
|
}
|
|
|
|
std::list<std::string> instances;
|
|
context.GetIndex().GetChildInstances(instances, resources[i].asString());
|
|
|
|
for (std::list<std::string>::const_iterator
|
|
it = instances.begin(); it != instances.end(); ++it)
|
|
{
|
|
std::string sopClassUid, sopInstanceUid;
|
|
DicomMap tags;
|
|
if (context.LookupOrReconstructMetadata(sopClassUid, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid) &&
|
|
context.GetIndex().GetAllMainDicomTags(tags, *it) &&
|
|
tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false))
|
|
{
|
|
sopClassUids.push_back(sopClassUid);
|
|
sopInstanceUids.push_back(sopInstanceUid);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (json.isMember(DICOM_INSTANCES))
|
|
{
|
|
const Json::Value& instances = json[DICOM_INSTANCES];
|
|
|
|
if (instances.type() != Json::arrayValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"The \"" + std::string(DICOM_INSTANCES) +
|
|
"\" field must provide an array of DICOM instances");
|
|
}
|
|
else
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
|
|
{
|
|
if (instances[i].type() == Json::arrayValue)
|
|
{
|
|
if (instances[i].size() != 2 ||
|
|
instances[i][0].type() != Json::stringValue ||
|
|
instances[i][1].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"An instance entry must provide an array with 2 strings: "
|
|
"SOP Class UID and SOP Instance UID");
|
|
}
|
|
else
|
|
{
|
|
sopClassUids.push_back(instances[i][0].asString());
|
|
sopInstanceUids.push_back(instances[i][1].asString());
|
|
}
|
|
}
|
|
else if (instances[i].type() == Json::objectValue)
|
|
{
|
|
if (!instances[i].isMember(SOP_CLASS_UID) ||
|
|
!instances[i].isMember(SOP_INSTANCE_UID) ||
|
|
instances[i][SOP_CLASS_UID].type() != Json::stringValue ||
|
|
instances[i][SOP_INSTANCE_UID].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"An instance entry must provide an object with 2 string fiels: "
|
|
"\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
|
|
std::string(SOP_INSTANCE_UID));
|
|
}
|
|
else
|
|
{
|
|
sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString());
|
|
sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"JSON array or object is expected to specify one "
|
|
"instance to be queried, found: " + instances[i].toStyledString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sopClassUids.size() != sopInstanceUids.size())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier();
|
|
|
|
if (sopClassUids.empty())
|
|
{
|
|
LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid;
|
|
}
|
|
|
|
{
|
|
const RemoteModalityParameters remote =
|
|
MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
|
|
|
|
const std::string& remoteAet = remote.GetApplicationEntityTitle();
|
|
const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
|
|
|
|
// Create a "pending" storage commitment report BEFORE the
|
|
// actual SCU call in order to avoid race conditions
|
|
context.GetStorageCommitmentReports().Store(
|
|
transactionUid, new StorageCommitmentReports::Report(remoteAet));
|
|
|
|
DicomAssociationParameters parameters(localAet, remote);
|
|
InjectAssociationTimeout(parameters, json);
|
|
|
|
std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end());
|
|
std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end());
|
|
DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b);
|
|
}
|
|
|
|
Json::Value result = Json::objectValue;
|
|
result["ID"] = transactionUid;
|
|
result["Path"] = "/storage-commitment/" + transactionUid;
|
|
call.GetOutput().AnswerJson(result);
|
|
}
|
|
}
|
|
|
|
|
|
static void GetStorageCommitmentReport(RestApiGetCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Get storage commitment report")
|
|
.SetDescription("Get the storage commitment report whose identifier is provided in the URL: "
|
|
"https://orthanc.uclouvain.be/book/users/storage-commitment.html#storage-commitment-scu")
|
|
.SetAnswerField("Status", RestApiCallDocumentation::Type_String,
|
|
"Can be `Success`, `Failure`, or `Pending` (the latter means that no report has been received yet)")
|
|
.SetAnswerField("RemoteAET", RestApiCallDocumentation::Type_String,
|
|
"AET of the remote DICOM modality")
|
|
.SetAnswerField("Failures", RestApiCallDocumentation::Type_JsonListOfObjects,
|
|
"List of failures that have been encountered during the storage commitment request")
|
|
.SetAnswerField("Success", RestApiCallDocumentation::Type_JsonListOfObjects,
|
|
"List of DICOM instances that have been acknowledged by the remote modality, "
|
|
"each one is reported as a JSON object containing the `SOPClassUID` and "
|
|
"`SOPInstanceUID` DICOM tags")
|
|
.SetUriArgument("id", "Identifier of the storage commitment report");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
const std::string& transactionUid = call.GetUriComponent("id", "");
|
|
|
|
{
|
|
StorageCommitmentReports::Accessor accessor(
|
|
context.GetStorageCommitmentReports(), transactionUid);
|
|
|
|
if (accessor.IsValid())
|
|
{
|
|
Json::Value json;
|
|
accessor.GetReport().Format(json);
|
|
call.GetOutput().AnswerJson(json);
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_InexistentItem,
|
|
"No storage commitment transaction with UID: " + transactionUid);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void RemoveAfterStorageCommitment(RestApiPostCall& call)
|
|
{
|
|
if (call.IsDocumentation())
|
|
{
|
|
call.GetDocumentation()
|
|
.SetTag("Networking")
|
|
.SetSummary("Remove after storage commitment")
|
|
.SetDescription("Remove out of Orthanc, the DICOM instances that have been reported to have been properly "
|
|
"received the storage commitment report whose identifier is provided in the URL. This is "
|
|
"only possible if the `Status` of the storage commitment report is `Success`. "
|
|
"https://orthanc.uclouvain.be/book/users/storage-commitment.html#removing-the-instances")
|
|
.SetUriArgument("id", "Identifier of the storage commitment report");
|
|
return;
|
|
}
|
|
|
|
ServerContext& context = OrthancRestApi::GetContext(call);
|
|
|
|
const std::string& transactionUid = call.GetUriComponent("id", "");
|
|
|
|
{
|
|
StorageCommitmentReports::Accessor accessor(
|
|
context.GetStorageCommitmentReports(), transactionUid);
|
|
|
|
if (!accessor.IsValid())
|
|
{
|
|
throw OrthancException(ErrorCode_InexistentItem,
|
|
"No storage commitment transaction with UID: " + transactionUid);
|
|
}
|
|
else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success)
|
|
{
|
|
throw OrthancException(ErrorCode_BadSequenceOfCalls,
|
|
"Cannot remove DICOM instances after failure "
|
|
"in storage commitment transaction: " + transactionUid);
|
|
}
|
|
else
|
|
{
|
|
std::vector<std::string> sopInstanceUids;
|
|
accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids);
|
|
|
|
for (size_t i = 0; i < sopInstanceUids.size(); i++)
|
|
{
|
|
std::vector<std::string> orthancId;
|
|
context.GetIndex().LookupIdentifierExact(
|
|
orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]);
|
|
|
|
for (size_t j = 0; j < orthancId.size(); j++)
|
|
{
|
|
CLOG(INFO, HTTP) << "Storage commitment - Removing SOP instance UID / Orthanc ID: "
|
|
<< sopInstanceUids[i] << " / " << orthancId[j];
|
|
|
|
Json::Value tmp;
|
|
context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance);
|
|
}
|
|
}
|
|
|
|
call.GetOutput().AnswerBuffer("{}", MimeType_Json);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void OrthancRestApi::RegisterModalities()
|
|
{
|
|
Register("/modalities", ListModalities);
|
|
Register("/modalities/{id}", ListModalityOperations);
|
|
Register("/modalities/{id}", UpdateModality);
|
|
Register("/modalities/{id}", DeleteModality);
|
|
Register("/modalities/{id}/echo", DicomEcho);
|
|
Register("/modalities/{id}/find-patient", DicomFindPatient);
|
|
Register("/modalities/{id}/find-study", DicomFindStudy);
|
|
Register("/modalities/{id}/find-series", DicomFindSeries);
|
|
Register("/modalities/{id}/find-instance", DicomFindInstance);
|
|
Register("/modalities/{id}/find", DicomFind);
|
|
Register("/modalities/{id}/store", DicomStore);
|
|
Register("/modalities/{id}/store-straight", DicomStoreStraight); // New in 1.6.1
|
|
Register("/modalities/{id}/move", DicomMove);
|
|
Register("/modalities/{id}/get", DicomGet);
|
|
Register("/modalities/{id}/configuration", GetModalityConfiguration); // New in 1.8.1
|
|
|
|
// For Query/Retrieve
|
|
Register("/modalities/{id}/query", DicomQuery);
|
|
Register("/queries", ListQueries);
|
|
Register("/queries/{id}", DeleteQuery);
|
|
Register("/queries/{id}", ListQueryOperations);
|
|
Register("/queries/{id}/answers", ListQueryAnswers);
|
|
Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
|
|
Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
|
|
Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
|
|
Register("/queries/{id}/answers/{index}/query-instances",
|
|
QueryAnswerChildren<ResourceType_Instance>);
|
|
Register("/queries/{id}/answers/{index}/query-series",
|
|
QueryAnswerChildren<ResourceType_Series>);
|
|
Register("/queries/{id}/answers/{index}/query-studies",
|
|
QueryAnswerChildren<ResourceType_Study>);
|
|
Register("/queries/{id}/level", GetQueryLevel);
|
|
Register("/queries/{id}/modality", GetQueryModality);
|
|
Register("/queries/{id}/query", GetQueryArguments);
|
|
Register("/queries/{id}/retrieve", RetrieveAllAnswers);
|
|
|
|
Register("/peers", ListPeers);
|
|
Register("/peers/{id}", ListPeerOperations);
|
|
Register("/peers/{id}", UpdatePeer);
|
|
Register("/peers/{id}", DeletePeer);
|
|
Register("/peers/{id}/store", PeerStore);
|
|
Register("/peers/{id}/system", PeerSystem);
|
|
Register("/peers/{id}/configuration", GetPeerConfiguration); // New in 1.8.1
|
|
Register("/peers/{id}/store-straight", PeerStoreStraight); // New in 1.9.1
|
|
|
|
Register("/modalities/{id}/find-worklist", DicomFindWorklist);
|
|
|
|
// Storage commitment
|
|
Register("/modalities/{id}/storage-commitment", StorageCommitmentScu);
|
|
Register("/storage-commitment/{id}", GetStorageCommitmentReport);
|
|
Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment);
|
|
|
|
Register("/tools/dicom-echo", DicomEchoTool); // New in 1.8.1
|
|
}
|
|
}
|