# HG changeset patch # User Sebastien Jodogne # Date 1382603591 -7200 # Node ID f27923072afda7a15fc2705d8b3bf4c82c77f079 # Parent 60d90e48e809c9e2aac119ad8444fbbc74eb2398# Parent b1a0990ad40c34eb2e559211c0c086477a378aaf merge diff -r b1a0990ad40c -r f27923072afd CMakeLists.txt --- a/CMakeLists.txt Thu Oct 24 10:32:33 2013 +0200 +++ b/CMakeLists.txt Thu Oct 24 10:33:11 2013 +0200 @@ -90,6 +90,7 @@ # Prepare the embedded files set(EMBEDDED_FILES PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql + PREPARE_DATABASE_V4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabaseV4.sql CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua ) @@ -209,6 +210,7 @@ OrthancServer/ServerContext.cpp OrthancServer/ServerEnumerations.cpp OrthancServer/ServerToolbox.cpp + OrthancServer/OrthancFindRequestHandler.cpp ) # Ensure autogenerated code is built before building ServerLibrary diff -r b1a0990ad40c -r f27923072afd Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -280,4 +280,110 @@ SetValue(tag, source.GetValue(tag)); } } + + + bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level) + { + DicomTag *tags = NULL; + size_t size; + + switch (level) + { + case ResourceType_Patient: + tags = patientTags; + size = sizeof(patientTags) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyTags; + size = sizeof(studyTags) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesTags; + size = sizeof(seriesTags) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceTags; + size = sizeof(instanceTags) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + for (size_t i = 0; i < size; i++) + { + if (tags[i] == tag) + { + return true; + } + } + + return false; + } + + bool DicomMap::IsMainDicomTag(const DicomTag& tag) + { + return (IsMainDicomTag(tag, ResourceType_Patient) || + IsMainDicomTag(tag, ResourceType_Study) || + IsMainDicomTag(tag, ResourceType_Series) || + IsMainDicomTag(tag, ResourceType_Instance)); + } + + + void DicomMap::GetMainDicomTagsInternal(std::set& result, ResourceType level) + { + DicomTag *tags = NULL; + size_t size; + + switch (level) + { + case ResourceType_Patient: + tags = patientTags; + size = sizeof(patientTags) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyTags; + size = sizeof(studyTags) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesTags; + size = sizeof(seriesTags) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceTags; + size = sizeof(instanceTags) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + for (size_t i = 0; i < size; i++) + { + result.insert(tags[i]); + } + } + + + void DicomMap::GetMainDicomTags(std::set& result, ResourceType level) + { + result.clear(); + GetMainDicomTagsInternal(result, level); + } + + + void DicomMap::GetMainDicomTags(std::set& result) + { + result.clear(); + GetMainDicomTagsInternal(result, ResourceType_Patient); + GetMainDicomTagsInternal(result, ResourceType_Study); + GetMainDicomTagsInternal(result, ResourceType_Series); + GetMainDicomTagsInternal(result, ResourceType_Instance); + } } diff -r b1a0990ad40c -r f27923072afd Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/DicomFormat/DicomMap.h Thu Oct 24 10:33:11 2013 +0200 @@ -35,7 +35,9 @@ #include "DicomTag.h" #include "DicomValue.h" #include "DicomString.h" +#include "../Enumerations.h" +#include #include #include @@ -63,6 +65,8 @@ void ExtractTags(DicomMap& source, const DicomTag* tags, size_t count) const; + + static void GetMainDicomTagsInternal(std::set& result, ResourceType level); public: DicomMap() @@ -148,5 +152,13 @@ void CopyTagIfExists(const DicomMap& source, const DicomTag& tag); + + static bool IsMainDicomTag(const DicomTag& tag, ResourceType level); + + static bool IsMainDicomTag(const DicomTag& tag); + + static void GetMainDicomTags(std::set& result, ResourceType level); + + static void GetMainDicomTags(std::set& result); }; } diff -r b1a0990ad40c -r f27923072afd Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/DicomFormat/DicomTag.h Thu Oct 24 10:33:11 2013 +0200 @@ -110,4 +110,9 @@ // DICOM tags used for fMRI (thanks to Will Ryder) static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105); static const DicomTag DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER(0x0020, 0x0100); + + // Tags for C-FIND and C-MOVE + static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005); + static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052); + static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061); } diff -r b1a0990ad40c -r f27923072afd Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/Enumerations.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -33,6 +33,7 @@ #include "Enumerations.h" #include "OrthancException.h" +#include "Toolbox.h" namespace Orthanc { @@ -222,4 +223,54 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + const char* EnumerationToString(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return "Patient"; + + case ResourceType_Study: + return "Study"; + + case ResourceType_Series: + return "Series"; + + case ResourceType_Instance: + return "Instance"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType StringToResourceType(const char* type) + { + std::string s(type); + Toolbox::ToUpperCase(s); + + if (s == "PATIENT") + { + return ResourceType_Patient; + } + else if (s == "STUDY") + { + return ResourceType_Study; + } + else if (s == "SERIES") + { + return ResourceType_Series; + } + else if (s == "INSTANCE" || s == "IMAGE") + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } } diff -r b1a0990ad40c -r f27923072afd Core/Enumerations.h --- a/Core/Enumerations.h Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/Enumerations.h Thu Oct 24 10:33:11 2013 +0200 @@ -228,9 +228,20 @@ FileContentType_Json = 2 }; + enum ResourceType + { + ResourceType_Patient = 1, + ResourceType_Study = 2, + ResourceType_Series = 3, + ResourceType_Instance = 4 + }; const char* EnumerationToString(HttpMethod method); const char* EnumerationToString(HttpStatus status); + + const char* EnumerationToString(ResourceType type); + + ResourceType StringToResourceType(const char* type); } diff -r b1a0990ad40c -r f27923072afd Core/Toolbox.cpp --- a/Core/Toolbox.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/Toolbox.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -42,6 +42,7 @@ #include #include #include +#include #if defined(_WIN32) #include @@ -726,4 +727,60 @@ throw OrthancException(ErrorCode_NotImplemented); } } + + + std::string Toolbox::WildcardToRegularExpression(const std::string& source) + { + // TODO - Speed up this with a regular expression + + std::string result = source; + + // Escape all special characters + boost::replace_all(result, "\\", "\\\\"); + boost::replace_all(result, "^", "\\^"); + boost::replace_all(result, ".", "\\."); + boost::replace_all(result, "$", "\\$"); + boost::replace_all(result, "|", "\\|"); + boost::replace_all(result, "(", "\\("); + boost::replace_all(result, ")", "\\)"); + boost::replace_all(result, "[", "\\["); + boost::replace_all(result, "]", "\\]"); + boost::replace_all(result, "+", "\\+"); + boost::replace_all(result, "/", "\\/"); + boost::replace_all(result, "{", "\\{"); + boost::replace_all(result, "}", "\\}"); + + // Convert wildcards '*' and '?' to their regex equivalents + boost::replace_all(result, "?", "."); + boost::replace_all(result, "*", ".*"); + + return result; + } + + + + void Toolbox::TokenizeString(std::vector& result, + const std::string& value, + char separator) + { + result.clear(); + + std::string currentItem; + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == separator) + { + result.push_back(currentItem); + currentItem.clear(); + } + else + { + currentItem.push_back(value[i]); + } + } + + result.push_back(currentItem); + } } + diff -r b1a0990ad40c -r f27923072afd Core/Toolbox.h --- a/Core/Toolbox.h Thu Oct 24 10:32:33 2013 +0200 +++ b/Core/Toolbox.h Thu Oct 24 10:33:11 2013 +0200 @@ -108,5 +108,11 @@ void UrlDecode(std::string& s); Endianness DetectEndianness(); + + std::string WildcardToRegularExpression(const std::string& s); + + void TokenizeString(std::vector& result, + const std::string& source, + char separator); } } diff -r b1a0990ad40c -r f27923072afd OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -807,7 +807,7 @@ db_.Execute(query); } - // Sanity check of the version of the database + // Check the version of the database std::string version = GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "Unknown"); bool ok = false; try @@ -815,9 +815,27 @@ LOG(INFO) << "Version of the Orthanc database: " << version; unsigned int v = boost::lexical_cast(version); - // This version of Orthanc is only compatible with version 3 of - // the DB schema (since Orthanc 0.3.2) - ok = (v == 3); + // This version of Orthanc is only compatible with versions 3 + // (Orthanc 0.3.2 to 0.6.1) and 4 (since Orthanc 0.6.2) of the + // DB schema + ok = (v == 3 || v == 4); + + if (v == 3) + { + LOG(WARNING) << "Upgrading the database from version 3 to version 4 (reconstructing the index)"; + + // Reconstruct the index for case insensitive queries in C-FIND + db_.Execute("DROP INDEX IF EXISTS MainDicomTagsIndexValues;"); + db_.Execute("DROP TABLE IF EXISTS AvailableTags;"); + + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_V4); + db_.Execute(query); + + db_.Execute("INSERT INTO AvailableTags SELECT DISTINCT tagGroup, tagElement FROM MainDicomTags;"); + + //SetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "4"); + } } catch (boost::bad_lexical_cast&) { @@ -828,6 +846,8 @@ throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } + CompleteMainDicomTags(); + signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; db_.Register(signalRemainingAncestor_); db_.Register(new Internals::SignalFileDeleted(listener_)); @@ -995,4 +1015,11 @@ result.push_back(s.ColumnInt64(0)); } } + + + void DatabaseWrapper::CompleteMainDicomTags() + { + std::set requiredTags; + + } } diff -r b1a0990ad40c -r f27923072afd OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/DatabaseWrapper.h Thu Oct 24 10:33:11 2013 +0200 @@ -72,6 +72,8 @@ int64_t since, unsigned int maxResults); + void CompleteMainDicomTags(); + public: void SetGlobalProperty(GlobalProperty property, const std::string& value); diff -r b1a0990ad40c -r f27923072afd OrthancServer/Internals/MoveScp.cpp --- a/OrthancServer/Internals/MoveScp.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/Internals/MoveScp.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -82,6 +82,13 @@ try { data.iterator_.reset(data.handler_->Handle(data.target_, data.input_)); + if (data.iterator_.get() == NULL) + { + // Internal error! + response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess; + return; + } + data.subOperationCount_ = data.iterator_->GetSubOperationCount(); data.failureCount_ = 0; data.warningCount_ = 0; diff -r b1a0990ad40c -r f27923072afd OrthancServer/OrthancFindRequestHandler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -0,0 +1,362 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2013 Medical Physics Department, CHU of Liege, + * 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 . + **/ + +#include "OrthancFindRequestHandler.h" + +#include +#include + +#include "../Core/DicomFormat/DicomArray.h" +#include "ServerToolbox.h" + +namespace Orthanc +{ + static std::string ToLowerCase(const std::string& s) + { + std::string result = s; + Toolbox::ToLowerCase(result); + return result; + } + + static bool ApplyRangeConstraint(const std::string& value, + const std::string& constraint) + { + size_t separator = constraint.find('-'); + std::string lower = ToLowerCase(constraint.substr(0, separator)); + std::string upper = ToLowerCase(constraint.substr(separator + 1)); + std::string v = ToLowerCase(value); + + if (lower.size() == 0 && upper.size() == 0) + { + return false; + } + + if (lower.size() == 0) + { + return v <= upper; + } + + if (upper.size() == 0) + { + return v >= lower; + } + + return (v >= lower && v <= upper); + } + + + static bool ApplyListConstraint(const std::string& value, + const std::string& constraint) + { + std::string v1 = ToLowerCase(value); + + std::vector items; + Toolbox::TokenizeString(items, constraint, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + Toolbox::ToLowerCase(items[i]); + if (items[i] == v1) + { + return true; + } + } + + return false; + } + + + static bool Matches(const std::string& value, + const std::string& constraint) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + + if (constraint.find('-') != std::string::npos) + { + return ApplyRangeConstraint(value, constraint); + } + + if (constraint.find('\\') != std::string::npos) + { + return ApplyListConstraint(value, constraint); + } + + if (constraint.find('*') != std::string::npos || + constraint.find('?') != std::string::npos) + { + // TODO - Cache the constructed regular expression + boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint), + boost::regex::icase /* case insensitive search */); + return boost::regex_match(value, pattern); + } + else + { + return ToLowerCase(value) == ToLowerCase(constraint); + } + } + + + static bool LookupOneInstance(std::string& result, + ServerIndex& index, + const std::string& id, + ResourceType type) + { + if (type == ResourceType_Instance) + { + result = id; + return true; + } + + std::string childId; + + { + std::list children; + index.GetChildInstances(children, id); + + if (children.size() == 0) + { + return false; + } + + childId = children.front(); + } + + return LookupOneInstance(result, index, childId, GetChildResourceType(type)); + } + + + static bool Matches(const Json::Value& resource, + const DicomArray& query) + { + for (size_t i = 0; i < query.GetSize(); i++) + { + if (query.GetElement(i).GetValue().IsNull() || + query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL || + query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET || + query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + continue; + } + + std::string tag = query.GetElement(i).GetTag().Format(); + std::string value; + if (resource.isMember(tag)) + { + value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); + } + + if (!Matches(value, query.GetElement(i).GetValue().AsString())) + { + return false; + } + } + + return true; + } + + + static void AddAnswer(DicomFindAnswers& answers, + const Json::Value& resource, + const DicomArray& query) + { + DicomMap result; + + for (size_t i = 0; i < query.GetSize(); i++) + { + if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL && + query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + std::string tag = query.GetElement(i).GetTag().Format(); + std::string value; + if (resource.isMember(tag)) + { + value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); + result.SetValue(query.GetElement(i).GetTag(), value); + } + } + } + + answers.Add(result); + } + + + static bool ApplyModalitiesInStudyFilter(Json::Value& filteredStudies, + const Json::Value& studies, + const DicomMap& input, + ServerIndex& index) + { + filteredStudies = Json::arrayValue; + + const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY); + if (v.IsNull()) + { + return false; + } + + // Move the allowed modalities into a "std::set" + std::vector tmp; + Toolbox::TokenizeString(tmp, v.AsString(), '\\'); + + std::set modalities; + for (size_t i = 0; i < tmp.size(); i++) + { + modalities.insert(tmp[i]); + } + + // Loop over the studies + for (Json::Value::ArrayIndex i = 0; i < studies.size(); i++) + { + try + { + // We are considering a single study. Check whether one of + // its child series matches one of the modalities. + Json::Value study; + if (index.LookupResource(study, studies[i].asString(), ResourceType_Study)) + { + // Loop over the series of the considered study. + for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) // (*) + { + Json::Value series; + if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) + { + // Get the modality of this series + if (series["MainDicomTags"].isMember("Modality")) + { + std::string modality = series["MainDicomTags"]["Modality"].asString(); + if (modalities.find(modality) != modalities.end()) + { + // This series of the considered study matches one + // of the required modalities. Take the study into + // consideration for future filtering. + filteredStudies.append(studies[i]); + + // We have finished considering this study. Break the study loop at (*). + break; + } + } + } + } + } + } + catch (OrthancException&) + { + // This resource has probably been deleted during the find request + } + } + + return true; + } + + + void OrthancFindRequestHandler::Handle(const DicomMap& input, + DicomFindAnswers& answers) + { + LOG(WARNING) << "Find-SCU request received"; + + /** + * Retrieve the query level. + **/ + + const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + if (levelTmp == NULL) + { + throw OrthancException(ErrorCode_BadRequest); + } + + ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); + + if (level != ResourceType_Patient && + level != ResourceType_Study && + level != ResourceType_Series) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + + /** + * Retrieve all the resources for this query level. + **/ + + Json::Value resources; + context_.GetIndex().GetAllUuids(resources, level); + assert(resources.type() == Json::arrayValue); + + // TODO : Speed up using MainDicomTags (to avoid looping over ALL + // the resources and reading the JSON file for each of them) + + + + /** + * Apply filtering on modalities for studies, if asked (this is an + * extension to standard DICOM) + * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND + **/ + + if (level == ResourceType_Study && + input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + Json::Value filtered; + if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex())) + { + resources = filtered; + } + } + + + /** + * Loop over all the resources for this query level. + **/ + + DicomArray query(input); + for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + { + try + { + std::string instance; + if (LookupOneInstance(instance, context_.GetIndex(), resources[i].asString(), level)) + { + Json::Value resource; + context_.ReadJson(resource, instance); + + if (Matches(resource, query)) + { + AddAnswer(answers, resource, query); + } + } + } + catch (OrthancException&) + { + // This resource has probably been deleted during the find request + } + } + } +} diff -r b1a0990ad40c -r f27923072afd OrthancServer/OrthancFindRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancFindRequestHandler.h Thu Oct 24 10:33:11 2013 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2013 Medical Physics Department, CHU of Liege, + * 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 . + **/ + +#pragma once + +#include "DicomProtocol/IFindRequestHandler.h" + +#include "ServerContext.h" + +namespace Orthanc +{ + class OrthancFindRequestHandler : public IFindRequestHandler + { + private: + ServerContext& context_; + + public: + OrthancFindRequestHandler(ServerContext& context) : + context_(context) + { + } + + virtual void Handle(const DicomMap& input, + DicomFindAnswers& answers); + }; +} diff -r b1a0990ad40c -r f27923072afd OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -464,4 +464,69 @@ target.push_back(lst[i].asString()); } } + + + void ConnectToModalityUsingSymbolicName(DicomUserConnection& connection, + const std::string& name) + { + std::string aet, address; + int port; + ModalityManufacturer manufacturer; + GetDicomModality(name, aet, address, port, manufacturer); + + LOG(WARNING) << "Connecting to remote DICOM modality: AET=" << aet << ", address=" << address << ", port=" << port; + + connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); + connection.SetDistantApplicationEntityTitle(aet); + connection.SetDistantHost(address); + connection.SetDistantPort(port); + connection.SetDistantManufacturer(manufacturer); + connection.Open(); + } + + + void ConnectToModalityUsingAETitle(DicomUserConnection& connection, + const std::string& aet) + { + std::set modalities; + GetListOfDicomModalities(modalities); + + std::string address; + int port; + ModalityManufacturer manufacturer; + bool found = false; + + for (std::set::const_iterator + it = modalities.begin(); it != modalities.end(); it++) + { + try + { + std::string thisAet; + GetDicomModality(*it, thisAet, address, port, manufacturer); + + if (aet == thisAet) + { + found = true; + break; + } + } + catch (OrthancException&) + { + } + } + + if (!found) + { + throw OrthancException("Unknown modality: " + aet); + } + + LOG(WARNING) << "Connecting to remote DICOM modality: AET=" << aet << ", address=" << address << ", port=" << port; + + connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); + connection.SetDistantApplicationEntityTitle(aet); + connection.SetDistantHost(address); + connection.SetDistantPort(port); + connection.SetDistantManufacturer(manufacturer); + connection.Open(); + } } diff -r b1a0990ad40c -r f27923072afd OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/OrthancInitialization.h Thu Oct 24 10:33:11 2013 +0200 @@ -37,6 +37,7 @@ #include #include #include "../Core/HttpServer/MongooseServer.h" +#include "DicomProtocol/DicomUserConnection.h" #include "ServerEnumerations.h" namespace Orthanc @@ -78,4 +79,10 @@ void GetGlobalListOfStringsParameter(std::list& target, const std::string& key); + + void ConnectToModalityUsingSymbolicName(DicomUserConnection& connection, + const std::string& name); + + void ConnectToModalityUsingAETitle(DicomUserConnection& connection, + const std::string& aet); } diff -r b1a0990ad40c -r f27923072afd OrthancServer/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/OrthancRestApi.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -71,21 +71,6 @@ // DICOM SCU ---------------------------------------------------------------- - static void ConnectToModality(DicomUserConnection& connection, - const std::string& name) - { - std::string aet, address; - int port; - ModalityManufacturer manufacturer; - GetDicomModality(name, aet, address, port, manufacturer); - connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); - connection.SetDistantApplicationEntityTitle(aet); - connection.SetDistantHost(address); - connection.SetDistantPort(port); - connection.SetDistantManufacturer(manufacturer); - connection.Open(); - } - static bool MergeQueryAndTemplate(DicomMap& result, const std::string& postData) { @@ -118,7 +103,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); DicomFindAnswers answers; connection.FindPatient(answers, m); @@ -144,7 +129,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); DicomFindAnswers answers; connection.FindStudy(answers, m); @@ -171,7 +156,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); DicomFindAnswers answers; connection.FindSeries(answers, m); @@ -199,7 +184,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); DicomFindAnswers answers; connection.FindInstance(answers, m); @@ -219,7 +204,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); DicomFindAnswers patients; connection.FindPatient(patients, m); @@ -350,7 +335,7 @@ } DicomUserConnection connection; - ConnectToModality(connection, remote); + ConnectToModalityUsingSymbolicName(connection, remote); for (std::list::const_iterator it = instances.begin(); it != instances.end(); it++) diff -r b1a0990ad40c -r f27923072afd OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/PrepareDatabase.sql Thu Oct 24 10:33:11 2013 +0200 @@ -67,7 +67,6 @@ CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); -CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); CREATE INDEX ChangesIndex ON Changes(internalId); diff -r b1a0990ad40c -r f27923072afd OrthancServer/PrepareDatabaseV4.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/PrepareDatabaseV4.sql Thu Oct 24 10:33:11 2013 +0200 @@ -0,0 +1,12 @@ +-- New in database version 4 +CREATE TABLE AvailableTags( + tagGroup INTEGER, + tagElement INTEGER, + PRIMARY KEY(tagGroup, tagElement) + ); + +-- Until database version 4, the following index was set to "COLLATE +-- BINARY". This implies case-sensitive searches, but DICOM C-Find +-- requires case-insensitive searches. +-- http://www.sqlite.org/optoverview.html#like_opt +CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE NOCASE); diff -r b1a0990ad40c -r f27923072afd OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -33,6 +33,7 @@ #include "../Core/OrthancException.h" #include "../Core/EnumerationDictionary.h" +#include "../Core/Toolbox.h" #include @@ -82,27 +83,6 @@ return dictMetadataType_.Translate(str); } - const char* EnumerationToString(ResourceType type) - { - switch (type) - { - case ResourceType_Patient: - return "Patient"; - - case ResourceType_Study: - return "Study"; - - case ResourceType_Series: - return "Series"; - - case ResourceType_Instance: - return "Instance"; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - std::string GetBasePath(ResourceType type, const std::string& publicId) { @@ -289,6 +269,4 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } - - } diff -r b1a0990ad40c -r f27923072afd OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Oct 24 10:33:11 2013 +0200 @@ -33,6 +33,8 @@ #include +#include "../Core/Enumerations.h" + namespace Orthanc { enum SeriesStatus @@ -71,14 +73,6 @@ GlobalProperty_AnonymizationSequence = 3 }; - enum ResourceType - { - ResourceType_Patient = 1, - ResourceType_Study = 2, - ResourceType_Series = 3, - ResourceType_Instance = 4 - }; - enum MetadataType { MetadataType_Instance_IndexInSeries = 1, @@ -122,8 +116,6 @@ MetadataType StringToMetadata(const std::string& str); - const char* EnumerationToString(ResourceType type); - std::string EnumerationToString(MetadataType type); const char* EnumerationToString(SeriesStatus status); diff -r b1a0990ad40c -r f27923072afd OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/OrthancServer/main.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -41,20 +41,22 @@ #include "../Core/Lua/LuaFunctionCall.h" #include "../Core/DicomFormat/DicomArray.h" #include "DicomProtocol/DicomServer.h" +#include "DicomProtocol/DicomUserConnection.h" #include "OrthancInitialization.h" #include "ServerContext.h" +#include "OrthancFindRequestHandler.h" using namespace Orthanc; -class MyStoreRequestHandler : public IStoreRequestHandler +class OrthancStoreRequestHandler : public IStoreRequestHandler { private: ServerContext& server_; public: - MyStoreRequestHandler(ServerContext& context) : + OrthancStoreRequestHandler(ServerContext& context) : server_(context) { } @@ -72,34 +74,92 @@ }; -class MyFindRequestHandler : public IFindRequestHandler + +class OrthancMoveRequestIterator : public IMoveRequestIterator +{ +private: + ServerContext& context_; + std::vector instances_; + DicomUserConnection connection_; + size_t position_; + +public: + OrthancMoveRequestIterator(ServerContext& context, + const std::string& target, + const std::string& publicId) : + context_(context), + position_(0) + { + LOG(INFO) << "Sending resource " << publicId << " to modality \"" << target << "\""; + + std::list tmp; + context_.GetIndex().GetChildInstances(tmp, publicId); + + instances_.reserve(tmp.size()); + for (std::list::iterator it = tmp.begin(); it != tmp.end(); it++) + { + instances_.push_back(*it); + } + + ConnectToModalityUsingAETitle(connection_, target); + } + + virtual unsigned int GetSubOperationCount() const + { + return instances_.size(); + } + + virtual Status DoNext() + { + if (position_ >= instances_.size()) + { + return Status_Failure; + } + + const std::string& id = instances_[position_++]; + + std::string dicom; + context_.ReadFile(dicom, id, FileContentType_Dicom); + connection_.Store(dicom); + + return Status_Success; + } +}; + + + +class OrthancMoveRequestHandler : public IMoveRequestHandler { private: ServerContext& context_; -public: - MyFindRequestHandler(ServerContext& context) : - context_(context) + bool LookupResource(std::string& publicId, + DicomTag tag, + const DicomMap& input) { + if (!input.HasTag(tag)) + { + return false; + } + + std::string value = input.GetValue(tag).AsString(); + + std::list ids; + context_.GetIndex().LookupTagValue(ids, tag, value); + + if (ids.size() != 1) + { + return false; + } + else + { + publicId = ids.front(); + return true; + } } - virtual void Handle(const DicomMap& input, - DicomFindAnswers& answers) - { - LOG(WARNING) << "Find-SCU request received"; - DicomArray a(input); - a.Print(stdout); - } -}; - - -class MyMoveRequestHandler : public IMoveRequestHandler -{ -private: - ServerContext& context_; - public: - MyMoveRequestHandler(ServerContext& context) : + OrthancMoveRequestHandler(ServerContext& context) : context_(context) { } @@ -108,8 +168,57 @@ virtual IMoveRequestIterator* Handle(const std::string& target, const DicomMap& input) { - LOG(WARNING) << "Move-SCU request received"; - return NULL; + LOG(WARNING) << "Move-SCU request received for AET \"" << target << "\""; + + + /** + * Retrieve the query level. + **/ + + const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + if (levelTmp == NULL) + { + throw OrthancException(ErrorCode_BadRequest); + } + + ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); + + + /** + * Lookup for the resource to be sent. + **/ + + bool ok; + std::string publicId; + + switch (level) + { + case ResourceType_Patient: + ok = LookupResource(publicId, DICOM_TAG_PATIENT_ID, input); + break; + + case ResourceType_Study: + ok = LookupResource(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); + break; + + case ResourceType_Series: + ok = LookupResource(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); + break; + + case ResourceType_Instance: + ok = LookupResource(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); + break; + + default: + ok = false; + } + + if (!ok) + { + throw OrthancException(ErrorCode_BadRequest); + } + + return new OrthancMoveRequestIterator(context_, target, publicId); } }; @@ -129,17 +238,17 @@ virtual IStoreRequestHandler* ConstructStoreRequestHandler() { - return new MyStoreRequestHandler(context_); + return new OrthancStoreRequestHandler(context_); } virtual IFindRequestHandler* ConstructFindRequestHandler() { - return new MyFindRequestHandler(context_); + return new OrthancFindRequestHandler(context_); } virtual IMoveRequestHandler* ConstructMoveRequestHandler() { - return new MyMoveRequestHandler(context_); + return new OrthancMoveRequestHandler(context_); } void Done() @@ -369,14 +478,13 @@ MyDicomServerFactory serverFactory(context); - { // DICOM server DicomServer dicomServer; dicomServer.SetCalledApplicationEntityTitleCheck(GetGlobalBoolParameter("DicomCheckCalledAet", false)); dicomServer.SetStoreRequestHandlerFactory(serverFactory); - //dicomServer.SetMoveRequestHandlerFactory(serverFactory); - //dicomServer.SetFindRequestHandlerFactory(serverFactory); + dicomServer.SetMoveRequestHandlerFactory(serverFactory); + dicomServer.SetFindRequestHandlerFactory(serverFactory); dicomServer.SetPortNumber(GetGlobalIntegerParameter("DicomPort", 4242)); dicomServer.SetApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); diff -r b1a0990ad40c -r f27923072afd Resources/CMake/BoostConfiguration.cmake --- a/Resources/CMake/BoostConfiguration.cmake Thu Oct 24 10:32:33 2013 +0200 +++ b/Resources/CMake/BoostConfiguration.cmake Thu Oct 24 10:33:11 2013 +0200 @@ -1,14 +1,14 @@ if (${STATIC_BUILD}) - SET(BOOST_STATIC 1) + set(BOOST_STATIC 1) else() include(FindBoost) - SET(BOOST_STATIC 0) + set(BOOST_STATIC 0) #set(Boost_DEBUG 1) #set(Boost_USE_STATIC_LIBS ON) find_package(Boost - COMPONENTS filesystem thread system date_time) + COMPONENTS filesystem thread system date_time regex) if (NOT Boost_FOUND) message(FATAL_ERROR "Unable to locate Boost on this system") @@ -30,7 +30,7 @@ #if (${Boost_VERSION} LESS 104800) # boost::locale is only available from 1.48.00 #message("Too old version of Boost (${Boost_LIB_VERSION}): Building the static version") - # SET(BOOST_STATIC 1) + # set(BOOST_STATIC 1) #endif() include_directories(${Boost_INCLUDE_DIRS}) @@ -40,12 +40,12 @@ if (BOOST_STATIC) # Parameters for Boost 1.54.0 - SET(BOOST_NAME boost_1_54_0) - SET(BOOST_BCP_SUFFIX bcpdigest-0.6.2) - SET(BOOST_MD5 "a464288a976ba133f9b325f454cb503d") - SET(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src") + set(BOOST_NAME boost_1_54_0) + set(BOOST_BCP_SUFFIX bcpdigest-0.6.2) + set(BOOST_MD5 "a464288a976ba133f9b325f454cb503d") + set(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src") - SET(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) + set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) DownloadPackage( "${BOOST_MD5}" "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz" @@ -80,7 +80,10 @@ message(FATAL_ERROR "Support your platform here") endif() + aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES) + list(APPEND BOOST_SOURCES + ${BOOST_REGEX_SOURCES} ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp ${BOOST_FILESYSTEM_SOURCES_DIR}/codecvt_error_category.cpp ${BOOST_FILESYSTEM_SOURCES_DIR}/operations.cpp diff -r b1a0990ad40c -r f27923072afd UnitTests/ServerIndex.cpp --- a/UnitTests/ServerIndex.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/UnitTests/ServerIndex.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -487,3 +487,11 @@ } + + +TEST(DicomMap, MainTags) +{ + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); +} diff -r b1a0990ad40c -r f27923072afd UnitTests/main.cpp --- a/UnitTests/main.cpp Thu Oct 24 10:32:33 2013 +0200 +++ b/UnitTests/main.cpp Thu Oct 24 10:33:11 2013 +0200 @@ -127,6 +127,10 @@ t = FromDcmtkBridge::ParseTag("0020-e040"); ASSERT_EQ(0x0020, t.GetGroup()); ASSERT_EQ(0xe040, t.GetElement()); + + // Test ==() and !=() operators + ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020)); + ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020)); } @@ -387,6 +391,13 @@ ASSERT_EQ("IndexInSeries", EnumerationToString(MetadataType_Instance_IndexInSeries)); ASSERT_EQ("LastUpdate", EnumerationToString(MetadataType_LastUpdate)); + ASSERT_EQ(ResourceType_Patient, StringToResourceType("PATienT")); + ASSERT_EQ(ResourceType_Study, StringToResourceType("STudy")); + ASSERT_EQ(ResourceType_Series, StringToResourceType("SeRiEs")); + ASSERT_EQ(ResourceType_Instance, StringToResourceType("INStance")); + ASSERT_EQ(ResourceType_Instance, StringToResourceType("IMagE")); + ASSERT_THROW(StringToResourceType("heLLo"), OrthancException); + ASSERT_EQ(2047, StringToMetadata("2047")); ASSERT_THROW(StringToMetadata("Ceci est un test"), OrthancException); ASSERT_THROW(RegisterUserMetadata(128, ""), OrthancException); // too low (< 1024) @@ -480,6 +491,37 @@ } +TEST(Toolbox, Wildcard) +{ + ASSERT_EQ("abcd", Toolbox::WildcardToRegularExpression("abcd")); + ASSERT_EQ("ab.*cd", Toolbox::WildcardToRegularExpression("ab*cd")); + ASSERT_EQ("ab..cd", Toolbox::WildcardToRegularExpression("ab??cd")); + ASSERT_EQ("a.*b.c.*d", Toolbox::WildcardToRegularExpression("a*b?c*d")); + ASSERT_EQ("a\\{b\\]", Toolbox::WildcardToRegularExpression("a{b]")); +} + + +TEST(Toolbox, Tokenize) +{ + std::vector t; + + Toolbox::TokenizeString(t, "", ','); + ASSERT_EQ(1, t.size()); + ASSERT_EQ("", t[0]); + + Toolbox::TokenizeString(t, "abc", ','); + ASSERT_EQ(1, t.size()); + ASSERT_EQ("abc", t[0]); + + Toolbox::TokenizeString(t, "ab,cd,ef,", ','); + ASSERT_EQ(4, t.size()); + ASSERT_EQ("ab", t[0]); + ASSERT_EQ("cd", t[1]); + ASSERT_EQ("ef", t[2]); + ASSERT_EQ("", t[3]); +} + + int main(int argc, char **argv) { // Initialize Google's logging library.