# HG changeset patch # User Sebastien Jodogne # Date 1354269454 -3600 # Node ID 209ca3f6db629a6c1b1b8c6dea8ed7e88abe4d63 # Parent 8a26a8e85edf00b5f2ae87a8100cedb28c9fc78a dicom-scu from rest diff -r 8a26a8e85edf -r 209ca3f6db62 OrthancServer/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi.cpp Fri Nov 30 09:45:29 2012 +0100 +++ b/OrthancServer/OrthancRestApi.cpp Fri Nov 30 10:57:34 2012 +0100 @@ -290,32 +290,13 @@ // DICOM bridge ------------------------------------------------------------- - if ((uri.size() == 2 || - uri.size() == 3) && + if (uri.size() == 3 && uri[0] == "modalities") { if (modalities_.find(uri[1]) == modalities_.end()) { // Unknown modality } - else if (uri.size() == 2) - { - if (method != "GET") - { - output.SendMethodNotAllowedError("POST"); - return; - } - else - { - existingResource = true; - result = Json::arrayValue; - result.append("find-patient"); - result.append("find-study"); - result.append("find-series"); - result.append("find"); - result.append("store"); - } - } else if (uri.size() == 3) { if (uri[2] != "find-patient" && diff -r 8a26a8e85edf -r 209ca3f6db62 OrthancServer/OrthancRestApi2.cpp --- a/OrthancServer/OrthancRestApi2.cpp Fri Nov 30 09:45:29 2012 +0100 +++ b/OrthancServer/OrthancRestApi2.cpp Fri Nov 30 10:57:34 2012 +0100 @@ -32,10 +32,11 @@ #include "OrthancRestApi2.h" -#include "OrthancInitialization.h" +#include "../Core/HttpServer/FilesystemHttpSender.h" +#include "../Core/Uuid.h" +#include "DicomProtocol/DicomUserConnection.h" #include "FromDcmtkBridge.h" -#include "../Core/Uuid.h" -#include "../Core/HttpServer/FilesystemHttpSender.h" +#include "OrthancInitialization.h" #include "ServerToolbox.h" #include @@ -44,14 +45,241 @@ #include -#define RETRIEVE_CONTEXT(call) \ - OrthancRestApi2& contextApi = \ - dynamic_cast(call.GetContext()); \ +#define RETRIEVE_CONTEXT(call) \ + OrthancRestApi2& contextApi = \ + dynamic_cast(call.GetContext()); \ ServerContext& context = contextApi.GetContext() +#define RETRIEVE_MODALITIES(call) \ + const OrthancRestApi2::Modalities& modalities = \ + dynamic_cast(call.GetContext()).GetModalities(); + + namespace Orthanc { + // DICOM SCU ---------------------------------------------------------------- + + static void ConnectToModality(DicomUserConnection& connection, + const std::string& name) + { + std::string aet, address; + int port; + GetDicomModality(name, aet, address, port); + connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); + connection.SetDistantApplicationEntityTitle(aet); + connection.SetDistantHost(address); + connection.SetDistantPort(port); + connection.Open(); + } + + static bool MergeQueryAndTemplate(DicomMap& result, + const std::string& postData) + { + Json::Value query; + Json::Reader reader; + + if (!reader.parse(postData, 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::FindTag(members[i]); + result.SetValue(t, query[members[i]].asString()); + } + + return true; + } + + static void DicomFindPatient(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindPatientTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindPatient(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindStudy(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindStudyTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindStudy(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindSeries(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindSeriesTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindSeries(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFind(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindPatientTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers patients; + connection.FindPatient(patients, m); + + // Loop over the found patients + Json::Value result = Json::arrayValue; + for (size_t i = 0; i < patients.GetSize(); i++) + { + Json::Value patient(Json::objectValue); + FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); + + DicomMap::SetupFindStudyTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); + + DicomFindAnswers studies; + connection.FindStudy(studies, m); + + patient["Studies"] = Json::arrayValue; + + // Loop over the found studies + for (size_t j = 0; j < studies.GetSize(); j++) + { + Json::Value study(Json::objectValue); + FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); + + DicomMap::SetupFindSeriesTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); + m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); + + DicomFindAnswers series; + connection.FindSeries(series, m); + + // Loop over the found series + study["Series"] = Json::arrayValue; + for (size_t k = 0; k < series.GetSize(); k++) + { + Json::Value series2(Json::objectValue); + FromDcmtkBridge::ToJson(series2, series.GetAnswer(k)); + study["Series"].append(series2); + } + + patient["Studies"].append(study); + } + + result.append(patient); + } + + call.GetOutput().AnswerJson(result); + } + + + static void DicomStore(RestApi::PostCall& call) + { + RETRIEVE_CONTEXT(call); + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + Json::Value found; + if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Series)) + { + // The UUID corresponds to a series + for (Json::Value::ArrayIndex i = 0; i < found["Instances"].size(); i++) + { + std::string instanceId = found["Instances"][i].asString(); + std::string dicom; + context.ReadFile(dicom, instanceId, AttachedFileType_Dicom); + connection.Store(dicom); + } + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Instance)) + { + // The UUID corresponds to an instance + std::string instanceId = call.GetPostBody(); + std::string dicom; + context.ReadFile(dicom, instanceId, AttachedFileType_Dicom); + connection.Store(dicom); + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + // The POST body is not a known resource, assume that it + // contains a raw DICOM instance + connection.Store(call.GetPostBody()); + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + } + + + // System information ------------------------------------------------------- static void ServeRoot(RestApi::GetCall& call) @@ -66,8 +294,11 @@ Json::Value result = Json::objectValue; result["Version"] = ORTHANC_VERSION; result["Name"] = GetGlobalStringParameter("Name", ""); - result["TotalCompressedSize"] = boost::lexical_cast(context.GetIndex().GetTotalCompressedSize()); - result["TotalUncompressedSize"] = boost::lexical_cast(context.GetIndex().GetTotalUncompressedSize()); + result["TotalCompressedSize"] = boost::lexical_cast + (context.GetIndex().GetTotalCompressedSize()); + result["TotalUncompressedSize"] = boost::lexical_cast + (context.GetIndex().GetTotalUncompressedSize()); + call.GetOutput().AnswerJson(result); } @@ -213,9 +444,6 @@ { RETRIEVE_CONTEXT(call); - CompressionType compressionType; - std::string fileUuid; - std::string publicId = call.GetUriComponent("id", ""); std::string frameId = call.GetUriComponent("frame", "0"); unsigned int frame; @@ -228,35 +456,31 @@ return; } - if (context.GetIndex().GetFile(fileUuid, compressionType, publicId, AttachedFileType_Dicom)) - { - assert(compressionType == CompressionType_None); - - std::string dicomContent, png; - context.GetFileStorage().ReadFile(dicomContent, fileUuid); + std::string publicId = call.GetUriComponent("id", ""); + std::string dicomContent, png; + context.ReadFile(dicomContent, publicId, AttachedFileType_Dicom); - try - { - FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); - call.GetOutput().AnswerBuffer(png, "image/png"); - } - catch (OrthancException& e) + try + { + FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); + call.GetOutput().AnswerBuffer(png, "image/png"); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) { - if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) - { - // The frame number is out of the range for this DICOM - // instance, the resource is not existent - } - else + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) { - std::string root = ""; - for (size_t i = 1; i < call.GetFullUri().size(); i++) - { - root += "../"; - } + root += "../"; + } - call.GetOutput().Redirect(root + "app/images/unsupported.png"); - } + call.GetOutput().Redirect(root + "app/images/unsupported.png"); } } } @@ -318,15 +542,19 @@ // DICOM bridge ------------------------------------------------------------- + static bool IsExistingModality(const OrthancRestApi2::Modalities& modalities, + const std::string& id) + { + return modalities.find(id) != modalities.end(); + } + static void ListModalities(RestApi::GetCall& call) { - const OrthancRestApi2::Modalities& m = - dynamic_cast(call.GetContext()).GetModalities(); + RETRIEVE_MODALITIES(call); Json::Value result = Json::arrayValue; - for (OrthancRestApi2::Modalities::const_iterator - it = m.begin(); it != m.end(); it++) + it = modalities.begin(); it != modalities.end(); it++) { result.append(*it); } @@ -335,6 +563,24 @@ } + static void ListModalityOperations(RestApi::GetCall& call) + { + RETRIEVE_MODALITIES(call); + + std::string id = call.GetUriComponent("id", ""); + if (IsExistingModality(modalities, id)) + { + Json::Value result = Json::arrayValue; + result.append("find-patient"); + result.append("find-study"); + result.append("find-series"); + result.append("find"); + result.append("store"); + call.GetOutput().AnswerJson(result); + } + } + + // Registration of the various REST handlers -------------------------------- @@ -346,7 +592,6 @@ Register("/", ServeRoot); Register("/system", GetSystemInformation); Register("/changes", GetChanges); - Register("/modalities", ListModalities); Register("/instances", UploadDicomFile); Register("/instances", ListResources); @@ -375,6 +620,14 @@ Register("/instances/{id}/image-uint8", GetImage); Register("/instances/{id}/image-uint16", GetImage); + Register("/modalities", ListModalities); + Register("/modalities/{id}", ListModalityOperations); + Register("/modalities/{id}/find-patient", DicomFindPatient); + Register("/modalities/{id}/find-study", DicomFindStudy); + Register("/modalities/{id}/find-series", DicomFindSeries); + Register("/modalities/{id}/find", DicomFind); + Register("/modalities/{id}/store", DicomStore); + // TODO : "content" } } diff -r 8a26a8e85edf -r 209ca3f6db62 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Fri Nov 30 09:45:29 2012 +0100 +++ b/OrthancServer/ServerContext.cpp Fri Nov 30 10:57:34 2012 +0100 @@ -36,6 +36,16 @@ #include + +/** + * IMPORTANT: We make the assumption that the same instance of + * FileStorage can be accessed from multiple threads. This seems OK + * since the filesystem implements the required locking mechanisms, + * but maybe a read-writer lock on the "FileStorage" could be + * useful. Conversely, "ServerIndex" already implements mutex-based + * locking. + **/ + namespace Orthanc { ServerContext::ServerContext(const boost::filesystem::path& path) : @@ -44,6 +54,11 @@ { } + void ServerContext::RemoveFile(const std::string& fileUuid) + { + storage_.Remove(fileUuid); + } + StoreStatus ServerContext::Store(const char* dicomFile, size_t dicomSize, const DicomMap& dicomSummary, @@ -102,17 +117,8 @@ void ServerContext::ReadJson(Json::Value& result, const std::string& instancePublicId) { - CompressionType compressionType; - std::string fileUuid; - if (!index_.GetFile(fileUuid, compressionType, instancePublicId, AttachedFileType_Json)) - { - throw OrthancException(ErrorCode_InternalError); - } - - assert(compressionType == CompressionType_None); - std::string s; - storage_.ReadFile(s, fileUuid); + ReadFile(s, instancePublicId, AttachedFileType_Json); Json::Reader reader; if (!reader.parse(s, result)) @@ -120,4 +126,20 @@ throw OrthancException("Corrupted JSON file"); } } + + + void ServerContext::ReadFile(std::string& result, + const std::string& instancePublicId, + AttachedFileType content) + { + CompressionType compressionType; + std::string fileUuid; + if (!index_.GetFile(fileUuid, compressionType, instancePublicId, content)) + { + throw OrthancException(ErrorCode_InternalError); + } + + assert(compressionType == CompressionType_None); + storage_.ReadFile(result, fileUuid); + } } diff -r 8a26a8e85edf -r 209ca3f6db62 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Fri Nov 30 09:45:29 2012 +0100 +++ b/OrthancServer/ServerContext.h Fri Nov 30 10:57:34 2012 +0100 @@ -38,6 +38,11 @@ namespace Orthanc { + /** + * This class is responsible for maintaining the storage area on the + * filesystem (including compression), as well as the index of the + * DICOM store. It implements the required locking mechanisms. + **/ class ServerContext { private: @@ -52,16 +57,7 @@ return index_; } - // TODO REMOVE THIS, SINCE IT IS NOT PROTECTED BY MUTEXES - FileStorage& GetFileStorage() - { - return storage_; - } - - void RemoveFile(const std::string& fileUuid) - { - storage_.Remove(fileUuid); - } + void RemoveFile(const std::string& fileUuid); StoreStatus Store(const char* dicomFile, size_t dicomSize, @@ -75,5 +71,10 @@ void ReadJson(Json::Value& result, const std::string& instancePublicId); + + // TODO CACHING MECHANISM AT THIS POINT + void ReadFile(std::string& result, + const std::string& instancePublicId, + AttachedFileType content); }; } diff -r 8a26a8e85edf -r 209ca3f6db62 Resources/Configuration.json --- a/Resources/Configuration.json Fri Nov 30 09:45:29 2012 +0100 +++ b/Resources/Configuration.json Fri Nov 30 10:57:34 2012 +0100 @@ -66,7 +66,12 @@ // The list of the known DICOM modalities "DicomModalities" : { - // "sample" : [ "SAMPLESCP", "192.168.100.42", 104 ] + /** + * Uncommenting the following line would enable Orthanc to + * connect to an instance of the "storescp" open-source DICOM + * store started by the command line "storescp 2000". + **/ + // "sample" : [ "STORESCP", "localhost", 2000 ] }, // The list of the known Orthanc peers (currently unused)