# HG changeset patch # User Sebastien Jodogne # Date 1544108288 -3600 # Node ID eff50153a7b30d704091bb4937a25c21779fd7e9 # Parent 3fabf9a673f65628842a03867470a6d6302e4577# Parent 63b724c7b046f5964dc8d093ecc623a77c943071 integration mainline->db-changes diff -r 3fabf9a673f6 -r eff50153a7b3 AUTHORS --- a/AUTHORS Thu Oct 18 10:48:11 2018 +0200 +++ b/AUTHORS Thu Dec 06 15:58:08 2018 +0100 @@ -14,7 +14,8 @@ 4000 Liege Belgium -* Osimis S.A. +* Osimis S.A. Rue du Bois Saint-Jean 15/1 4102 Seraing Belgium + http://www.osimis.io/ diff -r 3fabf9a673f6 -r eff50153a7b3 CMakeLists.txt --- a/CMakeLists.txt Thu Oct 18 10:48:11 2018 +0200 +++ b/CMakeLists.txt Thu Dec 06 15:58:08 2018 +0100 @@ -58,6 +58,7 @@ OrthancServer/DicomInstanceToStore.cpp OrthancServer/ExportedResource.cpp OrthancServer/LuaScripting.cpp + OrthancServer/OrthancConfiguration.cpp OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/OrthancHttpHandler.cpp OrthancServer/OrthancInitialization.cpp diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Cache/SharedArchive.cpp --- a/Core/Cache/SharedArchive.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Cache/SharedArchive.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -47,6 +47,8 @@ { delete it->second; archive_.erase(it); + + lru_.Invalidate(id); } } @@ -59,7 +61,7 @@ if (it == that.archive_.end()) { - throw OrthancException(ErrorCode_InexistentItem); + item_ = NULL; } else { @@ -69,6 +71,20 @@ } + IDynamicObject& SharedArchive::Accessor::GetItem() const + { + if (item_ == NULL) + { + // "IsValid()" should have been called + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *item_; + } + } + + SharedArchive::SharedArchive(size_t maxSize) : maxSize_(maxSize) { @@ -96,12 +112,12 @@ if (archive_.size() == maxSize_) { // The quota has been reached, remove the oldest element - std::string oldest = lru_.RemoveOldest(); - RemoveInternal(oldest); + RemoveInternal(lru_.GetOldest()); } std::string id = Toolbox::GenerateUuid(); RemoveInternal(id); // Should never be useful because of UUID + archive_[id] = obj; lru_.Add(id); @@ -113,7 +129,6 @@ { boost::mutex::scoped_lock lock(mutex_); RemoveInternal(id); - lru_.Invalidate(id); } @@ -121,14 +136,14 @@ { items.clear(); - boost::mutex::scoped_lock lock(mutex_); + { + boost::mutex::scoped_lock lock(mutex_); - for (Archive::const_iterator it = archive_.begin(); - it != archive_.end(); ++it) - { - items.push_back(it->first); + for (Archive::const_iterator it = archive_.begin(); + it != archive_.end(); ++it) + { + items.push_back(it->first); + } } } } - - diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Cache/SharedArchive.h --- a/Core/Cache/SharedArchive.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Cache/SharedArchive.h Thu Dec 06 15:58:08 2018 +0100 @@ -72,10 +72,12 @@ Accessor(SharedArchive& that, const std::string& id); - IDynamicObject& GetItem() const + bool IsValid() const { - return *item_; - } + return item_ != NULL; + } + + IDynamicObject& GetItem() const; }; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Compression/DeflateBaseCompressor.cpp --- a/Core/Compression/DeflateBaseCompressor.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Compression/DeflateBaseCompressor.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -45,8 +45,8 @@ { if (level >= 10) { - LOG(ERROR) << "Zlib compression level must be between 0 (no compression) and 9 (highest compression)"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Zlib compression level must be between 0 (no compression) and 9 (highest compression)"); } compressionLevel_ = level; @@ -63,8 +63,7 @@ if (compressedSize < sizeof(uint64_t)) { - LOG(ERROR) << "The compressed buffer is ill-formed"; - throw OrthancException(ErrorCode_CorruptedFile); + throw OrthancException(ErrorCode_CorruptedFile, "The compressed buffer is ill-formed"); } uint64_t size; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Compression/GzipCompressor.cpp --- a/Core/Compression/GzipCompressor.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Compression/GzipCompressor.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -271,8 +271,8 @@ // The uncompressed size was not that properly guess, presumably // because of a file size over 4GB. Should fallback to // stream-based decompression. - LOG(ERROR) << "The uncompressed size of a gzip-encoded buffer was not properly guessed"; - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, + "The uncompressed size of a gzip-encoded buffer was not properly guessed"); } } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Compression/ZipWriter.cpp --- a/Core/Compression/ZipWriter.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Compression/ZipWriter.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -124,8 +124,8 @@ if (path_.size() == 0) { - LOG(ERROR) << "Please call SetOutputPath() before creating the file"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Please call SetOutputPath() before creating the file"); } hasFileInZip_ = false; @@ -168,8 +168,8 @@ { if (level >= 10) { - LOG(ERROR) << "ZIP compression level must be between 0 (no compression) and 9 (highest compression)"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "ZIP compression level must be between 0 (no compression) and 9 (highest compression)"); } Close(); @@ -228,8 +228,7 @@ { if (!hasFileInZip_) { - LOG(ERROR) << "Call first OpenFile()"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Call first OpenFile()"); } const size_t maxBytesInAStep = std::numeric_limits::max(); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Compression/ZlibCompressor.cpp --- a/Core/Compression/ZlibCompressor.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Compression/ZlibCompressor.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -117,8 +117,8 @@ if (!HasPrefixWithUncompressedSize()) { - LOG(ERROR) << "Cannot guess the uncompressed size of a zlib-encoded buffer"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot guess the uncompressed size of a zlib-encoded buffer"); } uint64_t uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomFormat/DicomInstanceHasher.cpp --- a/Core/DicomFormat/DicomInstanceHasher.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomFormat/DicomInstanceHasher.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -53,7 +53,7 @@ seriesUid_.size() == 0 || instanceUid_.size() == 0) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID"); } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomFormat/DicomTag.h Thu Dec 06 15:58:08 2018 +0100 @@ -175,6 +175,17 @@ static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000); static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400); static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010); + static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010); + static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011); + static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040); + static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060); + static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015); + static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101); + static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070); + static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020); + static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000); + static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201); + static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000); // Tags used within the Stone of Orthanc static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomFormat/DicomValue.h --- a/Core/DicomFormat/DicomValue.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomFormat/DicomValue.h Thu Dec 06 15:58:08 2018 +0100 @@ -33,8 +33,9 @@ #pragma once +#include "../Enumerations.h" + #include -#include #include #include @@ -92,7 +93,7 @@ void FormatDataUriScheme(std::string& target) const { - FormatDataUriScheme(target, "application/octet-stream"); + FormatDataUriScheme(target, MIME_BINARY); } #endif diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomNetworking/DicomServer.cpp --- a/Core/DicomNetworking/DicomServer.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomNetworking/DicomServer.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -316,8 +316,8 @@ { if (modalities_ == NULL) { - LOG(ERROR) << "No list of modalities was provided to the DICOM server"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No list of modalities was provided to the DICOM server"); } Stop(); @@ -327,8 +327,8 @@ (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_); if (cond.bad()) { - LOG(ERROR) << "cannot create network: " << cond.text(); - throw OrthancException(ErrorCode_DicomPortInUse); + throw OrthancException(ErrorCode_DicomPortInUse, + "cannot create network: " + std::string(cond.text())); } continue_ = true; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -164,8 +164,8 @@ { if (cond.bad()) { - LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text()); - throw OrthancException(ErrorCode_NetworkProtocol); + throw OrthancException(ErrorCode_NetworkProtocol, + "DicomUserConnection: " + std::string(cond.text())); } } @@ -173,8 +173,8 @@ { if (!IsOpen()) { - LOG(ERROR) << "DicomUserConnection: First open the connection"; - throw OrthancException(ErrorCode_NetworkProtocol); + throw OrthancException(ErrorCode_NetworkProtocol, + "DicomUserConnection: First open the connection"); } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomNetworking/Internals/FindScp.cpp --- a/Core/DicomNetworking/Internals/FindScp.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomNetworking/Internals/FindScp.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -200,9 +200,9 @@ assert(data.modalities_ != NULL); if (!data.modalities_->LookupAETitle(modality, *data.remoteAet_)) { - LOG(ERROR) << "Modality with AET \"" << *data.remoteAet_ - << "\" is not defined in the \"DicomModalities\" configuration option"; - throw OrthancException(ErrorCode_UnknownModality); + throw OrthancException(ErrorCode_UnknownModality, + "Modality with AET \"" + (*data.remoteAet_) + + "\" is not defined in the \"DicomModalities\" configuration option"); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomNetworking/RemoteModalityParameters.cpp --- a/Core/DicomNetworking/RemoteModalityParameters.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -87,8 +87,9 @@ if (value <= 0 || value >= 65535) { - LOG(ERROR) << "A TCP port number must be in range [1..65534], but found: " << value; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "A TCP port number must be in range [1..65534], but found: " + + boost::lexical_cast(value)); } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomParsing/DicomModification.cpp --- a/Core/DicomParsing/DicomModification.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomParsing/DicomModification.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -256,8 +256,8 @@ { if (!identifierGenerator_->Apply(mapped, original, level, currentSource_)) { - LOG(ERROR) << "Unable to generate an anonymized ID"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Unable to generate an anonymized ID"); } } @@ -873,28 +873,28 @@ // Sanity checks at the patient level if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID)) { - LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a patient, her PatientID is required to be modified"); } if (!allowManualIdentifiers_) { if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a patient, the StudyInstanceUID cannot be manually modified"); } if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a patient, the SeriesInstanceUID cannot be manually modified"); } if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a patient, the SopInstanceUID cannot be manually modified"); } } @@ -902,22 +902,22 @@ // Sanity checks at the study level if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID)) { - LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a study, the parent PatientID cannot be manually modified"); } if (!allowManualIdentifiers_) { if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a study, the SeriesInstanceUID cannot be manually modified"); } if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a study, the SopInstanceUID cannot be manually modified"); } } @@ -925,22 +925,22 @@ // Sanity checks at the series level if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID)) { - LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a series, the parent PatientID cannot be manually modified"); } if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a series, the parent StudyInstanceUID cannot be manually modified"); } if (!allowManualIdentifiers_) { if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) { - LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying a series, the SopInstanceUID cannot be manually modified"); } } @@ -948,20 +948,20 @@ // Sanity checks at the instance level if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID)) { - LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying an instance, the parent PatientID cannot be manually modified"); } if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { - LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying an instance, the parent StudyInstanceUID cannot be manually modified"); } if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) { - LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified"); } @@ -1082,10 +1082,10 @@ if (!force && IsDatabaseKey(tag)) { - LOG(ERROR) << "Marking tag \"" << name << "\" as to be " - << (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") - << " requires the \"Force\" option to be set to true"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Marking tag \"" + name + "\" as to be " + + (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") + + " requires the \"Force\" option to be set to true"); } switch (operation) @@ -1126,9 +1126,9 @@ if (!force && IsDatabaseKey(tag)) { - LOG(ERROR) << "Marking tag \"" << name << "\" as to be replaced " - << "requires the \"Force\" option to be set to true"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Marking tag \"" + name + "\" as to be replaced " + + "requires the \"Force\" option to be set to true"); } target.Replace(tag, value, false); @@ -1153,8 +1153,8 @@ } else { - LOG(ERROR) << "Member \"" << member << "\" should be a Boolean value"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Member \"" + member + "\" should be a Boolean value"); } } @@ -1271,8 +1271,8 @@ { if (identifierGenerator_ != NULL) { - LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot serialize a DicomModification with a custom identifier generator"); } value = Json::objectValue; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -119,16 +119,16 @@ if (!dictionary.loadDictionary(tmp.GetPath().c_str())) { - LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " - << "your TEMP directory does not contain special characters."; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot read embedded dictionary. Under Windows, make sure that " + "your TEMP directory does not contain special characters."); } #else if (!dictionary.loadFromMemory(content)) { - LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " - << "your TEMP directory does not contain special characters."; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot read embedded dictionary. Under Windows, make sure that " + "your TEMP directory does not contain special characters."); } #endif } @@ -289,8 +289,9 @@ /* make sure data dictionary is loaded */ if (!dcmDataDict.isDictionaryLoaded()) { - LOG(ERROR) << "No DICOM dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "No DICOM dictionary loaded, check environment variable: " + + std::string(DCM_DICT_ENVIRONMENT_VARIABLE)); } { @@ -298,8 +299,8 @@ DcmTag key(0x0010, 0x1030); // This is PatientWeight if (key.getEVR() != EVR_DS) { - LOG(ERROR) << "The DICOM dictionary has not been correctly read"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "The DICOM dictionary has not been correctly read"); } } } @@ -370,8 +371,7 @@ char buf[128]; sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009", tag.GetGroup(), tag.GetElement()); - LOG(ERROR) << buf; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf)); } entry.reset(new DcmDictEntry(tag.GetGroup(), @@ -392,8 +392,8 @@ if (locker->findEntry(name.c_str())) { - LOG(ERROR) << "Cannot register two tags with the same symbolic name \"" << name << "\""; - throw OrthancException(ErrorCode_AlreadyExistingTag); + throw OrthancException(ErrorCode_AlreadyExistingTag, + "Cannot register two tags with the same symbolic name \"" + name + "\""); } locker->addEntry(entry.release()); @@ -1511,7 +1511,7 @@ const std::string* decoded = &utf8Value; if (decodeDataUriScheme && - boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY)) { std::string mime; if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value)) @@ -1673,9 +1673,9 @@ if (!ok) { - LOG(ERROR) << "While creating a DICOM instance, tag (" << tag.Format() - << ") has out-of-range value: \"" << *decoded << "\""; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "While creating a DICOM instance, tag (" + tag.Format() + + ") has out-of-range value: \"" + (*decoded) + "\""); } } @@ -1800,8 +1800,9 @@ (value.asString().length() != 0 && !GetDicomEncoding(encoding, value.asCString()))) { - LOG(ERROR) << "Unknown encoding while creating DICOM from JSON: " << value; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Unknown encoding while creating DICOM from JSON: " + + value.toStyledString()); } if (value.asString().length() == 0) @@ -1924,8 +1925,9 @@ result->transferInit(); if (!result->read(is).good()) { - LOG(ERROR) << "Cannot parse an invalid DICOM file (size: " << size << " bytes)"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot parse an invalid DICOM file (size: " + + boost::lexical_cast(size) + " bytes)"); } result->loadAllDataIntoMemory(); @@ -2044,8 +2046,8 @@ if (output.type() != Json::objectValue) { - LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table"; - throw OrthancException(ErrorCode_LuaBadOutput); + throw OrthancException(ErrorCode_LuaBadOutput, + "Lua: IncomingFindRequestFilter must return a table"); } Json::Value::Members members = output.getMemberNames(); @@ -2054,8 +2056,9 @@ { if (output[members[i]].type() != Json::stringValue) { - LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table mapping names of DICOM tags to strings"; - throw OrthancException(ErrorCode_LuaBadOutput); + throw OrthancException(ErrorCode_LuaBadOutput, + "Lua: IncomingFindRequestFilter must return a table " + "mapping names of DICOM tags to strings"); } DicomTag tag(ParseTag(members[i])); @@ -2186,8 +2189,8 @@ std::string s = Toolbox::ConvertFromUtf8(newValue, encoding); if (element.putString(s.c_str()) != EC_Normal) { - LOG(ERROR) << "Cannot replace value of tag: " << tag.Format(); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot replace value of tag: " + tag.Format()); } break; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomParsing/Internals/DicomImageDecoder.cpp --- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -633,8 +633,8 @@ &dataset, frame, startFragment, &uncompressed[0], uncompressed.size(), decompressedColorModel).good()) { - LOG(ERROR) << "Cannot decode a palette image"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot decode a palette image"); } return DecodeLookupTable(target, info, dataset, @@ -648,8 +648,8 @@ &dataset, frame, startFragment, target->GetBuffer(), target->GetSize(), decompressedColorModel).good()) { - LOG(ERROR) << "Cannot decode a non-palette image"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot decode a non-palette image"); } return target.release(); @@ -806,8 +806,8 @@ } } - LOG(ERROR) << "Cannot decode a DICOM image with the built-in decoder"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot decode a DICOM image with the built-in decoder"); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -82,8 +82,10 @@ #include "ParsedDicomFile.h" #include "FromDcmtkBridge.h" +#include "Internals/DicomFrameIndex.h" #include "ToDcmtkBridge.h" -#include "Internals/DicomFrameIndex.h" + +#include "../Images/PamReader.h" #include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" @@ -160,8 +162,6 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 - static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; - static void ParseTagAndGroup(DcmTagKey& key, const std::string& tag) { @@ -246,27 +246,28 @@ virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/, bool /*deflateAllowed*/) + ORTHANC_OVERRIDE { // No support for compression return HttpCompression_None; } - virtual bool HasContentFilename(std::string& filename) + virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE { return false; } - virtual std::string GetContentType() + virtual std::string GetContentType() ORTHANC_OVERRIDE { - return ""; + return EnumerationToString(MimeType_Binary); } - virtual uint64_t GetContentLength() + virtual uint64_t GetContentLength() ORTHANC_OVERRIDE { return length_; } - virtual bool ReadNextChunk() + virtual bool ReadNextChunk() ORTHANC_OVERRIDE { assert(offset_ <= length_); @@ -291,20 +292,21 @@ if (!cond.good()) { - LOG(ERROR) << "Error while sending a DICOM field: " << cond.text(); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Error while sending a DICOM field: " + + std::string(cond.text())); } return true; } } - virtual const char *GetChunkContent() + virtual const char *GetChunkContent() ORTHANC_OVERRIDE { return chunk_.c_str(); } - virtual size_t GetChunkSize() + virtual size_t GetChunkSize() ORTHANC_OVERRIDE { return chunkSize_; } @@ -362,14 +364,14 @@ { if (pixelItem->getLength() == 0) { - output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM); + output.AnswerBuffer(NULL, 0, MimeType_Binary); return true; } Uint8* buffer = NULL; if (pixelItem->getUint8Array(buffer).good() && buffer) { - output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM); + output.AnswerBuffer(buffer, pixelItem->getLength(), MimeType_Binary); return true; } } @@ -692,7 +694,7 @@ const std::string* decoded = &utf8Value; if (decodeDataUriScheme && - boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY)) { std::string mime; if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value)) @@ -825,7 +827,7 @@ std::string serialized; if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) { - output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM); + output.AnswerBuffer(serialized, MimeType_Binary); } } #endif @@ -954,8 +956,8 @@ } else if (tmp->IsBinary()) { - LOG(ERROR) << "Invalid binary string in the SpecificCharacterSet (0008,0005) tag"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Invalid binary string in the SpecificCharacterSet (0008,0005) tag"); } else if (tmp->IsNull() || tmp->GetContent().empty()) @@ -972,9 +974,9 @@ } else { - LOG(ERROR) << "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" - << tmp->GetContent() << "\""; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" + + tmp->GetContent() + "\""); } } @@ -1064,40 +1066,46 @@ bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme) { - std::string mime, content; - if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme)) + std::string mimeString, content; + if (!Toolbox::DecodeDataUriScheme(mimeString, content, dataUriScheme)) { return false; } - Toolbox::ToLowerCase(mime); + Toolbox::ToLowerCase(mimeString); + MimeType mime = StringToMimeType(mimeString); - if (mime == "image/png") + switch (mime) { + case MimeType_Png: #if ORTHANC_ENABLE_PNG == 1 - EmbedImage(mime, content); + EmbedImage(mime, content); + break; #else - LOG(ERROR) << "Orthanc was compiled without support of PNG"; - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, + "Orthanc was compiled without support of PNG"); #endif - } - else if (mime == "image/jpeg") - { + + case MimeType_Jpeg: #if ORTHANC_ENABLE_JPEG == 1 - EmbedImage(mime, content); + EmbedImage(mime, content); + break; #else - LOG(ERROR) << "Orthanc was compiled without support of JPEG"; - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, + "Orthanc was compiled without support of JPEG"); #endif - } - else if (mime == "application/pdf") - { - EmbedPdf(content); - } - else - { - LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime; - throw OrthancException(ErrorCode_NotImplemented); + + case MimeType_Pam: + EmbedImage(mime, content); + break; + + case MimeType_Pdf: + EmbedPdf(content); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented, + "Unsupported MIME type for the content of a new DICOM file: " + mime); } return true; @@ -1113,29 +1121,44 @@ } -#if (ORTHANC_ENABLE_JPEG == 1 && \ - ORTHANC_ENABLE_PNG == 1) - void ParsedDicomFile::EmbedImage(const std::string& mime, + void ParsedDicomFile::EmbedImage(MimeType mime, const std::string& content) { - if (mime == "image/png") - { - PngReader reader; - reader.ReadFromMemory(content); - EmbedImage(reader); - } - else if (mime == "image/jpeg") + switch (mime) { - JpegReader reader; - reader.ReadFromMemory(content); - EmbedImage(reader); - } - else - { - throw OrthancException(ErrorCode_NotImplemented); + +#if ORTHANC_ENABLE_JPEG == 1 + case MimeType_Jpeg: + { + JpegReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + break; + } +#endif + +#if ORTHANC_ENABLE_PNG == 1 + case MimeType_Png: + { + PngReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + break; + } +#endif + + case MimeType_Pam: + { + PamReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); } } -#endif void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor) @@ -1162,7 +1185,9 @@ ReplacePlainString(DICOM_TAG_COLUMNS, boost::lexical_cast(accessor.GetWidth())); ReplacePlainString(DICOM_TAG_ROWS, boost::lexical_cast(accessor.GetHeight())); ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "1"); - ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1"); + + // The "Number of frames" must only be present in multi-frame images + //ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1"); if (accessor.GetFormat() == PixelFormat_SignedGrayscale16) { @@ -1173,14 +1198,14 @@ ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0"); // Unsigned pixels } - ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0"); // Color channels are interleaved - SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); // by default, greyscale images are in MONOCHROME2 - unsigned int bytesPerPixel = 0; switch (accessor.GetFormat()) { case PixelFormat_Grayscale8: + // By default, grayscale images are MONOCHROME2 + SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); + ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8"); ReplacePlainString(DICOM_TAG_BITS_STORED, "8"); ReplacePlainString(DICOM_TAG_HIGH_BIT, "7"); @@ -1195,10 +1220,18 @@ ReplacePlainString(DICOM_TAG_BITS_STORED, "8"); ReplacePlainString(DICOM_TAG_HIGH_BIT, "7"); bytesPerPixel = 3; + + // "Planar configuration" must only present if "Samples per + // Pixel" is greater than 1 + ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0"); // Color channels are interleaved + break; case PixelFormat_Grayscale16: case PixelFormat_SignedGrayscale16: + // By default, grayscale images are MONOCHROME2 + SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); + ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16"); ReplacePlainString(DICOM_TAG_BITS_STORED, "16"); ReplacePlainString(DICOM_TAG_HIGH_BIT, "15"); @@ -1337,8 +1370,7 @@ if (pdf.size() < 5 || // (*) strncmp("%PDF-", pdf.c_str(), 5) != 0) { - LOG(ERROR) << "Not a PDF file"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file"); } InvalidateCache(); @@ -1346,7 +1378,7 @@ ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage); ReplacePlainString(FromDcmtkBridge::Convert(DCM_Modality), "OT"); ReplacePlainString(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD"); - ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf"); + ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), MIME_PDF); //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1"); std::auto_ptr element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument)); @@ -1388,7 +1420,7 @@ if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) || !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) || sop != UID_EncapsulatedPDFStorage || - mime != "application/pdf") + mime != MIME_PDF) { return false; } @@ -1456,7 +1488,7 @@ void ParsedDicomFile::GetRawFrame(std::string& target, - std::string& mime, + MimeType& mime, unsigned int frameId) { if (pimpl_->frameIndex_.get() == NULL) @@ -1470,16 +1502,16 @@ switch (transferSyntax) { case EXS_JPEGProcess1: - mime = "image/jpeg"; + mime = MimeType_Jpeg; break; case EXS_JPEG2000LosslessOnly: case EXS_JPEG2000: - mime = "image/jp2"; + mime = MimeType_Jpeg2000; break; default: - mime = "application/octet-stream"; + mime = MimeType_Binary; break; } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/DicomParsing/ParsedDicomFile.h --- a/Core/DicomParsing/ParsedDicomFile.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.h Thu Dec 06 15:58:08 2018 +0100 @@ -183,11 +183,8 @@ void EmbedImage(const ImageAccessor& accessor); -#if (ORTHANC_ENABLE_JPEG == 1 && \ - ORTHANC_ENABLE_PNG == 1) - void EmbedImage(const std::string& mime, + void EmbedImage(MimeType mime, const std::string& content); -#endif Encoding GetEncoding() const; @@ -223,7 +220,7 @@ bool ExtractPdf(std::string& pdf); void GetRawFrame(std::string& target, // OUT - std::string& mime, // OUT + MimeType& mime, // OUT unsigned int frameId); // IN unsigned int GetFramesCount() const; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/EnumerationDictionary.h --- a/Core/EnumerationDictionary.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/EnumerationDictionary.h Thu Dec 06 15:58:08 2018 +0100 @@ -84,13 +84,10 @@ Enumeration Translate(const std::string& str) const { - try + if (Toolbox::IsInteger(str)) { return static_cast(boost::lexical_cast(str)); } - catch (boost::bad_lexical_cast&) - { - } typename StringToEnumeration::const_iterator found = stringToEnumeration_.find(str); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Enumerations.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -44,6 +44,18 @@ namespace Orthanc { + static const char* const MIME_CSS = "text/css"; + static const char* const MIME_DICOM = "application/dicom"; + static const char* const MIME_GIF = "image/gif"; + static const char* const MIME_GZIP = "application/gzip"; + static const char* const MIME_HTML = "text/html"; + static const char* const MIME_JAVASCRIPT = "application/javascript"; + static const char* const MIME_JPEG2000 = "image/jp2"; + static const char* const MIME_PLAIN_TEXT = "text/plain"; + static const char* const MIME_WEB_ASSEMBLY = "application/wasm"; + static const char* const MIME_XML_2 = "text/xml"; + static const char* const MIME_ZIP = "application/zip"; + // This function is autogenerated by the script // "Resources/GenerateErrorCodes.py" const char* EnumerationToString(ErrorCode error) @@ -1019,6 +1031,67 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + const char* EnumerationToString(MimeType mime) + { + switch (mime) + { + case MimeType_Binary: + return MIME_BINARY; + + case MimeType_Dicom: + return MIME_DICOM; + + case MimeType_Jpeg: + return MIME_JPEG; + + case MimeType_Jpeg2000: + return MIME_JPEG2000; + + case MimeType_Json: + return MIME_JSON; + + case MimeType_Pdf: + return MIME_PDF; + + case MimeType_Png: + return MIME_PNG; + + case MimeType_Xml: + return MIME_XML; + + case MimeType_PlainText: + return MIME_PLAIN_TEXT; + + case MimeType_Pam: + return MIME_PAM; + + case MimeType_Html: + return MIME_HTML; + + case MimeType_Gzip: + return MIME_GZIP; + + case MimeType_JavaScript: + return MIME_JAVASCRIPT; + + case MimeType_Css: + return MIME_CSS; + + case MimeType_WebAssembly: + return MIME_WEB_ASSEMBLY; + + case MimeType_Gif: + return MIME_GIF; + + case MimeType_Zip: + return MIME_ZIP; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } Encoding StringToEncoding(const char* encoding) @@ -1304,8 +1377,7 @@ if (throwIfUnsupported) { - LOG(ERROR) << s; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, s); } else { @@ -1441,8 +1513,8 @@ } else { - LOG(ERROR) << "Unknown modality manufacturer: \"" << manufacturer << "\""; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown modality manufacturer: \"" + manufacturer + "\""); } if (obsolete) @@ -1535,6 +1607,84 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + MimeType StringToMimeType(const std::string& mime) + { + if (mime == MIME_BINARY) + { + return MimeType_Binary; + } + else if (mime == MIME_DICOM) + { + return MimeType_Dicom; + } + else if (mime == MIME_JPEG) + { + return MimeType_Jpeg; + } + else if (mime == MIME_JPEG2000) + { + return MimeType_Jpeg2000; + } + else if (mime == MIME_JSON) + { + return MimeType_Json; + } + else if (mime == MIME_PDF) + { + return MimeType_Pdf; + } + else if (mime == MIME_PNG) + { + return MimeType_Png; + } + else if (mime == MIME_XML || + mime == MIME_XML_2) + { + return MimeType_Xml; + } + else if (mime == MIME_PLAIN_TEXT) + { + return MimeType_PlainText; + } + else if (mime == MIME_PAM) + { + return MimeType_Pam; + } + else if (mime == MIME_HTML) + { + return MimeType_Html; + } + else if (mime == MIME_GZIP) + { + return MimeType_Gzip; + } + else if (mime == MIME_JAVASCRIPT) + { + return MimeType_JavaScript; + } + else if (mime == MIME_CSS) + { + return MimeType_Css; + } + else if (mime == MIME_WEB_ASSEMBLY) + { + return MimeType_WebAssembly; + } + else if (mime == MIME_GIF) + { + return MimeType_Gif; + } + else if (mime == MIME_ZIP) + { + return MimeType_Zip; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } unsigned int GetBytesPerPixel(PixelFormat format) @@ -1836,6 +1986,27 @@ case ErrorCode_DatabaseUnavailable: return HttpStatus_503_ServiceUnavailable; + case ErrorCode_CreateDicomNotString: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomOverrideTag: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomUseContent: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomNoPayload: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomUseDataUriScheme: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomBadParent: + return HttpStatus_400_BadRequest; + + case ErrorCode_CreateDicomParentIsInstance: + return HttpStatus_400_BadRequest; + default: return HttpStatus_500_InternalServerError; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Enumerations.h --- a/Core/Enumerations.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Enumerations.h Thu Dec 06 15:58:08 2018 +0100 @@ -36,6 +36,7 @@ #include +// Macro "ORTHANC_FORCE_INLINE" forces a function/method to be inlined #if defined(_MSC_VER) # define ORTHANC_FORCE_INLINE __forceinline #elif defined(__GNUC__) || defined(__clang__) || defined(__EMSCRIPTEN__) @@ -45,8 +46,65 @@ #endif +// Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override" +// and "final" keywords introduced in C++11, to do compile-time +// checking of virtual methods +#if __cplusplus >= 201103L +// C++11 is enabled +# define ORTHANC_OVERRIDE override +# define ORTHANC_FINAL final +#else +// C++11 is disabled +# define ORTHANC_OVERRIDE +# define ORTHANC_FINAL +#endif + + namespace Orthanc { + static const char* const URI_SCHEME_PREFIX_BINARY = "data:application/octet-stream;base64,"; + + static const char* const MIME_BINARY = "application/octet-stream"; + static const char* const MIME_JPEG = "image/jpeg"; + static const char* const MIME_JSON = "application/json"; + static const char* const MIME_JSON_UTF8 = "application/json; charset=utf-8"; + static const char* const MIME_PDF = "application/pdf"; + static const char* const MIME_PNG = "image/png"; + static const char* const MIME_XML = "application/xml"; + static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8"; + + /** + * "No Internet Media Type (aka MIME type, content type) for PBM has + * been registered with IANA, but the unofficial value + * image/x-portable-arbitrarymap is assigned by this specification, + * to be consistent with conventional values for the older Netpbm + * formats." http://netpbm.sourceforge.net/doc/pam.html + **/ + static const char* const MIME_PAM = "image/x-portable-arbitrarymap"; + + + enum MimeType + { + MimeType_Binary, + MimeType_Dicom, + MimeType_Html, + MimeType_Jpeg, + MimeType_Jpeg2000, + MimeType_Json, + MimeType_Pam, + MimeType_Pdf, + MimeType_PlainText, + MimeType_Png, + MimeType_Xml, + MimeType_Gzip, + MimeType_JavaScript, + MimeType_Css, + MimeType_WebAssembly, + MimeType_Gif, + MimeType_Zip + }; + + enum Endianness { Endianness_Unknown, @@ -662,6 +720,8 @@ const char* EnumerationToString(JobState state); + const char* EnumerationToString(MimeType mime); + Encoding StringToEncoding(const char* encoding); ResourceType StringToResourceType(const char* type); @@ -682,6 +742,8 @@ JobState StringToJobState(const std::string& state); RequestOrigin StringToRequestOrigin(const std::string& origin); + + MimeType StringToMimeType(const std::string& mime); unsigned int GetBytesPerPixel(PixelFormat format); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/FileStorage/StorageAccessor.h --- a/Core/FileStorage/StorageAccessor.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/FileStorage/StorageAccessor.h Thu Dec 06 15:58:08 2018 +0100 @@ -110,10 +110,24 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void AnswerFile(HttpOutput& output, const FileInfo& info, + MimeType mime) + { + AnswerFile(output, info, EnumerationToString(mime)); + } + + void AnswerFile(HttpOutput& output, + const FileInfo& info, const std::string& mime); void AnswerFile(RestApiOutput& output, const FileInfo& info, + MimeType mime) + { + AnswerFile(output, info, EnumerationToString(mime)); + } + + void AnswerFile(RestApiOutput& output, + const FileInfo& info, const std::string& mime); #endif }; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpClient.cpp --- a/Core/HttpClient.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpClient.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -77,8 +77,9 @@ #if ORTHANC_ENABLE_SSL == 1 return GetHttpStatus(curl_easy_perform(curl), curl, status); #else - LOG(ERROR) << "Orthanc was compiled without SSL support, cannot make HTTPS request"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Orthanc was compiled without SSL support, " + "cannot make HTTPS request"); #endif } } @@ -220,15 +221,15 @@ { if (code == CURLE_NOT_BUILT_IN) { - LOG(ERROR) << "Your libcurl does not contain a required feature, " - << "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Your libcurl does not contain a required feature, " + "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF"); } if (code != CURLE_OK) { - LOG(ERROR) << "libCURL error: " + std::string(curl_easy_strerror(code)); - throw OrthancException(ErrorCode_NetworkProtocol); + throw OrthancException(ErrorCode_NetworkProtocol, + "libCURL error: " + std::string(curl_easy_strerror(code))); } return code; @@ -502,8 +503,8 @@ if (!clientCertificateFile_.empty() && pkcs11Enabled_) { - LOG(ERROR) << "Cannot enable both client certificates and PKCS#11 authentication"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Cannot enable both client certificates and PKCS#11 authentication"); } if (pkcs11Enabled_) @@ -517,12 +518,13 @@ } else { - LOG(ERROR) << "Cannot use PKCS#11 for a HTTPS request, because it has not been initialized"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot use PKCS#11 for a HTTPS request, " + "because it has not been initialized"); } #else - LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without support for PKCS#11"); #endif } else if (!clientCertificateFile_.empty()) @@ -544,8 +546,9 @@ CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str())); } #else - LOG(ERROR) << "This version of Orthanc is compiled without OpenSSL support, cannot use HTTPS client authentication"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without OpenSSL support, " + "cannot use HTTPS client authentication"); #endif } @@ -836,15 +839,15 @@ if (!SystemToolbox::IsRegularFile(certificateFile)) { - LOG(ERROR) << "Cannot open certificate file: " << certificateFile; - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open certificate file: " + certificateFile); } if (!certificateKeyFile.empty() && !SystemToolbox::IsRegularFile(certificateKeyFile)) { - LOG(ERROR) << "Cannot open key file: " << certificateKeyFile; - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open key file: " + certificateKeyFile); } clientCertificateFile_ = certificateFile; @@ -862,8 +865,8 @@ << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)"); GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose); #else - LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without support for PKCS#11"); #endif } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/EmbeddedResourceHttpHandler.cpp --- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -36,6 +36,7 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../SystemToolbox.h" #include "HttpOutput.h" #include @@ -77,14 +78,14 @@ } std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size()); - std::string contentType = Toolbox::AutodetectMimeType(resourcePath); + MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath); try { const void* buffer = EmbeddedResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str()); size_t size = EmbeddedResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str()); - output.SetContentType(contentType.c_str()); + output.SetContentType(contentType); output.Answer(buffer, size); } catch (OrthancException&) diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/FilesystemHttpHandler.cpp --- a/Core/HttpServer/FilesystemHttpHandler.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -107,7 +107,7 @@ s += " "; s += ""; - output.SetContentType("text/html"); + output.SetContentType(MimeType_Html); output.Answer(s); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/HttpContentNegociation.cpp --- a/Core/HttpServer/HttpContentNegociation.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/HttpContentNegociation.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -159,8 +159,9 @@ } else { - LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException( + ErrorCode_BadRequest, + "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value); } } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/HttpFileSender.cpp --- a/Core/HttpServer/HttpFileSender.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/HttpFileSender.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -36,6 +36,7 @@ #include "../OrthancException.h" #include "../Toolbox.h" +#include "../SystemToolbox.h" #include @@ -47,7 +48,7 @@ if (contentType_.empty()) { - contentType_ = Toolbox::AutodetectMimeType(filename); + contentType_ = SystemToolbox::AutodetectMimeType(filename); } } @@ -69,7 +70,7 @@ { if (contentType_.empty()) { - return "application/octet-stream"; + return MIME_BINARY; } else { diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/HttpFileSender.h --- a/Core/HttpServer/HttpFileSender.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/HttpFileSender.h Thu Dec 06 15:58:08 2018 +0100 @@ -44,6 +44,11 @@ std::string filename_; public: + void SetContentType(MimeType contentType) + { + contentType_ = EnumerationToString(contentType); + } + void SetContentType(const std::string& contentType) { contentType_ = contentType; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -151,8 +151,9 @@ } else { - LOG(ERROR) << "Because of keep-alive connections, the entire body must be sent at once or Content-Length must be given"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Because of keep-alive connections, the entire body must " + "be sent at once or Content-Length must be given"); } } @@ -198,8 +199,8 @@ if (hasContentLength_ && contentPosition_ + length > contentLength_) { - LOG(ERROR) << "The body size exceeds what was declared with SetContentSize()"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The body size exceeds what was declared with SetContentSize()"); } if (length > 0) @@ -233,15 +234,15 @@ } else { - LOG(ERROR) << "The body size has not reached what was declared with SetContentSize()"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The body size has not reached what was declared with SetContentSize()"); } break; case State_WritingMultipart: - LOG(ERROR) << "Cannot invoke CloseBody() with multipart outputs"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot invoke CloseBody() with multipart outputs"); case State_Done: return; // Ignore @@ -296,8 +297,8 @@ status == HttpStatus_401_Unauthorized || status == HttpStatus_405_MethodNotAllowed) { - LOG(ERROR) << "Please use the dedicated methods to this HTTP status code in HttpOutput"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Please use the dedicated methods to this HTTP status code in HttpOutput"); } stateMachine_.SetHttpStatus(status); @@ -322,6 +323,7 @@ stateMachine_.SendBody(NULL, 0); } + void HttpOutput::Answer(const void* buffer, size_t length) { @@ -407,8 +409,8 @@ if (keepAlive_) { - LOG(ERROR) << "Multipart answers are not implemented together with keep-alive connections"; - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, + "Multipart answers are not implemented together with keep-alive connections"); } if (state_ != State_WritingHeader) @@ -432,9 +434,9 @@ { if (!Toolbox::StartsWith(*it, "Set-Cookie: ")) { - LOG(ERROR) << "The only headers that can be set in multipart answers " - << "are Set-Cookie (here: " << *it << " is set)"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The only headers that can be set in multipart answers " + "are Set-Cookie (here: " + *it + " is set)"); } header += *it; @@ -589,7 +591,7 @@ std::string contentType = stream.GetContentType(); if (contentType.empty()) { - contentType = "application/octet-stream"; + contentType = MIME_BINARY; } stateMachine_.SetContentType(contentType.c_str()); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/HttpOutput.h --- a/Core/HttpServer/HttpOutput.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/HttpOutput.h Thu Dec 06 15:58:08 2018 +0100 @@ -165,9 +165,14 @@ SendStatus(status, message.c_str(), message.size()); } - void SetContentType(const char* contentType) + void SetContentType(MimeType contentType) { - stateMachine_.SetContentType(contentType); + stateMachine_.SetContentType(EnumerationToString(contentType)); + } + + void SetContentType(const std::string& contentType) + { + stateMachine_.SetContentType(contentType.c_str()); } void SetContentFilename(const char* filename) diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -813,8 +813,8 @@ // Now convert native exceptions as OrthancException catch (boost::bad_lexical_cast&) { - LOG(ERROR) << "Syntax error in some user-supplied data"; - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, + "Syntax error in some user-supplied data"); } catch (std::runtime_error&) { @@ -823,13 +823,13 @@ } catch (std::bad_alloc&) { - LOG(ERROR) << "The server hosting Orthanc is running out of memory"; - throw OrthancException(ErrorCode_NotEnoughMemory); + throw OrthancException(ErrorCode_NotEnoughMemory, + "The server hosting Orthanc is running out of memory"); } catch (...) { - LOG(ERROR) << "An unhandled exception was generated inside the HTTP server"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "An unhandled exception was generated inside the HTTP server"); } } catch (OrthancException& e) @@ -919,6 +919,7 @@ httpCompression_ = true; exceptionFormatter_ = NULL; realm_ = ORTHANC_REALM; + threadsCount_ = 50; // Default value in mongoose #if ORTHANC_ENABLE_SSL == 1 // Check for the Heartbleed exploit @@ -957,6 +958,7 @@ if (!IsRunning()) { std::string port = boost::lexical_cast(port_); + std::string numThreads = boost::lexical_cast(threadsCount_); if (ssl_) { @@ -975,6 +977,9 @@ // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"), #endif + + // Set the number of threads + "num_threads", numThreads.c_str(), // Set the SSL certificate, if any. This must be the last option. ssl_ ? "ssl_certificate" : NULL, @@ -1125,4 +1130,16 @@ return *handler_; } + + + void MongooseServer::SetThreadsCount(unsigned int threads) + { + if (threads <= 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Stop(); + threadsCount_ = threads; + } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/HttpServer/MongooseServer.h --- a/Core/HttpServer/MongooseServer.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/HttpServer/MongooseServer.h Thu Dec 06 15:58:08 2018 +0100 @@ -96,6 +96,7 @@ bool httpCompression_; IHttpExceptionFormatter* exceptionFormatter_; std::string realm_; + unsigned int threadsCount_; bool IsRunning() const; @@ -198,5 +199,12 @@ { realm_ = realm; } + + void SetThreadsCount(unsigned int threads); + + unsigned int GetThreadsCount() const + { + return threadsCount_; + } }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/Font.cpp --- a/Core/Images/Font.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/Font.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -42,8 +42,10 @@ # include "../SystemToolbox.h" #endif +#include "../OrthancException.h" #include "../Toolbox.h" -#include "../OrthancException.h" +#include "Image.h" +#include "ImageProcessing.h" #include #include @@ -151,6 +153,16 @@ #endif +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 + void Font::LoadFromResource(EmbeddedResources::FileResourceId resource) + { + std::string content; + EmbeddedResources::GetFileResource(content, resource); + LoadFromMemory(content); + } +#endif + + static unsigned int MyMin(unsigned int a, unsigned int b) { @@ -335,4 +347,83 @@ DrawInternal(target, utf8, x, y, color); } + + void Font::ComputeTextExtent(unsigned int& width, + unsigned int& height, + const std::string& utf8) const + { + width = 0; + height = 0; + +#if ORTHANC_ENABLE_LOCALE == 1 + std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1); +#else + // If the locale support is disabled, simply drop non-ASCII + // characters from the source UTF-8 string + std::string s = Toolbox::ConvertToAscii(utf8); +#endif + + // Compute the text extent + unsigned int x = 0; + unsigned int y = 0; + + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] == '\n') + { + // Go to the next line + x = 0; + y += (maxHeight_ + 1); + } + else + { + Characters::const_iterator c = characters_.find(s[i]); + if (c != characters_.end()) + { + x += c->second->advance_; + + unsigned int bottom = y + c->second->top_ + c->second->height_; + if (bottom > height) + { + height = bottom; + } + + if (x > width) + { + width = x; + } + } + } + } + } + + + ImageAccessor* Font::Render(const std::string& utf8, + PixelFormat format, + uint8_t r, + uint8_t g, + uint8_t b) const + { + unsigned int width, height; + ComputeTextExtent(width, height, utf8); + + std::auto_ptr target(new Image(format, width, height, false)); + ImageProcessing::Set(*target, 0, 0, 0, 255); + Draw(*target, utf8, 0, 0, r, g, b); + + return target.release(); + } + + + ImageAccessor* Font::RenderAlpha(const std::string& utf8) const + { + unsigned int width, height; + ComputeTextExtent(width, height, utf8); + + std::auto_ptr target(new Image(PixelFormat_Grayscale8, width, height, false)); + ImageProcessing::Set(*target, 0); + Draw(*target, utf8, 0, 0, 255); + + return target.release(); + } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/Font.h --- a/Core/Images/Font.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/Font.h Thu Dec 06 15:58:08 2018 +0100 @@ -33,6 +33,14 @@ #pragma once +#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES) +# error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined +#endif + +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 +# include // Autogenerated file +#endif + #include "ImageAccessor.h" #include @@ -88,6 +96,10 @@ void LoadFromFile(const std::string& path); #endif +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 + void LoadFromResource(EmbeddedResources::FileResourceId resource); +#endif + const std::string& GetName() const { return name_; @@ -111,5 +123,17 @@ uint8_t r, uint8_t g, uint8_t b) const; + + void ComputeTextExtent(unsigned int& width, + unsigned int& height, + const std::string& utf8) const; + + ImageAccessor* Render(const std::string& utf8, + PixelFormat format, + uint8_t r, + uint8_t g, + uint8_t b) const; + + ImageAccessor* RenderAlpha(const std::string& utf8) const; }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/FontRegistry.cpp --- a/Core/Images/FontRegistry.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/FontRegistry.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -88,4 +88,18 @@ return *fonts_[i]; } } + + const Font* FontRegistry::FindFont(const std::string& fontName) const + { + for (Fonts::const_iterator it = fonts_.begin(); it != fonts_.end(); it++) + { + if ((*it)->GetName() == fontName) + { + return *it; + } + } + + return NULL; + } + } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/FontRegistry.h --- a/Core/Images/FontRegistry.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/FontRegistry.h Thu Dec 06 15:58:08 2018 +0100 @@ -71,5 +71,7 @@ } const Font& GetFont(size_t i) const; + + const Font* FindFont(const std::string& fontName) const; }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/ImageAccessor.cpp --- a/Core/Images/ImageAccessor.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/ImageAccessor.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -107,11 +107,8 @@ { if (readOnly_) { -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "Trying to write on a read-only image"; -#endif - - throw OrthancException(ErrorCode_ReadOnly); + throw OrthancException(ErrorCode_ReadOnly, + "Trying to write to a read-only image"); } return buffer_; @@ -135,11 +132,8 @@ { if (readOnly_) { -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "Trying to write on a read-only image"; -#endif - - throw OrthancException(ErrorCode_ReadOnly); + throw OrthancException(ErrorCode_ReadOnly, + "Trying to write to a read-only image"); } if (buffer_ != NULL) @@ -299,10 +293,8 @@ { if (readOnly_) { -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "Trying to modify the format of a read-only image"; -#endif - throw OrthancException(ErrorCode_ReadOnly); + throw OrthancException(ErrorCode_ReadOnly, + "Trying to modify the format of a read-only image"); } if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_)) diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/ImageProcessing.cpp --- a/Core/Images/ImageProcessing.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/ImageProcessing.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -53,12 +53,15 @@ const TargetType minValue = std::numeric_limits::min(); const TargetType maxValue = std::numeric_limits::max(); - for (unsigned int y = 0; y < source.GetHeight(); y++) + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { TargetType* t = reinterpret_cast(target.GetRow(y)); const SourceType* s = reinterpret_cast(source.GetConstRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++) + for (unsigned int x = 0; x < width; x++, t++, s++) { if (static_cast(*s) < static_cast(minValue)) { @@ -83,12 +86,15 @@ { assert(sizeof(float) == 4); - for (unsigned int y = 0; y < source.GetHeight(); y++) + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { float* t = reinterpret_cast(target.GetRow(y)); const SourceType* s = reinterpret_cast(source.GetConstRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++) + for (unsigned int x = 0; x < width; x++, t++, s++) { *t = static_cast(*s); } @@ -96,6 +102,30 @@ } + template + static void ConvertFloatToGrayscale(ImageAccessor& target, + const ImageAccessor& source) + { + typedef typename PixelTraits::PixelType TargetType; + + assert(sizeof(float) == 4); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + TargetType* q = reinterpret_cast(target.GetRow(y)); + const float* p = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + PixelTraits::FloatToPixel(*q, *p); + } + } + } + + template static void ConvertColorToGrayscale(ImageAccessor& target, const ImageAccessor& source) @@ -105,12 +135,15 @@ const TargetType minValue = std::numeric_limits::min(); const TargetType maxValue = std::numeric_limits::max(); - for (unsigned int y = 0; y < source.GetHeight(); y++) + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { TargetType* t = reinterpret_cast(target.GetRow(y)); const uint8_t* s = reinterpret_cast(source.GetConstRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s += 3) + for (unsigned int x = 0; x < width; x++, t++, s += 3) { // Y = 0.2126 R + 0.7152 G + 0.0722 B int32_t v = (2126 * static_cast(s[0]) + @@ -134,17 +167,48 @@ } + static void MemsetZeroInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth(); + const size_t pitch = image.GetPitch(); + + uint8_t *p = reinterpret_cast(image.GetBuffer()); + + for (unsigned int y = 0; y < height; y++) + { + memset(p, 0, lineSize); + p += pitch; + } + } + + template static void SetInternal(ImageAccessor& image, int64_t constant) { - for (unsigned int y = 0; y < image.GetHeight(); y++) + if (constant == 0 && + (image.GetFormat() == PixelFormat_Grayscale8 || + image.GetFormat() == PixelFormat_Grayscale16 || + image.GetFormat() == PixelFormat_Grayscale32 || + image.GetFormat() == PixelFormat_Grayscale64 || + image.GetFormat() == PixelFormat_SignedGrayscale16)) { - PixelType* p = reinterpret_cast(image.GetRow(y)); + MemsetZeroInternal(image); + } + else + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); - for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + for (unsigned int y = 0; y < height; y++) { - *p = static_cast(constant); + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = static_cast(constant); + } } } } @@ -167,9 +231,10 @@ minValue = std::numeric_limits::max(); maxValue = std::numeric_limits::min(); + const unsigned int height = source.GetHeight(); const unsigned int width = source.GetWidth(); - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const PixelType* p = reinterpret_cast(source.GetConstRow(y)); @@ -202,11 +267,14 @@ const int64_t minValue = std::numeric_limits::min(); const int64_t maxValue = std::numeric_limits::max(); - for (unsigned int y = 0; y < image.GetHeight(); y++) + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { PixelType* p = reinterpret_cast(image.GetRow(y)); - for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + for (unsigned int x = 0; x < width; x++, p++) { int64_t v = static_cast(*p) + constant; @@ -240,9 +308,11 @@ const int64_t minValue = std::numeric_limits::min(); const int64_t maxValue = std::numeric_limits::max(); - const unsigned int width = image.GetWidth(); - for (unsigned int y = 0; y < image.GetHeight(); y++) + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { PixelType* p = reinterpret_cast(image.GetRow(y)); @@ -334,7 +404,7 @@ throw OrthancException(ErrorCode_IncompatibleImageFormat); } - unsigned int lineSize = GetBytesPerPixel(source.GetFormat()) * source.GetWidth(); + unsigned int lineSize = source.GetBytesPerPixel() * source.GetWidth(); assert(source.GetPitch() >= lineSize && target.GetPitch() >= lineSize); @@ -354,6 +424,9 @@ throw OrthancException(ErrorCode_IncompatibleImageSize); } + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + if (source.GetFormat() == target.GetFormat()) { Copy(target, source); @@ -451,14 +524,15 @@ return; } + if (target.GetFormat() == PixelFormat_Grayscale8 && source.GetFormat() == PixelFormat_RGBA32) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, q++) + for (unsigned int x = 0; x < width; x++, q++) { *q = static_cast((2126 * static_cast(p[0]) + 7152 * static_cast(p[1]) + @@ -473,11 +547,11 @@ if (target.GetFormat() == PixelFormat_Grayscale8 && source.GetFormat() == PixelFormat_BGRA32) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, q++) + for (unsigned int x = 0; x < width; x++, q++) { *q = static_cast((2126 * static_cast(p[2]) + 7152 * static_cast(p[1]) + @@ -492,11 +566,11 @@ if (target.GetFormat() == PixelFormat_RGB24 && source.GetFormat() == PixelFormat_RGBA32) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = p[0]; q[1] = p[1]; @@ -512,11 +586,11 @@ if (target.GetFormat() == PixelFormat_RGB24 && source.GetFormat() == PixelFormat_BGRA32) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = p[2]; q[1] = p[1]; @@ -532,11 +606,11 @@ if (target.GetFormat() == PixelFormat_RGBA32 && source.GetFormat() == PixelFormat_RGB24) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = p[0]; q[1] = p[1]; @@ -553,11 +627,11 @@ if (target.GetFormat() == PixelFormat_RGB24 && source.GetFormat() == PixelFormat_Grayscale8) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = *p; q[1] = *p; @@ -574,11 +648,11 @@ target.GetFormat() == PixelFormat_BGRA32) && source.GetFormat() == PixelFormat_Grayscale8) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = *p; q[1] = *p; @@ -595,11 +669,11 @@ if (target.GetFormat() == PixelFormat_BGRA32 && source.GetFormat() == PixelFormat_Grayscale16) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { uint8_t value = (*p < 256 ? *p : 255); q[0] = value; @@ -617,11 +691,11 @@ if (target.GetFormat() == PixelFormat_BGRA32 && source.GetFormat() == PixelFormat_SignedGrayscale16) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const int16_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { uint8_t value; if (*p < 0) @@ -652,11 +726,11 @@ if (target.GetFormat() == PixelFormat_BGRA32 && source.GetFormat() == PixelFormat_RGB24) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = p[2]; q[1] = p[1]; @@ -673,11 +747,11 @@ if (target.GetFormat() == PixelFormat_RGB24 && source.GetFormat() == PixelFormat_RGB48) { - for (unsigned int y = 0; y < source.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); uint8_t* q = reinterpret_cast(target.GetRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { q[0] = p[0] >> 8; q[1] = p[1] >> 8; @@ -690,6 +764,20 @@ return; } + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_Float32) + { + ConvertFloatToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_Float32) + { + ConvertFloatToGrayscale(target, source); + return; + } + throw OrthancException(ErrorCode_NotImplemented); } @@ -701,51 +789,23 @@ switch (image.GetFormat()) { case PixelFormat_Grayscale8: - memset(image.GetBuffer(), static_cast(value), image.GetPitch() * image.GetHeight()); + SetInternal(image, value); return; case PixelFormat_Grayscale16: - if (value == 0) - { - memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); - } - else - { - SetInternal(image, value); - } + SetInternal(image, value); return; case PixelFormat_Grayscale32: - if (value == 0) - { - memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); - } - else - { - SetInternal(image, value); - } + SetInternal(image, value); return; case PixelFormat_Grayscale64: - if (value == 0) - { - memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); - } - else - { - SetInternal(image, value); - } + SetInternal(image, value); return; case PixelFormat_SignedGrayscale16: - if (value == 0) - { - memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); - } - else - { - SetInternal(image, value); - } + SetInternal(image, value); return; case PixelFormat_Float32: @@ -797,11 +857,14 @@ throw OrthancException(ErrorCode_NotImplemented); } - for (unsigned int y = 0; y < image.GetHeight(); y++) + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) { uint8_t* q = reinterpret_cast(image.GetRow(y)); - for (unsigned int x = 0; x < image.GetWidth(); x++) + for (unsigned int x = 0; x < width; x++) { for (unsigned int i = 0; i < size; i++) { @@ -885,7 +948,7 @@ { case PixelFormat_Float32: { - assert(sizeof(float) == 32); + assert(sizeof(float) == 4); float a, b; GetMinMaxValueInternal(a, b, image); minValue = a; @@ -1016,15 +1079,18 @@ void ImageProcessing::Invert(ImageAccessor& image) { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + switch (image.GetFormat()) { case PixelFormat_Grayscale8: { - for (unsigned int y = 0; y < image.GetHeight(); y++) + for (unsigned int y = 0; y < height; y++) { uint8_t* p = reinterpret_cast(image.GetRow(y)); - for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + for (unsigned int x = 0; x < width; x++, p++) { *p = 255 - (*p); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/JpegReader.cpp --- a/Core/Images/JpegReader.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/JpegReader.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -121,8 +121,9 @@ { jpeg_destroy_decompress(&cinfo); fclose(fp); - LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage(); - throw OrthancException(ErrorCode_InternalError); + + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG decoding: " + jerr.GetMessage()); } // Below this line, we are under the scope of a "setjmp" @@ -159,8 +160,8 @@ if (setjmp(jerr.GetJumpBuffer())) { jpeg_destroy_decompress(&cinfo); - LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage(); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG decoding: " + jerr.GetMessage()); } // Below this line, we are under the scope of a "setjmp" diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/JpegWriter.cpp --- a/Core/Images/JpegWriter.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/JpegWriter.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -149,8 +149,8 @@ */ jpeg_destroy_compress(&cinfo); fclose(fp); - LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage(); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG encoding: " + jerr.GetMessage()); } // Do not allocate data on the stack below this line! @@ -193,8 +193,8 @@ free(data); } - LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage(); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG encoding: " + jerr.GetMessage()); } // Do not allocate data on the stack below this line! diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/PamReader.cpp --- a/Core/Images/PamReader.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/PamReader.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -244,4 +244,11 @@ content_ = buffer; ParseContent(); } + + void PamReader::ReadFromMemory(const void* buffer, + size_t size) + { + content_.assign(reinterpret_cast(buffer), size); + ParseContent(); + } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Images/PamReader.h --- a/Core/Images/PamReader.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Images/PamReader.h Thu Dec 06 15:58:08 2018 +0100 @@ -54,5 +54,8 @@ #endif void ReadFromMemory(const std::string& buffer); + + void ReadFromMemory(const void* buffer, + size_t size); }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/GenericJobUnserializer.cpp --- a/Core/JobsEngine/GenericJobUnserializer.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/GenericJobUnserializer.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -55,8 +55,8 @@ } else { - LOG(ERROR) << "Cannot unserialize job of type: " << type; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize job of type: " + type); } } @@ -71,8 +71,8 @@ } else { - LOG(ERROR) << "Cannot unserialize operation of type: " << type; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize operation of type: " + type); } } @@ -91,8 +91,8 @@ } else { - LOG(ERROR) << "Cannot unserialize value of type: " << type; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize value of type: " + type); } } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/IJob.h --- a/Core/JobsEngine/IJob.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/IJob.h Thu Dec 06 15:58:08 2018 +0100 @@ -65,5 +65,11 @@ virtual void GetPublicContent(Json::Value& value) = 0; virtual bool Serialize(Json::Value& value) = 0; + + // This function can only be called if the job has reached its + // "success" state + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key) = 0; }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/JobsEngine.cpp --- a/Core/JobsEngine/JobsEngine.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/JobsEngine.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -156,9 +156,9 @@ } - JobsEngine::JobsEngine() : + JobsEngine::JobsEngine(size_t maxCompletedJobs) : state_(State_Setup), - registry_(new JobsRegistry), + registry_(new JobsRegistry(maxCompletedJobs)), threadSleep_(200), workers_(1) { @@ -198,7 +198,9 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } - registry_.reset(new JobsRegistry(unserializer, serialized)); + assert(registry_.get() != NULL); + const size_t maxCompletedJobs = registry_->GetMaxCompletedJobs(); + registry_.reset(new JobsRegistry(unserializer, serialized, maxCompletedJobs)); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/JobsEngine.h --- a/Core/JobsEngine/JobsEngine.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/JobsEngine.h Thu Dec 06 15:58:08 2018 +0100 @@ -68,7 +68,7 @@ size_t workerIndex); public: - JobsEngine(); + JobsEngine(size_t maxCompletedJobs); ~JobsEngine(); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/JobsRegistry.cpp --- a/Core/JobsEngine/JobsRegistry.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/JobsRegistry.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -47,7 +47,6 @@ static const char* JOB = "Job"; static const char* JOBS = "Jobs"; static const char* JOBS_REGISTRY = "JobsRegistry"; - static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs"; static const char* CREATION_TIME = "CreationTime"; static const char* LAST_CHANGE_TIME = "LastChangeTime"; static const char* RUNTIME = "Runtime"; @@ -264,7 +263,7 @@ // as a "RunningJob" instance is running. We do not use a // mutex at the "JobHandler" level, as serialization would be // blocked while a step in the job is running. Instead, we - // save a snapshot of the serialized job. + // save a snapshot of the serialized job. (*) if (lastStatus_.HasSerialized()) { @@ -292,7 +291,7 @@ } else { - LOG(INFO) << "Job backup is not supported for job of type: " << jobType_; + VLOG(1) << "Job backup is not supported for job of type: " << jobType_; return false; } } @@ -435,20 +434,19 @@ void JobsRegistry::ForgetOldCompletedJobs() { - if (maxCompletedJobs_ != 0) + while (completedJobs_.size() > maxCompletedJobs_) { - while (completedJobs_.size() > maxCompletedJobs_) - { - assert(completedJobs_.front() != NULL); + assert(completedJobs_.front() != NULL); + + std::string id = completedJobs_.front()->GetId(); + assert(jobsIndex_.find(id) != jobsIndex_.end()); - std::string id = completedJobs_.front()->GetId(); - assert(jobsIndex_.find(id) != jobsIndex_.end()); + jobsIndex_.erase(id); + delete(completedJobs_.front()); + completedJobs_.pop_front(); + } - jobsIndex_.erase(id); - delete(completedJobs_.front()); - completedJobs_.pop_front(); - } - } + CheckInvariants(); } @@ -458,26 +456,48 @@ job.SetState(success ? JobState_Success : JobState_Failure); completedJobs_.push_back(&job); - ForgetOldCompletedJobs(); - someJobComplete_.notify_all(); } void JobsRegistry::MarkRunningAsCompleted(JobHandler& job, - bool success) + CompletedReason reason) { - LOG(INFO) << "Job has completed with " << (success ? "success" : "failure") - << ": " << job.GetId(); + const char* tmp; + + switch (reason) + { + case CompletedReason_Success: + tmp = "success"; + break; + + case CompletedReason_Failure: + tmp = "success"; + break; + + case CompletedReason_Canceled: + tmp = "cancel"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + LOG(INFO) << "Job has completed with " << tmp << ": " << job.GetId(); CheckInvariants(); assert(job.GetState() == JobState_Running); - SetCompletedJob(job, success); + SetCompletedJob(job, reason == CompletedReason_Success); + + if (reason == CompletedReason_Canceled) + { + job.SetLastErrorCode(ErrorCode_CanceledJob); + } if (observer_ != NULL) { - if (success) + if (reason == CompletedReason_Success) { observer_->SignalJobSuccess(job.GetId()); } @@ -487,7 +507,9 @@ } } - CheckInvariants(); + // WARNING: The following call might make "job" invalid if the job + // history size is empty + ForgetOldCompletedJobs(); } @@ -558,8 +580,14 @@ maxCompletedJobs_ = n; ForgetOldCompletedJobs(); + } + + size_t JobsRegistry::GetMaxCompletedJobs() + { + boost::mutex::scoped_lock lock(mutex_); CheckInvariants(); + return maxCompletedJobs_; } @@ -603,18 +631,45 @@ } + bool JobsRegistry::GetJobOutput(std::string& output, + MimeType& mime, + const std::string& job, + const std::string& key) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(job); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + + if (handler.GetState() == JobState_Success) + { + return handler.GetJob().GetOutput(output, mime, key); + } + else + { + return false; + } + } + } + + void JobsRegistry::SubmitInternal(std::string& id, - JobHandler* handlerRaw, - bool keepLastChangeTime) + JobHandler* handler) { - if (handlerRaw == NULL) + if (handler == NULL) { throw OrthancException(ErrorCode_NullPointer); } - std::auto_ptr handler(handlerRaw); - - boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime(); + std::auto_ptr protection(handler); { boost::mutex::scoped_lock lock(mutex_); @@ -623,13 +678,15 @@ id = handler->GetId(); int priority = handler->GetPriority(); + jobsIndex_.insert(std::make_pair(id, protection.release())); + switch (handler->GetState()) { case JobState_Pending: case JobState_Retry: case JobState_Running: handler->SetState(JobState_Pending); - pendingJobs_.push(handler.get()); + pendingJobs_.push(handler); pendingJobAvailable_.notify_one(); break; @@ -645,18 +702,13 @@ break; default: - LOG(ERROR) << "A job should not be loaded from state: " - << EnumerationToString(handler->GetState()); - throw OrthancException(ErrorCode_InternalError); + { + std::string details = ("A job should not be loaded from state: " + + std::string(EnumerationToString(handler->GetState()))); + throw OrthancException(ErrorCode_InternalError, details); + } } - if (keepLastChangeTime) - { - handler->SetLastStateChangeTime(lastChangeTime); - } - - jobsIndex_.insert(std::make_pair(id, handler.release())); - LOG(INFO) << "New job submitted with priority " << priority << ": " << id; if (observer_ != NULL) @@ -664,7 +716,9 @@ observer_->SignalJobSubmitted(id); } - CheckInvariants(); + // WARNING: The following call might make "handler" invalid if + // the job history size is empty + ForgetOldCompletedJobs(); } } @@ -673,7 +727,7 @@ IJob* job, // Takes ownership int priority) { - SubmitInternal(id, new JobHandler(job, priority), false); + SubmitInternal(id, new JobHandler(job, priority)); } @@ -681,7 +735,7 @@ int priority) { std::string id; - SubmitInternal(id, new JobHandler(job, priority), false); + SubmitInternal(id, new JobHandler(job, priority)); } @@ -904,7 +958,10 @@ throw OrthancException(ErrorCode_InternalError); } - CheckInvariants(); + // WARNING: The following call might make "handler" invalid if + // the job history size is empty + ForgetOldCompletedJobs(); + return true; } } @@ -1091,17 +1148,12 @@ switch (targetState_) { case JobState_Failure: - registry_.MarkRunningAsCompleted(*handler_, false); - - if (canceled_) - { - handler_->SetLastErrorCode(ErrorCode_CanceledJob); - } - + registry_.MarkRunningAsCompleted + (*handler_, canceled_ ? CompletedReason_Canceled : CompletedReason_Failure); break; case JobState_Success: - registry_.MarkRunningAsCompleted(*handler_, true); + registry_.MarkRunningAsCompleted(*handler_, CompletedReason_Success); break; case JobState_Paused: @@ -1293,7 +1345,6 @@ target = Json::objectValue; target[TYPE] = JOBS_REGISTRY; - target[MAX_COMPLETED_JOBS] = static_cast(maxCompletedJobs_); target[JOBS] = Json::objectValue; for (JobsIndex::const_iterator it = jobsIndex_.begin(); @@ -1309,7 +1360,9 @@ JobsRegistry::JobsRegistry(IJobUnserializer& unserializer, - const Json::Value& s) : + const Json::Value& s, + size_t maxCompletedJobs) : + maxCompletedJobs_(maxCompletedJobs), observer_(NULL) { if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY || @@ -1319,17 +1372,28 @@ throw OrthancException(ErrorCode_BadFileFormat); } - maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS); - Json::Value::Members members = s[JOBS].getMemberNames(); for (Json::Value::Members::const_iterator it = members.begin(); it != members.end(); ++it) { std::auto_ptr job(new JobHandler(unserializer, s[JOBS][*it], *it)); - + + const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime(); + std::string id; - SubmitInternal(id, job.release(), true); + SubmitInternal(id, job.release()); + + // Check whether the job has not been removed (which could be + // the case if the "maxCompletedJobs_" value gets smaller) + JobsIndex::iterator found = jobsIndex_.find(id); + if (found != jobsIndex_.end()) + { + // The job still lies in the history: Update the time of its + // last change to the time that was serialized + assert(found->second != NULL); + found->second->SetLastStateChangeTime(lastChangeTime); + } } } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/JobsRegistry.h --- a/Core/JobsEngine/JobsRegistry.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/JobsRegistry.h Thu Dec 06 15:58:08 2018 +0100 @@ -71,6 +71,13 @@ }; private: + enum CompletedReason + { + CompletedReason_Success, + CompletedReason_Failure, + CompletedReason_Canceled + }; + class JobHandler; struct PriorityComparator @@ -115,7 +122,7 @@ bool success); void MarkRunningAsCompleted(JobHandler& job, - bool success); + CompletedReason reason); void MarkRunningAsRetry(JobHandler& job, unsigned int timeout); @@ -130,28 +137,35 @@ void RemoveRetryJob(JobHandler* handler); void SubmitInternal(std::string& id, - JobHandler* handler, - bool keepLastChangeTime); + JobHandler* handler); public: - JobsRegistry() : - maxCompletedJobs_(10), + JobsRegistry(size_t maxCompletedJobs) : + maxCompletedJobs_(maxCompletedJobs), observer_(NULL) { } JobsRegistry(IJobUnserializer& unserializer, - const Json::Value& s); + const Json::Value& s, + size_t maxCompletedJobs); ~JobsRegistry(); void SetMaxCompletedJobs(size_t i); + size_t GetMaxCompletedJobs(); + void ListJobs(std::set& target); bool GetJobInfo(JobInfo& target, const std::string& id); + bool GetJobOutput(std::string& output, + MimeType& mime, + const std::string& job, + const std::string& key); + void Serialize(Json::Value& target); void Submit(std::string& id, diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/Operations/SequenceOfOperationsJob.h --- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Thu Dec 06 15:58:08 2018 +0100 @@ -71,6 +71,8 @@ std::list observers_; TimeoutDicomConnectionManager connectionManager_; + void NotifyDone() const; + public: SequenceOfOperationsJob(); @@ -96,8 +98,8 @@ public: Lock(SequenceOfOperationsJob& that) : - that_(that), - lock_(that.mutex_) + that_(that), + lock_(that.mutex_) { } @@ -145,6 +147,13 @@ virtual bool Serialize(Json::Value& value); + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key) + { + return false; + } + void AwakeTrailingSleep() { operationAdded_.notify_one(); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/JobsEngine/SetOfCommandsJob.h --- a/Core/JobsEngine/SetOfCommandsJob.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/JobsEngine/SetOfCommandsJob.h Thu Dec 06 15:58:08 2018 +0100 @@ -131,5 +131,12 @@ virtual void GetPublicContent(Json::Value& value); virtual bool Serialize(Json::Value& target); + + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key) + { + return false; + } }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Lua/LuaContext.cpp --- a/Core/Lua/LuaContext.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Lua/LuaContext.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -587,8 +587,7 @@ std::string description(lua_tostring(lua_, -1)); lua_pop(lua_, 1); /* pop error message from the stack */ - LOG(ERROR) << "Error while executing Lua script: " << description; - throw OrthancException(ErrorCode_CannotExecuteLua); + throw OrthancException(ErrorCode_CannotExecuteLua, description); } if (output != NULL) diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Lua/LuaFunctionCall.cpp --- a/Core/Lua/LuaFunctionCall.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Lua/LuaFunctionCall.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -105,9 +105,8 @@ std::string description(lua_tostring(context_.lua_, -1)); lua_pop(context_.lua_, 1); /* pop error message from the stack */ - LOG(ERROR) << description; - throw OrthancException(ErrorCode_CannotExecuteLua); + throw OrthancException(ErrorCode_CannotExecuteLua, description); } if (lua_gettop(context_.lua_) < numOutputs) diff -r 3fabf9a673f6 -r eff50153a7b3 Core/OrthancException.h --- a/Core/OrthancException.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/OrthancException.h Thu Dec 06 15:58:08 2018 +0100 @@ -33,19 +33,39 @@ #pragma once +#include "Enumerations.h" +#include "Logging.h" + #include #include -#include "Enumerations.h" +#include namespace Orthanc { class OrthancException { - protected: + private: + OrthancException(); // Forbidden + + OrthancException& operator= (const OrthancException&); // Forbidden + ErrorCode errorCode_; HttpStatus httpStatus_; + // New in Orthanc 1.4.3 + std::auto_ptr details_; + public: + OrthancException(const OrthancException& other) : + errorCode_(other.errorCode_), + httpStatus_(other.httpStatus_) + { + if (other.details_.get() != NULL) + { + details_.reset(new std::string(*other.details_)); + } + } + explicit OrthancException(ErrorCode errorCode) : errorCode_(errorCode), httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)) @@ -53,12 +73,43 @@ } OrthancException(ErrorCode errorCode, + const std::string& details, + bool log = true) : + errorCode_(errorCode), + httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), + details_(new std::string(details)) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + + OrthancException(ErrorCode errorCode, HttpStatus httpStatus) : errorCode_(errorCode), httpStatus_(httpStatus) { } + OrthancException(ErrorCode errorCode, + HttpStatus httpStatus, + const std::string& details, + bool log = true) : + errorCode_(errorCode), + httpStatus_(httpStatus), + details_(new std::string(details)) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + ErrorCode GetErrorCode() const { return errorCode_; @@ -73,5 +124,22 @@ { return EnumerationToString(errorCode_); } + + bool HasDetails() const + { + return details_.get() != NULL; + } + + const char* GetDetails() const + { + if (details_.get() == NULL) + { + return ""; + } + else + { + return details_->c_str(); + } + } }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Pkcs11.cpp --- a/Core/Pkcs11.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Pkcs11.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -187,17 +187,17 @@ ENGINE* engine = ENGINE_new(); if (!engine) { - LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot create an OpenSSL engine for PKCS#11"); } // Create a PKCS#11 context using libp11 context_ = pkcs11_new(); if (!context_) { - LOG(ERROR) << "Cannot create a libp11 context for PKCS#11"; ENGINE_free(engine); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot create a libp11 context for PKCS#11"); } if (!ENGINE_set_id(engine, PKCS11_ENGINE_ID) || @@ -223,10 +223,10 @@ // Make OpenSSL know about our PKCS#11 engine !ENGINE_add(engine)) { - LOG(ERROR) << "Cannot initialize the OpenSSL engine for PKCS#11"; pkcs11_finish(context_); ENGINE_free(engine); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize the OpenSSL engine for PKCS#11"); } // If the "ENGINE_add" worked, it gets a structural @@ -253,28 +253,29 @@ { if (pkcs11Initialized_) { - LOG(ERROR) << "The PKCS#11 engine has already been initialized"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The PKCS#11 engine has already been initialized"); } if (module.empty() || !SystemToolbox::IsRegularFile(module)) { - LOG(ERROR) << "The PKCS#11 module must be a path to one shared library (DLL or .so)"; - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException( + ErrorCode_InexistentFile, + "The PKCS#11 module must be a path to one shared library (DLL or .so)"); } ENGINE* engine = LoadEngine(); if (!engine) { - LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot create an OpenSSL engine for PKCS#11"); } if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0)) { - LOG(ERROR) << "Cannot configure the OpenSSL dynamic engine for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot configure the OpenSSL dynamic engine for PKCS#11"); } if (verbose) @@ -285,14 +286,14 @@ if (!pin.empty() && !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) { - LOG(ERROR) << "Cannot set the PIN code for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot set the PIN code for PKCS#11"); } if (!ENGINE_init(engine)) { - LOG(ERROR) << "Cannot initialize the OpenSSL dynamic engine for PKCS#11"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize the OpenSSL dynamic engine for PKCS#11"); } LOG(WARNING) << "The PKCS#11 engine has been successfully initialized"; diff -r 3fabf9a673f6 -r eff50153a7b3 Core/RestApi/RestApi.cpp --- a/Core/RestApi/RestApi.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/RestApi/RestApi.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -197,12 +197,12 @@ Toolbox::TokenizeString(accepted, it->second, ';'); for (size_t i = 0; i < accepted.size(); i++) { - if (accepted[i] == "application/xml") + if (accepted[i] == MIME_XML) { wrappedOutput.SetConvertJsonToXml(true); } - if (accepted[i] == "application/json") + if (accepted[i] == MIME_JSON) { wrappedOutput.SetConvertJsonToXml(false); } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/RestApi/RestApiOutput.cpp --- a/Core/RestApi/RestApiOutput.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -96,25 +96,28 @@ #if ORTHANC_ENABLE_PUGIXML == 1 std::string s; Toolbox::JsonToXml(s, value); - output_.SetContentType("application/xml; charset=utf-8"); + + output_.SetContentType(MIME_XML_UTF8); output_.Answer(s); #else - LOG(ERROR) << "Orthanc was compiled without XML support"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Orthanc was compiled without XML support"); #endif } else { Json::StyledWriter writer; - output_.SetContentType("application/json; charset=utf-8"); - output_.Answer(writer.write(value)); + std::string s = writer.write(value); + + output_.SetContentType(MIME_JSON_UTF8); + output_.Answer(s); } alreadySent_ = true; } void RestApiOutput::AnswerBuffer(const std::string& buffer, - const std::string& contentType) + MimeType contentType) { AnswerBuffer(buffer.size() == 0 ? NULL : buffer.c_str(), buffer.size(), contentType); @@ -122,10 +125,10 @@ void RestApiOutput::AnswerBuffer(const void* buffer, size_t length, - const std::string& contentType) + MimeType contentType) { CheckStatus(); - output_.SetContentType(contentType.c_str()); + output_.SetContentType(contentType); output_.Answer(buffer, length); alreadySent_ = true; } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/RestApi/RestApiOutput.h --- a/Core/RestApi/RestApiOutput.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/RestApi/RestApiOutput.h Thu Dec 06 15:58:08 2018 +0100 @@ -75,11 +75,11 @@ void AnswerJson(const Json::Value& value); void AnswerBuffer(const std::string& buffer, - const std::string& contentType); + MimeType contentType); void AnswerBuffer(const void* buffer, size_t length, - const std::string& contentType); + MimeType contentType); void SignalError(HttpStatus status); diff -r 3fabf9a673f6 -r eff50153a7b3 Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/SerializationToolbox.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -47,7 +47,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "String value expected in field: " + field); } else { @@ -64,7 +65,8 @@ (value[field.c_str()].type() != Json::intValue && value[field.c_str()].type() != Json::uintValue)) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Integer value expected in field: " + field); } else { @@ -80,7 +82,8 @@ if (tmp < 0) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Unsigned integer value expected in field: " + field); } else { @@ -96,7 +99,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::booleanValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Boolean value expected in field: " + field); } else { @@ -113,7 +117,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::arrayValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected in field: " + field); } const Json::Value& arr = value[field.c_str()]; @@ -124,7 +129,8 @@ { if (arr[i].type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected in field: " + field); } else { @@ -172,7 +178,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::arrayValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Set of DICOM tags expected in field: " + field); } const Json::Value& arr = value[field.c_str()]; @@ -186,7 +193,8 @@ if (arr[i].type() != Json::stringValue || !DicomTag::ParseHexadecimal(tag, arr[i].asCString())) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Set of DICOM tags expected in field: " + field); } else { @@ -204,7 +212,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::objectValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of strings to strings expected in field: " + field); } const Json::Value& source = value[field.c_str()]; @@ -219,7 +228,8 @@ if (tmp.type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of string to strings expected in field: " + field); } else { @@ -237,7 +247,8 @@ !value.isMember(field.c_str()) || value[field.c_str()].type() != Json::objectValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of DICOM tags to strings expected in field: " + field); } const Json::Value& source = value[field.c_str()]; @@ -255,7 +266,8 @@ if (!DicomTag::ParseHexadecimal(tag, members[i].c_str()) || tmp.type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of DICOM tags to strings expected in field: " + field); } else { diff -r 3fabf9a673f6 -r eff50153a7b3 Core/SharedLibrary.cpp --- a/Core/SharedLibrary.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/SharedLibrary.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -119,8 +119,9 @@ if (result == NULL) { - LOG(ERROR) << "Shared library does not expose function \"" << name << "\""; - throw OrthancException(ErrorCode_SharedLibrary); + throw OrthancException( + ErrorCode_SharedLibrary, + "Shared library does not expose function \"" + name + "\""); } else { diff -r 3fabf9a673f6 -r eff50153a7b3 Core/SystemToolbox.cpp --- a/Core/SystemToolbox.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/SystemToolbox.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -38,6 +38,7 @@ #if defined(_WIN32) # include # include // For "_spawnvp()" and "_getpid()" +# include // For "environ" #else # include // For "execvp()" # include // For "waitpid()" @@ -72,6 +73,51 @@ #include +/*========================================================================= + The section below comes from the Boost 1.68.0 project: + https://github.com/boostorg/program_options/blob/boost-1.68.0/src/parsers.cpp + + Copyright Vladimir Prus 2002-2004. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt + or copy at http://www.boost.org/LICENSE_1_0.txt) + =========================================================================*/ + +// The 'environ' should be declared in some cases. E.g. Linux man page says: +// (This variable must be declared in the user program, but is declared in +// the header file unistd.h in case the header files came from libc4 or libc5, +// and in case they came from glibc and _GNU_SOURCE was defined.) +// To be safe, declare it here. + +// It appears that on Mac OS X the 'environ' variable is not +// available to dynamically linked libraries. +// See: http://article.gmane.org/gmane.comp.lib.boost.devel/103843 +// See: http://lists.gnu.org/archive/html/bug-guile/2004-01/msg00013.html +#if defined(__APPLE__) && defined(__DYNAMIC__) +// The proper include for this is crt_externs.h, however it's not +// available on iOS. The right replacement is not known. See +// https://svn.boost.org/trac/boost/ticket/5053 +extern "C" +{ + extern char ***_NSGetEnviron(void); +} +# define environ (*_NSGetEnviron()) +#else +# if defined(__MWERKS__) +# include +# else +# if !defined(_WIN32) || defined(__COMO_VERSION__) +extern char** environ; +# endif +# endif +#endif + + +/*========================================================================= + End of section from the Boost 1.68.0 project + =========================================================================*/ + + namespace Orthanc { static bool finish_; @@ -170,8 +216,8 @@ { if (!IsRegularFile(path)) { - LOG(ERROR) << "The path does not point to a regular file: " << path; - throw OrthancException(ErrorCode_RegularFileExpected); + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + path); } boost::filesystem::ifstream f; @@ -198,8 +244,8 @@ { if (!IsRegularFile(path)) { - LOG(ERROR) << "The path does not point to a regular file: " << path; - throw OrthancException(ErrorCode_RegularFileExpected); + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + path); } boost::filesystem::ifstream f; @@ -438,11 +484,7 @@ if (pid == -1) { // Error in fork() -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "Cannot fork a child process"; -#endif - - throw OrthancException(ErrorCode_SystemCommand); + throw OrthancException(ErrorCode_SystemCommand, "Cannot fork a child process"); } else if (pid == 0) { @@ -461,11 +503,9 @@ if (status != 0) { -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "System command failed with status code " << status; -#endif - - throw OrthancException(ErrorCode_SystemCommand); + throw OrthancException(ErrorCode_SystemCommand, + "System command failed with status code " + + boost::lexical_cast(status)); } } @@ -578,4 +618,117 @@ return threads; } } + + + MimeType SystemToolbox::AutodetectMimeType(const std::string& path) + { + std::string extension = boost::filesystem::extension(path); + Toolbox::ToLowerCase(extension); + + // http://en.wikipedia.org/wiki/Mime_types + // Text types + if (extension == ".txt") + { + return MimeType_PlainText; + } + else if (extension == ".html") + { + return MimeType_Html; + } + else if (extension == ".xml") + { + return MimeType_Xml; + } + else if (extension == ".css") + { + return MimeType_Css; + } + + // Application types + else if (extension == ".js") + { + return MimeType_JavaScript; + } + else if (extension == ".json") + { + return MimeType_Json; + } + else if (extension == ".pdf") + { + return MimeType_Pdf; + } + else if (extension == ".wasm") + { + return MimeType_WebAssembly; + } + + // Images types + else if (extension == ".jpg" || + extension == ".jpeg") + { + return MimeType_Jpeg; + } + else if (extension == ".gif") + { + return MimeType_Gif; + } + else if (extension == ".png") + { + return MimeType_Png; + } + else if (extension == ".pam") + { + return MimeType_Pam; + } + else + { + return MimeType_Binary; + } + } + + + void SystemToolbox::GetEnvironmentVariables(std::map& env) + { + env.clear(); + + for (char **p = environ; *p != NULL; p++) + { + std::string v(*p); + size_t pos = v.find('='); + + if (pos != std::string::npos) + { + std::string key = v.substr(0, pos); + std::string value = v.substr(pos + 1); + env[key] = value; + } + } + } + + + std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory, + const std::string& relativePath) + { + boost::filesystem::path base(baseDirectory); + boost::filesystem::path relative(relativePath); + + /** + The following lines should be equivalent to this one: + + return (base / relative).string(); + + However, for some unknown reason, some versions of Boost do not + make the proper path resolution when "baseDirectory" is an + absolute path. So, a hack is used below. + **/ + + if (relative.is_absolute()) + { + return relative.string(); + } + else + { + return (base / relative).string(); + } + } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/SystemToolbox.h --- a/Core/SystemToolbox.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/SystemToolbox.h Thu Dec 06 15:58:08 2018 +0100 @@ -43,6 +43,7 @@ #include "Enumerations.h" +#include #include #include #include @@ -100,5 +101,12 @@ bool utc); unsigned int GetHardwareConcurrency(); + + MimeType AutodetectMimeType(const std::string& path); + + void GetEnvironmentVariables(std::map& env); + + std::string InterpretRelativePath(const std::string& baseDirectory, + const std::string& relativePath); } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Toolbox.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -40,8 +40,13 @@ #include #include #include -#include -#include +#include + +#if BOOST_VERSION >= 106600 +# include +#else +# include +#endif #include #include @@ -51,6 +56,8 @@ #if ORTHANC_ENABLE_MD5 == 1 +// TODO - Could be replaced by starting +// with Boost >= 1.66.0 # include "../Resources/ThirdParty/md5/md5.h" #endif @@ -310,55 +317,6 @@ } - std::string Toolbox::AutodetectMimeType(const std::string& path) - { - std::string contentType; - size_t lastDot = path.rfind('.'); - size_t lastSlash = path.rfind('/'); - - if (lastDot == std::string::npos || - (lastSlash != std::string::npos && lastDot < lastSlash)) - { - // No trailing dot, unable to detect the content type - } - else - { - const char* extension = &path[lastDot + 1]; - - // http://en.wikipedia.org/wiki/Mime_types - // Text types - if (!strcmp(extension, "txt")) - contentType = "text/plain"; - else if (!strcmp(extension, "html")) - contentType = "text/html"; - else if (!strcmp(extension, "xml")) - contentType = "text/xml"; - else if (!strcmp(extension, "css")) - contentType = "text/css"; - - // Application types - else if (!strcmp(extension, "js")) - contentType = "application/javascript"; - else if (!strcmp(extension, "json")) - contentType = "application/json"; - else if (!strcmp(extension, "pdf")) - contentType = "application/pdf"; - else if (!strcmp(extension, "wasm")) - contentType = "application/wasm"; - - // Images types - else if (!strcmp(extension, "jpg") || !strcmp(extension, "jpeg")) - contentType = "image/jpeg"; - else if (!strcmp(extension, "gif")) - contentType = "image/gif"; - else if (!strcmp(extension, "png")) - contentType = "image/png"; - } - - return contentType; - } - - std::string Toolbox::FlattenUri(const UriComponents& components, size_t fromLevel) { @@ -568,22 +526,25 @@ std::string Toolbox::ConvertToUtf8(const std::string& source, Encoding sourceEncoding) { - if (sourceEncoding == Encoding_Utf8) - { - // Already in UTF-8: No conversion is required - return source; - } - - if (sourceEncoding == Encoding_Ascii) - { - return ConvertToAscii(source); - } - - const char* encoding = GetBoostLocaleEncoding(sourceEncoding); - + // The "::skip" flag makes boost skip invalid UTF-8 + // characters. This can occur in badly-encoded DICOM files. + try { - return boost::locale::conv::to_utf(source, encoding); + if (sourceEncoding == Encoding_Utf8) + { + // Already in UTF-8: No conversion is required + return boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); + } + else if (sourceEncoding == Encoding_Ascii) + { + return ConvertToAscii(source); + } + else + { + const char* encoding = GetBoostLocaleEncoding(sourceEncoding); + return boost::locale::conv::to_utf(source, encoding, boost::locale::conv::skip); + } } catch (std::runtime_error&) { @@ -598,22 +559,25 @@ std::string Toolbox::ConvertFromUtf8(const std::string& source, Encoding targetEncoding) { - if (targetEncoding == Encoding_Utf8) - { - // Already in UTF-8: No conversion is required - return source; - } - - if (targetEncoding == Encoding_Ascii) - { - return ConvertToAscii(source); - } - - const char* encoding = GetBoostLocaleEncoding(targetEncoding); - + // The "::skip" flag makes boost skip invalid UTF-8 + // characters. This can occur in badly-encoded DICOM files. + try { - return boost::locale::conv::from_utf(source, encoding); + if (targetEncoding == Encoding_Utf8) + { + // Already in UTF-8: No conversion is required. + return boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); + } + else if (targetEncoding == Encoding_Ascii) + { + return ConvertToAscii(source); + } + else + { + const char* encoding = GetBoostLocaleEncoding(targetEncoding); + return boost::locale::conv::from_utf(source, encoding, boost::locale::conv::skip); + } } catch (std::runtime_error&) { @@ -624,6 +588,14 @@ #endif + static bool IsAsciiCharacter(uint8_t c) + { + return (c != 0 && + c <= 127 && + (c == '\n' || !iscntrl(c))); + } + + bool Toolbox::IsAsciiString(const void* data, size_t size) { @@ -631,7 +603,7 @@ for (size_t i = 0; i < size; i++, p++) { - if (*p > 127 || *p == 0 || iscntrl(*p)) + if (!IsAsciiCharacter(*p)) { return false; } @@ -654,7 +626,7 @@ result.reserve(source.size() + 1); for (size_t i = 0; i < source.size(); i++) { - if (source[i] <= 127 && source[i] >= 0 && !iscntrl(source[i])) + if (IsAsciiCharacter(source[i])) { result.push_back(source[i]); } @@ -1420,8 +1392,8 @@ if (!ok && !SetGlobalLocale(NULL)) { - LOG(ERROR) << "Cannot initialize global locale"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize global locale"); } } @@ -1437,8 +1409,8 @@ { if (globalLocale_.get() == NULL) { - LOG(ERROR) << "No global locale was set, call Toolbox::InitializeGlobalLocale()"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No global locale was set, call Toolbox::InitializeGlobalLocale()"); } /** @@ -1468,9 +1440,9 @@ * "utf_to_utf" in order to convert to/from std::wstring. **/ - std::wstring w = boost::locale::conv::utf_to_utf(source); + std::wstring w = boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); w = boost::algorithm::to_upper_copy(w, *globalLocale_); - return boost::locale::conv::utf_to_utf(w); + return boost::locale::conv::utf_to_utf(w, boost::locale::conv::skip); } #endif @@ -1525,6 +1497,98 @@ #endif return s; } + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + class VariableFormatter + { + public: + typedef std::map Dictionary; + + private: + const Dictionary& dictionary_; + + public: + VariableFormatter(const Dictionary& dictionary) : + dictionary_(dictionary) + { + } + + template + Out operator()(const boost::smatch& what, + Out out) const + { + if (!what[1].str().empty()) + { + // Variable without a default value + Dictionary::const_iterator found = dictionary_.find(what[1]); + + if (found != dictionary_.end()) + { + const std::string& value = found->second; + out = std::copy(value.begin(), value.end(), out); + } + } + else + { + // Variable with a default value + std::string key; + std::string defaultValue; + + if (!what[2].str().empty()) + { + key = what[2].str(); + defaultValue = what[3].str(); + } + else if (!what[4].str().empty()) + { + key = what[4].str(); + defaultValue = what[5].str(); + } + else if (!what[6].str().empty()) + { + key = what[6].str(); + defaultValue = what[7].str(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + Dictionary::const_iterator found = dictionary_.find(key); + + if (found == dictionary_.end()) + { + out = std::copy(defaultValue.begin(), defaultValue.end(), out); + } + else + { + const std::string& value = found->second; + out = std::copy(value.begin(), value.end(), out); + } + } + + return out; + } + }; + } + + + std::string Toolbox::SubstituteVariables(const std::string& source, + const std::map& dictionary) + { + const boost::regex pattern("\\$\\{([^:]*?)\\}|" // ${what[1]} + "\\$\\{([^:]*?):-([^'\"]*?)\\}|" // ${what[2]:-what[3]} + "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|" // ${what[4]:-"what[5]"} + "\\$\\{([^:]*?):-'([^']*?)'\\}"); // ${what[6]:-'what[7]'} + + VariableFormatter formatter(dictionary); + + return boost::regex_replace(source, pattern, formatter); + } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/Toolbox.h --- a/Core/Toolbox.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/Toolbox.h Thu Dec 06 15:58:08 2018 +0100 @@ -117,8 +117,6 @@ bool IsChildUri(const UriComponents& baseUri, const UriComponents& testedUri); - std::string AutodetectMimeType(const std::string& path); - std::string FlattenUri(const UriComponents& components, size_t fromLevel = 0); @@ -238,6 +236,9 @@ void FinalizeOpenSsl(); std::string GenerateUuid(); + + std::string SubstituteVariables(const std::string& source, + const std::map& dictionary); } } diff -r 3fabf9a673f6 -r eff50153a7b3 Core/WebServiceParameters.cpp --- a/Core/WebServiceParameters.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Core/WebServiceParameters.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -92,8 +92,7 @@ if (!Toolbox::StartsWith(url, "http://") && !Toolbox::StartsWith(url, "https://")) { - LOG(ERROR) << "Bad URL: " << url; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "Bad URL: " + url); } // Add trailing slash if needed @@ -142,8 +141,9 @@ if (certificateKeyPassword.empty()) { - LOG(ERROR) << "The password for the HTTPS certificate is not provided: " << certificateFile; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException( + ErrorCode_BadFileFormat, + "The password for the HTTPS certificate is not provided: " + certificateFile); } certificateFile_ = certificateFile; @@ -173,8 +173,8 @@ } else if (peer.size() == 2) { - LOG(ERROR) << "The HTTP password is not provided"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "The HTTP password is not provided"); } else if (peer.size() == 3) { @@ -364,8 +364,9 @@ { if (IsReservedKey(key)) { - LOG(ERROR) << "Cannot use this reserved key to name an user property: " << key; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException( + ErrorCode_ParameterOutOfRange, + "Cannot use this reserved key to name an user property: " + key); } else { @@ -488,15 +489,15 @@ { if (!SystemToolbox::IsRegularFile(certificateFile_)) { - LOG(ERROR) << "Cannot open certificate file: " << certificateFile_; - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open certificate file: " + certificateFile_); } if (!certificateKeyFile_.empty() && !SystemToolbox::IsRegularFile(certificateKeyFile_)) { - LOG(ERROR) << "Cannot open key file: " << certificateKeyFile_; - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open key file: " + certificateKeyFile_); } } } diff -r 3fabf9a673f6 -r eff50153a7b3 NEWS --- a/NEWS Thu Oct 18 10:48:11 2018 +0200 +++ b/NEWS Thu Dec 06 15:58:08 2018 +0100 @@ -6,6 +6,10 @@ ------- * Possibility to restrict the allowed DICOM commands for each modality +* The Orthanc configuration file can use environment variables +* New configuration options: + - "DicomModalitiesInDatabase" to store the definitions of modalities in the database + - "OrthancPeersInDatabase" to store the definitions of Orthanc peers in the database Orthanc Explorer ---------------- @@ -16,16 +20,37 @@ REST API -------- +* API Version has been upgraded to 1.2 +* Asynchronous generation of ZIP archives and DICOM medias * New URI: "/studies/.../merge" to merge a study * 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 options to URI "/queries/.../answers": "?expand" and "?simplify" +* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option) + +Plugins +------- + +* New function in the SDK: "OrthancPluginSetHttpErrorDetails()" Maintenance ----------- +* "SynchronousCMove" is now "true" by default * New modality manufacturer: "GE" for GE Healthcare EA and AW * Executing a query/retrieve from the REST API now creates a job * Fix: Closing DICOM associations after running query/retrieve from REST API -* Fix: Allow creation of MONOCHROME1 greyscale images in tools/create-dicom +* Fix: Allow creation of MONOCHROME1 grayscale images in tools/create-dicom +* Remove invalid characters from badly-encoded UTF-8 strings (impacts PostgreSQL) +* Orthanc starts even if jobs from a previous execution cannot be unserialized +* New CMake option "ENABLE_DCMTK_LOG" to disable logging internal to DCMTK +* Fix issue 114 (Boost 1.68 doesn't support SHA-1 anymore) +* Support of "JobsHistorySize" set to zero +* Upgraded dependencies for static and Windows builds: + - boost 1.68.0 + - lua 5.3.5 + Version 1.4.2 (2018-09-20) ========================== diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancExplorer/explorer.js --- a/OrthancExplorer/explorer.js Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancExplorer/explorer.js Thu Dec 06 15:58:08 2018 +0100 @@ -839,7 +839,7 @@ success: function(s) { var ancestor = s.RemainingAncestor; if (ancestor == null) - $.mobile.changePage('#find-patients'); + $.mobile.changePage('#lookup'); else $.mobile.changePage('#' + ancestor.Type.toLowerCase() + '?uuid=' + ancestor.ID); } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -366,8 +366,8 @@ if (!ok) { - LOG(ERROR) << "Incompatible version of the Orthanc database: " << tmp; - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "Incompatible version of the Orthanc database: " + tmp); } signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -146,14 +146,18 @@ } - struct DicomInstanceToStore::PImpl + class DicomInstanceToStore::PImpl { - DicomInstanceOrigin origin_; - SmartContainer buffer_; - SmartContainer parsed_; - SmartContainer summary_; - SmartContainer json_; - MetadataMap metadata_; + public: + DicomInstanceOrigin origin_; + SmartContainer buffer_; + SmartContainer parsed_; + SmartContainer summary_; + SmartContainer json_; + MetadataMap metadata_; + + private: + std::auto_ptr hasher_; void ComputeMissingInformation() { @@ -184,8 +188,8 @@ if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), *parsed_.GetContent().GetDcmtkObject().getDataset())) { - LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Unable to serialize a DICOM file to a memory buffer"); } } @@ -225,6 +229,7 @@ } + public: const char* GetBufferData() { ComputeMissingInformation(); @@ -284,6 +289,22 @@ } + DicomInstanceHasher& GetHasher() + { + if (hasher_.get() == NULL) + { + hasher_.reset(new DicomInstanceHasher(GetSummary())); + } + + if (hasher_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *hasher_; + } + + bool LookupTransferSyntax(std::string& result) { ComputeMissingInformation(); @@ -396,4 +417,10 @@ { return pimpl_->LookupTransferSyntax(result); } + + + DicomInstanceHasher& DicomInstanceToStore::GetHasher() + { + return pimpl_->GetHasher(); + } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/DicomInstanceToStore.h --- a/OrthancServer/DicomInstanceToStore.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.h Thu Dec 06 15:58:08 2018 +0100 @@ -33,6 +33,7 @@ #pragma once +#include "../Core/DicomFormat/DicomInstanceHasher.h" #include "../Core/DicomFormat/DicomMap.h" #include "DicomInstanceOrigin.h" #include "ServerEnumerations.h" @@ -49,7 +50,7 @@ typedef std::map, std::string> MetadataMap; private: - struct PImpl; + class PImpl; boost::shared_ptr pimpl_; public: @@ -84,5 +85,7 @@ const Json::Value& GetJson(); bool LookupTransferSyntax(std::string& result); + + DicomInstanceHasher& GetHasher(); }; } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/LuaScripting.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -34,7 +34,7 @@ #include "PrecompiledHeadersServer.h" #include "LuaScripting.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" #include "ServerContext.h" @@ -414,7 +414,11 @@ int LuaScripting::GetOrthancConfiguration(lua_State *state) { Json::Value configuration; - Configuration::GetConfiguration(configuration); + + { + OrthancConfiguration::ReaderLock lock; + configuration = lock.GetJson(); + } LuaContext::GetLuaContext(state).PushJson(configuration); @@ -445,7 +449,12 @@ } std::string name = parameters["Modality"].asString(); - RemoteModalityParameters modality = Configuration::GetModalityUsingSymbolicName(name); + RemoteModalityParameters modality; + + { + OrthancConfiguration::ReaderLock configLock; + modality = configLock.GetConfiguration().GetModalityUsingSymbolicName(name); + } // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()" return lock.AddStoreScuOperation(localAet, modality); @@ -453,17 +462,18 @@ if (operation == "store-peer") { + OrthancConfiguration::ReaderLock configLock; std::string name = parameters["Peer"].asString(); WebServiceParameters peer; - if (Configuration::GetOrthancPeer(peer, name)) + if (configLock.GetConfiguration().LookupOrthancPeer(peer, name)) { return lock.AddStorePeerOperation(peer); } else { - LOG(ERROR) << "No peer with symbolic name: " << name; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "No peer with symbolic name: " + name); } } @@ -742,17 +752,19 @@ void LuaScripting::LoadGlobalConfiguration() { + OrthancConfiguration::ReaderLock configLock; + lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); std::list luaScripts; - Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts"); + configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts"); LuaScripting::Lock lock(*this); for (std::list::const_iterator it = luaScripts.begin(); it != luaScripts.end(); ++it) { - std::string path = Configuration::InterpretStringParameterAsPath(*it); + std::string path = configLock.GetConfiguration().InterpretStringParameterAsPath(*it); LOG(INFO) << "Installing the Lua scripts from: " << path; std::string script; SystemToolbox::ReadFile(script, path); diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancConfiguration.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancConfiguration.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -0,0 +1,829 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 . + **/ + + +#include "PrecompiledHeadersServer.h" +#include "OrthancConfiguration.h" + +#include "../Core/HttpServer/MongooseServer.h" +#include "../Core/Logging.h" +#include "../Core/OrthancException.h" +#include "../Core/SystemToolbox.h" +#include "../Core/Toolbox.h" + +#include "ServerIndex.h" + + +static const char* const DICOM_MODALITIES = "DicomModalities"; +static const char* const DICOM_MODALITIES_IN_DB = "DicomModalitiesInDatabase"; +static const char* const ORTHANC_PEERS = "OrthancPeers"; +static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase"; + +namespace Orthanc +{ + static void AddFileToConfiguration(Json::Value& target, + const boost::filesystem::path& path) + { + std::map env; + SystemToolbox::GetEnvironmentVariables(env); + + LOG(WARNING) << "Reading the configuration from: " << path; + + Json::Value config; + + { + std::string content; + SystemToolbox::ReadFile(content, path.string()); + + content = Toolbox::SubstituteVariables(content, env); + + Json::Value tmp; + Json::Reader reader; + if (!reader.parse(content, tmp) || + tmp.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadJson, + "The configuration file does not follow the JSON syntax: " + path.string()); + } + + Toolbox::CopyJsonWithoutComments(config, tmp); + } + + if (target.size() == 0) + { + target = config; + } + else + { + // Merge the newly-added file with the previous content of "target" + Json::Value::Members members = config.getMemberNames(); + for (Json::Value::ArrayIndex i = 0; i < members.size(); i++) + { + if (target.isMember(members[i])) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The configuration section \"" + members[i] + + "\" is defined in 2 different configuration files"); + } + else + { + target[members[i]] = config[members[i]]; + } + } + } + } + + + static void ScanFolderForConfiguration(Json::Value& target, + const char* folder) + { + using namespace boost::filesystem; + + LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files"; + + directory_iterator end_it; // default construction yields past-the-end + for (directory_iterator it(folder); + it != end_it; + ++it) + { + if (!is_directory(it->status())) + { + std::string extension = boost::filesystem::extension(it->path()); + Toolbox::ToLowerCase(extension); + + if (extension == ".json") + { + AddFileToConfiguration(target, it->path().string()); + } + } + } + } + + + static void ReadConfiguration(Json::Value& target, + const char* configurationFile) + { + target = Json::objectValue; + + if (configurationFile != NULL) + { + if (!boost::filesystem::exists(configurationFile)) + { + throw OrthancException(ErrorCode_InexistentFile, + "Inexistent path to configuration: " + + std::string(configurationFile)); + } + + if (boost::filesystem::is_directory(configurationFile)) + { + ScanFolderForConfiguration(target, configurationFile); + } + else + { + AddFileToConfiguration(target, configurationFile); + } + } + else + { +#if ORTHANC_STANDALONE == 1 + // No default path for the standalone configuration + LOG(WARNING) << "Using the default Orthanc configuration"; + return; + +#else + // In a non-standalone build, we use the + // "Resources/Configuration.json" from the Orthanc source code + + boost::filesystem::path p = ORTHANC_PATH; + p /= "Resources"; + p /= "Configuration.json"; + + AddFileToConfiguration(target, p); +#endif + } + } + + + static void CheckAlphanumeric(const std::string& s) + { + for (size_t j = 0; j < s.size(); j++) + { + if (!isalnum(s[j]) && + s[j] != '-') + { + throw OrthancException(ErrorCode_BadFileFormat, + "Only alphanumeric and dash characters are allowed " + "in the names of modalities/peers, but found: " + s); + } + } + } + + + void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source) + { + modalities_.clear(); + + if (source.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Bad format of the \"" + std::string(DICOM_MODALITIES) + + "\" configuration section"); + } + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + CheckAlphanumeric(name); + + RemoteModalityParameters modality; + modality.Unserialize(source[name]); + modalities_[name] = modality; + } + } + + + void OrthancConfiguration::LoadPeersFromJson(const Json::Value& source) + { + peers_.clear(); + + if (source.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Bad format of the \"" + std::string(ORTHANC_PEERS) + + "\" configuration section"); + } + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + CheckAlphanumeric(name); + + WebServiceParameters peer; + peer.Unserialize(source[name]); + peers_[name] = peer; + } + } + + + void OrthancConfiguration::LoadModalities() + { + if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false)) + { + // Modalities are stored in the database + if (serverIndex_ == NULL) + { + throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}"); + + Json::Reader reader; + Json::Value modalities; + if (reader.parse(property, modalities)) + { + LoadModalitiesFromJson(modalities); + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot unserialize the list of modalities from the Orthanc database"); + } + } + } + else + { + // Modalities are stored in the configuration files + if (json_.isMember(DICOM_MODALITIES)) + { + LoadModalitiesFromJson(json_[DICOM_MODALITIES]); + } + else + { + modalities_.clear(); + } + } + } + + void OrthancConfiguration::LoadPeers() + { + if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false)) + { + // Peers are stored in the database + if (serverIndex_ == NULL) + { + throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}"); + + Json::Reader reader; + Json::Value peers; + if (reader.parse(property, peers)) + { + LoadPeersFromJson(peers); + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot unserialize the list of peers from the Orthanc database"); + } + } + } + else + { + // Peers are stored in the configuration files + if (json_.isMember(ORTHANC_PEERS)) + { + LoadPeersFromJson(json_[ORTHANC_PEERS]); + } + else + { + peers_.clear(); + } + } + } + + + void OrthancConfiguration::SaveModalitiesToJson(Json::Value& target) + { + target = Json::objectValue; + + for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it) + { + Json::Value modality; + it->second.Serialize(modality, true /* force advanced format */); + + target[it->first] = modality; + } + } + + + void OrthancConfiguration::SavePeersToJson(Json::Value& target) + { + target = Json::objectValue; + + for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it) + { + Json::Value peer; + it->second.Serialize(peer, + false /* use simple format if possible */, + true /* include passwords */); + + target[it->first] = peer; + } + } + + + void OrthancConfiguration::SaveModalities() + { + if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false)) + { + // Modalities are stored in the database + if (serverIndex_ == NULL) + { + throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Json::Value modalities; + SaveModalitiesToJson(modalities); + + Json::FastWriter writer; + std::string s = writer.write(modalities); + + serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s); + } + } + else + { + // Modalities are stored in the configuration files + if (!modalities_.empty() || + json_.isMember(DICOM_MODALITIES)) + { + SaveModalitiesToJson(json_[DICOM_MODALITIES]); + } + } + } + + + void OrthancConfiguration::SavePeers() + { + if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false)) + { + // Peers are stored in the database + if (serverIndex_ == NULL) + { + throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Json::Value peers; + SavePeersToJson(peers); + + Json::FastWriter writer; + std::string s = writer.write(peers); + + serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s); + } + } + else + { + // Peers are stored in the configuration files + if (!peers_.empty() || + json_.isMember(ORTHANC_PEERS)) + { + SavePeersToJson(json_[ORTHANC_PEERS]); + } + } + } + + + OrthancConfiguration& OrthancConfiguration::GetInstance() + { + static OrthancConfiguration configuration; + return configuration; + } + + + std::string OrthancConfiguration::GetStringParameter(const std::string& parameter, + const std::string& defaultValue) const + { + if (json_.isMember(parameter)) + { + if (json_[parameter].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadParameterType, + "The configuration option \"" + parameter + "\" must be a string"); + } + else + { + return json_[parameter].asString(); + } + } + else + { + return defaultValue; + } + } + + + int OrthancConfiguration::GetIntegerParameter(const std::string& parameter, + int defaultValue) const + { + if (json_.isMember(parameter)) + { + if (json_[parameter].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadParameterType, + "The configuration option \"" + parameter + "\" must be an integer"); + } + else + { + return json_[parameter].asInt(); + } + } + else + { + return defaultValue; + } + } + + + unsigned int OrthancConfiguration::GetUnsignedIntegerParameter( + const std::string& parameter, + unsigned int defaultValue) const + { + int v = GetIntegerParameter(parameter, defaultValue); + + if (v < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "The configuration option \"" + parameter + "\" must be a positive integer"); + } + else + { + return static_cast(v); + } + } + + + bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter, + bool defaultValue) const + { + if (json_.isMember(parameter)) + { + if (json_[parameter].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadParameterType, + "The configuration option \"" + parameter + + "\" must be a Boolean (true or false)"); + } + else + { + return json_[parameter].asBool(); + } + } + else + { + return defaultValue; + } + } + + + void OrthancConfiguration::Read(const char* configurationFile) + { + // Read the content of the configuration + configurationFileArg_ = configurationFile; + ReadConfiguration(json_, configurationFile); + + // Adapt the paths to the configurations + defaultDirectory_ = boost::filesystem::current_path(); + configurationAbsolutePath_ = ""; + + if (configurationFile) + { + if (boost::filesystem::is_directory(configurationFile)) + { + defaultDirectory_ = boost::filesystem::path(configurationFile); + configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string(); + } + else + { + defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path(); + configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string(); + } + } + else + { +#if ORTHANC_STANDALONE != 1 + // In a non-standalone build, we use the + // "Resources/Configuration.json" from the Orthanc source code + + boost::filesystem::path p = ORTHANC_PATH; + p /= "Resources"; + p /= "Configuration.json"; + configurationAbsolutePath_ = boost::filesystem::absolute(p).string(); +#endif + } + } + + + void OrthancConfiguration::LoadModalitiesAndPeers() + { + LoadModalities(); + LoadPeers(); + } + + + void OrthancConfiguration::GetDicomModalityUsingSymbolicName( + RemoteModalityParameters& modality, + const std::string& name) const + { + Modalities::const_iterator found = modalities_.find(name); + + if (found == modalities_.end()) + { + throw OrthancException(ErrorCode_InexistentItem, + "No modality with symbolic name: " + name); + } + else + { + modality = found->second; + } + } + + + bool OrthancConfiguration::LookupOrthancPeer(WebServiceParameters& peer, + const std::string& name) const + { + Peers::const_iterator found = peers_.find(name); + + if (found == peers_.end()) + { + LOG(ERROR) << "No peer with symbolic name: " << name; + return false; + } + else + { + peer = found->second; + return true; + } + } + + + void OrthancConfiguration::GetListOfDicomModalities(std::set& target) const + { + target.clear(); + + for (Modalities::const_iterator + it = modalities_.begin(); it != modalities_.end(); ++it) + { + target.insert(it->first); + } + } + + + void OrthancConfiguration::GetListOfOrthancPeers(std::set& target) const + { + target.clear(); + + for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it) + { + target.insert(it->first); + } + } + + + void OrthancConfiguration::SetupRegisteredUsers(MongooseServer& httpServer) const + { + httpServer.ClearUsers(); + + if (!json_.isMember("RegisteredUsers")) + { + return; + } + + const Json::Value& users = json_["RegisteredUsers"]; + if (users.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); + } + + Json::Value::Members usernames = users.getMemberNames(); + for (size_t i = 0; i < usernames.size(); i++) + { + const std::string& username = usernames[i]; + std::string password = users[username].asString(); + httpServer.RegisterUser(username.c_str(), password.c_str()); + } + } + + + std::string OrthancConfiguration::InterpretStringParameterAsPath( + const std::string& parameter) const + { + return SystemToolbox::InterpretRelativePath(defaultDirectory_.string(), parameter); + } + + + void OrthancConfiguration::GetListOfStringsParameter(std::list& target, + const std::string& key) const + { + target.clear(); + + if (!json_.isMember(key)) + { + return; + } + + const Json::Value& lst = json_[key]; + + if (lst.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings"); + } + + for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++) + { + target.push_back(lst[i].asString()); + } + } + + + bool OrthancConfiguration::IsSameAETitle(const std::string& aet1, + const std::string& aet2) const + { + if (GetBooleanParameter("StrictAetComparison", false)) + { + // Case-sensitive matching + return aet1 == aet2; + } + else + { + // Case-insensitive matching (default) + std::string tmp1, tmp2; + Toolbox::ToLowerCase(tmp1, aet1); + Toolbox::ToLowerCase(tmp2, aet2); + return tmp1 == tmp2; + } + } + + + bool OrthancConfiguration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality, + const std::string& aet) const + { + for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it) + { + if (IsSameAETitle(aet, it->second.GetApplicationEntityTitle())) + { + modality = it->second; + return true; + } + } + + return false; + } + + + bool OrthancConfiguration::IsKnownAETitle(const std::string& aet, + const std::string& ip) const + { + RemoteModalityParameters modality; + + if (!LookupDicomModalityUsingAETitle(modality, aet)) + { + LOG(WARNING) << "Modality \"" << aet + << "\" is not listed in the \"DicomModalities\" configuration option"; + return false; + } + else if (!GetBooleanParameter("DicomCheckModalityHost", false) || + ip == modality.GetHost()) + { + return true; + } + else + { + LOG(WARNING) << "Forbidding access from AET \"" << aet + << "\" given its hostname (" << ip << ") does not match " + << "the \"DicomModalities\" configuration option (" + << modality.GetHost() << " was expected)"; + return false; + } + } + + + RemoteModalityParameters + OrthancConfiguration::GetModalityUsingSymbolicName(const std::string& name) const + { + RemoteModalityParameters modality; + GetDicomModalityUsingSymbolicName(modality, name); + + return modality; + } + + + RemoteModalityParameters + OrthancConfiguration::GetModalityUsingAet(const std::string& aet) const + { + RemoteModalityParameters modality; + + if (LookupDicomModalityUsingAETitle(modality, aet)) + { + return modality; + } + else + { + throw OrthancException(ErrorCode_InexistentItem, + "Unknown modality for AET: " + aet); + } + } + + + void OrthancConfiguration::UpdateModality(const std::string& symbolicName, + const RemoteModalityParameters& modality) + { + modalities_[symbolicName] = modality; + SaveModalities(); + } + + + void OrthancConfiguration::RemoveModality(const std::string& symbolicName) + { + modalities_.erase(symbolicName); + SaveModalities(); + } + + + void OrthancConfiguration::UpdatePeer(const std::string& symbolicName, + const WebServiceParameters& peer) + { + peer.CheckClientCertificate(); + + peers_[symbolicName] = peer; + SavePeers(); + } + + + void OrthancConfiguration::RemovePeer(const std::string& symbolicName) + { + peers_.erase(symbolicName); + SavePeers(); + } + + + void OrthancConfiguration::Format(std::string& result) const + { + Json::StyledWriter w; + result = w.write(json_); + } + + + void OrthancConfiguration::SetDefaultEncoding(Encoding encoding) + { + SetDefaultDicomEncoding(encoding); + + // Propagate the encoding to the configuration file that is + // stored in memory + json_["DefaultEncoding"] = EnumerationToString(encoding); + } + + + bool OrthancConfiguration::HasConfigurationChanged() const + { + Json::Value current; + ReadConfiguration(current, configurationFileArg_); + + Json::FastWriter writer; + std::string a = writer.write(json_); + std::string b = writer.write(current); + + return a != b; + } + + + void OrthancConfiguration::SetServerIndex(ServerIndex& index) + { + serverIndex_ = &index; + } + + + void OrthancConfiguration::ResetServerIndex() + { + serverIndex_ = NULL; + } +} diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancConfiguration.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancConfiguration.h Thu Dec 06 15:58:08 2018 +0100 @@ -0,0 +1,228 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 . + **/ + + +#pragma once + +#include "../Core/Images/FontRegistry.h" +#include "../Core/WebServiceParameters.h" +#include "../Core/DicomNetworking/RemoteModalityParameters.h" + +#include + +#include +#include +#include + +namespace Orthanc +{ + class MongooseServer; + class ServerIndex; + + class OrthancConfiguration : public boost::noncopyable + { + private: + typedef std::map Modalities; + typedef std::map Peers; + + boost::shared_mutex mutex_; + Json::Value json_; + boost::filesystem::path defaultDirectory_; + std::string configurationAbsolutePath_; + FontRegistry fontRegistry_; + const char* configurationFileArg_; + Modalities modalities_; + Peers peers_; + ServerIndex* serverIndex_; + + OrthancConfiguration() : + configurationFileArg_(NULL) + { + } + + void LoadModalitiesFromJson(const Json::Value& source); + + void LoadPeersFromJson(const Json::Value& source); + + void LoadModalities(); + + void LoadPeers(); + + void SaveModalitiesToJson(Json::Value& target); + + void SavePeersToJson(Json::Value& target); + + void SaveModalities(); + + void SavePeers(); + + static OrthancConfiguration& GetInstance(); + + public: + class ReaderLock : public boost::noncopyable + { + private: + OrthancConfiguration& configuration_; + boost::shared_lock lock_; + + public: + ReaderLock() : + configuration_(GetInstance()), + lock_(configuration_.mutex_) + { + } + + const OrthancConfiguration& GetConfiguration() const + { + return configuration_; + } + + const Json::Value& GetJson() const + { + return configuration_.json_; + } + }; + + + class WriterLock : public boost::noncopyable + { + private: + OrthancConfiguration& configuration_; + boost::unique_lock lock_; + + public: + WriterLock() : + configuration_(GetInstance()), + lock_(configuration_.mutex_) + { + } + + OrthancConfiguration& GetConfiguration() + { + return configuration_; + } + + const OrthancConfiguration& GetConfiguration() const + { + return configuration_; + } + + const Json::Value& GetJson() const + { + return configuration_.json_; + } + }; + + + const std::string& GetConfigurationAbsolutePath() const + { + return configurationAbsolutePath_; + } + + const FontRegistry& GetFontRegistry() const + { + return fontRegistry_; + } + + void Read(const char* configurationFile); + + void LoadModalitiesAndPeers(); + + void RegisterFont(EmbeddedResources::FileResourceId resource) + { + fontRegistry_.AddFromResource(resource); + } + + std::string GetStringParameter(const std::string& parameter, + const std::string& defaultValue) const; + + int GetIntegerParameter(const std::string& parameter, + int defaultValue) const; + + unsigned int GetUnsignedIntegerParameter(const std::string& parameter, + unsigned int defaultValue) const; + + bool GetBooleanParameter(const std::string& parameter, + bool defaultValue) const; + + void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality, + const std::string& name) const; + + bool LookupOrthancPeer(WebServiceParameters& peer, + const std::string& name) const; + + void GetListOfDicomModalities(std::set& target) const; + + void GetListOfOrthancPeers(std::set& target) const; + + void SetupRegisteredUsers(MongooseServer& httpServer) const; + + std::string InterpretStringParameterAsPath(const std::string& parameter) const; + + void GetListOfStringsParameter(std::list& target, + const std::string& key) const; + + bool IsSameAETitle(const std::string& aet1, + const std::string& aet2) const; + + bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality, + const std::string& aet) const; + + bool IsKnownAETitle(const std::string& aet, + const std::string& ip) const; + + RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name) const; + + RemoteModalityParameters GetModalityUsingAet(const std::string& aet) const; + + void UpdateModality(const std::string& symbolicName, + const RemoteModalityParameters& modality); + + void RemoveModality(const std::string& symbolicName); + + void UpdatePeer(const std::string& symbolicName, + const WebServiceParameters& peer); + + void RemovePeer(const std::string& symbolicName); + + + void Format(std::string& result) const; + + void SetDefaultEncoding(Encoding encoding); + + bool HasConfigurationChanged() const; + + void SetServerIndex(ServerIndex& index); + + void ResetServerIndex(); + }; +} diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -38,7 +38,7 @@ #include "../Core/Lua/LuaFunctionCall.h" #include "../Core/Logging.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "Search/LookupResource.h" #include "ServerToolbox.h" @@ -258,16 +258,21 @@ // The metadata "SopClassUid" is available for each of these instances StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); } - else if (Configuration::GetGlobalBoolParameter("AllowFindSopClassesInStudy", false)) - { - ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances); - StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); - } else { - result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false); - LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) " - << "in C-FIND requests is disabled"; + OrthancConfiguration::ReaderLock lock; + + if (lock.GetConfiguration().GetBooleanParameter("AllowFindSopClassesInStudy", false)) + { + ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances); + StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); + } + else + { + result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false); + LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) " + << "in C-FIND requests is disabled"; + } } } } @@ -547,8 +552,8 @@ levelTmp->IsNull() || levelTmp->IsBinary()) { - LOG(ERROR) << "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)"); } ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); @@ -590,7 +595,12 @@ LookupResource lookup(level); - const bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false); + bool caseSensitivePN; + + { + OrthancConfiguration::ReaderLock lock; + caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false); + } for (size_t i = 0; i < query.GetSize(); i++) { diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -39,22 +39,14 @@ #endif #include "OrthancInitialization.h" -#include "ServerContext.h" -#include "../Core/HttpClient.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/FileStorage/FilesystemStorage.h" #include "../Core/Logging.h" #include "../Core/OrthancException.h" -#include "../Core/Toolbox.h" -#include "../Core/FileStorage/FilesystemStorage.h" -#include "ServerEnumerations.h" #include "DatabaseWrapper.h" -#include "../Core/DicomParsing/FromDcmtkBridge.h" - -#include -#include -#include -#include +#include "OrthancConfiguration.h" #include // For dcmDisableGethostbyaddr() @@ -62,239 +54,11 @@ namespace Orthanc { - static boost::recursive_mutex globalMutex_; - static Json::Value configuration_; - static boost::filesystem::path defaultDirectory_; - static std::string configurationAbsolutePath_; - static FontRegistry fontRegistry_; - static const char* configurationFileArg_ = NULL; - - - static std::string GetGlobalStringParameterInternal(const std::string& parameter, - const std::string& defaultValue) - { - if (configuration_.isMember(parameter)) - { - if (configuration_[parameter].type() != Json::stringValue) - { - LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a string"; - throw OrthancException(ErrorCode_BadParameterType); - } - else - { - return configuration_[parameter].asString(); - } - } - else - { - return defaultValue; - } - } - - - static bool GetGlobalBoolParameterInternal(const std::string& parameter, - bool defaultValue) + static void RegisterUserMetadata(const Json::Value& config) { - if (configuration_.isMember(parameter)) - { - if (configuration_[parameter].type() != Json::booleanValue) - { - LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a Boolean (true or false)"; - throw OrthancException(ErrorCode_BadParameterType); - } - else - { - return configuration_[parameter].asBool(); - } - } - else - { - return defaultValue; - } - } - - - - static void AddFileToConfiguration(Json::Value& target, - const boost::filesystem::path& path) - { - LOG(WARNING) << "Reading the configuration from: " << path; - - Json::Value config; - - { - std::string content; - SystemToolbox::ReadFile(content, path.string()); - - Json::Value tmp; - Json::Reader reader; - if (!reader.parse(content, tmp) || - tmp.type() != Json::objectValue) - { - LOG(ERROR) << "The configuration file does not follow the JSON syntax: " << path; - throw OrthancException(ErrorCode_BadJson); - } - - Toolbox::CopyJsonWithoutComments(config, tmp); - } - - if (target.size() == 0) - { - target = config; - } - else - { - Json::Value::Members members = config.getMemberNames(); - for (Json::Value::ArrayIndex i = 0; i < members.size(); i++) - { - if (target.isMember(members[i])) - { - LOG(ERROR) << "The configuration section \"" << members[i] << "\" is defined in 2 different configuration files"; - throw OrthancException(ErrorCode_BadFileFormat); - } - else - { - target[members[i]] = config[members[i]]; - } - } - } - } - - - static void ScanFolderForConfiguration(Json::Value& target, - const char* folder) - { - using namespace boost::filesystem; - - LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files"; - - directory_iterator end_it; // default construction yields past-the-end - for (directory_iterator it(folder); - it != end_it; - ++it) + if (config.isMember("UserMetadata")) { - if (!is_directory(it->status())) - { - std::string extension = boost::filesystem::extension(it->path()); - Toolbox::ToLowerCase(extension); - - if (extension == ".json") - { - AddFileToConfiguration(target, it->path().string()); - } - } - } - } - - - static void ReadConfiguration(Json::Value& target, - const char* configurationFile) - { - target = Json::objectValue; - - if (configurationFile) - { - if (!boost::filesystem::exists(configurationFile)) - { - LOG(ERROR) << "Inexistent path to configuration: " << configurationFile; - throw OrthancException(ErrorCode_InexistentFile); - } - - if (boost::filesystem::is_directory(configurationFile)) - { - ScanFolderForConfiguration(target, configurationFile); - } - else - { - AddFileToConfiguration(target, configurationFile); - } - } - else - { -#if ORTHANC_STANDALONE == 1 - // No default path for the standalone configuration - LOG(WARNING) << "Using the default Orthanc configuration"; - return; - -#else - // In a non-standalone build, we use the - // "Resources/Configuration.json" from the Orthanc source code - - boost::filesystem::path p = ORTHANC_PATH; - p /= "Resources"; - p /= "Configuration.json"; - - AddFileToConfiguration(target, p); -#endif - } - } - - - - static void ReadGlobalConfiguration(const char* configurationFile) - { - // Read the content of the configuration - configurationFileArg_ = configurationFile; - ReadConfiguration(configuration_, configurationFile); - - // Adapt the paths to the configurations - defaultDirectory_ = boost::filesystem::current_path(); - configurationAbsolutePath_ = ""; - - if (configurationFile) - { - if (boost::filesystem::is_directory(configurationFile)) - { - defaultDirectory_ = boost::filesystem::path(configurationFile); - configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string(); - } - else - { - defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path(); - configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string(); - } - } - else - { -#if ORTHANC_STANDALONE != 1 - // In a non-standalone build, we use the - // "Resources/Configuration.json" from the Orthanc source code - - boost::filesystem::path p = ORTHANC_PATH; - p /= "Resources"; - p /= "Configuration.json"; - configurationAbsolutePath_ = boost::filesystem::absolute(p).string(); -#endif - } - } - - - static void ValidateGlobalConfiguration() - { - std::set ids; - - Configuration::GetListOfOrthancPeers(ids); - for (std::set::const_iterator it = ids.begin(); it != ids.end(); ++it) - { - WebServiceParameters peer; - Configuration::GetOrthancPeer(peer, *it); - peer.CheckClientCertificate(); - } - - Configuration::GetListOfDicomModalities(ids); - for (std::set::const_iterator it = ids.begin(); it != ids.end(); ++it) - { - RemoteModalityParameters modality; - Configuration::GetDicomModalityUsingSymbolicName(modality, *it); - } - } - - - static void RegisterUserMetadata() - { - if (configuration_.isMember("UserMetadata")) - { - const Json::Value& parameter = configuration_["UserMetadata"]; + const Json::Value& parameter = config["UserMetadata"]; Json::Value::Members members = parameter.getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -303,8 +67,8 @@ if (!parameter[name].isInt()) { - LOG(ERROR) << "Not a number in this user-defined metadata: " << name; - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, + "Not a number in this user-defined metadata: " + name); } int metadata = parameter[name].asInt(); @@ -326,17 +90,17 @@ } - static void RegisterUserContentType() + static void RegisterUserContentType(const Json::Value& config) { - if (configuration_.isMember("UserContentType")) + if (config.isMember("UserContentType")) { - const Json::Value& parameter = configuration_["UserContentType"]; + const Json::Value& parameter = config["UserContentType"]; Json::Value::Members members = parameter.getMemberNames(); for (size_t i = 0; i < members.size(); i++) { const std::string& name = members[i]; - std::string mime = "application/octet-stream"; + std::string mime = MIME_BINARY; const Json::Value& value = parameter[name]; int contentType; @@ -355,8 +119,8 @@ } else { - LOG(ERROR) << "Not a number in this user-defined attachment type: " << name; - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, + "Not a number in this user-defined attachment type: " + name); } LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " @@ -419,8 +183,9 @@ !config.isMember("Module") || config["Module"].type() != Json::stringValue) { - LOG(ERROR) << "No path to the PKCS#11 module (DLL or .so) is provided for HTTPS client authentication"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "No path to the PKCS#11 module (DLL or .so) is provided " + "for HTTPS client authentication"); } std::string pin; @@ -432,8 +197,8 @@ } else { - LOG(ERROR) << "The PIN number in the PKCS#11 configuration must be a string"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "The PIN number in the PKCS#11 configuration must be a string"); } } @@ -446,8 +211,8 @@ } else { - LOG(ERROR) << "The Verbose option in the PKCS#11 configuration must be a Boolean"; - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, + "The Verbose option in the PKCS#11 configuration must be a Boolean"); } } @@ -458,29 +223,28 @@ void OrthancInitialize(const char* configurationFile) { - boost::recursive_mutex::scoped_lock lock(globalMutex_); + OrthancConfiguration::WriterLock lock; Toolbox::InitializeOpenSsl(); InitializeServerEnumerations(); // Read the user-provided configuration - ReadGlobalConfiguration(configurationFile); - ValidateGlobalConfiguration(); + lock.GetConfiguration().Read(configurationFile); - if (configuration_.isMember("Locale")) + if (lock.GetJson().isMember("Locale")) { - std::string locale = GetGlobalStringParameterInternal("Locale", ""); - Toolbox::InitializeGlobalLocale(configuration_["Locale"].asCString()); + std::string locale = lock.GetConfiguration().GetStringParameter("Locale", ""); + Toolbox::InitializeGlobalLocale(lock.GetJson()["Locale"].asCString()); } else { Toolbox::InitializeGlobalLocale(NULL); } - if (configuration_.isMember("DefaultEncoding")) + if (lock.GetJson().isMember("DefaultEncoding")) { - std::string encoding = GetGlobalStringParameterInternal("DefaultEncoding", ""); + std::string encoding = lock.GetConfiguration().GetStringParameter("DefaultEncoding", ""); SetDefaultDicomEncoding(StringToEncoding(encoding.c_str())); } else @@ -488,22 +252,23 @@ SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING); } - if (configuration_.isMember("Pkcs11")) + if (lock.GetJson().isMember("Pkcs11")) { - ConfigurePkcs11(configuration_["Pkcs11"]); + ConfigurePkcs11(lock.GetJson()["Pkcs11"]); } HttpClient::GlobalInitialize(); - RegisterUserMetadata(); - RegisterUserContentType(); + RegisterUserMetadata(lock.GetJson()); + RegisterUserContentType(lock.GetJson()); - FromDcmtkBridge::InitializeDictionary(GetGlobalBoolParameterInternal("LoadPrivateDictionary", true)); - LoadCustomDictionary(configuration_); + bool loadPrivate = lock.GetConfiguration().GetBooleanParameter("LoadPrivateDictionary", true); + FromDcmtkBridge::InitializeDictionary(loadPrivate); + LoadCustomDictionary(lock.GetJson()); FromDcmtkBridge::InitializeCodecs(); - fontRegistry_.AddFromResource(EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); + lock.GetConfiguration().RegisterFont(EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */ dcmDisableGethostbyaddr.set(OFTrue); @@ -513,7 +278,8 @@ void OrthancFinalize() { - boost::recursive_mutex::scoped_lock lock(globalMutex_); + OrthancConfiguration::WriterLock lock; + HttpClient::GlobalFinalize(); FromDcmtkBridge::FinalizeCodecs(); Toolbox::FinalizeOpenSsl(); @@ -521,531 +287,16 @@ } - std::string Configuration::GetGlobalStringParameter(const std::string& parameter, - const std::string& defaultValue) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - return GetGlobalStringParameterInternal(parameter, defaultValue); - } - - - int Configuration::GetGlobalIntegerParameter(const std::string& parameter, - int defaultValue) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (configuration_.isMember(parameter)) - { - if (configuration_[parameter].type() != Json::intValue) - { - LOG(ERROR) << "The configuration option \"" << parameter << "\" must be an integer"; - throw OrthancException(ErrorCode_BadParameterType); - } - else - { - return configuration_[parameter].asInt(); - } - } - else - { - return defaultValue; - } - } - - - unsigned int Configuration::GetGlobalUnsignedIntegerParameter(const std::string& parameter, - unsigned int defaultValue) - { - int v = GetGlobalIntegerParameter(parameter, defaultValue); - - if (v < 0) - { - LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a positive integer"; - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - return static_cast(v); - } - } - - - bool Configuration::GetGlobalBoolParameter(const std::string& parameter, - bool defaultValue) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - return GetGlobalBoolParameterInternal(parameter, defaultValue); - } - - - void Configuration::GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality, - const std::string& name) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("DicomModalities")) - { - LOG(ERROR) << "No modality with symbolic name: " << name; - throw OrthancException(ErrorCode_InexistentItem); - } - - const Json::Value& modalities = configuration_["DicomModalities"]; - if (modalities.type() != Json::objectValue || - !modalities.isMember(name)) - { - LOG(ERROR) << "No modality with symbolic name: " << name; - throw OrthancException(ErrorCode_InexistentItem); - } - - try - { - modality.Unserialize(modalities[name]); - } - catch (OrthancException&) - { - LOG(ERROR) << "Syntax error in the definition of DICOM modality \"" << name - << "\". Please check your configuration file."; - throw; - } - } - - - bool Configuration::GetOrthancPeer(WebServiceParameters& peer, - const std::string& name) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("OrthancPeers")) - { - return false; - } - - try - { - const Json::Value& modalities = configuration_["OrthancPeers"]; - if (modalities.type() != Json::objectValue || - !modalities.isMember(name)) - { - return false; - } - else - { - peer.Unserialize(modalities[name]); - return true; - } - } - catch (OrthancException&) - { - LOG(ERROR) << "Syntax error in the definition of peer \"" << name - << "\". Please check your configuration file."; - throw; - } - } - - - static bool ReadKeys(std::set& target, - const char* parameter, - bool onlyAlphanumeric) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - target.clear(); - - if (!configuration_.isMember(parameter)) - { - return true; - } - - const Json::Value& modalities = configuration_[parameter]; - if (modalities.type() != Json::objectValue) - { - LOG(ERROR) << "Bad format of the \"DicomModalities\" configuration section"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - Json::Value::Members members = modalities.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - if (onlyAlphanumeric) - { - for (size_t j = 0; j < members[i].size(); j++) - { - if (!isalnum(members[i][j]) && members[i][j] != '-') - { - return false; - } - } - } - - target.insert(members[i]); - } - - return true; - } - - - void Configuration::GetListOfDicomModalities(std::set& target) - { - target.clear(); - - if (!ReadKeys(target, "DicomModalities", true)) - { - LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of the modalities"; - throw OrthancException(ErrorCode_BadFileFormat); - } - } - - - void Configuration::GetListOfOrthancPeers(std::set& target) - { - target.clear(); - - if (!ReadKeys(target, "OrthancPeers", true)) - { - LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of Orthanc peers"; - throw OrthancException(ErrorCode_BadFileFormat); - } - } - - - - void Configuration::SetupRegisteredUsers(MongooseServer& httpServer) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - httpServer.ClearUsers(); - - if (!configuration_.isMember("RegisteredUsers")) - { - return; - } - - const Json::Value& users = configuration_["RegisteredUsers"]; - if (users.type() != Json::objectValue) - { - LOG(ERROR) << "Badly formatted list of users"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - Json::Value::Members usernames = users.getMemberNames(); - for (size_t i = 0; i < usernames.size(); i++) - { - const std::string& username = usernames[i]; - std::string password = users[username].asString(); - httpServer.RegisterUser(username.c_str(), password.c_str()); - } - } - - - std::string Configuration::InterpretRelativePath(const std::string& baseDirectory, - const std::string& relativePath) - { - boost::filesystem::path base(baseDirectory); - boost::filesystem::path relative(relativePath); - - /** - The following lines should be equivalent to this one: - - return (base / relative).string(); - - However, for some unknown reason, some versions of Boost do not - make the proper path resolution when "baseDirectory" is an - absolute path. So, a hack is used below. - **/ - - if (relative.is_absolute()) - { - return relative.string(); - } - else - { - return (base / relative).string(); - } - } - - std::string Configuration::InterpretStringParameterAsPath(const std::string& parameter) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - return InterpretRelativePath(defaultDirectory_.string(), parameter); - } - - - void Configuration::GetGlobalListOfStringsParameter(std::list& target, - const std::string& key) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - target.clear(); - - if (!configuration_.isMember(key)) - { - return; - } - - const Json::Value& lst = configuration_[key]; - - if (lst.type() != Json::arrayValue) - { - LOG(ERROR) << "Badly formatted list of strings"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++) - { - target.push_back(lst[i].asString()); - } - } - - - bool Configuration::IsSameAETitle(const std::string& aet1, - const std::string& aet2) - { - if (GetGlobalBoolParameter("StrictAetComparison", false)) - { - // Case-sensitive matching - return aet1 == aet2; - } - else - { - // Case-insensitive matching (default) - std::string tmp1, tmp2; - Toolbox::ToLowerCase(tmp1, aet1); - Toolbox::ToLowerCase(tmp2, aet2); - return tmp1 == tmp2; - } - } - - - bool Configuration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality, - const std::string& aet) - { - std::set modalities; - GetListOfDicomModalities(modalities); - - for (std::set::const_iterator - it = modalities.begin(); it != modalities.end(); ++it) - { - try - { - GetDicomModalityUsingSymbolicName(modality, *it); - - if (IsSameAETitle(aet, modality.GetApplicationEntityTitle())) - { - return true; - } - } - catch (OrthancException&) - { - } - } - - return false; - } - - - bool Configuration::IsKnownAETitle(const std::string& aet, - const std::string& ip) - { - RemoteModalityParameters modality; - - if (!LookupDicomModalityUsingAETitle(modality, aet)) - { - LOG(WARNING) << "Modality \"" << aet - << "\" is not listed in the \"DicomModalities\" configuration option"; - return false; - } - else if (!Configuration::GetGlobalBoolParameter("DicomCheckModalityHost", false) || - ip == modality.GetHost()) - { - return true; - } - else - { - LOG(WARNING) << "Forbidding access from AET \"" << aet - << "\" given its hostname (" << ip << ") does not match " - << "the \"DicomModalities\" configuration option (" - << modality.GetHost() << " was expected)"; - return false; - } - } - - - RemoteModalityParameters Configuration::GetModalityUsingSymbolicName(const std::string& name) - { - RemoteModalityParameters modality; - GetDicomModalityUsingSymbolicName(modality, name); - - return modality; - } - - - RemoteModalityParameters Configuration::GetModalityUsingAet(const std::string& aet) - { - RemoteModalityParameters modality; - - if (LookupDicomModalityUsingAETitle(modality, aet)) - { - return modality; - } - else - { - LOG(ERROR) << "Unknown modality for AET: " << aet; - throw OrthancException(ErrorCode_InexistentItem); - } - } - - - void Configuration::UpdateModality(ServerContext& context, - const std::string& symbolicName, - const RemoteModalityParameters& modality) - { - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("DicomModalities")) - { - configuration_["DicomModalities"] = Json::objectValue; - } - - Json::Value& modalities = configuration_["DicomModalities"]; - if (modalities.type() != Json::objectValue) - { - LOG(ERROR) << "Bad file format for modality: " << symbolicName; - throw OrthancException(ErrorCode_BadFileFormat); - } - - modalities.removeMember(symbolicName); - - Json::Value v; - modality.Serialize(v, true /* force advanced format */); - modalities[symbolicName] = v; - } - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.HasPlugins()) - { - context.GetPlugins().SignalUpdatedModalities(); - } -#endif - } - - - void Configuration::RemoveModality(ServerContext& context, - const std::string& symbolicName) - { - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("DicomModalities")) - { - LOG(ERROR) << "No modality with symbolic name: " << symbolicName; - throw OrthancException(ErrorCode_BadFileFormat); - } - - Json::Value& modalities = configuration_["DicomModalities"]; - if (modalities.type() != Json::objectValue) - { - LOG(ERROR) << "Bad file format for the \"DicomModalities\" configuration section"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - modalities.removeMember(symbolicName.c_str()); - } - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.HasPlugins()) - { - context.GetPlugins().SignalUpdatedModalities(); - } -#endif - } - - - void Configuration::UpdatePeer(ServerContext& context, - const std::string& symbolicName, - const WebServiceParameters& peer) - { - peer.CheckClientCertificate(); - - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("OrthancPeers")) - { - LOG(ERROR) << "No peer with symbolic name: " << symbolicName; - configuration_["OrthancPeers"] = Json::objectValue; - } - - Json::Value& peers = configuration_["OrthancPeers"]; - if (peers.type() != Json::objectValue) - { - LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - peers.removeMember(symbolicName); - - Json::Value v; - peer.Serialize(v, - false /* use simple format if possible */, - true /* include passwords */); - peers[symbolicName] = v; - } - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.HasPlugins()) - { - context.GetPlugins().SignalUpdatedPeers(); - } -#endif - } - - - void Configuration::RemovePeer(ServerContext& context, - const std::string& symbolicName) - { - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - - if (!configuration_.isMember("OrthancPeers")) - { - LOG(ERROR) << "No peer with symbolic name: " << symbolicName; - throw OrthancException(ErrorCode_BadFileFormat); - } - - Json::Value& peers = configuration_["OrthancPeers"]; - if (peers.type() != Json::objectValue) - { - LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section"; - throw OrthancException(ErrorCode_BadFileFormat); - } - - peers.removeMember(symbolicName.c_str()); - } - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.HasPlugins()) - { - context.GetPlugins().SignalUpdatedPeers(); - } -#endif - } - - - - const std::string& Configuration::GetConfigurationAbsolutePath() - { - return configurationAbsolutePath_; - } - - static IDatabaseWrapper* CreateSQLiteWrapper() { - std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); + OrthancConfiguration::ReaderLock lock; + + std::string storageDirectoryStr = + lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage"); // Open the database - boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath( - Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr)); + boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath( + lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr)); LOG(WARNING) << "SQLite index directory: " << indexDirectory; @@ -1114,12 +365,17 @@ static IStorageArea* CreateFilesystemStorage() { - std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); + OrthancConfiguration::ReaderLock lock; - boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr); + std::string storageDirectoryStr = + lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage"); + + boost::filesystem::path storageDirectory = + lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr); + LOG(WARNING) << "Storage directory: " << storageDirectory; - if (Configuration::GetGlobalBoolParameter("StoreDicom", true)) + if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true)) { return new FilesystemStorage(storageDirectory.string()); } @@ -1131,66 +387,14 @@ } - IDatabaseWrapper* Configuration::CreateDatabaseWrapper() + IDatabaseWrapper* CreateDatabaseWrapper() { return CreateSQLiteWrapper(); } - IStorageArea* Configuration::CreateStorageArea() + IStorageArea* CreateStorageArea() { return CreateFilesystemStorage(); } - - - void Configuration::GetConfiguration(Json::Value& result) - { - boost::recursive_mutex::scoped_lock lock(globalMutex_); - result = configuration_; - } - - - void Configuration::FormatConfiguration(std::string& result) - { - Json::Value config; - GetConfiguration(config); - - Json::StyledWriter w; - result = w.write(config); - } - - - const FontRegistry& Configuration::GetFontRegistry() - { - return fontRegistry_; - } - - - void Configuration::SetDefaultEncoding(Encoding encoding) - { - SetDefaultDicomEncoding(encoding); - - { - // Propagate the encoding to the configuration file that is - // stored in memory - boost::recursive_mutex::scoped_lock lock(globalMutex_); - configuration_["DefaultEncoding"] = EnumerationToString(encoding); - } - } - - - bool Configuration::HasConfigurationChanged() - { - Json::Value starting; - GetConfiguration(starting); - - Json::Value current; - ReadConfiguration(current, configurationFileArg_); - - Json::FastWriter writer; - std::string a = writer.write(starting); - std::string b = writer.write(current); - - return a != b; - } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancInitialization.h Thu Dec 06 15:58:08 2018 +0100 @@ -33,108 +33,16 @@ #pragma once -#include -#include -#include -#include - #include "../Core/FileStorage/IStorageArea.h" -#include "../Core/HttpServer/MongooseServer.h" -#include "../Core/Images/FontRegistry.h" -#include "../Core/WebServiceParameters.h" -#include "../Core/DicomNetworking/RemoteModalityParameters.h" - #include "IDatabaseWrapper.h" -#include "ServerEnumerations.h" - namespace Orthanc { - class ServerContext; - void OrthancInitialize(const char* configurationFile = NULL); void OrthancFinalize(); - class Configuration - { - private: - Configuration(); // Forbidden, this is a static class - - public: - static std::string GetGlobalStringParameter(const std::string& parameter, - const std::string& defaultValue); - - static int GetGlobalIntegerParameter(const std::string& parameter, - int defaultValue); - - static unsigned int GetGlobalUnsignedIntegerParameter(const std::string& parameter, - unsigned int defaultValue); - - static bool GetGlobalBoolParameter(const std::string& parameter, - bool defaultValue); - - static void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality, - const std::string& name); - - static bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality, - const std::string& aet); - - static bool GetOrthancPeer(WebServiceParameters& peer, - const std::string& name); - - static void GetListOfDicomModalities(std::set& target); - - static void GetListOfOrthancPeers(std::set& target); - - static void SetupRegisteredUsers(MongooseServer& httpServer); - - static std::string InterpretRelativePath(const std::string& baseDirectory, - const std::string& relativePath); - - static std::string InterpretStringParameterAsPath(const std::string& parameter); - - static void GetGlobalListOfStringsParameter(std::list& target, - const std::string& key); + IDatabaseWrapper* CreateDatabaseWrapper(); - static bool IsKnownAETitle(const std::string& aet, - const std::string& ip); - - static bool IsSameAETitle(const std::string& aet1, - const std::string& aet2); - - static RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name); - - static RemoteModalityParameters GetModalityUsingAet(const std::string& aet); - - static void UpdateModality(ServerContext& context, - const std::string& symbolicName, - const RemoteModalityParameters& modality); - - static void RemoveModality(ServerContext& context, - const std::string& symbolicName); - - static void UpdatePeer(ServerContext& context, - const std::string& symbolicName, - const WebServiceParameters& peer); - - static void RemovePeer(ServerContext& context, - const std::string& symbolicName); - - static const std::string& GetConfigurationAbsolutePath(); - - static IDatabaseWrapper* CreateDatabaseWrapper(); - - static IStorageArea* CreateStorageArea(); - - static void GetConfiguration(Json::Value& result); - - static void FormatConfiguration(std::string& result); - - static const FontRegistry& GetFontRegistry(); - - static void SetDefaultEncoding(Encoding encoding); - - static bool HasConfigurationChanged(); - }; + IStorageArea* CreateStorageArea(); } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -34,7 +34,7 @@ #include "PrecompiledHeadersServer.h" #include "OrthancMoveRequestHandler.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" @@ -70,7 +70,8 @@ originatorAet_(originatorAet), originatorId_(originatorId) { - LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\""; + LOG(INFO) << "Sending resource " << publicId << " to modality \"" + << targetAet << "\" in synchronous mode"; std::list tmp; context_.GetIndex().GetChildInstances(tmp, publicId); @@ -81,7 +82,10 @@ instances_.push_back(*it); } - remote_ = Configuration::GetModalityUsingAet(targetAet); + { + OrthancConfiguration::ReaderLock lock; + remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet); + } } virtual unsigned int GetSubOperationCount() const @@ -130,12 +134,17 @@ job_(new DicomModalityStoreJob(context)), position_(0) { - LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\""; + LOG(INFO) << "Sending resource " << publicId << " to modality \"" + << targetAet << "\" in asynchronous mode"; job_->SetDescription("C-MOVE"); job_->SetPermissive(true); job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle()); - job_->SetRemoteModality(Configuration::GetModalityUsingAet(targetAet)); + + { + OrthancConfiguration::ReaderLock lock; + job_->SetRemoteModality(lock.GetConfiguration().GetModalityUsingAet(targetAet)); + } if (originatorId != 0) { @@ -238,7 +247,12 @@ const std::string& originatorAet, uint16_t originatorId) { - bool synchronous = Configuration::GetGlobalBoolParameter("SynchronousCMove", false); + bool synchronous; + + { + OrthancConfiguration::ReaderLock lock; + synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove", true); + } if (synchronous) { diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -208,7 +208,8 @@ static void StoreCreatedInstance(std::string& id /* out */, RestApiPostCall& call, - ParsedDicomFile& dicom) + ParsedDicomFile& dicom, + bool sendAnswer) { DicomInstanceToStore toStore; toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); @@ -221,6 +222,11 @@ { throw OrthancException(ErrorCode_CannotStoreInstance); } + + if (sendAnswer) + { + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); + } } @@ -265,7 +271,7 @@ { if (tags.type() != Json::objectValue) { - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array"); } // Inject the user-specified tags @@ -290,7 +296,7 @@ tag != DICOM_TAG_STUDY_TIME && dicom.HasTag(tag)) { - throw OrthancException(ErrorCode_CreateDicomOverrideTag); + throw OrthancException(ErrorCode_CreateDicomOverrideTag, name); } if (tag == DICOM_TAG_PIXEL_DATA) @@ -356,7 +362,7 @@ dicom->ReplacePlainString(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast(i + 1)); dicom->ReplacePlainString(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast(i + 1)); - StoreCreatedInstance(someInstance, call, *dicom); + StoreCreatedInstance(someInstance, call, *dicom, false); } } catch (OrthancException&) @@ -403,8 +409,8 @@ const char* tmp = request["Tags"]["SpecificCharacterSet"].asCString(); if (!GetDicomEncoding(encoding, tmp)) { - LOG(ERROR) << "Unknown specific character set: " << std::string(tmp); - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown specific character set: " + std::string(tmp)); } } else @@ -583,10 +589,7 @@ } std::string id; - StoreCreatedInstance(id, call, dicom); - OrthancRestApi::GetApi(call).AnswerStoredResource(call, id, ResourceType_Instance, StoreStatus_Success); - - return; + StoreCreatedInstance(id, call, dicom, true); } @@ -610,8 +613,7 @@ CreateDicomV1(dicom, call, request); std::string id; - StoreCreatedInstance(id, call, dicom); - OrthancRestApi::GetApi(call).AnswerStoredResource(call, id, ResourceType_Instance, StoreStatus_Success); + StoreCreatedInstance(id, call, dicom, true); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -40,20 +40,46 @@ namespace Orthanc { - void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call, - const std::string& publicId, - ResourceType resourceType, - StoreStatus status) const + static void SetupResourceAnswer(Json::Value& result, + const std::string& publicId, + ResourceType resourceType, + StoreStatus status) { - Json::Value result = Json::objectValue; + result = Json::objectValue; if (status != StoreStatus_Failure) { result["ID"] = publicId; result["Path"] = GetBasePath(resourceType, publicId); } + + result["Status"] = EnumerationToString(status); + } - result["Status"] = EnumerationToString(status); + + void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call, + DicomInstanceToStore& instance, + StoreStatus status) const + { + Json::Value result; + SetupResourceAnswer(result, instance.GetHasher().HashInstance(), + ResourceType_Instance, status); + + result["ParentPatient"] = instance.GetHasher().HashPatient(); + result["ParentStudy"] = instance.GetHasher().HashStudy(); + result["ParentSeries"] = instance.GetHasher().HashSeries(); + + call.GetOutput().AnswerJson(result); + } + + + void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call, + const std::string& publicId, + ResourceType resourceType, + StoreStatus status) const + { + Json::Value result; + SetupResourceAnswer(result, publicId, resourceType, status); call.GetOutput().AnswerJson(result); } @@ -62,14 +88,14 @@ { OrthancRestApi::GetApi(call).leaveBarrier_ = true; OrthancRestApi::GetApi(call).resetRequestReceived_ = true; - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } void OrthancRestApi::ShutdownOrthanc(RestApiPostCall& call) { OrthancRestApi::GetApi(call).leaveBarrier_ = true; - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); LOG(WARNING) << "Shutdown request received"; } @@ -100,7 +126,7 @@ std::string publicId; StoreStatus status = context.Store(publicId, toStore); - OrthancRestApi::GetApi(call).AnswerStoredResource(call, publicId, ResourceType_Instance, status); + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); } @@ -147,6 +173,105 @@ static const char* KEY_PRIORITY = "Priority"; static const char* KEY_SYNCHRONOUS = "Synchronous"; static const char* KEY_ASYNCHRONOUS = "Asynchronous"; + + + bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous, + const Json::Value& body) + { + if (body.type() != Json::objectValue) + { + return isDefaultSynchronous; + } + else if (body.isMember(KEY_SYNCHRONOUS)) + { + return SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS); + } + else if (body.isMember(KEY_ASYNCHRONOUS)) + { + return !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS); + } + else + { + return isDefaultSynchronous; + } + } + + + unsigned int OrthancRestApi::GetJobRequestPriority(const Json::Value& body) + { + if (body.type() != Json::objectValue || + !body.isMember(KEY_PRIORITY)) + { + return 0; // Default priority + } + else + { + return SerializationToolbox::ReadInteger(body, KEY_PRIORITY); + } + } + + + void OrthancRestApi::SubmitGenericJob(RestApiOutput& output, + ServerContext& context, + IJob* job, + bool synchronous, + int priority) + { + std::auto_ptr raii(job); + + if (job == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + if (synchronous) + { + Json::Value successContent; + if (context.GetJobsEngine().GetRegistry().SubmitAndWait + (successContent, raii.release(), priority)) + { + // Success in synchronous execution + output.AnswerJson(successContent); + } + else + { + // Error during synchronous execution + output.SignalError(HttpStatus_500_InternalServerError); + } + } + else + { + // Asynchronous mode: Submit the job, but don't wait for its completion + std::string id; + context.GetJobsEngine().GetRegistry().Submit + (id, raii.release(), priority); + + Json::Value v; + v["ID"] = id; + v["Path"] = "/jobs/" + id; + output.AnswerJson(v); + } + } + + + void OrthancRestApi::SubmitGenericJob(RestApiPostCall& call, + IJob* job, + bool isDefaultSynchronous, + const Json::Value& body) const + { + std::auto_ptr raii(job); + + if (body.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + bool synchronous = IsSynchronousJobRequest(isDefaultSynchronous, body); + int priority = GetJobRequestPriority(body); + + SubmitGenericJob(call.GetOutput(), context_, raii.release(), synchronous, priority); + } + void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call, SetOfCommandsJob* job, @@ -155,11 +280,6 @@ { std::auto_ptr raii(job); - if (job == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - if (body.type() != Json::objectValue) { throw OrthancException(ErrorCode_BadFileFormat); @@ -176,66 +296,6 @@ job->SetPermissive(false); } - int priority = 0; - - if (body.isMember(KEY_PRIORITY)) - { - priority = SerializationToolbox::ReadInteger(body, KEY_PRIORITY); - } - - bool synchronous = isDefaultSynchronous; - - if (body.isMember(KEY_SYNCHRONOUS)) - { - synchronous = SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS); - } - else if (body.isMember(KEY_ASYNCHRONOUS)) - { - synchronous = !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS); - } - - if (synchronous) - { - Json::Value successContent; - if (context_.GetJobsEngine().GetRegistry().SubmitAndWait - (successContent, raii.release(), priority)) - { - // Success in synchronous execution - call.GetOutput().AnswerJson(successContent); - } - else - { - // Error during synchronous execution - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); - } - } - else - { - // Asynchronous mode: Submit the job, but don't wait for its completion - std::string id; - context_.GetJobsEngine().GetRegistry().Submit(id, raii.release(), priority); - - Json::Value v; - v["ID"] = id; - v["Path"] = "/jobs/" + id; - call.GetOutput().AnswerJson(v); - } - } - - - void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call, - SetOfCommandsJob* job, - bool isDefaultSynchronous) const - { - std::auto_ptr raii(job); - - Json::Value body; - - if (!call.ParseJsonRequest(body)) - { - body = Json::objectValue; - } - - SubmitCommandsJob(call, raii.release(), isDefaultSynchronous, body); + SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Thu Dec 06 15:58:08 2018 +0100 @@ -44,6 +44,7 @@ { class ServerContext; class ServerIndex; + class DicomInstanceToStore; class OrthancRestApi : public RestApi { @@ -93,18 +94,34 @@ static ServerIndex& GetIndex(RestApiCall& call); + void AnswerStoredInstance(RestApiPostCall& call, + DicomInstanceToStore& instance, + StoreStatus status) const; + void AnswerStoredResource(RestApiPostCall& call, const std::string& publicId, ResourceType resourceType, StoreStatus status) const; + static bool IsSynchronousJobRequest(bool isDefaultSynchronous, + const Json::Value& body); + + static unsigned int GetJobRequestPriority(const Json::Value& body); + + static void SubmitGenericJob(RestApiOutput& output, + ServerContext& context, + IJob* job, + bool synchronous, + int priority); + + void SubmitGenericJob(RestApiPostCall& call, + IJob* job, + bool isDefaultSynchronous, + const Json::Value& body) const; + void SubmitCommandsJob(RestApiPostCall& call, SetOfCommandsJob* job, bool isDefaultSynchronous, const Json::Value& body) const; - - void SubmitCommandsJob(RestApiPostCall& call, - SetOfCommandsJob* job, - bool isDefaultSynchronous) const; }; } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -35,40 +35,93 @@ #include "OrthancRestApi.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" +#include "../../Core/SerializationToolbox.h" #include "../ServerJobs/ArchiveJob.h" namespace Orthanc { - static bool AddResourcesOfInterest(ArchiveJob& job, - RestApiPostCall& call) + static const char* const KEY_RESOURCES = "Resources"; + static const char* const KEY_EXTENDED = "Extended"; + + static void AddResourcesOfInterestFromArray(ArchiveJob& job, + const Json::Value& resources) { - Json::Value resources; - if (call.ParseJsonRequest(resources) && - resources.type() == Json::arrayValue) + if (resources.type() != Json::arrayValue) { - for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + throw OrthancException(ErrorCode_BadFileFormat, + "Expected a list of strings (Orthanc identifiers)"); + } + + for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + { + if (resources[i].type() != Json::stringValue) { - if (resources[i].type() != Json::stringValue) - { - return false; // Bad request - } - + throw OrthancException(ErrorCode_BadFileFormat, + "Expected a list of strings (Orthanc identifiers)"); + } + else + { job.AddResource(resources[i].asString()); } + } + } - return true; + + static void AddResourcesOfInterest(ArchiveJob& job /* inout */, + const Json::Value& body /* in */) + { + if (body.type() == Json::arrayValue) + { + AddResourcesOfInterestFromArray(job, body); + } + else if (body.type() == Json::objectValue) + { + if (body.isMember(KEY_RESOURCES)) + { + AddResourcesOfInterestFromArray(job, body[KEY_RESOURCES]); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing field " + std::string(KEY_RESOURCES) + + " in the JSON body"); + } } else { - return false; + throw OrthancException(ErrorCode_BadFileFormat); } } - static void SubmitJob(RestApiCall& call, - boost::shared_ptr& tmp, + static void GetJobParameters(bool& synchronous, /* out */ + bool& extended, /* out */ + int& priority, /* out */ + const Json::Value& body, /* in */ + const bool defaultExtended /* in */) + { + synchronous = OrthancRestApi::IsSynchronousJobRequest + (true /* synchronous by default */, body); + + priority = OrthancRestApi::GetJobRequestPriority(body); + + if (body.type() == Json::objectValue && + body.isMember(KEY_EXTENDED)) + { + extended = SerializationToolbox::ReadBoolean(body, KEY_EXTENDED); + } + else + { + extended = defaultExtended; + } + } + + + static void SubmitJob(RestApiOutput& output, ServerContext& context, std::auto_ptr& job, + int priority, + bool synchronous, const std::string& filename) { if (job.get() == NULL) @@ -78,94 +131,147 @@ job->SetDescription("REST API"); - Json::Value publicContent; - if (context.GetJobsEngine().GetRegistry().SubmitAndWait - (publicContent, job.release(), 0 /* TODO priority */)) + if (synchronous) { - // The archive is now created: Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp->GetPath()); - sender.SetContentType("application/zip"); - sender.SetContentFilename(filename); + boost::shared_ptr tmp(new TemporaryFile); + job->SetSynchronousTarget(tmp); + + Json::Value publicContent; + if (context.GetJobsEngine().GetRegistry().SubmitAndWait + (publicContent, job.release(), priority)) + { + // The archive is now created: Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp->GetPath()); + sender.SetContentType(MimeType_Gzip); + sender.SetContentFilename(filename); - // Send the ZIP - call.GetOutput().AnswerStream(sender); + // Send the ZIP + output.AnswerStream(sender); + } + else + { + output.SignalError(HttpStatus_500_InternalServerError); + } } else { - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); - } + OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority); + } } - static void CreateBatchArchive(RestApiPostCall& call) + template + static void CreateBatch(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); - boost::shared_ptr tmp(new TemporaryFile); - std::auto_ptr job(new ArchiveJob(tmp, context, false, false)); - - if (AddResourcesOfInterest(*job, call)) + Json::Value body; + if (call.ParseJsonRequest(body)) { - SubmitJob(call, tmp, context, job, "Archive.zip"); + bool synchronous, extended; + int priority; + GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + + std::auto_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); + AddResourcesOfInterest(*job, body); + SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip"); } - } - - - template - static void CreateBatchMedia(RestApiPostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - boost::shared_ptr tmp(new TemporaryFile); - std::auto_ptr job(new ArchiveJob(tmp, context, true, Extended)); - - if (AddResourcesOfInterest(*job, call)) + else { - SubmitJob(call, tmp, context, job, "Archive.zip"); + throw OrthancException(ErrorCode_BadFileFormat, + "Expected a list of resources to archive in the body"); } } - static void CreateArchive(RestApiGetCall& call) + template + static void CreateSingleGet(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); - boost::shared_ptr tmp(new TemporaryFile); - std::auto_ptr job(new ArchiveJob(tmp, context, false, false)); + bool extended; + if (IS_MEDIA) + { + extended = call.HasArgument("extended"); + } + else + { + extended = false; + } + + std::auto_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); job->AddResource(id); - SubmitJob(call, tmp, context, job, id + ".zip"); + SubmitJob(call.GetOutput(), context, job, 0 /* priority */, + true /* synchronous */, id + ".zip"); } - static void CreateMedia(RestApiGetCall& call) + template + static void CreateSinglePost(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); - boost::shared_ptr tmp(new TemporaryFile); - std::auto_ptr job(new ArchiveJob(tmp, context, true, call.HasArgument("extended"))); - job->AddResource(id); - - SubmitJob(call, tmp, context, job, id + ".zip"); + Json::Value body; + if (call.ParseJsonRequest(body)) + { + bool synchronous, extended; + int priority; + GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + + std::auto_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); + job->AddResource(id); + SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip"); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } } - + void OrthancRestApi::RegisterArchive() { - Register("/patients/{id}/archive", CreateArchive); - Register("/studies/{id}/archive", CreateArchive); - Register("/series/{id}/archive", CreateArchive); + Register("/patients/{id}/archive", + CreateSingleGet); + Register("/studies/{id}/archive", + CreateSingleGet); + Register("/series/{id}/archive", + CreateSingleGet); + + Register("/patients/{id}/archive", + CreateSinglePost); + Register("/studies/{id}/archive", + CreateSinglePost); + Register("/series/{id}/archive", + CreateSinglePost); - Register("/patients/{id}/media", CreateMedia); - Register("/studies/{id}/media", CreateMedia); - Register("/series/{id}/media", CreateMedia); + Register("/patients/{id}/media", + CreateSingleGet); + Register("/studies/{id}/media", + CreateSingleGet); + Register("/series/{id}/media", + CreateSingleGet); - Register("/tools/create-archive", CreateBatchArchive); - Register("/tools/create-media", CreateBatchMedia); - Register("/tools/create-media-extended", CreateBatchMedia); + Register("/patients/{id}/media", + CreateSinglePost); + Register("/studies/{id}/media", + CreateSinglePost); + Register("/series/{id}/media", + CreateSinglePost); + + Register("/tools/create-archive", + CreateBatch); + Register("/tools/create-media", + CreateBatch); + Register("/tools/create-media-extended", + CreateBatch); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestChanges.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -98,7 +98,7 @@ static void DeleteChanges(RestApiDeleteCall& call) { OrthancRestApi::GetIndex(call).DeleteChanges(); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -130,7 +130,7 @@ static void DeleteExports(RestApiDeleteCall& call) { OrthancRestApi::GetIndex(call).DeleteExportedResources(); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -37,7 +37,7 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" -#include "../OrthancInitialization.h" +#include "../OrthancConfiguration.h" #include "../QueryRetrieveHandler.h" #include "../ServerJobs/DicomModalityStoreJob.h" #include "../ServerJobs/DicomMoveScuJob.h" @@ -47,6 +47,13 @@ namespace Orthanc { + static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name) + { + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().GetModalityUsingSymbolicName(name); + } + + /*************************************************************************** * DICOM C-Echo SCU ***************************************************************************/ @@ -57,7 +64,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); try { @@ -67,7 +74,7 @@ if (connection.Echo()) { // Echo has succeeded - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); return; } } @@ -181,7 +188,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); @@ -216,7 +223,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); @@ -252,7 +259,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); @@ -289,7 +296,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); @@ -331,7 +338,7 @@ const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomUserConnection connection(localAet, remote); connection.Open(); @@ -491,13 +498,30 @@ 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++) { - result.append(boost::lexical_cast(i)); + if (expand) + { + // New in Orthanc 1.4.3 + 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(i)); + } } call.GetOutput().AnswerJson(result); @@ -589,14 +613,14 @@ static void GetQueryLevel(RestApiGetCall& call) { QueryAccessor query(call); - call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), "text/plain"); + call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText); } static void GetQueryModality(RestApiGetCall& call) { QueryAccessor query(call); - call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), "text/plain"); + call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText); } @@ -604,7 +628,7 @@ { ServerContext& context = OrthancRestApi::GetContext(call); context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", "")); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -696,6 +720,13 @@ } } + 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()) @@ -709,7 +740,7 @@ return false; } - if (Configuration::GetGlobalBoolParameter("LogExportedResources", false)) + if (logExportedResources) { context.GetIndex().LogExportedResource(stripped, remote); } @@ -740,7 +771,7 @@ (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */); job->SetLocalAet(localAet); - job->SetRemoteModality(Configuration::GetModalityUsingSymbolicName(remote)); + job->SetRemoteModality(MyGetModalityUsingSymbolicName(remote)); if (moveOriginatorID != 0) { @@ -784,7 +815,7 @@ (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle()); const RemoteModalityParameters source = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomUserConnection connection(localAet, source); connection.Open(); @@ -798,7 +829,7 @@ } // Move has succeeded - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } @@ -815,8 +846,10 @@ static void ListPeers(RestApiGetCall& call) { + OrthancConfiguration::ReaderLock lock; + OrthancRestApi::SetOfStrings peers; - Configuration::GetListOfOrthancPeers(peers); + lock.GetConfiguration().GetListOfOrthancPeers(peers); if (call.HasArgument("expand")) { @@ -826,7 +859,7 @@ { WebServiceParameters peer; - if (Configuration::GetOrthancPeer(peer, *it)) + if (lock.GetConfiguration().LookupOrthancPeer(peer, *it)) { Json::Value jsonPeer = Json::objectValue; // only return the minimum information to identify the @@ -857,8 +890,10 @@ static void ListPeerOperations(RestApiGetCall& call) { + OrthancConfiguration::ReaderLock lock; + OrthancRestApi::SetOfStrings peers; - Configuration::GetListOfOrthancPeers(peers); + lock.GetConfiguration().GetListOfOrthancPeers(peers); std::string id = call.GetUriComponent("id", ""); if (IsExistingPeer(peers, id)) @@ -878,8 +913,10 @@ if (GetInstancesToExport(request, *job, remote, call)) { + OrthancConfiguration::ReaderLock lock; + WebServiceParameters peer; - if (Configuration::GetOrthancPeer(peer, remote)) + if (lock.GetConfiguration().LookupOrthancPeer(peer, remote)) { job->SetPeer(peer); OrthancRestApi::GetApi(call).SubmitCommandsJob @@ -887,8 +924,8 @@ } else { - LOG(ERROR) << "No peer with symbolic name: " << remote; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "No peer with symbolic name: " + remote); } } } @@ -904,8 +941,10 @@ static void ListModalities(RestApiGetCall& call) { + OrthancConfiguration::ReaderLock lock; + OrthancRestApi::SetOfStrings modalities; - Configuration::GetListOfDicomModalities(modalities); + lock.GetConfiguration().GetListOfDicomModalities(modalities); if (call.HasArgument("expand")) { @@ -913,7 +952,7 @@ for (OrthancRestApi::SetOfStrings::const_iterator it = modalities.begin(); it != modalities.end(); ++it) { - const RemoteModalityParameters& remote = Configuration::GetModalityUsingSymbolicName(*it); + const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it); Json::Value info; remote.Serialize(info, true /* force advanced format */); @@ -936,8 +975,10 @@ static void ListModalityOperations(RestApiGetCall& call) { + OrthancConfiguration::ReaderLock lock; + OrthancRestApi::SetOfStrings modalities; - Configuration::GetListOfDicomModalities(modalities); + lock.GetConfiguration().GetListOfDicomModalities(modalities); std::string id = call.GetUriComponent("id", ""); if (IsExistingModality(modalities, id)) @@ -957,8 +998,15 @@ { RemoteModalityParameters modality; modality.Unserialize(json); - Configuration::UpdateModality(context, call.GetUriComponent("id", ""), modality); - call.GetOutput().AnswerBuffer("", "text/plain"); + + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality); + } + + context.SignalUpdatedModalities(); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } } @@ -967,8 +1015,14 @@ { ServerContext& context = OrthancRestApi::GetContext(call); - Configuration::RemoveModality(context, call.GetUriComponent("id", "")); - call.GetOutput().AnswerBuffer("", "text/plain"); + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", "")); + } + + context.SignalUpdatedModalities(); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -982,8 +1036,15 @@ { WebServiceParameters peer; peer.Unserialize(json); - Configuration::UpdatePeer(context, call.GetUriComponent("id", ""), peer); - call.GetOutput().AnswerBuffer("", "text/plain"); + + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer); + } + + context.SignalUpdatedPeers(); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } } @@ -992,8 +1053,14 @@ { ServerContext& context = OrthancRestApi::GetContext(call); - Configuration::RemovePeer(context, call.GetUriComponent("id", "")); - call.GetOutput().AnswerBuffer("", "text/plain"); + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", "")); + } + + context.SignalUpdatedPeers(); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -1006,7 +1073,7 @@ { const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); RemoteModalityParameters remote = - Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); std::auto_ptr query(ParsedDicomFile::CreateFromJson(json, static_cast(0))); diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -39,7 +39,7 @@ #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h" #include "../../Core/HttpServer/HttpContentNegociation.h" #include "../../Core/Logging.h" -#include "../OrthancInitialization.h" +#include "../OrthancConfiguration.h" #include "../Search/LookupResource.h" #include "../ServerContext.h" #include "../ServerToolbox.h" @@ -151,14 +151,16 @@ { if (!call.HasArgument("limit")) { - LOG(ERROR) << "Missing \"limit\" argument for GET request against: " << call.FlattenUri(); - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); } if (!call.HasArgument("since")) { - LOG(ERROR) << "Missing \"since\" argument for GET request against: " << call.FlattenUri(); - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); } size_t since = boost::lexical_cast(call.GetArgument("since", "")); @@ -201,7 +203,7 @@ { std::string publicId = call.GetUriComponent("id", ""); bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); - call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); + call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText); } @@ -218,12 +220,12 @@ if (body == "0") { context.GetIndex().SetProtectedPatient(publicId, false); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } else if (body == "1") { context.GetIndex().SetProtectedPatient(publicId, true); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } else { @@ -256,7 +258,7 @@ call.BodyToString(target); SystemToolbox::WriteFile(dicom, target); - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } @@ -284,7 +286,7 @@ // is present std::string full; context.ReadDicomAsJson(full, publicId); - call.GetOutput().AnswerBuffer(full, "application/json"); + call.GetOutput().AnswerBuffer(full, MimeType_Json); } } @@ -340,7 +342,7 @@ std::auto_ptr& image_; ImageExtractionMode mode_; bool invert_; - std::string format_; + MimeType format_; std::string answer_; public: @@ -360,27 +362,19 @@ void EncodeUsingPng() { - format_ = "image/png"; + format_ = MimeType_Png; DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_); } void EncodeUsingPam() { - /** - * "No Internet Media Type (aka MIME type, content type) for - * PBM has been registered with IANA, but the unofficial value - * image/x-portable-arbitrarymap is assigned by this - * specification, to be consistent with conventional values - * for the older Netpbm formats." - * http://netpbm.sourceforge.net/doc/pam.html - **/ - format_ = "image/x-portable-arbitrarymap"; + format_ = MimeType_Pam; DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_); } void EncodeUsingJpeg(uint8_t quality) { - format_ = "image/jpeg"; + format_ = MimeType_Jpeg; DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality); } }; @@ -448,8 +442,9 @@ if (!ok) { - LOG(ERROR) << "Bad quality for a JPEG encoding (must be a number between 0 and 100): " << v; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException( + ErrorCode_BadRequest, + "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v); } } @@ -537,7 +532,7 @@ } catch (OrthancException& e) { - if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || e.GetErrorCode() == ErrorCode_UnknownResource) { // The frame number is out of the range for this DICOM // instance, the resource is not existent @@ -552,19 +547,20 @@ call.GetOutput().Redirect(root + "app/images/unsupported.png"); } + return; } ImageToEncode image(decoded, mode, invert); HttpContentNegociation negociation; EncodePng png(image); - negociation.Register("image/png", png); + negociation.Register(MIME_PNG, png); EncodeJpeg jpeg(image, call); - negociation.Register("image/jpeg", jpeg); + negociation.Register(MIME_JPEG, jpeg); EncodePam pam(image); - negociation.Register("image/x-portable-arbitrarymap", pam); + negociation.Register(MIME_PAM, pam); if (negociation.Apply(call.GetHttpHeaders())) { @@ -604,7 +600,7 @@ std::string result; decoded->ToMatlabString(result); - call.GetOutput().AnswerBuffer(result, "text/plain"); + call.GetOutput().AnswerBuffer(result, MimeType_PlainText); } @@ -624,7 +620,8 @@ } std::string publicId = call.GetUriComponent("id", ""); - std::string raw, mime; + std::string raw; + MimeType mime; { ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId); @@ -636,7 +633,7 @@ GzipCompressor gzip; std::string compressed; gzip.Compress(compressed, raw.empty() ? NULL : raw.c_str(), raw.size()); - call.GetOutput().AnswerBuffer(compressed, "application/gzip"); + call.GetOutput().AnswerBuffer(compressed, MimeType_Gzip); } else { @@ -717,7 +714,7 @@ std::string value; if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata)) { - call.GetOutput().AnswerBuffer(value, "text/plain"); + call.GetOutput().AnswerBuffer(value, MimeType_PlainText); } } @@ -733,7 +730,7 @@ if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata { OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } else { @@ -757,7 +754,7 @@ { // It is forbidden to modify internal metadata OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } else { @@ -858,7 +855,7 @@ // Return the raw data (possibly compressed), as stored on the filesystem std::string content; context.ReadAttachment(content, publicId, type, false); - call.GetOutput().AnswerBuffer(content, "application/octet-stream"); + call.GetOutput().AnswerBuffer(content, MimeType_Binary); } } @@ -868,7 +865,7 @@ FileInfo info; if (GetAttachmentInfo(info, call)) { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedSize()), "text/plain"); + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedSize()), MimeType_PlainText); } } @@ -878,7 +875,7 @@ FileInfo info; if (GetAttachmentInfo(info, call)) { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedSize()), "text/plain"); + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedSize()), MimeType_PlainText); } } @@ -889,7 +886,7 @@ if (GetAttachmentInfo(info, call) && info.GetUncompressedMD5() != "") { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedMD5()), "text/plain"); + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedMD5()), MimeType_PlainText); } } @@ -900,7 +897,7 @@ if (GetAttachmentInfo(info, call) && info.GetCompressedMD5() != "") { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedMD5()), "text/plain"); + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedMD5()), MimeType_PlainText); } } @@ -950,7 +947,7 @@ if (ok) { LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5"; - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } else { @@ -971,7 +968,7 @@ if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize())) { - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } else { @@ -993,23 +990,28 @@ { allowed = true; } - else if (Configuration::GetGlobalBoolParameter("StoreDicom", true) && - contentType == FileContentType_DicomAsJson) - { - allowed = true; - } else { - // It is forbidden to delete internal attachments, except for - // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary - // would be automatically reconstructed on the next GET call) - allowed = false; + OrthancConfiguration::ReaderLock lock; + + if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) && + contentType == FileContentType_DicomAsJson) + { + allowed = true; + } + else + { + // It is forbidden to delete internal attachments, except for + // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary + // would be automatically reconstructed on the next GET call) + allowed = false; + } } if (allowed) { OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } else { @@ -1028,7 +1030,7 @@ FileContentType contentType = StringToContentType(name); OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression); - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } @@ -1038,7 +1040,7 @@ if (GetAttachmentInfo(info, call)) { std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; - call.GetOutput().AnswerBuffer(answer, "text/plain"); + call.GetOutput().AnswerBuffer(answer, MimeType_PlainText); } } @@ -1475,7 +1477,7 @@ if (locker.GetDicom().ExtractPdf(pdf)) { - call.GetOutput().AnswerBuffer(pdf, "application/pdf"); + call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf); return; } } @@ -1537,7 +1539,7 @@ } } - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -1546,7 +1548,7 @@ { ServerContext& context = OrthancRestApi::GetContext(call); ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", "")); - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -1563,7 +1565,7 @@ ServerToolbox::ReconstructResource(context, *study); } - call.GetOutput().AnswerBuffer("", "text/plain"); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -34,7 +34,7 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" -#include "../OrthancInitialization.h" +#include "../OrthancConfiguration.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Plugins/Engine/PluginsManager.h" #include "../../Plugins/Engine/OrthancPlugins.h" @@ -55,12 +55,16 @@ Json::Value result = Json::objectValue; result["ApiVersion"] = ORTHANC_API_VERSION; + result["Version"] = ORTHANC_VERSION; result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion(); - result["DicomAet"] = Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"); - result["DicomPort"] = Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242); - result["HttpPort"] = Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042); - result["Name"] = Configuration::GetGlobalStringParameter("Name", ""); - result["Version"] = ORTHANC_VERSION; + + { + OrthancConfiguration::ReaderLock lock; + result["DicomAet"] = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); + result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242); + result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042); + result["Name"] = lock.GetConfiguration().GetStringParameter("Name", ""); + } result["StorageAreaPlugin"] = Json::nullValue; result["DatabaseBackendPlugin"] = Json::nullValue; @@ -99,19 +103,19 @@ std::string level = call.GetArgument("level", ""); if (level == "patient") { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), "text/plain"); + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), MimeType_PlainText); } else if (level == "study") { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), "text/plain"); + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), MimeType_PlainText); } else if (level == "series") { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), "text/plain"); + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), MimeType_PlainText); } else if (level == "instance") { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), "text/plain"); + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText); } } @@ -128,13 +132,13 @@ lock.GetLua().Execute(result, command); } - call.GetOutput().AnswerBuffer(result, "text/plain"); + call.GetOutput().AnswerBuffer(result, MimeType_PlainText); } template static void GetNowIsoString(RestApiGetCall& call) { - call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), "text/plain"); + call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText); } @@ -142,14 +146,14 @@ { std::string statement; GetFileResource(statement, EmbeddedResources::DICOM_CONFORMANCE_STATEMENT); - call.GetOutput().AnswerBuffer(statement, "text/plain"); + call.GetOutput().AnswerBuffer(statement, MimeType_PlainText); } static void GetDefaultEncoding(RestApiGetCall& call) { Encoding encoding = GetDefaultDicomEncoding(); - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText); } @@ -157,9 +161,12 @@ { Encoding encoding = StringToEncoding(call.GetBodyData()); - Configuration::SetDefaultEncoding(encoding); + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().SetDefaultEncoding(encoding); + } - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText); } @@ -265,7 +272,7 @@ #endif } - call.GetOutput().AnswerBuffer(s, "application/javascript"); + call.GetOutput().AnswerBuffer(s, MimeType_JavaScript); } @@ -318,6 +325,27 @@ } + static void GetJobOutput(RestApiGetCall& call) + { + std::string job = call.GetUriComponent("id", ""); + std::string key = call.GetUriComponent("key", ""); + + std::string value; + MimeType mime; + + if (OrthancRestApi::GetContext(call).GetJobsEngine(). + GetRegistry().GetJobOutput(value, mime, job, key)) + { + call.GetOutput().AnswerBuffer(value, mime); + } + else + { + throw OrthancException(ErrorCode_InexistentItem, + "Job has no such output: " + key); + } + } + + enum JobAction { JobAction_Cancel, @@ -357,7 +385,7 @@ if (ok) { - call.GetOutput().AnswerBuffer("{}", "application/json"); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } } @@ -385,5 +413,6 @@ Register("/jobs/{id}/pause", ApplyJobAction); Register("/jobs/{id}/resubmit", ApplyJobAction); Register("/jobs/{id}/resume", ApplyJobAction); + Register("/jobs/{id}/{key}", GetJobOutput); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/QueryRetrieveHandler.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -34,7 +34,7 @@ #include "PrecompiledHeadersServer.h" #include "QueryRetrieveHandler.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/Logging.h" @@ -119,7 +119,11 @@ { Invalidate(); modalityName_ = symbolicName; - Configuration::GetDicomModalityUsingSymbolicName(modality_, symbolicName); + + { + OrthancConfiguration::ReaderLock lock; + lock.GetConfiguration().GetDicomModalityUsingSymbolicName(modality_, symbolicName); + } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/Search/HierarchicalMatcher.cpp --- a/OrthancServer/Search/HierarchicalMatcher.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/Search/HierarchicalMatcher.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -38,7 +38,7 @@ #include "../../Core/OrthancException.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/DicomParsing/ToDcmtkBridge.h" -#include "../OrthancInitialization.h" +#include "../OrthancConfiguration.h" #include @@ -46,9 +46,14 @@ { HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query) { - Setup(*query.GetDcmtkObject().getDataset(), - Configuration::GetGlobalBoolParameter("CaseSensitivePN", false), - query.GetEncoding()); + bool caseSensitivePN; + + { + OrthancConfiguration::ReaderLock lock; + caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false); + } + + Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, query.GetEncoding()); } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerContext.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -40,7 +40,7 @@ #include "../Core/HttpServer/HttpStreamTranscoder.h" #include "../Core/Logging.h" #include "../Plugins/Engine/OrthancPlugins.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" #include "Search/LookupResource.h" #include "ServerJobs/OrthancJobUnserializer.h" @@ -170,8 +170,7 @@ } catch (OrthancException& e) { - LOG(ERROR) << "Cannot unserialize the jobs engine: " << e.What(); - throw; + LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What(); } } else @@ -181,11 +180,9 @@ } else { - LOG(WARNING) << "Not reloading the jobs from the last execution of Orthanc"; + LOG(INFO) << "Not reloading the jobs from the last execution of Orthanc"; } - //jobsEngine_.GetRegistry().SetMaxCompleted // TODO - jobsEngine_.GetRegistry().SetObserver(*this); jobsEngine_.Start(); isJobsEngineUnserialized_ = true; @@ -217,7 +214,8 @@ ServerContext::ServerContext(IDatabaseWrapper& database, IStorageArea& area, - bool unitTesting) : + bool unitTesting, + size_t maxCompletedJobs) : index_(*this, database, (unitTesting ? 20 : 500)), area_(area), compressionEnabled_(false), @@ -227,16 +225,25 @@ mainLua_(*this), filterLua_(*this), luaListener_(*this), + jobsEngine_(maxCompletedJobs), #if ORTHANC_ENABLE_PLUGINS == 1 plugins_(NULL), #endif done_(false), haveJobsChanged_(false), - isJobsEngineUnserialized_(false), - queryRetrieveArchive_(Configuration::GetGlobalUnsignedIntegerParameter("QueryRetrieveSize", 10)), - defaultLocalAet_(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")) + isJobsEngineUnserialized_(false) { - jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2)); + { + OrthancConfiguration::ReaderLock lock; + + queryRetrieveArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 10))); + mediaArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); + defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); + jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); + } + jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); listeners_.push_back(ServerListener(luaListener_, "Lua")); @@ -316,10 +323,7 @@ { StorageAccessor accessor(area_); - { - DicomInstanceHasher hasher(dicom.GetSummary()); - resultPublicId = hasher.HashInstance(); - } + resultPublicId = dicom.GetHasher().HashInstance(); Json::Value simplifiedTags; ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human); @@ -537,8 +541,8 @@ if (!AddAttachment(instancePublicId, FileContentType_DicomAsJson, result.c_str(), result.size())) { - LOG(WARNING) << "Cannot associate the DICOM-as-JSON summary to instance: " << instancePublicId; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId); } } } @@ -597,8 +601,9 @@ FileInfo attachment; if (!index_.LookupAttachment(attachment, instancePublicId, content)) { - LOG(WARNING) << "Unable to read attachment " << EnumerationToString(content) << " of instance " << instancePublicId; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Unable to read attachment " + EnumerationToString(content) + + " of instance " + instancePublicId); } if (uncompressIfNeeded) @@ -825,4 +830,26 @@ job.AddInstance(*it); } } + + + void ServerContext::SignalUpdatedModalities() + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins()) + { + GetPlugins().SignalUpdatedModalities(); + } +#endif + } + + + void ServerContext::SignalUpdatedPeers() + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins()) + { + GetPlugins().SignalUpdatedPeers(); + } +#endif + } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerContext.h Thu Dec 06 15:58:08 2018 +0100 @@ -160,12 +160,19 @@ DicomCacheProvider provider_; boost::mutex dicomCacheMutex_; MemoryCache dicomCache_; - JobsEngine jobsEngine_; LuaScripting mainLua_; LuaScripting filterLua_; LuaServerListener luaListener_; - + std::auto_ptr mediaArchive_; + + // The "JobsEngine" must be *after* "LuaScripting", as + // "LuaScripting" embeds "LuaJobManager" that registers as an + // observer to "SequenceOfOperationsJob", whose lifetime + // corresponds to that of "JobsEngine". It must also be after + // "mediaArchive_", as jobs might access this archive. + JobsEngine jobsEngine_; + #if ORTHANC_ENABLE_PLUGINS == 1 OrthancPlugins* plugins_; #endif @@ -180,7 +187,7 @@ boost::thread changeThread_; boost::thread saveJobsThread_; - SharedArchive queryRetrieveArchive_; + std::auto_ptr queryRetrieveArchive_; std::string defaultLocalAet_; OrthancHttpHandler httpHandler_; @@ -206,7 +213,8 @@ ServerContext(IDatabaseWrapper& database, IStorageArea& area, - bool unitTesting); + bool unitTesting, + size_t maxCompletedJobs); ~ServerContext(); @@ -301,7 +309,12 @@ SharedArchive& GetQueryRetrieveArchive() { - return queryRetrieveArchive_; + return *queryRetrieveArchive_; + } + + SharedArchive& GetMediaArchive() + { + return *mediaArchive_; } const std::string& GetDefaultLocalApplicationEntityTitle() const @@ -346,5 +359,9 @@ void AddChildInstances(SetOfInstancesJob& job, const std::string& publicId); + + void SignalUpdatedModalities(); + + void SignalUpdatedPeers(); }; } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -176,13 +176,13 @@ switch (type) { case FileContentType_Dicom: - return "application/dicom"; + return EnumerationToString(MimeType_Dicom); case FileContentType_DicomAsJson: - return "application/json"; + return MIME_JSON_UTF8; default: - return "application/octet-stream"; + return EnumerationToString(MimeType_Binary); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Dec 06 15:58:08 2018 +0100 @@ -93,8 +93,10 @@ GlobalProperty_FlushSleep = 2, GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, - GlobalProperty_TotalCompressedSize = 6, // Reserved for Orthanc > 1.4.1 - GlobalProperty_TotalUncompressedSize = 7, // Reserved for Orthanc > 1.4.1 + GlobalProperty_TotalCompressedSize = 6, // Reserved for Orthanc > 1.4.3 + GlobalProperty_TotalUncompressedSize = 7, // Reserved for Orthanc > 1.4.3 + GlobalProperty_Modalities = 20, // New in Orthanc 1.4.3 + GlobalProperty_Peers = 21, // New in Orthanc 1.4.3 // Reserved values for internal use by the database plugins GlobalProperty_DatabasePatchLevel = 4, diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerIndex.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -40,7 +40,7 @@ #include "ServerIndexChange.h" #include "EmbeddedResources.h" -#include "OrthancInitialization.h" +#include "OrthancConfiguration.h" #include "../Core/DicomParsing/ParsedDicomFile.h" #include "ServerToolbox.h" #include "../Core/Toolbox.h" @@ -621,8 +621,6 @@ instanceMetadata.clear(); - DicomInstanceHasher hasher(instanceToStore.GetSummary()); - try { Transaction t(*this); @@ -631,14 +629,14 @@ { ResourceType type; int64_t tmp; - if (db_.LookupResource(tmp, type, hasher.HashInstance())) + if (db_.LookupResource(tmp, type, instanceToStore.GetHasher().HashInstance())) { assert(type == ResourceType_Instance); if (overwrite_) { // Overwrite the old instance - LOG(INFO) << "Overwriting instance: " << hasher.HashInstance(); + LOG(INFO) << "Overwriting instance: " << instanceToStore.GetHasher().HashInstance(); db_.DeleteResource(tmp); } else @@ -658,10 +656,10 @@ instanceSize += it->GetCompressedSize(); } - Recycle(instanceSize, hasher.HashPatient()); + Recycle(instanceSize, instanceToStore.GetHasher().HashPatient()); // Create the instance - int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance); + int64_t instance = CreateResource(instanceToStore.GetHasher().HashInstance(), ResourceType_Instance); ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary); // Detect up to which level the patient/study/series/instance @@ -674,26 +672,26 @@ { ResourceType dummy; - if (db_.LookupResource(series, dummy, hasher.HashSeries())) + if (db_.LookupResource(series, dummy, instanceToStore.GetHasher().HashSeries())) { assert(dummy == ResourceType_Series); // The patient, the study and the series already exist - bool ok = (db_.LookupResource(patient, dummy, hasher.HashPatient()) && - db_.LookupResource(study, dummy, hasher.HashStudy())); + bool ok = (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()) && + db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())); assert(ok); } - else if (db_.LookupResource(study, dummy, hasher.HashStudy())) + else if (db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())) { assert(dummy == ResourceType_Study); // New series: The patient and the study already exist isNewSeries = true; - bool ok = db_.LookupResource(patient, dummy, hasher.HashPatient()); + bool ok = db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()); assert(ok); } - else if (db_.LookupResource(patient, dummy, hasher.HashPatient())) + else if (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient())) { assert(dummy == ResourceType_Patient); @@ -713,21 +711,21 @@ // Create the series if needed if (isNewSeries) { - series = CreateResource(hasher.HashSeries(), ResourceType_Series); + series = CreateResource(instanceToStore.GetHasher().HashSeries(), ResourceType_Series); ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary); } // Create the study if needed if (isNewStudy) { - study = CreateResource(hasher.HashStudy(), ResourceType_Study); + study = CreateResource(instanceToStore.GetHasher().HashStudy(), ResourceType_Study); ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary); } // Create the patient if needed if (isNewPatient) { - patient = CreateResource(hasher.HashPatient(), ResourceType_Patient); + patient = CreateResource(instanceToStore.GetHasher().HashPatient(), ResourceType_Patient); ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary); } @@ -853,13 +851,13 @@ SeriesStatus seriesStatus = GetSeriesStatus(series); if (seriesStatus == SeriesStatus_Complete) { - LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, hasher.HashSeries()); + LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); } // Mark the parent resources of this instance as unstable - MarkAsUnstable(series, ResourceType_Series, hasher.HashSeries()); - MarkAsUnstable(study, ResourceType_Study, hasher.HashStudy()); - MarkAsUnstable(patient, ResourceType_Patient, hasher.HashPatient()); + MarkAsUnstable(series, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); + MarkAsUnstable(study, ResourceType_Study, instanceToStore.GetHasher().HashStudy()); + MarkAsUnstable(patient, ResourceType_Patient, instanceToStore.GetHasher().HashPatient()); t.Commit(instanceSize); @@ -1933,7 +1931,13 @@ void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, unsigned int threadSleep) { - int stableAge = Configuration::GetGlobalUnsignedIntegerParameter("StableAge", 60); + int stableAge; + + { + OrthancConfiguration::ReaderLock lock; + stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60); + } + if (stableAge <= 0) { stableAge = 60; diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerIndex.h Thu Dec 06 15:58:08 2018 +0100 @@ -38,7 +38,6 @@ #include "../Core/Cache/LeastRecentlyUsedIndex.h" #include "../Core/SQLite/Connection.h" #include "../Core/DicomFormat/DicomMap.h" -#include "../Core/DicomFormat/DicomInstanceHasher.h" #include "ServerEnumerations.h" #include "IDatabaseWrapper.h" diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/ServerJobs/ArchiveJob.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -47,7 +47,12 @@ static const uint64_t MEGA_BYTES = 1024 * 1024; static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024; -static const char* MEDIA_IMAGES_FOLDER = "IMAGES"; + +static const char* const MEDIA_IMAGES_FOLDER = "IMAGES"; +static const char* const KEY_DESCRIPTION = "Description"; +static const char* const KEY_INSTANCES_COUNT = "InstancesCount"; +static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB"; + namespace Orthanc { @@ -778,11 +783,9 @@ }; - ArchiveJob::ArchiveJob(boost::shared_ptr& target, - ServerContext& context, + ArchiveJob::ArchiveJob(ServerContext& context, bool isMedia, bool enableExtendedSopClass) : - target_(target), context_(context), archive_(new ArchiveIndex(ResourceType_Patient)), // root isMedia_(isMedia), @@ -791,52 +794,143 @@ instancesCount_(0), uncompressedSize_(0) { + } + + + ArchiveJob::~ArchiveJob() + { + if (!mediaArchiveId_.empty()) + { + context_.GetMediaArchive().Remove(mediaArchiveId_); + } + } + + + void ArchiveJob::SetSynchronousTarget(boost::shared_ptr& target) + { if (target.get() == NULL) { throw OrthancException(ErrorCode_NullPointer); } + else if (writer_.get() != NULL || // Already started + synchronousTarget_.get() != NULL || + asynchronousTarget_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + synchronousTarget_ = target; + } } + void ArchiveJob::SetDescription(const std::string& description) + { + if (writer_.get() != NULL) // Already started + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + description_ = description; + } + } + + void ArchiveJob::AddResource(const std::string& publicId) { if (writer_.get() != NULL) // Already started { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - - ResourceIdentifiers resource(context_.GetIndex(), publicId); - archive_->Add(context_.GetIndex(), resource); + else + { + ResourceIdentifiers resource(context_.GetIndex(), publicId); + archive_->Add(context_.GetIndex(), resource); + } } void ArchiveJob::Reset() { - LOG(ERROR) << "Cannot resubmit the creation of an archive"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot resubmit the creation of an archive"); } void ArchiveJob::Start() { + TemporaryFile* target = NULL; + + if (synchronousTarget_.get() == NULL) + { + asynchronousTarget_.reset(new TemporaryFile); + target = asynchronousTarget_.get(); + } + else + { + target = synchronousTarget_.get(); + } + if (writer_.get() != NULL) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - writer_.reset(new ZipWriterIterator(*target_, context_, *archive_, + writer_.reset(new ZipWriterIterator(*target, context_, *archive_, isMedia_, enableExtendedSopClass_)); instancesCount_ = writer_->GetInstancesCount(); uncompressedSize_ = writer_->GetUncompressedSize(); } + + + namespace + { + class DynamicTemporaryFile : public IDynamicObject + { + private: + std::auto_ptr file_; + + public: + DynamicTemporaryFile(TemporaryFile* f) : file_(f) + { + if (f == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + const TemporaryFile& GetFile() const + { + assert(file_.get() != NULL); + return *file_; + } + }; + } + + void ArchiveJob::FinalizeTarget() + { + writer_.reset(); // Flush all the results + + if (asynchronousTarget_.get() != NULL) + { + // Asynchronous behavior: Move the resulting file into the media archive + mediaArchiveId_ = context_.GetMediaArchive().Add( + new DynamicTemporaryFile(asynchronousTarget_.release())); + } + } + + JobStepResult ArchiveJob::Step() { assert(writer_.get() != NULL); - if (target_.unique()) + if (synchronousTarget_.get() != NULL && + synchronousTarget_.unique()) { LOG(WARNING) << "A client has disconnected while creating an archive"; return JobStepResult::Failure(ErrorCode_NetworkProtocol); @@ -844,7 +938,7 @@ if (writer_->GetStepsCount() == 0) { - writer_.reset(); // Flush all the results + FinalizeTarget(); return JobStepResult::Success(); } else @@ -855,7 +949,7 @@ if (currentStep_ == writer_->GetStepsCount()) { - writer_.reset(); // Flush all the results + FinalizeTarget(); return JobStepResult::Success(); } else @@ -893,12 +987,41 @@ } } - + void ArchiveJob::GetPublicContent(Json::Value& value) { - value["Description"] = description_; - value["InstancesCount"] = instancesCount_; - value["UncompressedSizeMB"] = + value = Json::objectValue; + value[KEY_DESCRIPTION] = description_; + value[KEY_INSTANCES_COUNT] = instancesCount_; + value[KEY_UNCOMPRESSED_SIZE_MB] = static_cast(uncompressedSize_ / MEGA_BYTES); } + + + bool ArchiveJob::GetOutput(std::string& output, + MimeType& mime, + const std::string& key) + { + if (key == "archive" && + !mediaArchiveId_.empty()) + { + SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_); + + if (accessor.IsValid()) + { + const DynamicTemporaryFile& f = dynamic_cast(accessor.GetItem()); + f.GetFile().Read(output); + mime = MimeType_Zip; + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/ArchiveJob.h --- a/OrthancServer/ServerJobs/ArchiveJob.h Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Thu Dec 06 15:58:08 2018 +0100 @@ -50,7 +50,8 @@ class ZipCommands; class ZipWriterIterator; - boost::shared_ptr target_; + boost::shared_ptr synchronousTarget_; + std::auto_ptr asynchronousTarget_; ServerContext& context_; boost::shared_ptr archive_; bool isMedia_; @@ -61,17 +62,20 @@ size_t currentStep_; unsigned int instancesCount_; uint64_t uncompressedSize_; + std::string mediaArchiveId_; + void FinalizeTarget(); + public: - ArchiveJob(boost::shared_ptr& target, - ServerContext& context, + ArchiveJob(ServerContext& context, bool isMedia, bool enableExtendedSopClass); + + virtual ~ArchiveJob(); + + void SetSynchronousTarget(boost::shared_ptr& synchronousTarget); - void SetDescription(const std::string& description) - { - description_ = description; - } + void SetDescription(const std::string& description); const std::string& GetDescription() const { @@ -100,5 +104,9 @@ { return false; // Cannot serialize this kind of job } + + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key); }; } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/LuaJobManager.cpp --- a/OrthancServer/ServerJobs/LuaJobManager.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/LuaJobManager.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -34,7 +34,7 @@ #include "../PrecompiledHeadersServer.h" #include "LuaJobManager.h" -#include "../OrthancInitialization.h" +#include "../OrthancConfiguration.h" #include "../../Core/Logging.h" #include "../../Core/JobsEngine/Operations/LogJobOperation.h" @@ -68,7 +68,11 @@ priority_(0), trailingTimeout_(5000) { - dicomTimeout_ = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); + { + OrthancConfiguration::ReaderLock lock; + dicomTimeout_ = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); + } + LOG(INFO) << "Lua: DICOM associations will be closed after " << dicomTimeout_ << " seconds of inactivity"; } @@ -139,6 +143,7 @@ // Need to create a new job, as the previous one is either // finished, or is getting too long that_.currentJob_ = new SequenceOfOperationsJob; + that_.currentJob_->Register(that_); that_.currentJob_->SetDescription("Lua"); { diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/ServerJobs/MergeStudyJob.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -61,8 +61,8 @@ { if (study == targetStudy_) { - LOG(ERROR) << "Cannot merge a study into the same study: " << study; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "Cannot merge a study into the same study: " + study); } else { @@ -183,8 +183,8 @@ if (!context_.GetIndex().LookupResourceType(type, targetStudy) || type != ResourceType_Study) { - LOG(ERROR) << "Cannot merge into an unknown study: " << targetStudy; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "Cannot merge into an unknown study: " + targetStudy); } @@ -257,8 +257,8 @@ } else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries)) { - LOG(ERROR) << "Cannot find this resource: " << studyOrSeries; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "Cannot find this resource: " + studyOrSeries); } else { @@ -273,9 +273,10 @@ break; default: - LOG(ERROR) << "This resource is neither a study, nor a series: " - << studyOrSeries << " is a " << EnumerationToString(level); - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "This resource is neither a study, nor a series: " + + studyOrSeries + " is a " + + std::string(EnumerationToString(level))); } } } @@ -291,14 +292,14 @@ } else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study)) { - LOG(ERROR) << "This resource is not a series: " << series; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "This resource is not a series: " + series); } else if (parent == targetStudy_) { - LOG(ERROR) << "Cannot merge series " << series - << " into its parent study " << targetStudy_; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "Cannot merge series " + series + + " into its parent study " + targetStudy_); } else { @@ -318,8 +319,8 @@ else if (!context_.GetIndex().LookupResourceType(actualLevel, study) || actualLevel != ResourceType_Study) { - LOG(ERROR) << "This resource is not a study: " << study; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "This resource is not a study: " + study); } else { diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -134,8 +134,8 @@ if (modification_.get() == NULL || output_.get() == NULL) { - LOG(ERROR) << "No modification was provided for this job"; - throw OrthancException(ErrorCode_BadSequenceOfCalls); + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No modification was provided for this job"); } @@ -212,8 +212,8 @@ std::string modifiedInstance; if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) { - LOG(ERROR) << "Error while storing a modified instance " << instance; - throw OrthancException(ErrorCode_CannotStoreInstance); + throw OrthancException(ErrorCode_CannotStoreInstance, + "Error while storing a modified instance " + instance); } assert(modifiedInstance == modifiedHasher.HashInstance()); diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/ServerJobs/SplitStudyJob.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -43,10 +43,11 @@ { if (allowedTags_.find(tag) == allowedTags_.end()) { - LOG(ERROR) << "Cannot modify the following tag while splitting a study " - << "(not in the patient/study modules): " - << FromDcmtkBridge::GetTagName(tag, "") << " (" << tag.Format() << ")"; - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Cannot modify the following tag while splitting a study " + "(not in the patient/study modules): " + + FromDcmtkBridge::GetTagName(tag, "") + + " (" + tag.Format() + ")"); } } @@ -173,8 +174,8 @@ if (!context_.GetIndex().LookupResourceType(type, sourceStudy) || type != ResourceType_Study) { - LOG(ERROR) << "Cannot split unknown study: " << sourceStudy; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "Cannot split unknown study " + sourceStudy); } } @@ -209,8 +210,8 @@ else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) || parent != sourceStudy_) { - LOG(ERROR) << "This series does not belong to the study to be split: " << series; - throw OrthancException(ErrorCode_UnknownResource); + throw OrthancException(ErrorCode_UnknownResource, + "This series does not belong to the study to be split: " + series); } else { diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/ServerToolbox.cpp --- a/OrthancServer/ServerToolbox.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/ServerToolbox.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -78,7 +78,10 @@ const Json::Value& source, DicomToJsonFormat format) { - assert(source.isObject()); + if (!source.isObject()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } target = Json::objectValue; Json::Value::Members members = source.getMemberNames(); @@ -299,17 +302,19 @@ tmp != level || !FindOneChildInstance(instance, database, resource, level)) { - LOG(ERROR) << "Cannot find an instance for " << EnumerationToString(level) - << " with identifier " << *it; - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot find an instance for " + + std::string(EnumerationToString(level)) + + " with identifier " + *it); } // Get the DICOM file attached to some instances in the resource FileInfo attachment; if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom)) { - LOG(ERROR) << "Cannot retrieve the DICOM file associated with instance " << database.GetPublicId(instance); - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, + "Cannot retrieve the DICOM file associated with instance " + + database.GetPublicId(instance)); } try diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/SliceOrdering.cpp --- a/OrthancServer/SliceOrdering.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/SliceOrdering.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -407,8 +407,8 @@ if (!SortUsingPositions() && !SortUsingIndexInSeries()) { - LOG(ERROR) << "Unable to order the slices of the series " << seriesId; - throw OrthancException(ErrorCode_CannotOrderSlices); + throw OrthancException(ErrorCode_CannotOrderSlices, + "Unable to order the slices of series " + seriesId); } } diff -r 3fabf9a673f6 -r eff50153a7b3 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/OrthancServer/main.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -36,19 +36,21 @@ #include -#include "../Core/Logging.h" -#include "../Core/HttpServer/EmbeddedResourceHttpHandler.h" -#include "../Core/HttpServer/FilesystemHttpHandler.h" -#include "../Core/Lua/LuaFunctionCall.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/DicomNetworking/DicomServer.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/HttpServer/EmbeddedResourceHttpHandler.h" +#include "../Core/HttpServer/FilesystemHttpHandler.h" +#include "../Core/HttpServer/MongooseServer.h" +#include "../Core/Logging.h" +#include "../Core/Lua/LuaFunctionCall.h" +#include "../Plugins/Engine/OrthancPlugins.h" +#include "OrthancConfiguration.h" +#include "OrthancFindRequestHandler.h" #include "OrthancInitialization.h" -#include "ServerContext.h" -#include "OrthancFindRequestHandler.h" #include "OrthancMoveRequestHandler.h" +#include "ServerContext.h" #include "ServerToolbox.h" -#include "../Plugins/Engine/OrthancPlugins.h" -#include "../Core/DicomParsing/FromDcmtkBridge.h" using namespace Orthanc; @@ -89,19 +91,21 @@ -class ModalitiesFromConfiguration : public Orthanc::DicomServer::IRemoteModalities +class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities { public: virtual bool IsSameAETitle(const std::string& aet1, const std::string& aet2) { - return Orthanc::Configuration::IsSameAETitle(aet1, aet2); + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().IsSameAETitle(aet1, aet2); } virtual bool LookupAETitle(RemoteModalityParameters& modality, const std::string& aet) { - return Orthanc::Configuration::LookupDicomModalityUsingAETitle(modality, aet); + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet); } }; @@ -128,8 +132,11 @@ { std::auto_ptr result(new OrthancFindRequestHandler(context_)); - result->SetMaxResults(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindResults", 0)); - result->SetMaxInstances(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindInstances", 0)); + { + OrthancConfiguration::ReaderLock lock; + result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0)); + result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0)); + } if (result->GetMaxResults() == 0) { @@ -176,8 +183,9 @@ OrthancApplicationEntityFilter(ServerContext& context) : context_(context) { - alwaysAllowEcho_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowEcho", true); - alwaysAllowStore_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowStore", true); + OrthancConfiguration::ReaderLock lock; + alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true); + alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true); } virtual bool IsAllowedConnection(const std::string& remoteIp, @@ -187,9 +195,16 @@ LOG(INFO) << "Incoming connection from AET " << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet; - return (alwaysAllowEcho_ || - alwaysAllowStore_ || - Configuration::IsKnownAETitle(remoteAet, remoteIp)); + if (alwaysAllowEcho_ || + alwaysAllowStore_) + { + return true; + } + else + { + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp); + } } virtual bool IsAllowedRequest(const std::string& remoteIp, @@ -197,7 +212,7 @@ const std::string& calledAet, DicomRequestType type) { - LOG(INFO) << "Incoming " << Orthanc::EnumerationToString(type) << " request from AET " + LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET " << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet; if (type == DicomRequestType_Echo && @@ -214,9 +229,10 @@ } else { + OrthancConfiguration::ReaderLock lock; + RemoteModalityParameters modality; - - if (Configuration::LookupDicomModalityUsingAETitle(modality, remoteAet)) + if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, remoteAet)) { return modality.IsRequestAllowed(type); } @@ -283,7 +299,10 @@ } } - return Configuration::GetGlobalBoolParameter(configuration, true); + { + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().GetBooleanParameter(configuration, true); + } } @@ -308,7 +327,10 @@ } } - return Configuration::GetGlobalBoolParameter(configuration, false); + { + OrthancConfiguration::ReaderLock lock; + return lock.GetConfiguration().GetBooleanParameter(configuration, false); + } } }; @@ -460,6 +482,11 @@ message["OrthancError"] = EnumerationToString(errorCode); message["OrthancStatus"] = errorCode; + if (exception.HasDetails()) + { + message["Details"] = exception.GetDetails(); + } + std::string info = message.toStyledString(); output.SendStatus(httpStatus, info); } @@ -652,11 +679,22 @@ static void LoadPlugins(OrthancPlugins& plugins) { std::list path; - Configuration::GetGlobalListOfStringsParameter(path, "Plugins"); + + { + OrthancConfiguration::ReaderLock lock; + lock.GetConfiguration().GetListOfStringsParameter(path, "Plugins"); + } + for (std::list::const_iterator it = path.begin(); it != path.end(); ++it) { - std::string path = Configuration::InterpretStringParameterAsPath(*it); + std::string path; + + { + OrthancConfiguration::ReaderLock lock; + path = lock.GetConfiguration().InterpretStringParameterAsPath(*it); + } + LOG(WARNING) << "Loading plugin(s) from: " << path; plugins.GetManager().RegisterPlugin(path); } @@ -693,7 +731,8 @@ { // Handling of SIGHUP - if (Configuration::HasConfigurationChanged()) + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().HasConfigurationChanged()) { LOG(WARNING) << "A SIGHUP signal has been received, resetting Orthanc"; Logging::Flush(); @@ -702,7 +741,8 @@ } else { - LOG(WARNING) << "A SIGHUP signal has been received, but is ignored as the configuration has not changed"; + LOG(WARNING) << "A SIGHUP signal has been received, but is ignored " + << "as the configuration has not changed on the disk"; Logging::Flush(); continue; } @@ -740,57 +780,74 @@ OrthancRestApi& restApi, OrthancPlugins* plugins) { - if (!Configuration::GetGlobalBoolParameter("HttpServerEnabled", true)) + bool httpServerEnabled; + + { + OrthancConfiguration::ReaderLock lock; + httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true); + } + + if (!httpServerEnabled) { LOG(WARNING) << "The HTTP server is disabled"; return WaitForExit(context, restApi); } - - MyHttpExceptionFormatter exceptionFormatter(Configuration::GetGlobalBoolParameter("HttpDescribeErrors", true), plugins); - - - // HTTP server - MyIncomingHttpRequestFilter httpFilter(context, plugins); - MongooseServer httpServer; - httpServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042)); - httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false)); - httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false)); - httpServer.SetHttpCompressionEnabled(Configuration::GetGlobalBoolParameter("HttpCompressionEnabled", true)); - httpServer.SetIncomingHttpRequestFilter(httpFilter); - httpServer.SetHttpExceptionFormatter(exceptionFormatter); - - httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false)); - Configuration::SetupRegisteredUsers(httpServer); - - if (Configuration::GetGlobalBoolParameter("SslEnabled", false)) - { - std::string certificate = Configuration::InterpretStringParameterAsPath( - Configuration::GetGlobalStringParameter("SslCertificate", "certificate.pem")); - httpServer.SetSslEnabled(true); - httpServer.SetSslCertificate(certificate.c_str()); - } else { - httpServer.SetSslEnabled(false); - } + MyIncomingHttpRequestFilter httpFilter(context, plugins); + MongooseServer httpServer; + bool httpDescribeErrors; - httpServer.Register(context.GetHttpHandler()); + { + OrthancConfiguration::ReaderLock lock; + + httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true); + + // HTTP server + //httpServer.SetThreadsCount(50); + httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042)); + httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false)); + httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", false)); + httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true)); + httpServer.SetAuthenticationEnabled(lock.GetConfiguration().GetBooleanParameter("AuthenticationEnabled", false)); + + lock.GetConfiguration().SetupRegisteredUsers(httpServer); - if (httpServer.GetPortNumber() < 1024) - { - LOG(WARNING) << "The HTTP port is privileged (" - << httpServer.GetPortNumber() << " is below 1024), " - << "make sure you run Orthanc as root/administrator"; - } + if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false)) + { + std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath( + lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem")); + httpServer.SetSslEnabled(true); + httpServer.SetSslCertificate(certificate.c_str()); + } + else + { + httpServer.SetSslEnabled(false); + } + } - httpServer.Start(); - - bool restart = WaitForExit(context, restApi); + MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins); + + httpServer.SetIncomingHttpRequestFilter(httpFilter); + httpServer.SetHttpExceptionFormatter(exceptionFormatter); + httpServer.Register(context.GetHttpHandler()); - httpServer.Stop(); - LOG(WARNING) << " HTTP server has stopped"; + if (httpServer.GetPortNumber() < 1024) + { + LOG(WARNING) << "The HTTP port is privileged (" + << httpServer.GetPortNumber() << " is below 1024), " + << "make sure you run Orthanc as root/administrator"; + } - return restart; + httpServer.Start(); + + bool restart = WaitForExit(context, restApi); + + httpServer.Stop(); + LOG(WARNING) << " HTTP server has stopped"; + + return restart; + } } @@ -798,84 +855,96 @@ OrthancRestApi& restApi, OrthancPlugins* plugins) { - if (!Configuration::GetGlobalBoolParameter("DicomServerEnabled", true)) + bool dicomServerEnabled; + + { + OrthancConfiguration::ReaderLock lock; + dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true); + } + + if (!dicomServerEnabled) { LOG(WARNING) << "The DICOM server is disabled"; return StartHttpServer(context, restApi, plugins); } - - MyDicomServerFactory serverFactory(context); - OrthancApplicationEntityFilter dicomFilter(context); - ModalitiesFromConfiguration modalities; + else + { + MyDicomServerFactory serverFactory(context); + OrthancApplicationEntityFilter dicomFilter(context); + ModalitiesFromConfiguration modalities; - // Setup the DICOM server - DicomServer dicomServer; - dicomServer.SetRemoteModalities(modalities); - dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false)); - dicomServer.SetStoreRequestHandlerFactory(serverFactory); - dicomServer.SetMoveRequestHandlerFactory(serverFactory); - dicomServer.SetFindRequestHandlerFactory(serverFactory); - dicomServer.SetAssociationTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScpTimeout", 30)); + // Setup the DICOM server + DicomServer dicomServer; + dicomServer.SetRemoteModalities(modalities); + dicomServer.SetStoreRequestHandlerFactory(serverFactory); + dicomServer.SetMoveRequestHandlerFactory(serverFactory); + dicomServer.SetFindRequestHandlerFactory(serverFactory); + { + OrthancConfiguration::ReaderLock lock; + dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false)); + dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30)); + dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242)); + dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC")); + } #if ORTHANC_ENABLE_PLUGINS == 1 - if (plugins != NULL) - { - if (plugins->HasWorklistHandler()) - { - dicomServer.SetWorklistRequestHandlerFactory(*plugins); - } - - if (plugins->HasFindHandler()) + if (plugins != NULL) { - dicomServer.SetFindRequestHandlerFactory(*plugins); - } + if (plugins->HasWorklistHandler()) + { + dicomServer.SetWorklistRequestHandlerFactory(*plugins); + } - if (plugins->HasMoveHandler()) - { - dicomServer.SetMoveRequestHandlerFactory(*plugins); + if (plugins->HasFindHandler()) + { + dicomServer.SetFindRequestHandlerFactory(*plugins); + } + + if (plugins->HasMoveHandler()) + { + dicomServer.SetMoveRequestHandlerFactory(*plugins); + } } - } #endif - dicomServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242)); - dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); - dicomServer.SetApplicationEntityFilter(dicomFilter); + dicomServer.SetApplicationEntityFilter(dicomFilter); - if (dicomServer.GetPortNumber() < 1024) - { - LOG(WARNING) << "The DICOM port is privileged (" - << dicomServer.GetPortNumber() << " is below 1024), " - << "make sure you run Orthanc as root/administrator"; - } + if (dicomServer.GetPortNumber() < 1024) + { + LOG(WARNING) << "The DICOM port is privileged (" + << dicomServer.GetPortNumber() << " is below 1024), " + << "make sure you run Orthanc as root/administrator"; + } - dicomServer.Start(); - LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() - << " on port: " << dicomServer.GetPortNumber(); + dicomServer.Start(); + LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() + << " on port: " << dicomServer.GetPortNumber(); - bool restart = false; - ErrorCode error = ErrorCode_Success; + bool restart = false; + ErrorCode error = ErrorCode_Success; - try - { - restart = StartHttpServer(context, restApi, plugins); - } - catch (OrthancException& e) - { - error = e.GetErrorCode(); - } + try + { + restart = StartHttpServer(context, restApi, plugins); + } + catch (OrthancException& e) + { + error = e.GetErrorCode(); + } - dicomServer.Stop(); - LOG(WARNING) << " DICOM server has stopped"; + dicomServer.Stop(); + LOG(WARNING) << " DICOM server has stopped"; - serverFactory.Done(); + serverFactory.Done(); - if (error != ErrorCode_Success) - { - throw OrthancException(error); + if (error != ErrorCode_Success) + { + throw OrthancException(error); + } + + return restart; } - - return restart; } @@ -934,9 +1003,10 @@ if (currentVersion > ORTHANC_DATABASE_VERSION) { - LOG(ERROR) << "The version of the database schema (" << currentVersion - << ") is too recent for this version of Orthanc. Please upgrade Orthanc."; - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "The version of the database schema (" + + boost::lexical_cast(currentVersion) + + ") is too recent for this version of Orthanc. Please upgrade Orthanc."); } LOG(WARNING) << "Upgrading the database from schema version " @@ -957,8 +1027,9 @@ currentVersion = database.GetDatabaseVersion(); if (ORTHANC_DATABASE_VERSION != currentVersion) { - LOG(ERROR) << "The database schema was not properly upgraded, it is still at version " << currentVersion; - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "The database schema was not properly upgraded, it is still at version " + + boost::lexical_cast(currentVersion)); } else { @@ -968,86 +1039,120 @@ } + +namespace +{ + class ServerContextConfigurator : public boost::noncopyable + { + private: + ServerContext& context_; + OrthancPlugins* plugins_; + + public: + ServerContextConfigurator(ServerContext& context, + OrthancPlugins* plugins) : + context_(context), + plugins_(plugins) + { + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().SetServerIndex(context.GetIndex()); + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (plugins_ != NULL) + { + plugins_->SetServerContext(context_); + context_.SetPlugins(*plugins_); + } +#endif + } + + ~ServerContextConfigurator() + { + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().ResetServerIndex(); + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (plugins_ != NULL) + { + plugins_->ResetServerContext(); + context_.ResetPlugins(); + } +#endif + } + }; +} + + static bool ConfigureServerContext(IDatabaseWrapper& database, IStorageArea& storageArea, OrthancPlugins *plugins, bool loadJobsFromDatabase) { - // These configuration options must be set before creating the - // ServerContext, otherwise the possible Lua scripts will not be - // able to properly issue HTTP/HTTPS queries - HttpClient::ConfigureSsl(Configuration::GetGlobalBoolParameter("HttpsVerifyPeers", true), - Configuration::InterpretStringParameterAsPath - (Configuration::GetGlobalStringParameter("HttpsCACertificates", ""))); - HttpClient::SetDefaultVerbose(Configuration::GetGlobalBoolParameter("HttpVerbose", false)); - HttpClient::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("HttpTimeout", 0)); - HttpClient::SetDefaultProxy(Configuration::GetGlobalStringParameter("HttpProxy", "")); + size_t maxCompletedJobs; + + { + OrthancConfiguration::ReaderLock lock; - DicomUserConnection::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScuTimeout", 10)); + // These configuration options must be set before creating the + // ServerContext, otherwise the possible Lua scripts will not be + // able to properly issue HTTP/HTTPS queries + HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true), + lock.GetConfiguration().InterpretStringParameterAsPath + (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", ""))); + HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false)); + HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0)); + HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", "")); + + DicomUserConnection::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10)); - ServerContext context(database, storageArea, false /* not running unit tests */); - context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false)); - context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true)); + maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10); + } + + ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs); + + { + OrthancConfiguration::ReaderLock lock; - // New option in Orthanc 1.4.2 - context.GetIndex().SetOverwriteInstances(Configuration::GetGlobalBoolParameter("OverwriteInstances", false)); + context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false)); + context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true)); + + // New option in Orthanc 1.4.2 + context.GetIndex().SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); - try - { - context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalUnsignedIntegerParameter("MaximumPatientCount", 0)); - } - catch (...) - { - context.GetIndex().SetMaximumPatientCount(0); - } + try + { + context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0)); + } + catch (...) + { + context.GetIndex().SetMaximumPatientCount(0); + } - try - { - uint64_t size = Configuration::GetGlobalUnsignedIntegerParameter("MaximumStorageSize", 0); - context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); - } - catch (...) - { - context.GetIndex().SetMaximumStorageSize(0); + try + { + uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0); + context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); + } + catch (...) + { + context.GetIndex().SetMaximumStorageSize(0); + } } - context.GetJobsEngine().GetRegistry().SetMaxCompletedJobs - (Configuration::GetGlobalUnsignedIntegerParameter("JobsHistorySize", 10)); + { + ServerContextConfigurator configurator(context, plugins); -#if ORTHANC_ENABLE_PLUGINS == 1 - if (plugins) - { - plugins->SetServerContext(context); - context.SetPlugins(*plugins); - } -#endif + { + OrthancConfiguration::WriterLock lock; + lock.GetConfiguration().LoadModalitiesAndPeers(); + } - bool restart = false; - ErrorCode error = ErrorCode_Success; - - try - { - restart = ConfigureHttpHandler(context, plugins, loadJobsFromDatabase); + return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase); } - catch (OrthancException& e) - { - error = e.GetErrorCode(); - } - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (plugins) - { - plugins->ResetServerContext(); - context.ResetPlugins(); - } -#endif - - if (error != ErrorCode_Success) - { - throw OrthancException(error); - } - - return restart; } @@ -1068,10 +1173,11 @@ } else if (currentVersion != ORTHANC_DATABASE_VERSION) { - LOG(ERROR) << "The database schema must be changed from version " - << currentVersion << " to " << ORTHANC_DATABASE_VERSION - << ": Please run Orthanc with the \"--upgrade\" argument"; - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "The database schema must be changed from version " + + boost::lexical_cast(currentVersion) + " to " + + boost::lexical_cast(ORTHANC_DATABASE_VERSION) + + ": Please run Orthanc with the \"--upgrade\" argument"); } bool success = ConfigureServerContext @@ -1104,7 +1210,7 @@ } else { - databasePtr.reset(Configuration::CreateDatabaseWrapper()); + databasePtr.reset(CreateDatabaseWrapper()); database = databasePtr.get(); } @@ -1115,7 +1221,7 @@ } else { - storage.reset(Configuration::CreateStorageArea()); + storage.reset(CreateStorageArea()); } assert(database != NULL); @@ -1126,8 +1232,8 @@ #elif ORTHANC_ENABLE_PLUGINS == 0 // The plugins are disabled - databasePtr.reset(Configuration::CreateDatabaseWrapper()); - storage.reset(Configuration::CreateStorageArea()); + databasePtr.reset(lock.GetConfiguration().CreateDatabaseWrapper()); + storage.reset(lock.GetConfiguration().CreateStorageArea()); return ConfigureDatabase(*databasePtr, *storage, NULL, upgradeDatabase, loadJobsFromDatabase); diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -280,8 +280,8 @@ { if (extensions_.getAllInternalIds == NULL) { - LOG(ERROR) << "The database plugin does not implement the GetAllInternalIds primitive"; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "The database plugin does not implement the GetAllInternalIds primitive"); } ResetAnswers(); @@ -618,8 +618,8 @@ { if (extensions_.lookupIdentifier3 == NULL) { - LOG(ERROR) << "The database plugin does not implement the LookupIdentifier3 primitive"; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "The database plugin does not implement the LookupIdentifier3 primitive"); } OrthancPluginDicomTag tmp; @@ -736,8 +736,8 @@ { if (extensions_.clearMainDicomTags == NULL) { - LOG(ERROR) << "Your custom index plugin does not implement the ClearMainDicomTags() extension"; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "Your custom index plugin does not implement the ClearMainDicomTags() extension"); } CheckSuccess(extensions_.clearMainDicomTags(payload_, id)); @@ -971,14 +971,15 @@ break; default: - LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "Unhandled type of answer for custom index plugin: " + + boost::lexical_cast(answer.type)); } } else if (type_ != answer.type) { - LOG(ERROR) << "Error in the plugin protocol: Cannot change the answer type"; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "Error in the plugin protocol: Cannot change the answer type"); } switch (answer.type) @@ -1098,8 +1099,9 @@ } default: - LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "Unhandled type of answer for custom index plugin: " + + boost::lexical_cast(answer.type)); } } } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -48,7 +48,7 @@ #include "../../Core/Toolbox.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/DicomParsing/ToDcmtkBridge.h" -#include "../../OrthancServer/OrthancInitialization.h" +#include "../../OrthancServer/OrthancConfiguration.h" #include "../../OrthancServer/ServerContext.h" #include "../../OrthancServer/ServerToolbox.h" #include "../../OrthancServer/Search/HierarchicalMatcher.h" @@ -271,8 +271,10 @@ public: OrthancPeers() { + OrthancConfiguration::ReaderLock lock; + std::set peers; - Configuration::GetListOfOrthancPeers(peers); + lock.GetConfiguration().GetListOfOrthancPeers(peers); names_.reserve(peers.size()); parameters_.reserve(peers.size()); @@ -281,7 +283,7 @@ it = peers.begin(); it != peers.end(); ++it) { WebServiceParameters peer; - if (Configuration::GetOrthancPeer(peer, *it)) + if (lock.GetConfiguration().LookupOrthancPeer(peer, *it)) { names_.push_back(*it); parameters_.push_back(peer); @@ -317,6 +319,56 @@ public: + class PluginHttpOutput : public boost::noncopyable + { + private: + HttpOutput& output_; + std::auto_ptr errorDetails_; + bool logDetails_; + + public: + PluginHttpOutput(HttpOutput& output) : + output_(output), + logDetails_(false) + { + } + + HttpOutput& GetOutput() + { + return output_; + } + + void SetErrorDetails(const std::string& details, + bool logDetails) + { + errorDetails_.reset(new std::string(details)); + logDetails_ = logDetails; + } + + bool HasErrorDetails() const + { + return errorDetails_.get() != NULL; + } + + bool IsLogDetails() const + { + return logDetails_; + } + + const std::string& GetErrorDetails() const + { + if (errorDetails_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *errorDetails_; + } + } + }; + + class RestCallback : public boost::noncopyable { private: @@ -324,7 +376,7 @@ OrthancPluginRestCallback callback_; bool lock_; - OrthancPluginErrorCode InvokeInternal(HttpOutput& output, + OrthancPluginErrorCode InvokeInternal(PluginHttpOutput& output, const std::string& flatUri, const OrthancPluginHttpRequest& request) { @@ -349,7 +401,7 @@ } OrthancPluginErrorCode Invoke(boost::recursive_mutex& restCallbackMutex, - HttpOutput& output, + PluginHttpOutput& output, const std::string& flatUri, const OrthancPluginHttpRequest& request) { @@ -731,7 +783,8 @@ OrthancPluginErrorCode error = apply_(driver_); if (error != OrthancPluginErrorCode_Success) { - LOG(ERROR) << "Error while doing C-Move from plugin: " << EnumerationToString(static_cast(error)); + LOG(ERROR) << "Error while doing C-Move from plugin: " + << EnumerationToString(static_cast(error)); return Status_Failure; } else @@ -814,8 +867,8 @@ if (driver == NULL) { - LOG(ERROR) << "Plugin cannot create a driver for an incoming C-MOVE request"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "Plugin cannot create a driver for an incoming C-MOVE request"); } unsigned int size = params_.getMoveSize(driver); @@ -1022,7 +1075,11 @@ } assert(callback != NULL); - OrthancPluginErrorCode error = callback->Invoke(pimpl_->restCallbackMutex_, output, flatUri, request); + + PImpl::PluginHttpOutput pluginOutput(output); + + OrthancPluginErrorCode error = callback->Invoke + (pimpl_->restCallbackMutex_, pluginOutput, flatUri, request); if (error == OrthancPluginErrorCode_Success && output.IsWritingMultipart()) @@ -1037,7 +1094,17 @@ else { GetErrorDictionary().LogError(error, false); - throw OrthancException(static_cast(error)); + + if (pluginOutput.HasErrorDetails()) + { + throw OrthancException(static_cast(error), + pluginOutput.GetErrorDetails(), + pluginOutput.IsLogDetails()); + } + else + { + throw OrthancException(static_cast(error)); + } } } @@ -1142,8 +1209,8 @@ if (pimpl_->worklistCallback_ != NULL) { - LOG(ERROR) << "Can only register one plugin to handle modality worklists"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "Can only register one plugin to handle modality worklists"); } else { @@ -1162,8 +1229,8 @@ if (pimpl_->findCallback_ != NULL) { - LOG(ERROR) << "Can only register one plugin to handle C-FIND requests"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "Can only register one plugin to handle C-FIND requests"); } else { @@ -1182,8 +1249,8 @@ if (pimpl_->moveCallbacks_.callback != NULL) { - LOG(ERROR) << "Can only register one plugin to handle C-MOVE requests"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "Can only register one plugin to handle C-MOVE requests"); } else { @@ -1244,9 +1311,9 @@ const _OrthancPluginAnswerBuffer& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->SetContentType(p.mimeType); - translatedOutput->Answer(p.answer, p.answerSize); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.SetContentType(p.mimeType); + translatedOutput.Answer(p.answer, p.answerSize); } @@ -1255,8 +1322,8 @@ const _OrthancPluginOutputPlusArgument& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->Redirect(p.argument); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.Redirect(p.argument); } @@ -1265,8 +1332,8 @@ const _OrthancPluginSendHttpStatusCode& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->SendStatus(static_cast(p.status)); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.SendStatus(static_cast(p.status)); } @@ -1275,16 +1342,16 @@ const _OrthancPluginSendHttpStatus& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); HttpStatus status = static_cast(p.status); if (p.bodySize > 0 && p.body != NULL) { - translatedOutput->SendStatus(status, p.body, p.bodySize); + translatedOutput.SendStatus(status, p.body, p.bodySize); } else { - translatedOutput->SendStatus(status); + translatedOutput.SendStatus(status); } } @@ -1294,8 +1361,8 @@ const _OrthancPluginOutputPlusArgument& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->SendUnauthorized(p.argument); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.SendUnauthorized(p.argument); } @@ -1304,8 +1371,8 @@ const _OrthancPluginOutputPlusArgument& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->SendMethodNotAllowed(p.argument); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.SendMethodNotAllowed(p.argument); } @@ -1314,8 +1381,8 @@ const _OrthancPluginSetHttpHeader& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->SetCookie(p.key, p.value); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.SetCookie(p.key, p.value); } @@ -1324,8 +1391,19 @@ const _OrthancPluginSetHttpHeader& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); - translatedOutput->AddHeader(p.key, p.value); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); + translatedOutput.AddHeader(p.key, p.value); + } + + + void OrthancPlugins::SetHttpErrorDetails(const void* parameters) + { + const _OrthancPluginSetHttpErrorDetails& p = + *reinterpret_cast(parameters); + + PImpl::PluginHttpOutput* output = + reinterpret_cast(p.output); + output->SetErrorDetails(p.details, p.log); } @@ -1354,7 +1432,7 @@ const _OrthancPluginCompressAndAnswerImage& p = *reinterpret_cast(parameters); - HttpOutput* translatedOutput = reinterpret_cast(p.output); + HttpOutput& translatedOutput = reinterpret_cast(p.output)->GetOutput(); ImageAccessor accessor; accessor.AssignReadOnly(Plugins::Convert(p.pixelFormat), p.width, p.height, p.pitch, p.buffer); @@ -1367,7 +1445,7 @@ { PngWriter writer; writer.WriteToMemory(compressed, accessor); - translatedOutput->SetContentType("image/png"); + translatedOutput.SetContentType(MimeType_Png); break; } @@ -1376,7 +1454,7 @@ JpegWriter writer; writer.SetQuality(p.quality); writer.WriteToMemory(compressed, accessor); - translatedOutput->SetContentType("image/jpeg"); + translatedOutput.SetContentType(MimeType_Jpeg); break; } @@ -1384,7 +1462,7 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - translatedOutput->Answer(compressed); + translatedOutput.Answer(compressed); } @@ -2105,19 +2183,23 @@ { const _OrthancPluginGetFontInfo& p = *reinterpret_cast(parameters); - const Font& font = Configuration::GetFontRegistry().GetFont(p.fontIndex); - - if (p.name != NULL) { - *(p.name) = font.GetName().c_str(); - } - else if (p.size != NULL) - { - *(p.size) = font.GetSize(); - } - else - { - throw OrthancException(ErrorCode_InternalError); + OrthancConfiguration::ReaderLock lock; + + const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex); + + if (p.name != NULL) + { + *(p.name) = font.GetName().c_str(); + } + else if (p.size != NULL) + { + *(p.size) = font.GetSize(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } } } @@ -2127,9 +2209,12 @@ const _OrthancPluginDrawText& p = *reinterpret_cast(parameters); ImageAccessor& target = *reinterpret_cast(p.image); - const Font& font = Configuration::GetFontRegistry().GetFont(p.fontIndex); - - font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b); + + { + OrthancConfiguration::ReaderLock lock; + const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex); + font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b); + } } @@ -2275,10 +2360,10 @@ const _OrthancPluginAnswerBuffer& p = *reinterpret_cast(parameters); - HttpOutput* output = reinterpret_cast(p.output); + HttpOutput& output = reinterpret_cast(p.output)->GetOutput(); std::map headers; // No custom headers - output->SendMultipartItem(p.answer, p.answerSize, headers); + output.SendMultipartItem(p.answer, p.answerSize, headers); } @@ -2288,7 +2373,7 @@ // connection was closed by the HTTP client. const _OrthancPluginSendMultipartItem2& p = *reinterpret_cast(parameters); - HttpOutput* output = reinterpret_cast(p.output); + HttpOutput& output = reinterpret_cast(p.output)->GetOutput(); std::map headers; for (uint32_t i = 0; i < p.headersCount; i++) @@ -2296,7 +2381,7 @@ headers[p.headersKeys[i]] = p.headersValues[i]; } - output->SendMultipartItem(p.answer, p.answerSize, headers); + output.SendMultipartItem(p.answer, p.answerSize, headers); } @@ -2311,8 +2396,8 @@ } else { - LOG(ERROR) << "Cannot invoke this service without a custom database back-end"; - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Cannot invoke this service without a custom database back-end"); } } @@ -2392,15 +2477,25 @@ case _OrthancPluginService_GetConfigurationPath: { - *reinterpret_cast(parameters)->result = - CopyString(Configuration::GetConfigurationAbsolutePath()); + std::string s; + + { + OrthancConfiguration::ReaderLock lock; + s = lock.GetConfiguration().GetConfigurationAbsolutePath(); + } + + *reinterpret_cast(parameters)->result = CopyString(s); return true; } case _OrthancPluginService_GetConfiguration: { std::string s; - Configuration::FormatConfiguration(s); + + { + OrthancConfiguration::ReaderLock lock; + lock.GetConfiguration().Format(s); + } *reinterpret_cast(parameters)->result = CopyString(s); return true; @@ -2490,6 +2585,10 @@ SetHttpHeader(parameters); return true; + case _OrthancPluginService_SetHttpErrorDetails: + SetHttpErrorDetails(parameters); + return true; + case _OrthancPluginService_LookupPatient: case _OrthancPluginService_LookupStudy: case _OrthancPluginService_LookupStudyWithAccessionNumber: @@ -2553,8 +2652,8 @@ { const _OrthancPluginStartMultipartAnswer& p = *reinterpret_cast(parameters); - HttpOutput* output = reinterpret_cast(p.output); - output->StartMultipart(p.subType, p.contentType); + HttpOutput& output = reinterpret_cast(p.output)->GetOutput(); + output.StartMultipart(p.subType, p.contentType); return true; } @@ -2665,7 +2764,12 @@ { const _OrthancPluginReturnSingleValue& p = *reinterpret_cast(parameters); - *(p.resultUint32) = Configuration::GetFontRegistry().GetSize(); + + { + OrthancConfiguration::ReaderLock lock; + *(p.resultUint32) = lock.GetConfiguration().GetFontRegistry().GetSize(); + } + return true; } @@ -3171,8 +3275,8 @@ if (pimpl_->database_.get() == NULL) { - LOG(ERROR) << "The service ReconstructMainDicomTags can only be invoked by custom database plugins"; - throw OrthancException(ErrorCode_DatabasePlugin); + throw OrthancException(ErrorCode_DatabasePlugin, + "The service ReconstructMainDicomTags can only be invoked by custom database plugins"); } IStorageArea& storage = *reinterpret_cast(p.storageArea); diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Thu Dec 06 15:58:08 2018 +0100 @@ -148,6 +148,8 @@ void SetHttpHeader(const void* parameters); + void SetHttpErrorDetails(const void* parameters); + void BufferCompression(const void* parameters); void UncompressImage(const void* parameters); diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Engine/PluginsJob.cpp --- a/Plugins/Engine/PluginsJob.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Engine/PluginsJob.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -148,8 +148,8 @@ if (!reader.parse(content, value) || value.type() != Json::objectValue) { - LOG(ERROR) << "A job plugin must provide a JSON object as its public content"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "A job plugin must provide a JSON object as its public content"); } } } @@ -169,8 +169,8 @@ if (!reader.parse(serialized, value) || value.type() != Json::objectValue) { - LOG(ERROR) << "A job plugin must provide a JSON object as its serialized content"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "A job plugin must provide a JSON object as its serialized content"); } @@ -178,8 +178,8 @@ if (value.isMember(KEY_TYPE)) { - LOG(ERROR) << "The \"Type\" field is for reserved use for serialized job"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_Plugin, + "The \"Type\" field is for reserved use for serialized job"); } value[KEY_TYPE] = type_; diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Engine/PluginsJob.h --- a/Plugins/Engine/PluginsJob.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Engine/PluginsJob.h Thu Dec 06 15:58:08 2018 +0100 @@ -71,6 +71,14 @@ virtual void GetPublicContent(Json::Value& value); virtual bool Serialize(Json::Value& value); + + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key) + { + // TODO + return false; + } }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Thu Dec 06 15:58:08 2018 +0100 @@ -119,7 +119,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 4 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 2 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 3 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -451,6 +451,7 @@ _OrthancPluginService_SendHttpStatus = 2010, _OrthancPluginService_CompressAndAnswerImage = 2011, _OrthancPluginService_SendMultipartItem2 = 2012, + _OrthancPluginService_SetHttpErrorDetails = 2013, /* Access to the Orthanc database and API */ _OrthancPluginService_GetDicomForInstance = 3000, @@ -1679,11 +1680,16 @@ * OrthancPluginInitialize() public function. * * Each REST callback is guaranteed to run in mutual exclusion. - * + * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param pathRegularExpression Regular expression for the URI. May contain groups. * @param callback The callback function to handle the REST call. * @see OrthancPluginRegisterRestCallbackNoLock() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback( @@ -1711,13 +1717,19 @@ * will NOT be invoked in mutual exclusion. This can be useful for * high-performance plugins that must handle concurrent requests * (Orthanc uses a pool of threads, one thread being assigned to - * each incoming HTTP request). Of course, it is up to the plugin to - * implement the required locking mechanisms. + * each incoming HTTP request). Of course, if using this function, + * it is up to the plugin to implement the required locking + * mechanisms. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param pathRegularExpression Regular expression for the URI. May contain groups. * @param callback The callback function to handle the REST call. * @see OrthancPluginRegisterRestCallback() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock( @@ -6416,6 +6428,46 @@ } + + typedef struct + { + OrthancPluginRestOutput* output; + const char* details; + uint8_t log; + } _OrthancPluginSetHttpErrorDetails; + + /** + * @brief Provide a detailed description for an HTTP error. + * + * This function sets the detailed description associated with an + * HTTP error. This description will be displayed in the "Details" + * field of the JSON body of the HTTP answer. It is only taken into + * consideration if the REST callback returns an error code that is + * different from "OrthancPluginErrorCode_Success", and if the + * "HttpDescribeErrors" configuration option of Orthanc is set to + * "true". + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param details The details of the error message. + * @param log Whether to also write the detailed error to the Orthanc logs. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* details, + uint8_t log) + { + _OrthancPluginSetHttpErrorDetails params; + params.output = output; + params.details = details; + params.log = log; + context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, ¶ms); + } + + + #ifdef __cplusplus } #endif diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Basic/Plugin.c --- a/Plugins/Samples/Basic/Plugin.c Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Basic/Plugin.c Thu Dec 06 15:58:08 2018 +0100 @@ -36,6 +36,14 @@ char buffer[1024]; uint32_t i; + if (request->method != OrthancPluginHttpMethod_Get) + { + // NB: Calling "OrthancPluginSendMethodNotAllowed(context, output, "GET");" + // is preferable. This is a sample to demonstrate "OrthancPluginSetHttpErrorDetails()". + OrthancPluginSetHttpErrorDetails(context, output, "This Callback1() can only be used by a GET call"); + return OrthancPluginErrorCode_ParameterOutOfRange; + } + sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, request->body); OrthancPluginLogWarning(context, buffer); @@ -184,7 +192,7 @@ if (request->method != OrthancPluginHttpMethod_Get) { OrthancPluginSendMethodNotAllowed(context, output, "GET"); - return 0; + return OrthancPluginErrorCode_Success; } isBuiltIn = strcmp("plugins", request->groups[0]); @@ -301,22 +309,51 @@ OrthancPluginResourceType resourceType, const char* resourceId) { + OrthancPluginMemoryBuffer tmp; + memset(&tmp, 0, sizeof(tmp)); + char info[1024]; - OrthancPluginMemoryBuffer tmp; - - sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType); + sprintf(info, "Change %d on resource %s of type %d", changeType, + (resourceId == NULL ? "" : resourceId), resourceType); OrthancPluginLogWarning(context, info); - if (changeType == OrthancPluginChangeType_NewInstance) + switch (changeType) { - sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId); - if (OrthancPluginRestApiGet(context, &tmp, info) == 0) + case OrthancPluginChangeType_NewInstance: + { + sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId); + if (OrthancPluginRestApiGet(context, &tmp, info) == 0) + { + sprintf(info, " Instance %s comes from the anonymization of instance", resourceId); + strncat(info, (const char*) tmp.data, tmp.size); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeMemoryBuffer(context, &tmp); + } + + break; + } + + case OrthancPluginChangeType_OrthancStarted: { - sprintf(info, " Instance %s comes from the anonymization of instance", resourceId); - strncat(info, (const char*) tmp.data, tmp.size); - OrthancPluginLogWarning(context, info); + /* Make REST requests to the built-in Orthanc API */ + OrthancPluginRestApiGet(context, &tmp, "/changes"); + OrthancPluginFreeMemoryBuffer(context, &tmp); + OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1"); OrthancPluginFreeMemoryBuffer(context, &tmp); + + /* Play with PUT by defining a new target modality. */ + sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]"); + OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info)); + + break; } + + case OrthancPluginChangeType_OrthancStopped: + OrthancPluginLogWarning(context, "Orthanc has stopped"); + break; + + default: + break; } return OrthancPluginErrorCode_Success; @@ -357,7 +394,6 @@ ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { - OrthancPluginMemoryBuffer tmp; char info[1024], *s; int counter, i; OrthancPluginDictionaryEntry entry; @@ -425,19 +461,8 @@ OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer."); OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');"); - /* Make REST requests to the built-in Orthanc API */ - memset(&tmp, 0, sizeof(tmp)); - OrthancPluginRestApiGet(context, &tmp, "/changes"); - OrthancPluginFreeMemoryBuffer(context, &tmp); - OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1"); - OrthancPluginFreeMemoryBuffer(context, &tmp); + customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world"); - /* Play with PUT by defining a new target modality. */ - sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]"); - OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info)); - - customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world"); - OrthancPluginRegisterDictionaryTag(context, 0x0014, 0x1020, OrthancPluginValueRepresentation_DA, "ValidationExpiryDate", 1, 1); diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Common/FullOrthancDataset.h --- a/Plugins/Samples/Common/FullOrthancDataset.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Common/FullOrthancDataset.h Thu Dec 06 15:58:08 2018 +0100 @@ -65,5 +65,10 @@ virtual bool GetSequenceSize(size_t& size, const DicomPath& path) const; + + FullOrthancDataset* Clone() const + { + return new FullOrthancDataset(this->root_); + } }; } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Common/OrthancPluginConnection.cpp --- a/Plugins/Samples/Common/OrthancPluginConnection.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Common/OrthancPluginConnection.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -40,7 +40,7 @@ void OrthancPluginConnection::RestApiGet(std::string& result, const std::string& uri) { - OrthancPlugins::MemoryBuffer buffer(context_); + OrthancPlugins::MemoryBuffer buffer; if (buffer.RestApiGet(uri, false)) { @@ -57,7 +57,7 @@ const std::string& uri, const std::string& body) { - OrthancPlugins::MemoryBuffer buffer(context_); + OrthancPlugins::MemoryBuffer buffer; if (buffer.RestApiPost(uri, body.c_str(), body.size(), false)) { @@ -74,7 +74,7 @@ const std::string& uri, const std::string& body) { - OrthancPlugins::MemoryBuffer buffer(context_); + OrthancPlugins::MemoryBuffer buffer; if (buffer.RestApiPut(uri, body.c_str(), body.size(), false)) { @@ -89,9 +89,9 @@ void OrthancPluginConnection::RestApiDelete(const std::string& uri) { - OrthancPlugins::MemoryBuffer buffer(context_); + OrthancPlugins::MemoryBuffer buffer; - if (!::OrthancPlugins::RestApiDelete(context_, uri, false)) + if (!::OrthancPlugins::RestApiDelete(uri, false)) { ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource); } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Common/OrthancPluginConnection.h --- a/Plugins/Samples/Common/OrthancPluginConnection.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Common/OrthancPluginConnection.h Thu Dec 06 15:58:08 2018 +0100 @@ -42,15 +42,7 @@ // This class is thread-safe class OrthancPluginConnection : public IOrthancConnection { - private: - OrthancPluginContext* context_; - public: - OrthancPluginConnection(OrthancPluginContext* context) : - context_(context) - { - } - virtual void RestApiGet(std::string& result, const std::string& uri); diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -39,6 +39,45 @@ namespace OrthancPlugins { + static OrthancPluginContext* globalContext_ = NULL; + + + void SetGlobalContext(OrthancPluginContext* context) + { + if (context == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + else if (globalContext_ == NULL) + { + globalContext_ = context; + } + else + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + } + + + bool HasGlobalContext() + { + return globalContext_ != NULL; + } + + + OrthancPluginContext* GetGlobalContext() + { + if (globalContext_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return globalContext_; + } + } + + void MemoryBuffer::Check(OrthancPluginErrorCode code) { if (code != OrthancPluginErrorCode_Success) @@ -76,8 +115,7 @@ } - MemoryBuffer::MemoryBuffer(OrthancPluginContext* context) : - context_(context) + MemoryBuffer::MemoryBuffer() { buffer_.data = NULL; buffer_.size = 0; @@ -88,7 +126,7 @@ { if (buffer_.data != NULL) { - OrthancPluginFreeMemoryBuffer(context_, &buffer_); + OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &buffer_); buffer_.data = NULL; buffer_.size = 0; } @@ -144,7 +182,7 @@ Json::Reader reader; if (!reader.parse(tmp, tmp + buffer_.size, target)) { - OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON"); + LogError("Cannot convert some memory buffer to JSON"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } } @@ -157,11 +195,11 @@ if (applyPlugins) { - return CheckHttp(OrthancPluginRestApiGetAfterPlugins(context_, &buffer_, uri.c_str())); + return CheckHttp(OrthancPluginRestApiGetAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str())); } else { - return CheckHttp(OrthancPluginRestApiGet(context_, &buffer_, uri.c_str())); + return CheckHttp(OrthancPluginRestApiGet(GetGlobalContext(), &buffer_, uri.c_str())); } } @@ -175,11 +213,11 @@ if (applyPlugins) { - return CheckHttp(OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize)); + return CheckHttp(OrthancPluginRestApiPostAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize)); } else { - return CheckHttp(OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body, bodySize)); + return CheckHttp(OrthancPluginRestApiPost(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize)); } } @@ -193,11 +231,11 @@ if (applyPlugins) { - return CheckHttp(OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize)); + return CheckHttp(OrthancPluginRestApiPutAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize)); } else { - return CheckHttp(OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body, bodySize)); + return CheckHttp(OrthancPluginRestApiPut(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize)); } } @@ -228,7 +266,7 @@ Json::FastWriter writer; std::string s = writer.write(tags); - Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), NULL, flags)); + Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), NULL, flags)); } void MemoryBuffer::CreateDicom(const Json::Value& tags, @@ -240,21 +278,21 @@ Json::FastWriter writer; std::string s = writer.write(tags); - Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), pixelData.GetObject(), flags)); + Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), pixelData.GetObject(), flags)); } void MemoryBuffer::ReadFile(const std::string& path) { Clear(); - Check(OrthancPluginReadFile(context_, &buffer_, path.c_str())); + Check(OrthancPluginReadFile(GetGlobalContext(), &buffer_, path.c_str())); } void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query) { Clear(); - Check(OrthancPluginWorklistGetDicomQuery(context_, &buffer_, query)); + Check(OrthancPluginWorklistGetDicomQuery(GetGlobalContext(), &buffer_, query)); } @@ -276,7 +314,7 @@ { if (str_ != NULL) { - OrthancPluginFreeString(context_, str_); + OrthancPluginFreeString(GetGlobalContext(), str_); str_ = NULL; } } @@ -299,14 +337,14 @@ { if (str_ == NULL) { - OrthancPluginLogError(context_, "Cannot convert an empty memory buffer to JSON"); + LogError("Cannot convert an empty memory buffer to JSON"); ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } Json::Reader reader; if (!reader.parse(str_, target)) { - OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON"); + LogError("Cannot convert some memory buffer to JSON"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } } @@ -317,8 +355,9 @@ OrthancPluginDicomToJsonFlags flags, uint32_t maxStringLength) { - OrthancString str(context_); - str.Assign(OrthancPluginDicomBufferToJson(context_, GetData(), GetSize(), format, flags, maxStringLength)); + OrthancString str; + str.Assign(OrthancPluginDicomBufferToJson + (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength)); str.ToJson(target); } @@ -328,7 +367,7 @@ const std::string& password) { Clear(); - return CheckHttp(OrthancPluginHttpGet(context_, &buffer_, url.c_str(), + return CheckHttp(OrthancPluginHttpGet(GetGlobalContext(), &buffer_, url.c_str(), username.empty() ? NULL : username.c_str(), password.empty() ? NULL : password.c_str())); } @@ -340,7 +379,7 @@ const std::string& password) { Clear(); - return CheckHttp(OrthancPluginHttpPost(context_, &buffer_, url.c_str(), + return CheckHttp(OrthancPluginHttpPost(GetGlobalContext(), &buffer_, url.c_str(), body.c_str(), body.size(), username.empty() ? NULL : username.c_str(), password.empty() ? NULL : password.c_str())); @@ -353,7 +392,7 @@ const std::string& password) { Clear(); - return CheckHttp(OrthancPluginHttpPut(context_, &buffer_, url.c_str(), + return CheckHttp(OrthancPluginHttpPut(GetGlobalContext(), &buffer_, url.c_str(), body.empty() ? NULL : body.c_str(), body.size(), username.empty() ? NULL : username.c_str(), @@ -364,17 +403,16 @@ void MemoryBuffer::GetDicomInstance(const std::string& instanceId) { Clear(); - Check(OrthancPluginGetDicomForInstance(context_, &buffer_, instanceId.c_str())); + Check(OrthancPluginGetDicomForInstance(GetGlobalContext(), &buffer_, instanceId.c_str())); } - bool HttpDelete(OrthancPluginContext* context_, - const std::string& url, + bool HttpDelete(const std::string& url, const std::string& username, const std::string& password) { OrthancPluginErrorCode error = OrthancPluginHttpDelete - (context_, url.c_str(), + (GetGlobalContext(), url.c_str(), username.empty() ? NULL : username.c_str(), password.empty() ? NULL : password.c_str()); @@ -394,15 +432,41 @@ } - OrthancConfiguration::OrthancConfiguration(OrthancPluginContext* context) : - context_(context) + void LogError(const std::string& message) + { + if (HasGlobalContext()) + { + OrthancPluginLogError(GetGlobalContext(), message.c_str()); + } + } + + + void LogWarning(const std::string& message) { - OrthancString str(context); - str.Assign(OrthancPluginGetConfiguration(context)); + if (HasGlobalContext()) + { + OrthancPluginLogWarning(GetGlobalContext(), message.c_str()); + } + } + + + void LogInfo(const std::string& message) + { + if (HasGlobalContext()) + { + OrthancPluginLogInfo(GetGlobalContext(), message.c_str()); + } + } + + + OrthancConfiguration::OrthancConfiguration() + { + OrthancString str; + str.Assign(OrthancPluginGetConfiguration(GetGlobalContext())); if (str.GetContent() == NULL) { - OrthancPluginLogError(context, "Cannot access the Orthanc configuration"); + LogError("Cannot access the Orthanc configuration"); ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } @@ -410,25 +474,12 @@ if (configuration_.type() != Json::objectValue) { - OrthancPluginLogError(context, "Unable to read the Orthanc configuration"); + LogError("Unable to read the Orthanc configuration"); ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } } - OrthancPluginContext* OrthancConfiguration::GetContext() const - { - if (context_ == NULL) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); - } - else - { - return context_; - } - } - - std::string OrthancConfiguration::GetPath(const std::string& key) const { if (path_.empty()) @@ -456,7 +507,6 @@ { assert(configuration_.type() == Json::objectValue); - target.context_ = context_; target.path_ = GetPath(key); if (!configuration_.isMember(key)) @@ -467,11 +517,8 @@ { if (configuration_[key].type() != Json::objectValue) { - if (context_ != NULL) - { - std::string s = "The configuration section \"" + target.path_ + "\" is not an associative array as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration section \"" + target.path_ + + "\" is not an associative array as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -493,11 +540,8 @@ if (configuration_[key].type() != Json::stringValue) { - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a string as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -528,11 +572,8 @@ return true; default: - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not an integer as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -550,11 +591,8 @@ if (tmp < 0) { - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not a positive integer as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a positive integer as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -578,11 +616,8 @@ if (configuration_[key].type() != Json::booleanValue) { - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not a Boolean as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a Boolean as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -617,11 +652,8 @@ return true; default: - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not an integer as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -680,12 +712,8 @@ break; } - if (context_ != NULL) - { - std::string s = ("The configuration option \"" + GetPath(key) + - "\" is not a list of strings as expected"); - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a list of strings as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -805,11 +833,8 @@ if (configuration_[key].type() != Json::objectValue) { - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a string as expected"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -826,11 +851,8 @@ } else { - if (context_ != NULL) - { - std::string s = "The configuration option \"" + GetPath(key) + "\" is not a dictionary mapping strings to strings"; - OrthancPluginLogError(context_, s.c_str()); - } + LogError("The configuration option \"" + GetPath(key) + + "\" is not a dictionary mapping strings to strings"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -842,7 +864,7 @@ { if (image_ != NULL) { - OrthancPluginFreeImage(context_, image_); + OrthancPluginFreeImage(GetGlobalContext(), image_); image_ = NULL; } } @@ -852,66 +874,51 @@ { if (image_ == NULL) { - OrthancPluginLogError(context_, "Trying to access a NULL image"); - ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); - } - } - - - OrthancImage::OrthancImage(OrthancPluginContext* context) : - context_(context), - image_(NULL) - { - if (context == NULL) - { + LogError("Trying to access a NULL image"); ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); } } - OrthancImage::OrthancImage(OrthancPluginContext* context, - OrthancPluginImage* image) : - context_(context), + OrthancImage::OrthancImage() : + image_(NULL) + { + } + + + OrthancImage::OrthancImage(OrthancPluginImage* image) : image_(image) { - if (context == NULL) + } + + + OrthancImage::OrthancImage(OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height) + { + image_ = OrthancPluginCreateImage(GetGlobalContext(), format, width, height); + + if (image_ == NULL) { - ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + LogError("Cannot create an image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } } - OrthancImage::OrthancImage(OrthancPluginContext* context, - OrthancPluginPixelFormat format, - uint32_t width, - uint32_t height) : - context_(context) - { - if (context == NULL) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); - } - else - { - image_ = OrthancPluginCreateImage(context, format, width, height); - } - } - - OrthancImage::OrthancImage(OrthancPluginContext* context, - OrthancPluginPixelFormat format, + OrthancImage::OrthancImage(OrthancPluginPixelFormat format, uint32_t width, uint32_t height, uint32_t pitch, - void* buffer) : - context_(context) + void* buffer) { - if (context == NULL) + image_ = OrthancPluginCreateImageAccessor + (GetGlobalContext(), format, width, height, pitch, buffer); + + if (image_ == NULL) { - ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); - } - else - { - image_ = OrthancPluginCreateImageAccessor(context, format, width, height, pitch, buffer); + LogError("Cannot create an image accessor"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } } @@ -919,10 +926,12 @@ size_t size) { Clear(); - image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Png); + + image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Png); + if (image_ == NULL) { - OrthancPluginLogError(context_, "Cannot uncompress a PNG image"); + LogError("Cannot uncompress a PNG image"); ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); } } @@ -932,10 +941,10 @@ size_t size) { Clear(); - image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Jpeg); + image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg); if (image_ == NULL) { - OrthancPluginLogError(context_, "Cannot uncompress a JPEG image"); + LogError("Cannot uncompress a JPEG image"); ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); } } @@ -946,10 +955,10 @@ unsigned int frame) { Clear(); - image_ = OrthancPluginDecodeDicomImage(context_, data, size, frame); + image_ = OrthancPluginDecodeDicomImage(GetGlobalContext(), data, size, frame); if (image_ == NULL) { - OrthancPluginLogError(context_, "Cannot uncompress a DICOM image"); + LogError("Cannot uncompress a DICOM image"); ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); } } @@ -958,35 +967,35 @@ OrthancPluginPixelFormat OrthancImage::GetPixelFormat() { CheckImageAvailable(); - return OrthancPluginGetImagePixelFormat(context_, image_); + return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_); } unsigned int OrthancImage::GetWidth() { CheckImageAvailable(); - return OrthancPluginGetImageWidth(context_, image_); + return OrthancPluginGetImageWidth(GetGlobalContext(), image_); } unsigned int OrthancImage::GetHeight() { CheckImageAvailable(); - return OrthancPluginGetImageHeight(context_, image_); + return OrthancPluginGetImageHeight(GetGlobalContext(), image_); } unsigned int OrthancImage::GetPitch() { CheckImageAvailable(); - return OrthancPluginGetImagePitch(context_, image_); + return OrthancPluginGetImagePitch(GetGlobalContext(), image_); } const void* OrthancImage::GetBuffer() { CheckImageAvailable(); - return OrthancPluginGetImageBuffer(context_, image_); + return OrthancPluginGetImageBuffer(GetGlobalContext(), image_); } @@ -995,7 +1004,7 @@ CheckImageAvailable(); OrthancPluginMemoryBuffer tmp; - OrthancPluginCompressPngImage(context_, &tmp, GetPixelFormat(), + OrthancPluginCompressPngImage(GetGlobalContext(), &tmp, GetPixelFormat(), GetWidth(), GetHeight(), GetPitch(), GetBuffer()); target.Assign(tmp); @@ -1008,7 +1017,7 @@ CheckImageAvailable(); OrthancPluginMemoryBuffer tmp; - OrthancPluginCompressJpegImage(context_, &tmp, GetPixelFormat(), + OrthancPluginCompressJpegImage(GetGlobalContext(), &tmp, GetPixelFormat(), GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality); target.Assign(tmp); @@ -1018,7 +1027,7 @@ void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output) { CheckImageAvailable(); - OrthancPluginCompressAndAnswerPngImage(context_, output, GetPixelFormat(), + OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(), GetWidth(), GetHeight(), GetPitch(), GetBuffer()); } @@ -1027,16 +1036,14 @@ uint8_t quality) { CheckImageAvailable(); - OrthancPluginCompressAndAnswerJpegImage(context_, output, GetPixelFormat(), + OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(), GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality); } #if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 - FindMatcher::FindMatcher(OrthancPluginContext* context, - const OrthancPluginWorklistQuery* worklist) : - context_(context), + FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) : matcher_(NULL), worklist_(worklist) { @@ -1047,14 +1054,12 @@ } - void FindMatcher::SetupDicom(OrthancPluginContext* context, - const void* query, - uint32_t size) + void FindMatcher::SetupDicom(const void* query, + uint32_t size) { - context_ = context; worklist_ = NULL; - matcher_ = OrthancPluginCreateFindMatcher(context_, query, size); + matcher_ = OrthancPluginCreateFindMatcher(GetGlobalContext(), query, size); if (matcher_ == NULL) { ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); @@ -1068,7 +1073,7 @@ if (matcher_ != NULL) { - OrthancPluginFreeFindMatcher(context_, matcher_); + OrthancPluginFreeFindMatcher(GetGlobalContext(), matcher_); } } @@ -1081,11 +1086,11 @@ if (matcher_ != NULL) { - result = OrthancPluginFindMatcherIsMatch(context_, matcher_, dicom, size); + result = OrthancPluginFindMatcherIsMatch(GetGlobalContext(), matcher_, dicom, size); } else if (worklist_ != NULL) { - result = OrthancPluginWorklistIsMatch(context_, worklist_, dicom, size); + result = OrthancPluginWorklistIsMatch(GetGlobalContext(), worklist_, dicom, size); } else { @@ -1108,100 +1113,142 @@ #endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */ + void AnswerJson(const Json::Value& value, + OrthancPluginRestOutput* output + ) + { + Json::StyledWriter writer; + std::string bodyString = writer.write(value); - bool RestApiGet(Json::Value& result, - OrthancPluginContext* context, - const std::string& uri, - bool applyPlugins) + OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json"); + } + + void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput *output) { - MemoryBuffer answer(context); + OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError); + } + + void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods); + } + + bool RestApiGetString(std::string& result, + const std::string& uri, + bool applyPlugins) + { + MemoryBuffer answer; if (!answer.RestApiGet(uri, applyPlugins)) { return false; } else { - answer.ToJson(result); + answer.ToString(result); return true; } } - bool RestApiPost(Json::Value& result, - OrthancPluginContext* context, - const std::string& uri, - const char* body, - size_t bodySize, - bool applyPlugins) + bool RestApiGet(Json::Value& result, + const std::string& uri, + bool applyPlugins) { - MemoryBuffer answer(context); - if (!answer.RestApiPost(uri, body, bodySize, applyPlugins)) + MemoryBuffer answer; + + if (!answer.RestApiGet(uri, applyPlugins)) { return false; } else { - answer.ToJson(result); + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } return true; } } bool RestApiPost(Json::Value& result, - OrthancPluginContext* context, + const std::string& uri, + const char* body, + size_t bodySize, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiPost(uri, body, bodySize, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } + return true; + } + } + + + bool RestApiPost(Json::Value& result, const std::string& uri, const Json::Value& body, bool applyPlugins) { Json::FastWriter writer; - return RestApiPost(result, context, uri, writer.write(body), applyPlugins); + return RestApiPost(result, uri, writer.write(body), applyPlugins); } bool RestApiPut(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const char* body, size_t bodySize, bool applyPlugins) { - MemoryBuffer answer(context); + MemoryBuffer answer; + if (!answer.RestApiPut(uri, body, bodySize, applyPlugins)) { return false; } else { - answer.ToJson(result); + if (!answer.IsEmpty()) // i.e, on a PUT to metadata/..., orthand returns an empty response + { + answer.ToJson(result); + } return true; } } bool RestApiPut(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const Json::Value& body, bool applyPlugins) { Json::FastWriter writer; - return RestApiPut(result, context, uri, writer.write(body), applyPlugins); + return RestApiPut(result, uri, writer.write(body), applyPlugins); } - bool RestApiDelete(OrthancPluginContext* context, - const std::string& uri, + bool RestApiDelete(const std::string& uri, bool applyPlugins) { OrthancPluginErrorCode error; if (applyPlugins) { - error = OrthancPluginRestApiDeleteAfterPlugins(context, uri.c_str()); + error = OrthancPluginRestApiDeleteAfterPlugins(GetGlobalContext(), uri.c_str()); } else { - error = OrthancPluginRestApiDelete(context, uri.c_str()); + error = OrthancPluginRestApiDelete(GetGlobalContext(), uri.c_str()); } if (error == OrthancPluginErrorCode_Success) @@ -1220,35 +1267,31 @@ } - void ReportMinimalOrthancVersion(OrthancPluginContext* context, - unsigned int major, + void ReportMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision) { - std::string s = ("Your version of the Orthanc core (" + - std::string(context->orthancVersion) + - ") is too old to run this plugin (version " + - boost::lexical_cast(major) + "." + - boost::lexical_cast(minor) + "." + - boost::lexical_cast(revision) + - " is required)"); - - OrthancPluginLogError(context, s.c_str()); + LogError("Your version of the Orthanc core (" + + std::string(GetGlobalContext()->orthancVersion) + + ") is too old to run this plugin (version " + + boost::lexical_cast(major) + "." + + boost::lexical_cast(minor) + "." + + boost::lexical_cast(revision) + + " is required)"); } - bool CheckMinimalOrthancVersion(OrthancPluginContext* context, - unsigned int major, + bool CheckMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision) { - if (context == NULL) + if (!HasGlobalContext()) { - OrthancPluginLogError(context, "Bad Orthanc context in the plugin"); + LogError("Bad Orthanc context in the plugin"); return false; } - if (!strcmp(context->orthancVersion, "mainline")) + if (!strcmp(GetGlobalContext()->orthancVersion, "mainline")) { // Assume compatibility with the mainline return true; @@ -1262,7 +1305,7 @@ #else sscanf #endif - (context->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 || + (GetGlobalContext()->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 || aa < 0 || bb < 0 || cc < 0) @@ -1313,6 +1356,58 @@ } } + const char* GetMimeType(const std::string& path) + { + size_t dot = path.find_last_of('.'); + + std::string extension = (dot == std::string::npos) ? "" : path.substr(dot); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); + + if (extension == ".html") + { + return "text/html"; + } + else if (extension == ".css") + { + return "text/css"; + } + else if (extension == ".js") + { + return "application/javascript"; + } + else if (extension == ".gif") + { + return "image/gif"; + } + else if (extension == ".svg") + { + return "image/svg+xml"; + } + else if (extension == ".json") + { + return "application/json"; + } + else if (extension == ".xml") + { + return "application/xml"; + } + else if (extension == ".wasm") + { + return "application/wasm"; + } + else if (extension == ".png") + { + return "image/png"; + } + else if (extension == ".jpg" || extension == ".jpeg") + { + return "image/jpeg"; + } + else + { + return "application/octet-stream"; + } + } @@ -1326,38 +1421,31 @@ } else { - std::string s = "Inexistent peer: " + name; - OrthancPluginLogError(context_, s.c_str()); + LogError("Inexistent peer: " + name); ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource); } } - OrthancPeers::OrthancPeers(OrthancPluginContext* context) : - context_(context), + OrthancPeers::OrthancPeers() : peers_(NULL), timeout_(0) { - if (context_ == NULL) - { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); - } - - peers_ = OrthancPluginGetPeers(context_); + peers_ = OrthancPluginGetPeers(GetGlobalContext()); if (peers_ == NULL) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); } - uint32_t count = OrthancPluginGetPeersCount(context_, peers_); + uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_); for (uint32_t i = 0; i < count; i++) { - const char* name = OrthancPluginGetPeerName(context_, peers_, i); + const char* name = OrthancPluginGetPeerName(GetGlobalContext(), peers_, i); if (name == NULL) { - OrthancPluginFreePeers(context_, peers_); + OrthancPluginFreePeers(GetGlobalContext(), peers_); ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); } @@ -1370,7 +1458,7 @@ { if (peers_ != NULL) { - OrthancPluginFreePeers(context_, peers_); + OrthancPluginFreePeers(GetGlobalContext(), peers_); } } @@ -1400,7 +1488,7 @@ } else { - const char* s = OrthancPluginGetPeerName(context_, peers_, static_cast(index)); + const char* s = OrthancPluginGetPeerName(GetGlobalContext(), peers_, static_cast(index)); if (s == NULL) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); @@ -1421,7 +1509,7 @@ } else { - const char* s = OrthancPluginGetPeerUrl(context_, peers_, static_cast(index)); + const char* s = OrthancPluginGetPeerUrl(GetGlobalContext(), peers_, static_cast(index)); if (s == NULL) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); @@ -1450,7 +1538,7 @@ } else { - const char* s = OrthancPluginGetPeerUserProperty(context_, peers_, static_cast(index), key.c_str()); + const char* s = OrthancPluginGetPeerUserProperty(GetGlobalContext(), peers_, static_cast(index), key.c_str()); if (s == NULL) { return false; @@ -1484,7 +1572,7 @@ OrthancPluginMemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi - (context_, &answer, NULL, &status, peers_, + (GetGlobalContext(), &answer, NULL, &status, peers_, static_cast(index), OrthancPluginHttpMethod_Get, uri.c_str(), 0, NULL, NULL, NULL, 0, timeout_); @@ -1514,7 +1602,7 @@ size_t index, const std::string& uri) const { - MemoryBuffer buffer(context_); + MemoryBuffer buffer; if (DoGet(buffer, index, uri)) { @@ -1532,7 +1620,7 @@ const std::string& name, const std::string& uri) const { - MemoryBuffer buffer(context_); + MemoryBuffer buffer; if (DoGet(buffer, name, uri)) { @@ -1562,7 +1650,7 @@ const std::string& uri, const std::string& body) const { - MemoryBuffer buffer(context_); + MemoryBuffer buffer; if (DoPost(buffer, index, uri, body)) { @@ -1581,7 +1669,7 @@ const std::string& uri, const std::string& body) const { - MemoryBuffer buffer(context_); + MemoryBuffer buffer; if (DoPost(buffer, name, uri, body)) { @@ -1608,7 +1696,7 @@ OrthancPluginMemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi - (context_, &answer, NULL, &status, peers_, + (GetGlobalContext(), &answer, NULL, &status, peers_, static_cast(index), OrthancPluginHttpMethod_Post, uri.c_str(), 0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_); @@ -1636,13 +1724,13 @@ OrthancPluginMemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi - (context_, &answer, NULL, &status, peers_, + (GetGlobalContext(), &answer, NULL, &status, peers_, static_cast(index), OrthancPluginHttpMethod_Put, uri.c_str(), 0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_); if (code == OrthancPluginErrorCode_Success) { - OrthancPluginFreeMemoryBuffer(context_, &answer); + OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &answer); return (status == 200); } else @@ -1673,13 +1761,13 @@ OrthancPluginMemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi - (context_, &answer, NULL, &status, peers_, + (GetGlobalContext(), &answer, NULL, &status, peers_, static_cast(index), OrthancPluginHttpMethod_Put, uri.c_str(), 0, NULL, NULL, NULL, 0, timeout_); if (code == OrthancPluginErrorCode_Success) { - OrthancPluginFreeMemoryBuffer(context_, &answer); + OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &answer); return (status == 200); } else @@ -1888,8 +1976,7 @@ } - OrthancPluginJob* OrthancJob::Create(OrthancPluginContext* context, - OrthancJob* job) + OrthancPluginJob* OrthancJob::Create(OrthancJob* job) { if (job == NULL) { @@ -1897,9 +1984,9 @@ } OrthancPluginJob* orthanc = OrthancPluginCreateJob( - context, job, CallbackFinalize, job->jobType_.c_str(), - CallbackGetProgress, CallbackGetContent, CallbackGetSerialized, - CallbackStep, CallbackStop, CallbackReset); + GetGlobalContext(), job, CallbackFinalize, job->jobType_.c_str(), + CallbackGetProgress, CallbackGetContent, CallbackGetSerialized, + CallbackStep, CallbackStop, CallbackReset); if (orthanc == NULL) { @@ -1912,25 +1999,24 @@ } - std::string OrthancJob::Submit(OrthancPluginContext* context, - OrthancJob* job, + std::string OrthancJob::Submit(OrthancJob* job, int priority) { - OrthancPluginJob* orthanc = Create(context, job); + OrthancPluginJob* orthanc = Create(job); - char* id = OrthancPluginSubmitJob(context, orthanc, priority); + char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority); if (id == NULL) { - OrthancPluginLogError(context, "Plugin cannot submit job"); - OrthancPluginFreeJob(context, orthanc); + LogError("Plugin cannot submit job"); + OrthancPluginFreeJob(GetGlobalContext(), orthanc); ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); } else { std::string tmp(id); tmp.assign(id); - OrthancPluginFreeString(context, id); + OrthancPluginFreeString(GetGlobalContext(), id); return tmp; } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Thu Dec 06 15:58:08 2018 +0100 @@ -20,7 +20,7 @@ * 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 @@ -48,10 +48,10 @@ #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) #define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) #endif @@ -71,6 +71,12 @@ # define HAS_ORTHANC_PLUGIN_JOB 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 3) +# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 1 +#else +# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 0 +#endif + namespace OrthancPlugins @@ -79,13 +85,20 @@ const char* url, const OrthancPluginHttpRequest* request); + + void SetGlobalContext(OrthancPluginContext* context); + + bool HasGlobalContext(); + + OrthancPluginContext* GetGlobalContext(); + + class OrthancImage; class MemoryBuffer : public boost::noncopyable { private: - OrthancPluginContext* context_; OrthancPluginMemoryBuffer buffer_; void Check(OrthancPluginErrorCode code); @@ -93,7 +106,7 @@ bool CheckHttp(OrthancPluginErrorCode code); public: - MemoryBuffer(OrthancPluginContext* context); + MemoryBuffer(); ~MemoryBuffer() { @@ -127,6 +140,11 @@ return buffer_.size; } + bool IsEmpty() const + { + return GetSize() == 0 || GetData() == NULL; + } + void Clear(); void ToString(std::string& target) const; @@ -187,12 +205,12 @@ bool HttpGet(const std::string& url, const std::string& username, const std::string& password); - + bool HttpPost(const std::string& url, const std::string& body, const std::string& username, const std::string& password); - + bool HttpPut(const std::string& url, const std::string& body, const std::string& username, @@ -205,14 +223,12 @@ class OrthancString : public boost::noncopyable { private: - OrthancPluginContext* context_; - char* str_; + char* str_; void Clear(); public: - OrthancString(OrthancPluginContext* context) : - context_(context), + OrthancString() : str_(NULL) { } @@ -240,20 +256,13 @@ class OrthancConfiguration : public boost::noncopyable { private: - OrthancPluginContext* context_; - Json::Value configuration_; // Necessarily a Json::objectValue - std::string path_; + Json::Value configuration_; // Necessarily a Json::objectValue + std::string path_; std::string GetPath(const std::string& key) const; public: - OrthancConfiguration() : context_(NULL) - { - } - - OrthancConfiguration(OrthancPluginContext* context); - - OrthancPluginContext* GetContext() const; + OrthancConfiguration(); const Json::Value& GetJson() const { @@ -310,7 +319,6 @@ class OrthancImage : public boost::noncopyable { private: - OrthancPluginContext* context_; OrthancPluginImage* image_; void Clear(); @@ -318,18 +326,15 @@ void CheckImageAvailable(); public: - OrthancImage(OrthancPluginContext* context); + OrthancImage(); - OrthancImage(OrthancPluginContext* context, - OrthancPluginImage* image); + OrthancImage(OrthancPluginImage* image); - OrthancImage(OrthancPluginContext* context, - OrthancPluginPixelFormat format, + OrthancImage(OrthancPluginPixelFormat format, uint32_t width, uint32_t height); - OrthancImage(OrthancPluginContext* context, - OrthancPluginPixelFormat format, + OrthancImage(OrthancPluginPixelFormat format, uint32_t width, uint32_t height, uint32_t pitch, @@ -382,29 +387,24 @@ class FindMatcher : public boost::noncopyable { private: - OrthancPluginContext* context_; OrthancPluginFindMatcher* matcher_; const OrthancPluginWorklistQuery* worklist_; - void SetupDicom(OrthancPluginContext* context, - const void* query, + void SetupDicom(const void* query, uint32_t size); public: - FindMatcher(OrthancPluginContext* context, - const OrthancPluginWorklistQuery* worklist); + FindMatcher(const OrthancPluginWorklistQuery* worklist); - FindMatcher(OrthancPluginContext* context, - const void* query, + FindMatcher(const void* query, uint32_t size) { - SetupDicom(context, query, size); + SetupDicom(query, size); } - FindMatcher(OrthancPluginContext* context, - const MemoryBuffer& dicom) + FindMatcher(const MemoryBuffer& dicom) { - SetupDicom(context, dicom.GetData(), dicom.GetSize()); + SetupDicom(dicom.GetData(), dicom.GetSize()); } ~FindMatcher(); @@ -421,99 +421,91 @@ bool RestApiGet(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, bool applyPlugins); + bool RestApiGetString(std::string& result, + const std::string& uri, + bool applyPlugins); + bool RestApiPost(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const char* body, size_t bodySize, bool applyPlugins); bool RestApiPost(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const Json::Value& body, bool applyPlugins); inline bool RestApiPost(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const std::string& body, bool applyPlugins) { - return RestApiPost(result, context, uri, body.empty() ? NULL : body.c_str(), + return RestApiPost(result, uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins); } + inline bool RestApiPost(Json::Value& result, + const std::string& uri, + const MemoryBuffer& body, + bool applyPlugins) + { + return RestApiPost(result, uri, body.GetData(), + body.GetSize(), applyPlugins); + } + bool RestApiPut(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const char* body, size_t bodySize, bool applyPlugins); bool RestApiPut(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const Json::Value& body, bool applyPlugins); inline bool RestApiPut(Json::Value& result, - OrthancPluginContext* context, const std::string& uri, const std::string& body, bool applyPlugins) { - return RestApiPut(result, context, uri, body.empty() ? NULL : body.c_str(), + return RestApiPut(result, uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins); } - bool RestApiDelete(OrthancPluginContext* context, - const std::string& uri, + bool RestApiDelete(const std::string& uri, bool applyPlugins); - bool HttpDelete(OrthancPluginContext* context, - const std::string& url, + bool HttpDelete(const std::string& url, const std::string& username, const std::string& password); - inline void LogError(OrthancPluginContext* context, - const std::string& message) - { - if (context != NULL) - { - OrthancPluginLogError(context, message.c_str()); - } - } + void AnswerJson(const Json::Value& value, + OrthancPluginRestOutput* output); + + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output); + + void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods); - inline void LogWarning(OrthancPluginContext* context, - const std::string& message) - { - if (context != NULL) - { - OrthancPluginLogWarning(context, message.c_str()); - } - } + const char* GetMimeType(const std::string& path); + + + void LogError(const std::string& message); - inline void LogInfo(OrthancPluginContext* context, - const std::string& message) - { - if (context != NULL) - { - OrthancPluginLogInfo(context, message.c_str()); - } - } + void LogWarning(const std::string& message); - void ReportMinimalOrthancVersion(OrthancPluginContext* context, - unsigned int major, + void LogInfo(const std::string& message); + + void ReportMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision); - bool CheckMinimalOrthancVersion(OrthancPluginContext* context, - unsigned int major, + bool CheckMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision); @@ -532,6 +524,18 @@ } catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) { +#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1 + if (HasGlobalContext() && + e.HasDetails()) + { + // The "false" instructs Orthanc not to log the detailed + // error message. This is to avoid duplicating the details, + // because "OrthancException" already does it on construction. + OrthancPluginSetHttpErrorDetails + (GetGlobalContext(), output, e.GetDetails(), false); + } +#endif + return static_cast(e.GetErrorCode()); } catch (boost::bad_lexical_cast&) @@ -547,17 +551,18 @@ template - void RegisterRestCallback(OrthancPluginContext* context, - const std::string& uri, + void RegisterRestCallback(const std::string& uri, bool isThreadSafe) { if (isThreadSafe) { - OrthancPluginRegisterRestCallbackNoLock(context, uri.c_str(), Internals::Protect); + OrthancPluginRegisterRestCallbackNoLock + (GetGlobalContext(), uri.c_str(), Internals::Protect); } else { - OrthancPluginRegisterRestCallback(context, uri.c_str(), Internals::Protect); + OrthancPluginRegisterRestCallback + (GetGlobalContext(), uri.c_str(), Internals::Protect); } } @@ -568,7 +573,6 @@ private: typedef std::map Index; - OrthancPluginContext *context_; OrthancPluginPeers *peers_; Index index_; uint32_t timeout_; @@ -576,7 +580,7 @@ size_t GetPeerIndex(const std::string& name) const; public: - OrthancPeers(OrthancPluginContext* context); + OrthancPeers(); ~OrthancPeers(); @@ -623,11 +627,11 @@ bool DoGet(Json::Value& target, size_t index, const std::string& uri) const; - + bool DoGet(Json::Value& target, const std::string& name, const std::string& uri) const; - + bool DoPost(MemoryBuffer& target, size_t index, const std::string& uri, @@ -642,7 +646,7 @@ size_t index, const std::string& uri, const std::string& body) const; - + bool DoPost(Json::Value& target, const std::string& name, const std::string& uri, @@ -715,11 +719,9 @@ virtual void Reset() = 0; - static OrthancPluginJob* Create(OrthancPluginContext* context, - OrthancJob* job /* takes ownership */); + static OrthancPluginJob* Create(OrthancJob* job /* takes ownership */); - static std::string Submit(OrthancPluginContext* context, - OrthancJob* job /* takes ownership */, + static std::string Submit(OrthancJob* job /* takes ownership */, int priority); }; #endif diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/ModalityWorklists/Plugin.cpp --- a/Plugins/Samples/ModalityWorklists/Plugin.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -28,7 +28,6 @@ #include #include -static OrthancPluginContext* context_ = NULL; static std::string folder_; static bool filterIssuerAet_ = false; @@ -40,18 +39,18 @@ const OrthancPlugins::FindMatcher& matcher, const std::string& path) { - OrthancPlugins::MemoryBuffer dicom(context_); + OrthancPlugins::MemoryBuffer dicom; dicom.ReadFile(path); if (matcher.IsMatch(dicom)) { // This DICOM file matches the worklist query, add it to the answers OrthancPluginErrorCode code = OrthancPluginWorklistAddAnswer - (context_, answers, query, dicom.GetData(), dicom.GetSize()); + (OrthancPlugins::GetGlobalContext(), answers, query, dicom.GetData(), dicom.GetSize()); if (code != OrthancPluginErrorCode_Success) { - OrthancPlugins::LogError(context_, "Error while adding an answer to a worklist request"); + OrthancPlugins::LogError("Error while adding an answer to a worklist request"); ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); } @@ -66,7 +65,7 @@ const char* issuerAet) { // Extract the DICOM instance underlying the C-Find query - OrthancPlugins::MemoryBuffer dicom(context_); + OrthancPlugins::MemoryBuffer dicom; dicom.GetDicomQuery(query); // Convert the DICOM as JSON, and dump it to the user in "--verbose" mode @@ -74,12 +73,12 @@ dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short, static_cast(0), 0); - OrthancPlugins::LogInfo(context_, "Received worklist query from remote modality " + + OrthancPlugins::LogInfo("Received worklist query from remote modality " + std::string(issuerAet) + ":\n" + json.toStyledString()); if (!filterIssuerAet_) { - return new OrthancPlugins::FindMatcher(context_, query); + return new OrthancPlugins::FindMatcher(query); } else { @@ -126,9 +125,10 @@ } // Encode the modified JSON as a DICOM instance, then convert it to a C-Find matcher - OrthancPlugins::MemoryBuffer modified(context_); + OrthancPlugins::MemoryBuffer modified; modified.CreateDicom(json, OrthancPluginCreateDicomFlags_None); - return new OrthancPlugins::FindMatcher(context_, modified); + + return new OrthancPlugins::FindMatcher(modified); } } @@ -170,7 +170,7 @@ // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query if (MatchWorklist(answers, query, *matcher, it->path().string())) { - OrthancPlugins::LogInfo(context_, "Worklist matched: " + it->path().string()); + OrthancPlugins::LogInfo("Worklist matched: " + it->path().string()); matchedWorklistCount++; } } @@ -179,17 +179,17 @@ std::ostringstream message; message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)"; - OrthancPlugins::LogInfo(context_, message.str()); + OrthancPlugins::LogInfo(message.str()); } catch (fs::filesystem_error&) { - OrthancPlugins::LogError(context_, "Inexistent folder while scanning for worklists: " + source.string()); + OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string()); return OrthancPluginErrorCode_DirectoryExpected; } // Uncomment the following line if too many answers are to be returned - // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers); + // OrthancPluginMarkWorklistAnswersIncomplete(OrthancPlugins::GetGlobalContext(), answers); return OrthancPluginErrorCode_Success; } @@ -204,22 +204,21 @@ { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { - context_ = c; + OrthancPlugins::SetGlobalContext(c); /* Check the version of the Orthanc core */ if (OrthancPluginCheckVersion(c) == 0) { - OrthancPlugins::ReportMinimalOrthancVersion(context_, - ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); return -1; } - OrthancPlugins::LogWarning(context_, "Sample worklist plugin is initializing"); - OrthancPluginSetDescription(context_, "Serve DICOM modality worklists from a folder with Orthanc."); + OrthancPlugins::LogWarning("Sample worklist plugin is initializing"); + OrthancPluginSetDescription(c, "Serve DICOM modality worklists from a folder with Orthanc."); - OrthancPlugins::OrthancConfiguration configuration(context_); + OrthancPlugins::OrthancConfiguration configuration; OrthancPlugins::OrthancConfiguration worklists; configuration.GetSection(worklists, "Worklists"); @@ -229,12 +228,12 @@ { if (worklists.LookupStringValue(folder_, "Database")) { - OrthancPlugins::LogWarning(context_, "The database of worklists will be read from folder: " + folder_); - OrthancPluginRegisterWorklistCallback(context_, Callback); + OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_); + OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback); } else { - OrthancPlugins::LogError(context_, "The configuration option \"Worklists.Database\" must contain a path"); + OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path"); return -1; } @@ -242,7 +241,7 @@ } else { - OrthancPlugins::LogWarning(context_, "Worklist server is disabled by the configuration file"); + OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file"); } return 0; @@ -251,7 +250,7 @@ ORTHANC_PLUGINS_API void OrthancPluginFinalize() { - OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing"); + OrthancPlugins::LogWarning("Sample worklist plugin is finalizing"); } diff -r 3fabf9a673f6 -r eff50153a7b3 Plugins/Samples/ServeFolders/Plugin.cpp --- a/Plugins/Samples/ServeFolders/Plugin.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/Plugins/Samples/ServeFolders/Plugin.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -33,7 +33,6 @@ -static OrthancPluginContext* context_ = NULL; static std::map extensions_; static std::map folders_; static const char* INDEX_URI = "/app/plugin-serve-folders.html"; @@ -46,9 +45,10 @@ if (!allowCache_) { // http://stackoverflow.com/a/2068407/881731 - OrthancPluginSetHttpHeader(context_, output, "Cache-Control", "no-cache, no-store, must-revalidate"); - OrthancPluginSetHttpHeader(context_, output, "Pragma", "no-cache"); - OrthancPluginSetHttpHeader(context_, output, "Expires", "0"); + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + OrthancPluginSetHttpHeader(context, output, "Cache-Control", "no-cache, no-store, must-revalidate"); + OrthancPluginSetHttpHeader(context, output, "Pragma", "no-cache"); + OrthancPluginSetHttpHeader(context, output, "Expires", "0"); } } @@ -89,7 +89,7 @@ } else { - OrthancPlugins::LogWarning(context_, "ServeFolders: Unknown MIME type for extension \"" + extension + "\""); + OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\""); return "application/octet-stream"; } } @@ -104,8 +104,8 @@ std::map::const_iterator found = folders_.find(uri); if (found == folders_.end()) { - OrthancPlugins::LogError(context_, "Unknown URI in plugin server-folders: " + uri); - OrthancPluginSendHttpStatusCode(context_, output, 404); + OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri); + OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404); return false; } else @@ -123,15 +123,15 @@ { if (generateETag_) { - OrthancPlugins::OrthancString md5(context_); - md5.Assign(OrthancPluginComputeMd5(context_, content, size)); + OrthancPlugins::OrthancString md5; + md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), content, size)); std::string etag = "\"" + std::string(md5.GetContent()) + "\""; - OrthancPluginSetHttpHeader(context_, output, "ETag", etag.c_str()); + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str()); } SetHttpHeaders(output); - OrthancPluginAnswerBuffer(context_, output, content, size, mime.c_str()); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, content, size, mime.c_str()); } @@ -143,7 +143,7 @@ if (request->method != OrthancPluginHttpMethod_Get) { - OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); return; } @@ -198,7 +198,7 @@ std::string path = folder + "/" + item.string(); std::string mime = GetMimeType(path); - OrthancPlugins::MemoryBuffer content(context_); + OrthancPlugins::MemoryBuffer content; try { @@ -209,9 +209,11 @@ ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile); } - boost::posix_time::ptime lastModification = boost::posix_time::from_time_t(fs::last_write_time(path)); + boost::posix_time::ptime lastModification = + boost::posix_time::from_time_t(fs::last_write_time(path)); std::string t = boost::posix_time::to_iso_string(lastModification); - OrthancPluginSetHttpHeader(context_, output, "Last-Modified", t.c_str()); + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), + output, "Last-Modified", t.c_str()); Answer(output, content.GetData(), content.GetSize(), mime); } @@ -225,7 +227,7 @@ { if (request->method != OrthancPluginHttpMethod_Get) { - OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); return; } @@ -258,7 +260,7 @@ { if (folders.type() != Json::objectValue) { - OrthancPlugins::LogError(context_, "The list of folders to be served is badly formatted (must be a JSON object)"); + OrthancPlugins::LogError("The list of folders to be served is badly formatted (must be a JSON object)"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -270,7 +272,7 @@ { if (folders[*it].type() != Json::stringValue) { - OrthancPlugins::LogError(context_, "The folder to be served \"" + *it + + OrthancPlugins::LogError("The folder to be served \"" + *it + "\" must be associated with a string value (its mapped URI)"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -292,7 +294,7 @@ if (baseUri.empty()) { - OrthancPlugins::LogError(context_, "The URI of a folder to be served cannot be empty"); + OrthancPlugins::LogError("The URI of a folder to be served cannot be empty"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -300,7 +302,7 @@ const std::string folder = folders[*it].asString(); if (!boost::filesystem::is_directory(folder)) { - OrthancPlugins::LogError(context_, "Trying and serve an inexistent folder: " + folder); + OrthancPlugins::LogError("Trying and serve an inexistent folder: " + folder); ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile); } @@ -309,7 +311,7 @@ // Register the callback to serve the folder { const std::string regex = "/(" + baseUri + ")/(.*)"; - OrthancPlugins::RegisterRestCallback(context_, regex.c_str(), true); + OrthancPlugins::RegisterRestCallback(regex.c_str(), true); } } } @@ -319,7 +321,7 @@ { if (extensions.type() != Json::objectValue) { - OrthancPlugins::LogError(context_, "The list of extensions is badly formatted (must be a JSON object)"); + OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -330,7 +332,7 @@ { if (extensions[*it].type() != Json::stringValue) { - OrthancPlugins::LogError(context_, "The file extension \"" + *it + + OrthancPlugins::LogError("The file extension \"" + *it + "\" must be associated with a string value (its MIME type)"); ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); } @@ -349,11 +351,12 @@ if (mime.empty()) { - OrthancPlugins::LogWarning(context_, "ServeFolders: Removing MIME type for file extension \"." + name + "\""); + OrthancPlugins::LogWarning("ServeFolders: Removing MIME type for file extension \"." + + name + "\""); } else { - OrthancPlugins::LogWarning(context_, "ServeFolders: Associating file extension \"." + name + + OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + "\" with MIME type \"" + mime + "\""); } } @@ -365,7 +368,7 @@ OrthancPlugins::OrthancConfiguration configuration; { - OrthancPlugins::OrthancConfiguration globalConfiguration(context_); + OrthancPlugins::OrthancConfiguration globalConfiguration; globalConfiguration.GetSection(configuration, "ServeFolders"); } @@ -384,7 +387,7 @@ if (configuration.LookupBooleanValue(tmp, "AllowCache")) { allowCache_ = tmp; - OrthancPlugins::LogWarning(context_, "ServeFolders: Requesting the HTTP client to " + + OrthancPlugins::LogWarning("ServeFolders: Requesting the HTTP client to " + std::string(tmp ? "enable" : "disable") + " its caching mechanism"); } @@ -392,7 +395,8 @@ if (configuration.LookupBooleanValue(tmp, "GenerateETag")) { generateETag_ = tmp; - OrthancPlugins::LogWarning(context_, "ServeFolders: The computation of an ETag for the served resources is " + + OrthancPlugins::LogWarning("ServeFolders: The computation of an ETag for the " + "served resources is " + std::string(tmp ? "enabled" : "disabled")); } @@ -403,7 +407,8 @@ if (folders_.empty()) { - OrthancPlugins::LogWarning(context_, "ServeFolders: Empty configuration file: No additional folder will be served!"); + OrthancPlugins::LogWarning("ServeFolders: Empty configuration file: " + "No additional folder will be served!"); } } @@ -412,22 +417,21 @@ { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) { - context_ = context; + OrthancPlugins::SetGlobalContext(context); /* Check the version of the Orthanc core */ - if (OrthancPluginCheckVersion(context_) == 0) + if (OrthancPluginCheckVersion(context) == 0) { - OrthancPlugins::ReportMinimalOrthancVersion(context_, - ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); return -1; } RegisterDefaultExtensions(); - OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc."); + OrthancPluginSetDescription(context, "Serve additional folders with the HTTP server of Orthanc."); OrthancPluginSetRootUri(context, INDEX_URI); - OrthancPlugins::RegisterRestCallback(context_, INDEX_URI, true); + OrthancPlugins::RegisterRestCallback(INDEX_URI, true); try { @@ -435,8 +439,8 @@ } catch (OrthancPlugins::PluginException& e) { - OrthancPlugins::LogError(context_, "Error while initializing the ServeFolders plugin: " + - std::string(e.What(context_))); + OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + + std::string(e.What(context))); } return 0; diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/BoostConfiguration.cmake --- a/Resources/CMake/BoostConfiguration.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/BoostConfiguration.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -53,10 +53,10 @@ ## Parameters for static compilation of Boost ## - set(BOOST_NAME boost_1_67_0) - set(BOOST_VERSION 1.67.0) - set(BOOST_BCP_SUFFIX bcpdigest-1.4.0) - set(BOOST_MD5 "fb3535a88e72c3d4c4d06b047b8e57fe") + set(BOOST_NAME boost_1_68_0) + set(BOOST_VERSION 1.68.0) + set(BOOST_BCP_SUFFIX bcpdigest-1.4.3) + set(BOOST_MD5 "2d272566a72343766c523e2e32313c65") set(BOOST_URL "http://www.orthanc-server.com/downloads/third-party/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz") set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) @@ -94,7 +94,7 @@ endif() include_directories( - ${BOOST_SOURCES_DIR} + BEFORE ${BOOST_SOURCES_DIR} ) add_definitions( diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/BoostConfiguration.sh --- a/Resources/CMake/BoostConfiguration.sh Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/BoostConfiguration.sh Thu Dec 06 15:58:08 2018 +0100 @@ -19,10 +19,11 @@ ## - Orthanc 1.3.0: Boost 1.64.0 ## - Orthanc 1.3.1: Boost 1.65.1 ## - Orthanc 1.3.2: Boost 1.66.0 -## - Orthanc >= 1.4.0: Boost 1.67.0 +## - Orthanc between 1.4.0 and 1.4.2: Boost 1.67.0 +## - Orthanc >= 1.4.3: Boost 1.68.0 -BOOST_VERSION=1_67_0 -ORTHANC_VERSION=1.4.0 +BOOST_VERSION=1_68_0 +ORTHANC_VERSION=1.4.3 rm -rf /tmp/boost_${BOOST_VERSION} rm -rf /tmp/bcp/boost_${BOOST_VERSION} diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/DcmtkConfiguration.cmake --- a/Resources/CMake/DcmtkConfiguration.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -324,6 +324,18 @@ -DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER} ) + if (NOT ENABLE_DCMTK_LOG) + # Disable logging internal to DCMTK + # https://groups.google.com/d/msg/orthanc-users/v2SzzAmY948/VxT1QVGiBAAJ + add_definitions( + -DDCMTK_LOG4CPLUS_DISABLE_FATAL=1 + -DDCMTK_LOG4CPLUS_DISABLE_ERROR=1 + -DDCMTK_LOG4CPLUS_DISABLE_WARN=1 + -DDCMTK_LOG4CPLUS_DISABLE_INFO=1 + -DDCMTK_LOG4CPLUS_DISABLE_DEBUG=1 + ) + endif() + include_directories( #${DCMTK_SOURCES_DIR} ${DCMTK_SOURCES_DIR}/config/include diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/LuaConfiguration.cmake --- a/Resources/CMake/LuaConfiguration.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/LuaConfiguration.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -1,7 +1,7 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_LUA) - SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.1.5) - SET(LUA_MD5 "2e115fe26e435e33b0d5c022e4490567") - SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.1.5.tar.gz") + SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5) + SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455") + SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.3.5.tar.gz") DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}") @@ -18,53 +18,83 @@ # shared libraries can call them set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--export-dynamic") + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + add_definitions(-DLUA_USE_LINUX=1) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + add_definitions( + -DLUA_USE_LINUX=1 + -DLUA_USE_READLINE=1 + ) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + add_definitions(-DLUA_USE_POSIX=1) + endif() + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - add_definitions(-DLUA_DL_DLL=1) # Enable loading of shared libraries (for Microsoft Windows) + add_definitions( + -DLUA_DL_DLL=1 # Enable loading of shared libraries (for Microsoft Windows) + ) elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") - add_definitions(-LUA_DL_DYLD=1) # Enable loading of shared libraries (for Apple OS X) + add_definitions( + -DLUA_USE_MACOSX=1 + -DLUA_DL_DYLD=1 # Enable loading of shared libraries (for Apple OS X) + ) else() message(FATAL_ERROR "Support your platform here") endif() endif() + add_definitions( + -DLUA_COMPAT_5_2=1 + ) + include_directories( ${LUA_SOURCES_DIR}/src ) set(LUA_SOURCES + # Don't compile the Lua command-line + #${LUA_SOURCES_DIR}/src/lua.c + #${LUA_SOURCES_DIR}/src/luac.c + # Core Lua ${LUA_SOURCES_DIR}/src/lapi.c - ${LUA_SOURCES_DIR}/src/lcode.c - ${LUA_SOURCES_DIR}/src/ldebug.c - ${LUA_SOURCES_DIR}/src/ldo.c - ${LUA_SOURCES_DIR}/src/ldump.c - ${LUA_SOURCES_DIR}/src/lfunc.c + ${LUA_SOURCES_DIR}/src/lcode.c + ${LUA_SOURCES_DIR}/src/lctype.c + ${LUA_SOURCES_DIR}/src/ldebug.c + ${LUA_SOURCES_DIR}/src/ldo.c + ${LUA_SOURCES_DIR}/src/ldump.c + ${LUA_SOURCES_DIR}/src/lfunc.c ${LUA_SOURCES_DIR}/src/lgc.c ${LUA_SOURCES_DIR}/src/llex.c - ${LUA_SOURCES_DIR}/src/lmem.c - ${LUA_SOURCES_DIR}/src/lobject.c - ${LUA_SOURCES_DIR}/src/lopcodes.c + ${LUA_SOURCES_DIR}/src/lmem.c + ${LUA_SOURCES_DIR}/src/lobject.c + ${LUA_SOURCES_DIR}/src/lopcodes.c ${LUA_SOURCES_DIR}/src/lparser.c - ${LUA_SOURCES_DIR}/src/lstate.c + ${LUA_SOURCES_DIR}/src/lstate.c ${LUA_SOURCES_DIR}/src/lstring.c ${LUA_SOURCES_DIR}/src/ltable.c ${LUA_SOURCES_DIR}/src/ltm.c - ${LUA_SOURCES_DIR}/src/lundump.c - ${LUA_SOURCES_DIR}/src/lvm.c + ${LUA_SOURCES_DIR}/src/lundump.c + ${LUA_SOURCES_DIR}/src/lvm.c ${LUA_SOURCES_DIR}/src/lzio.c # Base Lua modules ${LUA_SOURCES_DIR}/src/lauxlib.c ${LUA_SOURCES_DIR}/src/lbaselib.c + ${LUA_SOURCES_DIR}/src/lbitlib.c + ${LUA_SOURCES_DIR}/src/lcorolib.c ${LUA_SOURCES_DIR}/src/ldblib.c ${LUA_SOURCES_DIR}/src/liolib.c ${LUA_SOURCES_DIR}/src/lmathlib.c + ${LUA_SOURCES_DIR}/src/loadlib.c ${LUA_SOURCES_DIR}/src/loslib.c + ${LUA_SOURCES_DIR}/src/lstrlib.c ${LUA_SOURCES_DIR}/src/ltablib.c - ${LUA_SOURCES_DIR}/src/lstrlib.c - ${LUA_SOURCES_DIR}/src/loadlib.c + ${LUA_SOURCES_DIR}/src/lutf8lib.c + ${LUA_SOURCES_DIR}/src/linit.c ) diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -125,6 +125,7 @@ ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp + ${ORTHANC_ROOT}/Core/EnumerationDictionary.h ${ORTHANC_ROOT}/Core/Enumerations.cpp ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp ${ORTHANC_ROOT}/Core/Logging.cpp diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/OrthancFrameworkParameters.cmake --- a/Resources/CMake/OrthancFrameworkParameters.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/OrthancFrameworkParameters.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -59,6 +59,7 @@ set(USE_DCMTK_360 OFF CACHE BOOL "Use older DCMTK version 3.6.0 in static builds (instead of default 3.6.2)") set(USE_DCMTK_362_PRIVATE_DIC ON CACHE BOOL "Use the dictionary of private tags from DCMTK 3.6.2 if using DCMTK 3.6.0") set(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK") +set(ENABLE_DCMTK_LOG ON CACHE BOOL "Enable logging internal to DCMTK") set(ENABLE_DCMTK_JPEG ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/CMake/UuidConfiguration.cmake --- a/Resources/CMake/UuidConfiguration.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/CMake/UuidConfiguration.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -5,10 +5,33 @@ SET(E2FSPROGS_URL "http://www.orthanc-server.com/downloads/third-party/e2fsprogs-1.43.8.tar.gz") SET(E2FSPROGS_MD5 "670b7a74a8ead5333acf21b9afc92b3c") + if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}") + + ## + ## Patch for OS X, in order to be compatible with Cocoa (used in Stone) + ## + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${ORTHANC_ROOT}/Resources/Patches/e2fsprogs-1.43.8-apple.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (FirstRun AND Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + include_directories( - ${E2FSPROGS_SOURCES_DIR}/lib + BEFORE ${E2FSPROGS_SOURCES_DIR}/lib ) set(UUID_SOURCES diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/Configuration.json --- a/Resources/Configuration.json Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/Configuration.json Thu Dec 06 15:58:08 2018 +0100 @@ -198,6 +198,10 @@ //} }, + // Whether to store the DICOM modalities in the Orthanc database + // instead of in this configuration file (new in Orthanc 1.4.3) + "DicomModalitiesInDatabase" : false, + // Whether the Orthanc SCP allows incoming C-Echo requests, even // from SCU modalities it does not know about (i.e. that are not // listed in the "DicomModalities" option above). Orthanc 1.3.0 @@ -248,6 +252,10 @@ // } }, + // Whether to store the Orthanc peers in the Orthanc database + // instead of in this configuration file (new in Orthanc 1.4.3) + "OrthancPeersInDatabase" : false, + // Parameters of the HTTP proxy to be used by Orthanc. If set to the // empty string, no HTTP proxy is used. For instance: // "HttpProxy" : "192.168.0.1:3128" @@ -273,7 +281,9 @@ // peers in HTTPS requests. From curl documentation ("--cacert" // option): "Tells curl to use the specified certificate file to // verify the peers. The file may contain multiple CA - // certificates. The certificate(s) must be in PEM format." + // certificates. The certificate(s) must be in PEM format." On + // Debian-based systems, this option can be set to + // "/etc/ssl/certs/ca-certificates.crt" "HttpsCACertificates" : "", @@ -412,12 +422,17 @@ // Whether to run DICOM C-Move operations synchronously. If set to // "false" (the default), each incoming C-Move request results in // creating a new background job. Up to Orthanc 1.3.2, the implicit - // behavior was to use synchronous C-Move. - "SynchronousCMove" : false, + // behavior was to use synchronous C-Move. Between Orthanc 1.4.0 and + // 1.4.2, the default behavior was set to asynchronous C-Move. Since + // Orthanc 1.4.3, the default behavior is synchronous C-Move + // (backward compatibility with Orthanc <= 1.3.2). + "SynchronousCMove" : true, // Maximum number of completed jobs that are kept in memory. A // processing job is considered as complete once it is tagged as - // "Success" or "Failure". + // "Success" or "Failure". Since Orthanc 1.4.3, a value of "0" + // indicates to keep no job in memory (i.e. jobs are removed from + // the history as soon as they are completed). "JobsHistorySize" : 10, // Specifies how Orthanc reacts when it receives a DICOM instance @@ -425,5 +440,12 @@ // instance replaces the old one. If set to "false", the new // instance is discarded and the old one is kept. Up to Orthanc // 1.4.1, the implicit behavior corresponded to "false". - "OverwriteInstances" : false + "OverwriteInstances" : false, + + // Maximum number of ZIP/media archives that are maintained by + // Orthanc, as a response to the asynchronous creation of archives. + // The least recently used archives get deleted as new archives are + // generated. This option was introduced in Orthanc 1.4.3, and has + // no effect on the synchronous generation of archives. + "MediaArchiveSize" : 1 } diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/DownloadOrthancFramework.cmake --- a/Resources/DownloadOrthancFramework.cmake Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/DownloadOrthancFramework.cmake Thu Dec 06 15:58:08 2018 +0100 @@ -87,6 +87,8 @@ set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0") set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1") + set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2") set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394") endif() diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/ErrorCodes.json Thu Dec 06 15:58:08 2018 +0100 @@ -421,37 +421,44 @@ "Description": "Cannot store an instance" }, { - "Code": 2019, + "Code": 2019, + "HttpStatus": 400, "Name": "CreateDicomNotString", "Description": "Only string values are supported when creating DICOM instances" }, { - "Code": 2020, + "Code": 2020, + "HttpStatus": 400, "Name": "CreateDicomOverrideTag", "Description": "Trying to override a value inherited from a parent module" }, { - "Code": 2021, + "Code": 2021, + "HttpStatus": 400, "Name": "CreateDicomUseContent", "Description": "Use \\\"Content\\\" to inject an image into a new DICOM instance" }, { - "Code": 2022, + "Code": 2022, + "HttpStatus": 400, "Name": "CreateDicomNoPayload", "Description": "No payload is present for one instance in the series" }, { - "Code": 2023, + "Code": 2023, + "HttpStatus": 400, "Name": "CreateDicomUseDataUriScheme", "Description": "The payload of the DICOM instance must be specified according to Data URI scheme" }, { - "Code": 2024, + "Code": 2024, + "HttpStatus": 400, "Name": "CreateDicomBadParent", "Description": "Trying to attach a new DICOM instance to an inexistent resource" }, { - "Code": 2025, + "Code": 2025, + "HttpStatus": 400, "Name": "CreateDicomParentIsInstance", "Description": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)" }, diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/Patches/boost-1.68.0-linux-standard-base.patch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/boost-1.68.0-linux-standard-base.patch Thu Dec 06 15:58:08 2018 +0100 @@ -0,0 +1,76 @@ +diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp +--- boost_1_68_0.orig/boost/move/adl_move_swap.hpp 2018-11-13 16:08:32.214434915 +0100 ++++ boost_1_68_0/boost/move/adl_move_swap.hpp 2018-11-13 16:09:03.558399048 +0100 +@@ -28,6 +28,8 @@ + //Try to avoid including , as it's quite big + #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB) + #include //Dinkum libraries define std::swap in utility which is lighter than algorithm ++#elif defined(__LSB_VERSION__) ++# include + #elif defined(BOOST_GNU_STDLIB) + //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions + //use the good old stl_algobase header, which is quite lightweight +diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp +--- boost_1_68_0.orig/boost/thread/detail/config.hpp 2018-11-13 16:08:32.210434920 +0100 ++++ boost_1_68_0/boost/thread/detail/config.hpp 2018-11-13 16:10:03.386329911 +0100 +@@ -417,7 +417,7 @@ + #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO + #elif defined(BOOST_THREAD_CHRONO_MAC_API) + #define BOOST_THREAD_HAS_MONO_CLOCK +-#elif defined(__ANDROID__) ++#elif defined(__LSB_VERSION__) || defined(__ANDROID__) + #define BOOST_THREAD_HAS_MONO_CLOCK + #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 + #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO +diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp +--- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp 2018-11-13 16:08:32.206434924 +0100 ++++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp 2018-11-13 16:11:08.374253901 +0100 +@@ -32,8 +32,11 @@ + namespace boost { + namespace detail { + ++// https://stackoverflow.com/a/15474269 ++#ifndef Q_MOC_RUN + // This namespace ensures that argument-dependent name lookup does not mess things up. + namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { ++#endif + + // 1. a function to have an instance of type T without requiring T to be default + // constructible +@@ -181,7 +184,9 @@ + BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); + }; + ++#ifndef Q_MOC_RUN + } // namespace impl ++#endif + } // namespace detail + + // this is the accessible definition of the trait to end user +diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp +--- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp 2018-11-13 16:08:32.206434924 +0100 ++++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp 2018-11-13 16:14:30.278012856 +0100 +@@ -45,8 +45,11 @@ + namespace boost { + namespace detail { + ++// https://stackoverflow.com/a/15474269 ++#ifndef Q_MOC_RUN + // This namespace ensures that argument-dependent name lookup does not mess things up. + namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { ++#endif + + // 1. a function to have an instance of type T without requiring T to be default + // constructible +@@ -194,7 +197,10 @@ + BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); + }; + ++ ++#ifndef Q_MOC_RUN + } // namespace impl ++#endif + } // namespace detail + + // this is the accessible definition of the trait to end user +Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~ diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/Patches/e2fsprogs-1.43.8-apple.patch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/e2fsprogs-1.43.8-apple.patch Thu Dec 06 15:58:08 2018 +0100 @@ -0,0 +1,24 @@ +diff -urEb e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in e2fsprogs-1.43.8/lib/uuid/uuid.h.in +--- e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in 2018-01-02 05:52:58.000000000 +0100 ++++ e2fsprogs-1.43.8/lib/uuid/uuid.h.in 2018-11-05 12:18:29.962235770 +0100 +@@ -35,6 +35,20 @@ + #ifndef _UUID_UUID_H + #define _UUID_UUID_H + ++ ++#if defined(__APPLE__) ++// This patch defines the "uuid_string_t" type on OS X, which is ++// required if linking against Cocoa (this occurs in Stone of Orthanc) ++#include ++#include ++ ++#ifndef _UUID_STRING_T ++#define _UUID_STRING_T ++typedef __darwin_uuid_string_t uuid_string_t; ++#endif /* _UUID_STRING_T */ ++#endif ++ ++ + #include + #ifndef _WIN32 + #include diff -r 3fabf9a673f6 -r eff50153a7b3 Resources/Samples/Lua/CallWebService.lua --- a/Resources/Samples/Lua/CallWebService.lua Thu Oct 18 10:48:11 2018 +0200 +++ b/Resources/Samples/Lua/CallWebService.lua Thu Dec 06 15:58:08 2018 +0100 @@ -5,7 +5,9 @@ -- Download and install the JSON module for Lua by Jeffrey Friedl -- http://regex.info/blog/lua/json -JSON = (loadstring(HttpGet('http://regex.info/code/JSON.lua'))) () + +-- NOTE : Replace "load" by "loadstring" for Lua <= 5.1 +JSON = (load(HttpGet('http://regex.info/code/JSON.lua'))) () SetHttpCredentials('alice', 'alicePassword') diff -r 3fabf9a673f6 -r eff50153a7b3 TODO --- a/TODO Thu Oct 18 10:48:11 2018 +0200 +++ b/TODO Thu Dec 06 15:58:08 2018 +0100 @@ -135,7 +135,6 @@ * Avoid direct calls to FromDcmtkBridge (make most of its methods private), go through ParsedDicomFile wherever possible - ================= Platform-specific ================= diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/ImageProcessingTests.cpp --- a/UnitTestsSources/ImageProcessingTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/ImageProcessingTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -94,12 +94,12 @@ std::auto_ptr image_; protected: - virtual void SetUp() + virtual void SetUp() ORTHANC_OVERRIDE { image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false)); } - virtual void TearDown() + virtual void TearDown() ORTHANC_OVERRIDE { image_.reset(NULL); } diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/ImageTests.cpp --- a/UnitTestsSources/ImageTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/ImageTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -43,9 +43,10 @@ #include "../Core/Images/PngWriter.h" #include "../Core/Images/PamReader.h" #include "../Core/Images/PamWriter.h" +#include "../Core/SystemToolbox.h" #include "../Core/Toolbox.h" #include "../Core/TemporaryFile.h" -#include "../OrthancServer/OrthancInitialization.h" // For the FontRegistry +#include "../OrthancServer/OrthancConfiguration.h" // For the FontRegistry #include @@ -264,8 +265,12 @@ Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480, false); memset(s.GetBuffer(), 0, s.GetPitch() * s.GetHeight()); - ASSERT_GE(1u, Orthanc::Configuration::GetFontRegistry().GetSize()); - Orthanc::Configuration::GetFontRegistry().GetFont(0).Draw(s, "Hello world É\n\rComment ça va ?\nq", 50, 60, 255, 0, 0); + { + Orthanc::OrthancConfiguration::ReaderLock lock; + ASSERT_GE(1u, lock.GetConfiguration().GetFontRegistry().GetSize()); + lock.GetConfiguration().GetFontRegistry().GetFont(0).Draw + (s, "Hello world É\n\rComment ça va ?\nq", 50, 60, 255, 0, 0); + } Orthanc::PngWriter w; w.WriteToFile("UnitTestsResults/font.png", s); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/LuaTests.cpp --- a/UnitTestsSources/LuaTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/LuaTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -293,7 +293,13 @@ // OpenSSL/HTTPS support is disabled in curl const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/"; +#if LUA_VERSION_NUM >= 502 + // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load" + lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()"); +#else lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()"); +#endif + const std::string url(BASE + "Product.json"); #endif diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/MemoryCacheTests.cpp --- a/UnitTestsSources/MemoryCacheTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/MemoryCacheTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -186,7 +186,7 @@ { } - virtual ~Integer() + virtual ~Integer() ORTHANC_OVERRIDE { LOG(INFO) << "Removing cache entry for " << value_; log_ += boost::lexical_cast(value_) + " "; @@ -198,7 +198,7 @@ public: std::string log_; - Orthanc::IDynamicObject* Provide(const std::string& s) + virtual Orthanc::IDynamicObject* Provide(const std::string& s) ORTHANC_OVERRIDE { LOG(INFO) << "Providing " << s; return new Integer(log_, boost::lexical_cast(s)); @@ -261,9 +261,25 @@ for (int i = 1; i < 100; i++) { a.Add(new S("Item " + boost::lexical_cast(i))); + // Continuously protect the two first items - try { Orthanc::SharedArchive::Accessor(a, first); } catch (Orthanc::OrthancException&) {} - try { Orthanc::SharedArchive::Accessor(a, second); } catch (Orthanc::OrthancException&) {} + { + Orthanc::SharedArchive::Accessor accessor(a, first); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("First item", dynamic_cast(accessor.GetItem()).GetValue()); + } + + { + Orthanc::SharedArchive::Accessor accessor(a, second); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("Second item", dynamic_cast(accessor.GetItem()).GetValue()); + } + + { + Orthanc::SharedArchive::Accessor accessor(a, "nope"); + ASSERT_FALSE(accessor.IsValid()); + ASSERT_THROW(accessor.GetItem(), Orthanc::OrthancException); + } } std::list i; diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -93,15 +93,15 @@ { } - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { } - virtual void Reset() + virtual void Reset() ORTHANC_OVERRIDE { } - virtual JobStepResult Step() + virtual JobStepResult Step() ORTHANC_OVERRIDE { if (fails_) { @@ -118,31 +118,38 @@ } } - virtual void Stop(JobStopReason reason) + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE { } - virtual float GetProgress() + virtual float GetProgress() ORTHANC_OVERRIDE { return static_cast(count_) / static_cast(steps_ - 1); } - virtual void GetJobType(std::string& type) + virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE { type = "DummyJob"; } - virtual bool Serialize(Json::Value& value) + virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE { value = Json::objectValue; value["Type"] = "DummyJob"; return true; } - virtual void GetPublicContent(Json::Value& value) + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE { value["hello"] = "world"; } + + virtual bool GetOutput(std::string& output, + MimeType& mime, + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } }; @@ -152,12 +159,12 @@ bool trailingStepDone_; protected: - virtual bool HandleInstance(const std::string& instance) + virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE { return (instance != "nope"); } - virtual bool HandleTrailingStep() + virtual bool HandleTrailingStep() ORTHANC_OVERRIDE { if (HasTrailingStep()) { @@ -201,11 +208,11 @@ return trailingStepDone_; } - virtual void Stop(JobStopReason reason) + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE { } - virtual void GetJobType(std::string& s) + virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE { s = "DummyInstancesJob"; } @@ -215,7 +222,7 @@ class DummyUnserializer : public GenericJobUnserializer { public: - virtual IJob* UnserializeJob(const Json::Value& value) + virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE { if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob") { @@ -329,7 +336,7 @@ TEST(JobsRegistry, Priority) { - JobsRegistry registry; + JobsRegistry registry(10); std::string i1, i2, i3, i4; registry.Submit(i1, new DummyJob(), 10); @@ -408,7 +415,7 @@ TEST(JobsRegistry, Simultaneous) { - JobsRegistry registry; + JobsRegistry registry(10); std::string i1, i2; registry.Submit(i1, new DummyJob(), 20); @@ -438,7 +445,7 @@ TEST(JobsRegistry, Resubmit) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -482,7 +489,7 @@ TEST(JobsRegistry, Retry) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -519,7 +526,7 @@ TEST(JobsRegistry, PausePending) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -542,7 +549,7 @@ TEST(JobsRegistry, PauseRunning) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -580,7 +587,7 @@ TEST(JobsRegistry, PauseRetry) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -617,7 +624,7 @@ TEST(JobsRegistry, Cancel) { - JobsRegistry registry; + JobsRegistry registry(10); std::string id; registry.Submit(id, new DummyJob(), 10); @@ -711,7 +718,7 @@ TEST(JobsEngine, SubmitAndWait) { - JobsEngine engine; + JobsEngine engine(10); engine.SetThreadSleep(10); engine.SetWorkersCount(3); engine.Start(); @@ -731,7 +738,7 @@ TEST(JobsEngine, DISABLED_SequenceOfOperationsJob) { - JobsEngine engine; + JobsEngine engine(10); engine.SetThreadSleep(10); engine.SetWorkersCount(3); engine.Start(); @@ -771,7 +778,7 @@ TEST(JobsEngine, DISABLED_Lua) { - JobsEngine engine; + JobsEngine engine(10); engine.SetThreadSleep(10); engine.SetWorkersCount(2); engine.Start(); @@ -1282,11 +1289,11 @@ OrthancJobsSerialization() { db_.Open(); - context_.reset(new ServerContext(db_, storage_, true /* running unit tests */)); + context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10)); context_->SetupJobsEngine(true, false); } - virtual ~OrthancJobsSerialization() + virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE { context_->Stop(); context_.reset(NULL); @@ -1475,8 +1482,7 @@ // ArchiveJob { - boost::shared_ptr tmp(new TemporaryFile); - ArchiveJob job(tmp, GetContext(), false, false); + ArchiveJob job(GetContext(), false, false); ASSERT_FALSE(job.Serialize(s)); // Cannot serialize this } @@ -1704,7 +1710,7 @@ std::string i1, i2; { - JobsRegistry registry; + JobsRegistry registry(10); registry.Submit(i1, new DummyJob(), 10); registry.Submit(i2, new SequenceOfOperationsJob(), 30); registry.Serialize(s); @@ -1712,7 +1718,7 @@ { DummyUnserializer unserializer; - JobsRegistry registry(unserializer, s); + JobsRegistry registry(unserializer, s, 10); Json::Value t; registry.Serialize(t); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/RestApiTests.cpp --- a/UnitTestsSources/RestApiTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/RestApiTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -286,7 +286,7 @@ virtual bool Visit(const RestApiHierarchy::Resource& resource, const UriComponents& uri, const IHttpHandler::Arguments& components, - const UriComponents& trailing) + const UriComponents& trailing) ORTHANC_OVERRIDE { return resource.Handle(*(RestApiGetCall*) NULL); } @@ -381,7 +381,7 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) + const std::string& subtype) ORTHANC_OVERRIDE { type_ = type; subtype_ = subtype; diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/SQLiteChromiumTests.cpp --- a/UnitTestsSources/SQLiteChromiumTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/SQLiteChromiumTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -60,16 +60,16 @@ { } - virtual ~SQLConnectionTest() + virtual ~SQLConnectionTest() ORTHANC_OVERRIDE { } - virtual void SetUp() + virtual void SetUp() ORTHANC_OVERRIDE { db_.OpenInMemory(); } - virtual void TearDown() + virtual void TearDown() ORTHANC_OVERRIDE { db_.Close(); } @@ -274,7 +274,7 @@ class SQLTransactionTest : public SQLConnectionTest { public: - virtual void SetUp() + virtual void SetUp() ORTHANC_OVERRIDE { SQLConnectionTest::SetUp(); ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/SQLiteTests.cpp --- a/UnitTestsSources/SQLiteTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/SQLiteTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -142,22 +142,22 @@ destroyed = false; } - virtual ~MyFunc() + virtual ~MyFunc() ORTHANC_OVERRIDE { destroyed = true; } - virtual const char* GetName() const + virtual const char* GetName() const ORTHANC_OVERRIDE { return "MYFUNC"; } - virtual unsigned int GetCardinality() const + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE { return 2; } - virtual void Compute(SQLite::FunctionContext& context) + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE { context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1)); } @@ -168,17 +168,17 @@ public: std::set deleted_; - virtual const char* GetName() const + virtual const char* GetName() const ORTHANC_OVERRIDE { return "MYDELETE"; } - virtual unsigned int GetCardinality() const + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE { return 1; } - virtual void Compute(SQLite::FunctionContext& context) + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE { deleted_.insert(context.GetIntValue(0)); context.SetNullResult(); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -70,20 +70,21 @@ } virtual void SignalRemainingAncestor(ResourceType type, - const std::string& publicId) + const std::string& publicId) + ORTHANC_OVERRIDE { ancestorId_ = publicId; ancestorType_ = type; } - virtual void SignalFileDeleted(const FileInfo& info) + virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE { const std::string fileUuid = info.GetUuid(); deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; } - virtual void SignalChange(const ServerIndexChange& change) + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE { if (change.GetChangeType() == ChangeType_Deleted) { @@ -108,7 +109,7 @@ { } - virtual void SetUp() + virtual void SetUp() ORTHANC_OVERRIDE { listener_.reset(new TestDatabaseListener); @@ -126,7 +127,7 @@ index_->Open(); } - virtual void TearDown() + virtual void TearDown() ORTHANC_OVERRIDE { index_->Close(); index_.reset(NULL); @@ -676,7 +677,7 @@ FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */); + ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -776,7 +777,7 @@ FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */); + ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -820,6 +821,11 @@ ids.push_back(hasher.HashStudy()); ids.push_back(hasher.HashSeries()); ids.push_back(hasher.HashInstance()); + + ASSERT_EQ(hasher.HashPatient(), toStore.GetHasher().HashPatient()); + ASSERT_EQ(hasher.HashStudy(), toStore.GetHasher().HashStudy()); + ASSERT_EQ(hasher.HashSeries(), toStore.GetHasher().HashSeries()); + ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance()); } index.ComputeStatistics(tmp); @@ -859,7 +865,7 @@ MemoryStorageArea storage; DatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */); + ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); context.SetCompressionEnabled(true); @@ -884,6 +890,7 @@ DicomInstanceToStore toStore; toStore.SetSummary(instance); toStore.SetOrigin(DicomInstanceOrigin::FromPlugins()); + ASSERT_EQ(id, toStore.GetHasher().HashInstance()); std::string id2; ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore)); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -301,26 +301,30 @@ TEST(Uri, AutodetectMimeType) { - ASSERT_EQ("", Toolbox::AutodetectMimeType("../NOTES")); - ASSERT_EQ("", Toolbox::AutodetectMimeType("")); - ASSERT_EQ("", Toolbox::AutodetectMimeType("/")); - ASSERT_EQ("", Toolbox::AutodetectMimeType("a/a")); - - ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("../NOTES.txt")); - ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("../coucou.xml/NOTES.txt")); - ASSERT_EQ("text/xml", Toolbox::AutodetectMimeType("../.xml")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("../NOTES")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("/")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("a/a")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\a")); - ASSERT_EQ("application/javascript", Toolbox::AutodetectMimeType("NOTES.js")); - ASSERT_EQ("application/json", Toolbox::AutodetectMimeType("NOTES.json")); - ASSERT_EQ("application/pdf", Toolbox::AutodetectMimeType("NOTES.pdf")); - ASSERT_EQ("text/css", Toolbox::AutodetectMimeType("NOTES.css")); - ASSERT_EQ("text/html", Toolbox::AutodetectMimeType("NOTES.html")); - ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("NOTES.txt")); - ASSERT_EQ("text/xml", Toolbox::AutodetectMimeType("NOTES.xml")); - ASSERT_EQ("image/gif", Toolbox::AutodetectMimeType("NOTES.gif")); - ASSERT_EQ("image/jpeg", Toolbox::AutodetectMimeType("NOTES.jpg")); - ASSERT_EQ("image/jpeg", Toolbox::AutodetectMimeType("NOTES.jpeg")); - ASSERT_EQ("image/png", Toolbox::AutodetectMimeType("NOTES.png")); + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../NOTES.txt")); + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../coucou.xml/NOTES.txt")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("..\\coucou.\\NOTES.xml")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.xml")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.XmL")); + + ASSERT_EQ(MimeType_JavaScript, SystemToolbox::AutodetectMimeType("NOTES.js")); + ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.json")); + ASSERT_EQ(MimeType_Pdf, SystemToolbox::AutodetectMimeType("NOTES.pdf")); + ASSERT_EQ(MimeType_Css, SystemToolbox::AutodetectMimeType("NOTES.css")); + ASSERT_EQ(MimeType_Html, SystemToolbox::AutodetectMimeType("NOTES.html")); + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("NOTES.txt")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("NOTES.xml")); + ASSERT_EQ(MimeType_Gif, SystemToolbox::AutodetectMimeType("NOTES.gif")); + ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpg")); + ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpeg")); + ASSERT_EQ(MimeType_Png, SystemToolbox::AutodetectMimeType("NOTES.png")); } TEST(Toolbox, ComputeMD5) @@ -440,6 +444,19 @@ ASSERT_EQ(0x00, static_cast(utf8[14])); // Null-terminated string } + +TEST(Toolbox, FixUtf8) +{ + // This is a Latin-1 test string: "crane" with a circumflex accent + const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; + + std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char)); + + ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1), Encoding_Latin1)); + ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8)); +} + + TEST(Toolbox, UrlDecode) { std::string s; @@ -472,14 +489,20 @@ s[2] = '\0'; ASSERT_EQ(10u, s.size()); ASSERT_FALSE(Toolbox::IsAsciiString(s)); + + ASSERT_TRUE(Toolbox::IsAsciiString("Hello\nworld")); + ASSERT_FALSE(Toolbox::IsAsciiString("Hello\rworld")); + + ASSERT_EQ("Hello\nworld", Toolbox::ConvertToAscii("Hello\nworld")); + ASSERT_EQ("Helloworld", Toolbox::ConvertToAscii("Hello\r\tworld")); } #if defined(__linux__) -TEST(OrthancInitialization, AbsoluteDirectory) +TEST(Toolbox, AbsoluteDirectory) { - ASSERT_EQ("/tmp/hello", Configuration::InterpretRelativePath("/tmp", "hello")); - ASSERT_EQ("/tmp", Configuration::InterpretRelativePath("/tmp", "/tmp")); + ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello")); + ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp")); } #endif @@ -686,6 +709,26 @@ ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused))); ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry))); ASSERT_THROW(StringToJobState("nope"), OrthancException); + + ASSERT_EQ(MimeType_Binary, StringToMimeType(EnumerationToString(MimeType_Binary))); + ASSERT_EQ(MimeType_Dicom, StringToMimeType(EnumerationToString(MimeType_Dicom))); + ASSERT_EQ(MimeType_Jpeg, StringToMimeType(EnumerationToString(MimeType_Jpeg))); + ASSERT_EQ(MimeType_Jpeg2000, StringToMimeType(EnumerationToString(MimeType_Jpeg2000))); + ASSERT_EQ(MimeType_Json, StringToMimeType(EnumerationToString(MimeType_Json))); + ASSERT_EQ(MimeType_Pdf, StringToMimeType(EnumerationToString(MimeType_Pdf))); + ASSERT_EQ(MimeType_Png, StringToMimeType(EnumerationToString(MimeType_Png))); + ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml))); + ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml")); + ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml")); + ASSERT_EQ(MimeType_PlainText, StringToMimeType(EnumerationToString(MimeType_PlainText))); + ASSERT_EQ(MimeType_Pam, StringToMimeType(EnumerationToString(MimeType_Pam))); + ASSERT_EQ(MimeType_Html, StringToMimeType(EnumerationToString(MimeType_Html))); + ASSERT_EQ(MimeType_Gzip, StringToMimeType(EnumerationToString(MimeType_Gzip))); + ASSERT_EQ(MimeType_JavaScript, StringToMimeType(EnumerationToString(MimeType_JavaScript))); + ASSERT_EQ(MimeType_Gif, StringToMimeType(EnumerationToString(MimeType_Gif))); + ASSERT_EQ(MimeType_WebAssembly, StringToMimeType(EnumerationToString(MimeType_WebAssembly))); + ASSERT_EQ(MimeType_Css, StringToMimeType(EnumerationToString(MimeType_Css))); + ASSERT_THROW(StringToMimeType("nope"), OrthancException); } @@ -1119,6 +1162,34 @@ } +TEST(Toolbox, SubstituteVariables) +{ + std::map env; + env["NOPE"] = "nope"; + env["WORLD"] = "world"; + + ASSERT_EQ("Hello world\r\nWorld \r\nDone world\r\n", + Toolbox::SubstituteVariables( + "Hello ${WORLD}\r\nWorld ${HELLO}\r\nDone ${WORLD}\r\n", + env)); + + ASSERT_EQ("world A a B world C 'c' D {\"a\":\"b\"} E ", + Toolbox::SubstituteVariables( + "${WORLD} A ${WORLD2:-a} B ${WORLD:-b} C ${WORLD2:-\"'c'\"} D ${WORLD2:-'{\"a\":\"b\"}'} E ${WORLD2:-}", + env)); + + SystemToolbox::GetEnvironmentVariables(env); + ASSERT_TRUE(env.find("NOPE") == env.end()); + + // The "PATH" environment variable should always be available on + // machines running the unit tests + ASSERT_TRUE(env.find("PATH") != env.end()); + + ASSERT_EQ("A" + env["PATH"] + "B", + Toolbox::SubstituteVariables("A${PATH}B", env)); +} + + int main(int argc, char **argv) { Logging::Initialize(); diff -r 3fabf9a673f6 -r eff50153a7b3 UnitTestsSources/VersionsTests.cpp --- a/UnitTestsSources/VersionsTests.cpp Thu Oct 18 10:48:11 2018 +0200 +++ b/UnitTestsSources/VersionsTests.cpp Thu Dec 06 15:58:08 2018 +0100 @@ -104,7 +104,7 @@ TEST(Versions, BoostStatic) { - ASSERT_STREQ("1_67", BOOST_LIB_VERSION); + ASSERT_STREQ("1_68", BOOST_LIB_VERSION); } TEST(Versions, CurlStatic) @@ -141,7 +141,7 @@ TEST(Version, LuaStatic) { - ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE); + ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE); } TEST(Version, LibIconvStatic)