# HG changeset patch # User Sebastien Jodogne # Date 1544202236 -3600 # Node ID 94c8222c52b7dfa762aeec9e239782a2ba980b8e # Parent 63b724c7b046f5964dc8d093ecc623a77c943071 New URIs to launch new C-FIND to explore the hierarchy of a C-FIND answer diff -r 63b724c7b046 -r 94c8222c52b7 Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Thu Dec 06 13:10:24 2018 +0100 +++ b/Core/SerializationToolbox.cpp Fri Dec 07 18:03:56 2018 +0100 @@ -36,10 +36,33 @@ #include "OrthancException.h" +#if ORTHANC_ENABLE_DCMTK == 1 +# include "DicomParsing/FromDcmtkBridge.h" +#endif + namespace Orthanc { namespace SerializationToolbox { + static bool ParseTagInternal(DicomTag& tag, + const char* name) + { +#if ORTHANC_ENABLE_DCMTK == 1 + try + { + tag = FromDcmtkBridge::ParseTag(name); + return true; + } + catch (OrthancException& e) + { + return false; + } +#else + return DicomTag::ParseHexadecimal(tag, name)) +#endif + } + + std::string ReadString(const Json::Value& value, const std::string& field) { @@ -191,7 +214,7 @@ DicomTag tag(0, 0); if (arr[i].type() != Json::stringValue || - !DicomTag::ParseHexadecimal(tag, arr[i].asCString())) + !ParseTagInternal(tag, arr[i].asCString())) { throw OrthancException(ErrorCode_BadFileFormat, "Set of DICOM tags expected in field: " + field); @@ -263,7 +286,7 @@ DicomTag tag(0, 0); - if (!DicomTag::ParseHexadecimal(tag, members[i].c_str()) || + if (!ParseTagInternal(tag, members[i].c_str()) || tmp.type() != Json::stringValue) { throw OrthancException(ErrorCode_BadFileFormat, diff -r 63b724c7b046 -r 94c8222c52b7 NEWS --- a/NEWS Thu Dec 06 13:10:24 2018 +0100 +++ b/NEWS Fri Dec 07 18:03:56 2018 +0100 @@ -26,8 +26,12 @@ * New URI: "/studies/.../split" to split a study * POST-ing a DICOM file to "/instances" also answers the patient/study/series ID * GET "/modalities/..." now returns a JSON object instead of a JSON array +* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option) * New options to URI "/queries/.../answers": "?expand" and "?simplify" -* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option) +* New URIs to launch new C-FIND to explore the hierarchy of a C-FIND answer: + - "/queries/.../answers/.../query-instances" to C-FIND child instances + - "/queries/.../answers/.../query-series" to C-FIND child series + - "/queries/.../answers/.../query-studies" to C-FIND child studies Plugins ------- diff -r 63b724c7b046 -r 94c8222c52b7 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Dec 06 13:10:24 2018 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri Dec 07 18:03:56 2018 +0100 @@ -47,6 +47,11 @@ namespace Orthanc { + static const char* const KEY_LEVEL = "Level"; + static const char* const KEY_QUERY = "Query"; + static const char* const KEY_RESOURCES = "Resources"; + + static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name) { OrthancConfiguration::ReaderLock lock; @@ -408,6 +413,27 @@ * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0 ***************************************************************************/ + static void AnswerQueryHandler(RestApiPostCall& call, + std::auto_ptr& 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); @@ -415,31 +441,27 @@ if (call.ParseJsonRequest(request) && request.type() == Json::objectValue && - request.isMember("Level") && request["Level"].type() == Json::stringValue && - (!request.isMember("Query") || request["Query"].type() == Json::objectValue)) + request.isMember(KEY_LEVEL) && request[KEY_LEVEL].type() == Json::stringValue && + (!request.isMember(KEY_QUERY) || request[KEY_QUERY].type() == Json::objectValue)) { std::auto_ptr handler(new QueryRetrieveHandler(context)); handler->SetModality(call.GetUriComponent("id", "")); - handler->SetLevel(StringToResourceType(request["Level"].asCString())); + handler->SetLevel(StringToResourceType(request[KEY_LEVEL].asCString())); - if (request.isMember("Query")) + if (request.isMember(KEY_QUERY)) { - Json::Value::Members tags = request["Query"].getMemberNames(); - for (size_t i = 0; i < tags.size(); i++) + std::map query; + SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY); + + for (std::map::const_iterator + it = query.begin(); it != query.end(); ++it) { - handler->SetQuery(FromDcmtkBridge::ParseTag(tags[i].c_str()), - request["Query"][tags[i]].asString()); + handler->SetQuery(it->first, it->second); } } - 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); + AnswerQueryHandler(call, handler); } } @@ -469,19 +491,28 @@ private: ServerContext& context_; SharedArchive::Accessor accessor_; - QueryRetrieveHandler& handler_; + QueryRetrieveHandler* handler_; public: QueryAccessor(RestApiCall& call) : context_(OrthancRestApi::GetContext(call)), accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")), - handler_(dynamic_cast(accessor_.GetItem())) + handler_(NULL) { + if (accessor_.IsValid()) + { + handler_ = &dynamic_cast(accessor_.GetItem()); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } QueryRetrieveHandler& GetHandler() const { - return handler_; + assert(handler_ != NULL); + return *handler_; } }; @@ -652,10 +683,98 @@ DicomMap map; query.GetHandler().GetAnswer(map, index); - RestApi::AutoListChildren(call); + 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 + static void AnswerQueryChildren(RestApiPostCall& call) + { + // New in Orthanc 1.4.3 + assert(CHILDREN_LEVEL == ResourceType_Study || + CHILDREN_LEVEL == ResourceType_Series || + CHILDREN_LEVEL == ResourceType_Instance); + + ServerContext& context = OrthancRestApi::GetContext(call); + + std::auto_ptr handler(new QueryRetrieveHandler(context)); + + { + const QueryAccessor parent(call); + const ResourceType level = parent.GetHandler().GetLevel(); + + const size_t index = boost::lexical_cast(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->SetModality(parent.GetHandler().GetModalitySymbolicName()); + handler->SetLevel(CHILDREN_LEVEL); + + if (request.isMember(KEY_QUERY)) + { + std::map query; + SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY); + + for (std::map::const_iterator + it = query.begin(); it != query.end(); ++it) + { + handler->SetQuery(it->first, it->second); + } + } + } + } + + AnswerQueryHandler(call, handler); + } + /*************************************************************************** @@ -701,12 +820,12 @@ else { if (request.type() != Json::objectValue || - !request.isMember("Resources")) + !request.isMember(KEY_RESOURCES)) { return false; } - resources = &request["Resources"]; + resources = &request[KEY_RESOURCES]; if (!resources->isArray()) { return false; @@ -794,20 +913,17 @@ Json::Value request; - static const char* RESOURCES = "Resources"; - static const char* LEVEL = "Level"; - if (!call.ParseJsonRequest(request) || request.type() != Json::objectValue || - !request.isMember(RESOURCES) || - !request.isMember(LEVEL) || - request[RESOURCES].type() != Json::arrayValue || - request[LEVEL].type() != Json::stringValue) + !request.isMember(KEY_RESOURCES) || + !request.isMember(KEY_LEVEL) || + request[KEY_RESOURCES].type() != Json::arrayValue || + request[KEY_LEVEL].type() != Json::stringValue) { throw OrthancException(ErrorCode_BadFileFormat); } - ResourceType level = StringToResourceType(request["Level"].asCString()); + ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); std::string localAet = Toolbox::GetJsonStringField (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); @@ -820,10 +936,10 @@ DicomUserConnection connection(localAet, source); connection.Open(); - for (Json::Value::ArrayIndex i = 0; i < request[RESOURCES].size(); i++) + for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) { DicomMap resource; - FromDcmtkBridge::FromJson(resource, request[RESOURCES][i]); + FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i]); connection.Move(targetAet, level, resource); } @@ -1075,7 +1191,8 @@ RemoteModalityParameters remote = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - std::auto_ptr query(ParsedDicomFile::CreateFromJson(json, static_cast(0))); + std::auto_ptr query + (ParsedDicomFile::CreateFromJson(json, static_cast(0))); DicomFindAnswers answers(true); @@ -1116,6 +1233,12 @@ 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", + AnswerQueryChildren); + Register("/queries/{id}/answers/{index}/query-series", + AnswerQueryChildren); + Register("/queries/{id}/answers/{index}/query-studies", + AnswerQueryChildren); Register("/queries/{id}/level", GetQueryLevel); Register("/queries/{id}/modality", GetQueryModality); Register("/queries/{id}/query", GetQueryArguments);