Mercurial > hg > orthanc
diff OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | OrthancServer/OrthancRestApi/OrthancRestModalities.cpp@5797ca4f3b8d |
children | 05b8fd21089c |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,1647 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../../Core/Cache/SharedArchive.h" +#include "../../Core/DicomNetworking/DicomAssociation.h" +#include "../../Core/DicomNetworking/DicomControlUserConnection.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" + +#include "../OrthancConfiguration.h" +#include "../QueryRetrieveHandler.h" +#include "../ServerContext.h" +#include "../ServerJobs/DicomModalityStoreJob.h" +#include "../ServerJobs/DicomMoveScuJob.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_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 SOP_CLASS_UID = "SOPClassUID"; + static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; + + 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) + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object"); + } + else if (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; + call.ParseJsonRequest(body); + return GetAssociationParameters(call, body); + } + + + /*************************************************************************** + * DICOM C-Echo SCU + ***************************************************************************/ + + static void DicomEcho(RestApiPostCall& call) + { + DicomControlUserConnection connection(GetAssociationParameters(call)); + + if (connection.Echo()) + { + // Echo has succeeded + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + return; + } + else + { + // Echo has failed + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } + } + + + + /*************************************************************************** + * 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) + { + 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)); + FindPatient(answers, connection, fields); + } + + Json::Value result; + answers.ToJson(result, true); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindStudy(RestApiPostCall& call) + { + 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)); + FindStudy(answers, connection, fields); + } + + Json::Value result; + answers.ToJson(result, true); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindSeries(RestApiPostCall& call) + { + 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)); + FindSeries(answers, connection, fields); + } + + Json::Value result; + answers.ToJson(result, true); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindInstance(RestApiPostCall& call) + { + 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)); + FindInstance(answers, connection, fields); + } + + Json::Value result; + answers.ToJson(result, true); + call.GetOutput().AnswerJson(result); + } + + + static void CopyTagIfExists(DicomMap& target, + ParsedDicomFile& source, + const DicomTag& tag) + { + std::string tmp; + if (source.GetTagValue(tmp, tag)) + { + target.SetValue(tag, tmp, false); + } + } + + + static void DicomFind(RestApiPostCall& call) + { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); + + DicomMap m; + DicomMap::SetupFindPatientTemplate(m); + if (!MergeQueryAndTemplate(m, call)) + { + return; + } + + DicomControlUserConnection connection(GetAssociationParameters(call)); + + 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, true); + + 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, true); + + 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, true); + 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) + { + 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 + { + 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()); + } + + AnswerQueryHandler(call, handler); + } + } + + + static void ListQueries(RestApiGetCall& call) + { + 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: + 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(RestApiCall& call, + const DicomMap& value, + bool simplify) + { + Json::Value full = Json::objectValue; + FromDcmtkBridge::ToJson(full, value, simplify); + call.GetOutput().AnswerJson(full); + } + } + + + static void ListQueryAnswers(RestApiGetCall& call) + { + const bool expand = call.HasArgument("expand"); + const bool simplify = call.HasArgument("simplify"); + + 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, simplify); + + result.append(json); + } + else + { + result.append(boost::lexical_cast<std::string>(i)); + } + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetQueryOneAnswer(RestApiGetCall& call) + { + size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + + QueryAccessor query(call); + + DicomMap map; + query.GetHandler().GetAnswer(map, index); + + AnswerDicomMap(call, map, call.HasArgument("simplify")); + } + + + static void SubmitRetrieveJob(RestApiPostCall& call, + bool allAnswers, + size_t index) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string targetAet; + int timeout = -1; + + Json::Value body; + if (call.ParseJsonRequest(body)) + { + targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); + timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1); + } + else + { + body = Json::objectValue; + if (call.GetBodySize() > 0) + { + call.BodyToString(targetAet); + } + else + { + targetAet = context.GetDefaultLocalApplicationEntityTitle(); + } + } + + std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context)); + + { + QueryAccessor query(call); + job->SetTargetAet(targetAet); + 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)); + } + + LOG(WARNING) << "Driving C-Move SCU on remote modality " + << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle() + << " to target modality " << targetAet; + + 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 RetrieveOneAnswer(RestApiPostCall& call) + { + size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + SubmitRetrieveJob(call, false, index); + } + + + static void RetrieveAllAnswers(RestApiPostCall& call) + { + SubmitRetrieveJob(call, true, 0); + } + + + static void GetQueryArguments(RestApiGetCall& call) + { + QueryAccessor query(call); + AnswerDicomMap(call, query.GetHandler().GetQuery(), call.HasArgument("simplify")); + } + + + static void GetQueryLevel(RestApiGetCall& call) + { + QueryAccessor query(call); + call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText); + } + + + static void GetQueryModality(RestApiGetCall& call) + { + QueryAccessor query(call); + call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText); + } + + + static void DeleteQuery(RestApiDeleteCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", "")); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + static void ListQueryOperations(RestApiGetCall& call) + { + // Ensure that the query of interest does exist + QueryAccessor query(call); + + RestApi::AutoListChildren(call); + } + + + static void ListQueryAnswerOperations(RestApiGetCall& call) + { + // 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); + + 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); + + 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) + { + 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, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle()); + int moveOriginatorID = Toolbox::GetJsonIntegerField + (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */); + + job->SetLocalAet(localAet); + job->SetRemoteModality(MyGetModalityUsingSymbolicName(remote)); + + if (moveOriginatorID != 0) + { + job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID); + } + + // New in Orthanc 1.6.0 + if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", 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) + { + 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); + } + + + /*************************************************************************** + * DICOM C-Move SCU + ***************************************************************************/ + + static void DicomMove(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + + 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()); + std::string targetAet = Toolbox::GetJsonStringField + (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); + + const RemoteModalityParameters source = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + DicomAssociationParameters params(localAet, source); + InjectAssociationTimeout(params, request); + + DicomControlUserConnection connection(params); + + for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) + { + DicomMap resource; + FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i]); + + connection.Move(targetAet, level, resource); + } + + // Move has succeeded + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + + + + /*************************************************************************** + * 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) + { + OrthancConfiguration::ReaderLock lock; + + OrthancRestApi::SetOfStrings peers; + lock.GetConfiguration().GetListOfOrthancPeers(peers); + + if (call.HasArgument("expand")) + { + 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) + { + 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) + { + 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); + + static const char* TRANSCODE = "Transcode"; + if (request.type() == Json::objectValue && + request.isMember(TRANSCODE)) + { + job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE)); + } + + { + 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) + { + 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); + } + } + + // DICOM bridge ------------------------------------------------------------- + + static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities, + const std::string& id) + { + return modalities.find(id) != modalities.end(); + } + + static void ListModalities(RestApiGetCall& call) + { + OrthancConfiguration::ReaderLock lock; + + OrthancRestApi::SetOfStrings modalities; + lock.GetConfiguration().GetListOfDicomModalities(modalities); + + if (call.HasArgument("expand")) + { + 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) + { + 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) + { + 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); + } + } + + + static void DeleteModality(RestApiDeleteCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", "")); + } + + context.SignalUpdatedModalities(); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + static void UpdatePeer(RestApiPutCall& call) + { + 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) + { + 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) + { + Json::Value json; + if (call.ParseJsonRequest(json)) + { + std::unique_ptr<ParsedDicomFile> query + (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0), + "" /* no private creator */)); + + DicomFindAnswers answers(true); + + { + DicomControlUserConnection connection(GetAssociationParameters(call, json)); + connection.FindWorklist(answers, *query); + } + + Json::Value result; + answers.ToJson(result, true); + 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"; + + 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, 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); + + 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) + { + 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) + { + 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++) + { + LOG(INFO) << "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); + + // 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("/modalities/{id}/find-worklist", DicomFindWorklist); + + // Storage commitment + Register("/modalities/{id}/storage-commitment", StorageCommitmentScu); + Register("/storage-commitment/{id}", GetStorageCommitmentReport); + Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment); + } +}