# HG changeset patch # User Sebastien Jodogne # Date 1447838181 -3600 # Node ID c131566b825255ce84dc83c563358f3f9c82702a # Parent 1b82bb0446d2ad0d7a7a1d5840b4a8ffb739cd3e# Parent 2dbf25006f888768e5026e4d717f4f4be65f5189 integration mainline->dcmtk-3.6.1 diff -r 1b82bb0446d2 -r c131566b8252 CMakeLists.txt --- a/CMakeLists.txt Wed Sep 23 10:29:06 2015 +0200 +++ b/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 @@ -10,8 +10,9 @@ # * Orthanc 0.3.1 = version 2 # * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3 # * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4 -# * Orthanc 0.8.5 -> mainline = version 5 -set(ORTHANC_DATABASE_VERSION 5) +# * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5 +# * Orthanc 0.9.5 -> mainline = version 6 +set(ORTHANC_DATABASE_VERSION 6) ##################################################################### @@ -95,6 +96,7 @@ Core/DicomFormat/DicomImageInformation.cpp Core/DicomFormat/DicomIntegerPixelAccessor.cpp Core/DicomFormat/DicomInstanceHasher.cpp + Core/DicomFormat/DicomValue.cpp Core/Enumerations.cpp Core/FileStorage/FilesystemStorage.cpp Core/FileStorage/StorageAccessor.cpp @@ -108,6 +110,7 @@ Core/HttpServer/MongooseServer.cpp Core/HttpServer/HttpFileSender.cpp Core/HttpServer/FilesystemHttpSender.cpp + Core/HttpServer/HttpContentNegociation.cpp Core/HttpServer/HttpStreamTranscoder.cpp Core/Logging.cpp Core/RestApi/RestApiCall.cpp @@ -116,9 +119,9 @@ Core/RestApi/RestApiPath.cpp Core/RestApi/RestApiOutput.cpp Core/RestApi/RestApi.cpp - Core/MultiThreading/BagOfRunnablesBySteps.cpp Core/MultiThreading/Mutex.cpp Core/MultiThreading/ReaderWriterLock.cpp + Core/MultiThreading/RunnableWorkersPool.cpp Core/MultiThreading/Semaphore.cpp Core/MultiThreading/SharedMessageQueue.cpp Core/Images/Font.cpp @@ -145,21 +148,27 @@ set(ORTHANC_SERVER_SOURCES + OrthancServer/DatabaseWrapper.cpp + OrthancServer/DatabaseWrapperBase.cpp + OrthancServer/DicomDirWriter.cpp + OrthancServer/DicomModification.cpp OrthancServer/DicomProtocol/DicomFindAnswers.cpp OrthancServer/DicomProtocol/DicomServer.cpp OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/DicomProtocol/RemoteModalityParameters.cpp OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp - OrthancServer/DicomModification.cpp + OrthancServer/ExportedResource.cpp OrthancServer/FromDcmtkBridge.cpp - OrthancServer/ParsedDicomFile.cpp - OrthancServer/DicomDirWriter.cpp OrthancServer/Internals/CommandDispatcher.cpp + OrthancServer/Internals/DicomImageDecoder.cpp OrthancServer/Internals/FindScp.cpp OrthancServer/Internals/MoveScp.cpp OrthancServer/Internals/StoreScp.cpp - OrthancServer/Internals/DicomImageDecoder.cpp + OrthancServer/LuaScripting.cpp + OrthancServer/OrthancFindRequestHandler.cpp + OrthancServer/OrthancHttpHandler.cpp OrthancServer/OrthancInitialization.cpp + OrthancServer/OrthancMoveRequestHandler.cpp OrthancServer/OrthancPeerParameters.cpp OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp @@ -168,20 +177,21 @@ OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp - OrthancServer/ServerIndex.cpp - OrthancServer/ToDcmtkBridge.cpp - OrthancServer/DatabaseWrapper.cpp + OrthancServer/ParsedDicomFile.cpp + OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/LookupIdentifierQuery.cpp + OrthancServer/Search/LookupResource.cpp + OrthancServer/Search/SetOfResources.cpp + OrthancServer/Search/ListConstraint.cpp + OrthancServer/Search/RangeConstraint.cpp + OrthancServer/Search/ValueConstraint.cpp + OrthancServer/Search/WildcardConstraint.cpp OrthancServer/ServerContext.cpp OrthancServer/ServerEnumerations.cpp + OrthancServer/ServerIndex.cpp OrthancServer/ServerToolbox.cpp - OrthancServer/OrthancFindRequestHandler.cpp - OrthancServer/OrthancMoveRequestHandler.cpp - OrthancServer/ExportedResource.cpp - OrthancServer/ResourceFinder.cpp - OrthancServer/DicomFindQuery.cpp - OrthancServer/QueryRetrieveHandler.cpp - OrthancServer/LuaScripting.cpp - OrthancServer/OrthancHttpHandler.cpp + OrthancServer/SliceOrdering.cpp + OrthancServer/ToDcmtkBridge.cpp # From "lua-scripting" branch OrthancServer/DicomInstanceToStore.cpp @@ -222,6 +232,7 @@ Plugins/Engine/OrthancPluginDatabase.cpp Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/PluginsEnumerations.cpp + Plugins/Engine/PluginsErrorDictionary.cpp Plugins/Engine/PluginsManager.cpp Plugins/Engine/SharedLibrary.cpp ) diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomArray.cpp --- a/Core/DicomFormat/DicomArray.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomArray.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -63,7 +63,8 @@ for (size_t i = 0; i < elements_.size(); i++) { DicomTag t = elements_[i]->GetTag(); - std::string s = elements_[i]->GetValue().AsString(); + const DicomValue& v = elements_[i]->GetValue(); + std::string s = v.IsNull() ? "(null)" : v.GetContent(); printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str()); } } diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomImageInformation.cpp --- a/Core/DicomFormat/DicomImageInformation.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomImageInformation.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -54,7 +54,7 @@ try { - std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).AsString(); + std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).GetContent(); Toolbox::ToUpperCase(p); if (p == "RGB") @@ -114,13 +114,13 @@ photometric_ = PhotometricInterpretation_Unknown; } - width_ = boost::lexical_cast(values.GetValue(DICOM_TAG_COLUMNS).AsString()); - height_ = boost::lexical_cast(values.GetValue(DICOM_TAG_ROWS).AsString()); - bitsAllocated_ = boost::lexical_cast(values.GetValue(DICOM_TAG_BITS_ALLOCATED).AsString()); + width_ = boost::lexical_cast(values.GetValue(DICOM_TAG_COLUMNS).GetContent()); + height_ = boost::lexical_cast(values.GetValue(DICOM_TAG_ROWS).GetContent()); + bitsAllocated_ = boost::lexical_cast(values.GetValue(DICOM_TAG_BITS_ALLOCATED).GetContent()); try { - samplesPerPixel_ = boost::lexical_cast(values.GetValue(DICOM_TAG_SAMPLES_PER_PIXEL).AsString()); + samplesPerPixel_ = boost::lexical_cast(values.GetValue(DICOM_TAG_SAMPLES_PER_PIXEL).GetContent()); } catch (OrthancException&) { @@ -129,7 +129,7 @@ try { - bitsStored_ = boost::lexical_cast(values.GetValue(DICOM_TAG_BITS_STORED).AsString()); + bitsStored_ = boost::lexical_cast(values.GetValue(DICOM_TAG_BITS_STORED).GetContent()); } catch (OrthancException&) { @@ -138,7 +138,7 @@ try { - highBit_ = boost::lexical_cast(values.GetValue(DICOM_TAG_HIGH_BIT).AsString()); + highBit_ = boost::lexical_cast(values.GetValue(DICOM_TAG_HIGH_BIT).GetContent()); } catch (OrthancException&) { @@ -147,7 +147,7 @@ try { - pixelRepresentation = boost::lexical_cast(values.GetValue(DICOM_TAG_PIXEL_REPRESENTATION).AsString()); + pixelRepresentation = boost::lexical_cast(values.GetValue(DICOM_TAG_PIXEL_REPRESENTATION).GetContent()); } catch (OrthancException&) { @@ -160,7 +160,7 @@ // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/ try { - planarConfiguration = boost::lexical_cast(values.GetValue(DICOM_TAG_PLANAR_CONFIGURATION).AsString()); + planarConfiguration = boost::lexical_cast(values.GetValue(DICOM_TAG_PLANAR_CONFIGURATION).GetContent()); } catch (OrthancException&) { @@ -179,9 +179,9 @@ try { - numberOfFrames_ = boost::lexical_cast(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).AsString()); + numberOfFrames_ = boost::lexical_cast(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); } - catch (OrthancException) + catch (OrthancException&) { // If the tag "NumberOfFrames" is absent, assume there is a single frame numberOfFrames_ = 1; diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomInstanceHasher.cpp --- a/Core/DicomFormat/DicomInstanceHasher.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomInstanceHasher.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -60,10 +60,10 @@ { const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID); - Setup(patientId == NULL ? "" : patientId->AsString(), - instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(), - instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(), - instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString()); + Setup(patientId == NULL ? "" : patientId->GetContent(), + instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(), + instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(), + instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent()); } const std::string& DicomInstanceHasher::HashPatient() diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -35,7 +35,6 @@ #include #include -#include "DicomString.h" #include "DicomArray.h" #include "../OrthancException.h" @@ -57,10 +56,10 @@ { //DicomTag(0x0010, 0x1020), // PatientSize //DicomTag(0x0010, 0x1030) // PatientWeight - DicomTag(0x0008, 0x0020), // StudyDate + DICOM_TAG_STUDY_DATE, DicomTag(0x0008, 0x0030), // StudyTime - DicomTag(0x0008, 0x1030), // StudyDescription DicomTag(0x0020, 0x0010), // StudyID + DICOM_TAG_STUDY_DESCRIPTION, DICOM_TAG_ACCESSION_NUMBER, DICOM_TAG_STUDY_INSTANCE_UID }; @@ -70,10 +69,10 @@ //DicomTag(0x0010, 0x1080), // MilitaryRank DicomTag(0x0008, 0x0021), // SeriesDate DicomTag(0x0008, 0x0031), // SeriesTime - DicomTag(0x0008, 0x0060), // Modality + DICOM_TAG_MODALITY, DicomTag(0x0008, 0x0070), // Manufacturer DicomTag(0x0008, 0x1010), // StationName - DicomTag(0x0008, 0x103e), // SeriesDescription + DICOM_TAG_SERIES_DESCRIPTION, DicomTag(0x0018, 0x0015), // BodyPartExamined DicomTag(0x0018, 0x0024), // SequenceName DicomTag(0x0018, 0x1030), // ProtocolName @@ -83,7 +82,8 @@ DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, DICOM_TAG_NUMBER_OF_SLICES, DICOM_TAG_NUMBER_OF_TIME_SLICES, - DICOM_TAG_SERIES_INSTANCE_UID + DICOM_TAG_SERIES_INSTANCE_UID, + DICOM_TAG_IMAGE_ORIENTATION_PATIENT // New in db v6 }; static DicomTag instanceTags[] = @@ -95,10 +95,41 @@ DICOM_TAG_INSTANCE_NUMBER, DICOM_TAG_NUMBER_OF_FRAMES, DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, - DICOM_TAG_SOP_INSTANCE_UID + DICOM_TAG_SOP_INSTANCE_UID, + DICOM_TAG_IMAGE_POSITION_PATIENT // New in db v6 }; + void DicomMap::LoadMainDicomTags(const DicomTag*& tags, + size_t& size, + ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + tags = patientTags; + size = sizeof(patientTags) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyTags; + size = sizeof(studyTags) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesTags; + size = sizeof(seriesTags) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceTags; + size = sizeof(instanceTags) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } void DicomMap::SetValue(uint16_t group, @@ -283,6 +314,7 @@ result.Remove(DICOM_TAG_NUMBER_OF_SLICES); result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS); result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES); + result.Remove(DICOM_TAG_IMAGE_ORIENTATION_PATIENT); } void DicomMap::SetupFindInstanceTemplate(DicomMap& result) diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomMap.h Wed Nov 18 10:16:21 2015 +0100 @@ -34,7 +34,6 @@ #include "DicomTag.h" #include "DicomValue.h" -#include "DicomString.h" #include "../Enumerations.h" #include @@ -105,14 +104,14 @@ void SetValue(const DicomTag& tag, const std::string& str) { - SetValue(tag, new DicomString(str)); + SetValue(tag, new DicomValue(str, false)); } void SetValue(uint16_t group, uint16_t element, const std::string& str) { - SetValue(group, element, new DicomString(str)); + SetValue(group, element, new DicomValue(str, false)); } bool HasTag(uint16_t group, uint16_t element) const @@ -173,5 +172,9 @@ void Print(FILE* fp) const; void GetTags(std::set& tags) const; + + static void LoadMainDicomTags(const DicomTag*& tags, + size_t& size, + ResourceType level); }; } diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomNullValue.h --- a/Core/DicomFormat/DicomNullValue.h Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "DicomValue.h" - -namespace Orthanc -{ - class DicomNullValue : public DicomValue - { - public: - DicomNullValue() - { - } - - virtual DicomValue* Clone() const - { - return new DicomNullValue(); - } - - virtual std::string AsString() const - { - return "(null)"; - } - - virtual bool IsNull() const - { - return true; - } - }; -} diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomString.h --- a/Core/DicomFormat/DicomString.h Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "DicomValue.h" - -namespace Orthanc -{ - class DicomString : public DicomValue - { - private: - std::string value_; - - public: - DicomString(const std::string& v) : value_(v) - { - } - - DicomString(const char* v) - { - if (v) - value_ = v; - else - value_ = ""; - } - - virtual DicomValue* Clone() const - { - return new DicomString(value_); - } - - virtual std::string AsString() const - { - return value_; - } - }; -} diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomTag.cpp --- a/Core/DicomFormat/DicomTag.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -114,6 +114,12 @@ if (*this == DICOM_TAG_PATIENT_NAME) return "PatientName"; + if (*this == DICOM_TAG_IMAGE_POSITION_PATIENT) + return "ImagePositionPatient"; + + if (*this == DICOM_TAG_IMAGE_ORIENTATION_PATIENT) + return "ImageOrientationPatient"; + return ""; } @@ -243,14 +249,4 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } - - - bool DicomTag::IsIdentifier() const - { - return (*this == DICOM_TAG_PATIENT_ID || - *this == DICOM_TAG_STUDY_INSTANCE_UID || - *this == DICOM_TAG_ACCESSION_NUMBER || - *this == DICOM_TAG_SERIES_INSTANCE_UID || - *this == DICOM_TAG_SOP_INSTANCE_UID); - } } diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Wed Nov 18 10:16:21 2015 +0100 @@ -86,8 +86,6 @@ static void AddTagsForModule(std::set& target, DicomModule module); - - bool IsIdentifier() const; }; // Aliases for the most useful tags @@ -109,6 +107,10 @@ static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010); static const DicomTag DICOM_TAG_ENCAPSULATED_DOCUMENT(0x0042, 0x0011); + static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030); + static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e); + static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060); + // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002); @@ -134,6 +136,8 @@ static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103); static const DicomTag DICOM_TAG_PLANAR_CONFIGURATION(0x0028, 0x0006); static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004); + static const DicomTag DICOM_TAG_IMAGE_ORIENTATION_PATIENT(0x0020, 0x0037); + static const DicomTag DICOM_TAG_IMAGE_POSITION_PATIENT(0x0020, 0x0032); // Tags related to date and time static const DicomTag DICOM_TAG_ACQUISITION_DATE(0x0008, 0x0022); diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomValue.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomFormat/DicomValue.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomValue.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" + +namespace Orthanc +{ + DicomValue::DicomValue(const DicomValue& other) : + type_(other.type_), + content_(other.content_) + { + } + + + DicomValue::DicomValue(const std::string& content, + bool isBinary) : + type_(isBinary ? Type_Binary : Type_String), + content_(content) + { + } + + + DicomValue::DicomValue(const char* data, + size_t size, + bool isBinary) : + type_(isBinary ? Type_Binary : Type_String) + { + content_.assign(data, size); + } + + + const std::string& DicomValue::GetContent() const + { + if (type_ == Type_Null) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return content_; + } + } + + + DicomValue* DicomValue::Clone() const + { + return new DicomValue(*this); + } + + +#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 + void DicomValue::FormatDataUriScheme(std::string& target, + const std::string& mime) const + { + Toolbox::EncodeBase64(target, GetContent()); + target.insert(0, "data:" + mime + ";base64,"); + } +#endif + +} diff -r 1b82bb0446d2 -r c131566b8252 Core/DicomFormat/DicomValue.h --- a/Core/DicomFormat/DicomValue.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/DicomFormat/DicomValue.h Wed Nov 18 10:16:21 2015 +0100 @@ -32,22 +32,60 @@ #pragma once -#include "../IDynamicObject.h" - #include +#include namespace Orthanc { - class DicomValue : public IDynamicObject + class DicomValue : public boost::noncopyable { - public: - virtual DicomValue* Clone() const = 0; + private: + enum Type + { + Type_Null, + Type_String, + Type_Binary + }; + + Type type_; + std::string content_; + + DicomValue(const DicomValue& other); - virtual std::string AsString() const = 0; + public: + DicomValue() : type_(Type_Null) + { + } + + DicomValue(const std::string& content, + bool isBinary); + + DicomValue(const char* data, + size_t size, + bool isBinary); + + const std::string& GetContent() const; - virtual bool IsNull() const + bool IsNull() const + { + return type_ == Type_Null; + } + + bool IsBinary() const { - return false; + return type_ == Type_Binary; } + + DicomValue* Clone() const; + +#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 + void FormatDataUriScheme(std::string& target, + const std::string& mime) const; + + void FormatDataUriScheme(std::string& target) const + { + FormatDataUriScheme(target, "application/octet-stream"); + } +#endif }; } diff -r 1b82bb0446d2 -r c131566b8252 Core/EnumerationDictionary.h --- a/Core/EnumerationDictionary.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/EnumerationDictionary.h Wed Nov 18 10:16:21 2015 +0100 @@ -60,6 +60,11 @@ stringToEnumeration_.clear(); } + bool Contains(Enumeration value) const + { + return enumerationToString_.find(value) != enumerationToString_.end(); + } + void Add(Enumeration value, const std::string& str) { // Check if these values are free diff -r 1b82bb0446d2 -r c131566b8252 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Enumerations.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -142,6 +142,18 @@ case ErrorCode_BadFont: return "Badly formatted font file"; + case ErrorCode_DatabasePlugin: + return "The plugin implementing a custom database back-end does not fulfill the proper interface"; + + case ErrorCode_StorageAreaPlugin: + return "Error in the plugin implementing a custom storage area"; + + case ErrorCode_EmptyRequest: + return "The request is empty"; + + case ErrorCode_NotAcceptable: + return "Cannot send a response which is acceptable according to the Accept HTTP header"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -304,11 +316,24 @@ case ErrorCode_DatabaseBackendAlreadyRegistered: return "Another plugin has already registered a custom database back-end"; - case ErrorCode_DatabasePlugin: - return "The plugin implementing a custom database back-end does not fulfill the proper interface"; + case ErrorCode_DatabaseNotInitialized: + return "Plugin trying to call the database during its initialization"; + + case ErrorCode_SslDisabled: + return "Orthanc has been built without SSL support"; + + case ErrorCode_CannotOrderSlices: + return "Unable to order the slices of the series"; default: - return "Unknown error code"; + if (error >= ErrorCode_START_PLUGINS) + { + return "Error encountered within some plugin"; + } + else + { + return "Unknown error code"; + } } } @@ -958,39 +983,6 @@ } - const char* GetMimeType(FileContentType type) - { - switch (type) - { - case FileContentType_Dicom: - return "application/dicom"; - - case FileContentType_DicomAsJson: - return "application/json"; - - default: - return "application/octet-stream"; - } - } - - - const char* GetFileExtension(FileContentType type) - { - switch (type) - { - case FileContentType_Dicom: - return ".dcm"; - - case FileContentType_DicomAsJson: - return ".json"; - - default: - // Unknown file type - return ""; - } - } - - ResourceType GetChildResourceType(ResourceType type) { switch (type) @@ -1142,8 +1134,18 @@ case ErrorCode_Unauthorized: return HttpStatus_401_Unauthorized; + case ErrorCode_NotAcceptable: + return HttpStatus_406_NotAcceptable; + default: return HttpStatus_500_InternalServerError; } } + + + bool IsUserContentType(FileContentType type) + { + return (type >= FileContentType_StartUser && + type <= FileContentType_EndUser); + } } diff -r 1b82bb0446d2 -r c131566b8252 Core/Enumerations.h --- a/Core/Enumerations.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Enumerations.h Wed Nov 18 10:16:21 2015 +0100 @@ -77,6 +77,10 @@ ErrorCode_BadJson = 28 /*!< Cannot parse a JSON document */, ErrorCode_Unauthorized = 29 /*!< Bad credentials were provided to an HTTP request */, ErrorCode_BadFont = 30 /*!< Badly formatted font file */, + ErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, + ErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, + ErrorCode_EmptyRequest = 33 /*!< The request is empty */, + ErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -131,7 +135,10 @@ ErrorCode_LuaReturnsNoString = 2035 /*!< The Lua function does not return a string */, ErrorCode_StorageAreaAlreadyRegistered = 2036 /*!< Another plugin has already registered a custom storage area */, ErrorCode_DatabaseBackendAlreadyRegistered = 2037 /*!< Another plugin has already registered a custom database back-end */, - ErrorCode_DatabasePlugin = 2038 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */ + ErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, + ErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, + ErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + ErrorCode_START_PLUGINS = 1000000 }; enum LogLevel @@ -448,10 +455,6 @@ bool GetDicomEncoding(Encoding& encoding, const char* specificCharacterSet); - const char* GetMimeType(FileContentType type); - - const char* GetFileExtension(FileContentType type); - ResourceType GetChildResourceType(ResourceType type); ResourceType GetParentResourceType(ResourceType type); @@ -461,4 +464,6 @@ const char* GetDicomSpecificCharacterSet(Encoding encoding); HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error); + + bool IsUserContentType(FileContentType type); } diff -r 1b82bb0446d2 -r c131566b8252 Core/FileStorage/StorageAccessor.cpp --- a/Core/FileStorage/StorageAccessor.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/FileStorage/StorageAccessor.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -126,20 +126,53 @@ } + void StorageAccessor::Read(Json::Value& content, + const FileInfo& info) + { + std::string s; + Read(s, info); + + Json::Reader reader; + if (!reader.parse(s, content)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + void StorageAccessor::SetupSender(BufferHttpSender& sender, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType()); - sender.SetContentType(GetMimeType(info.GetContentType())); - sender.SetContentFilename(info.GetUuid() + std::string(GetFileExtension(info.GetContentType()))); + sender.SetContentType(mime); + + const char* extension; + switch (info.GetContentType()) + { + case FileContentType_Dicom: + extension = ".dcm"; + break; + + case FileContentType_DicomAsJson: + extension = ".json"; + break; + + default: + // Non-standard content type + extension = ""; + } + + sender.SetContentFilename(info.GetUuid() + std::string(extension)); } void StorageAccessor::AnswerFile(HttpOutput& output, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { BufferHttpSender sender; - SetupSender(sender, info); + SetupSender(sender, info, mime); HttpStreamTranscoder transcoder(sender, info.GetCompressionType()); output.Answer(transcoder); @@ -147,10 +180,11 @@ void StorageAccessor::AnswerFile(RestApiOutput& output, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { BufferHttpSender sender; - SetupSender(sender, info); + SetupSender(sender, info, mime); HttpStreamTranscoder transcoder(sender, info.GetCompressionType()); output.AnswerStream(transcoder); diff -r 1b82bb0446d2 -r c131566b8252 Core/FileStorage/StorageAccessor.h --- a/Core/FileStorage/StorageAccessor.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/FileStorage/StorageAccessor.h Wed Nov 18 10:16:21 2015 +0100 @@ -41,6 +41,7 @@ #include #include #include +#include namespace Orthanc { @@ -50,7 +51,8 @@ IStorageArea& area_; void SetupSender(BufferHttpSender& sender, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); public: StorageAccessor(IStorageArea& area) : area_(area) @@ -75,15 +77,20 @@ void Read(std::string& content, const FileInfo& info); + void Read(Json::Value& content, + const FileInfo& info); + void Remove(const FileInfo& info) { area_.Remove(info.GetUuid(), info.GetContentType()); } void AnswerFile(HttpOutput& output, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); void AnswerFile(RestApiOutput& output, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); }; } diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpContentNegociation.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpContentNegociation.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,265 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpContentNegociation.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + +namespace Orthanc +{ + HttpContentNegociation::Handler::Handler(const std::string& type, + const std::string& subtype, + IHandler& handler) : + type_(type), + subtype_(subtype), + handler_(handler) + { + } + + + bool HttpContentNegociation::Handler::IsMatch(const std::string& type, + const std::string& subtype) const + { + if (type == "*" && subtype == "*") + { + return true; + } + + if (subtype == "*" && type == type_) + { + return true; + } + + return type == type_ && subtype == subtype_; + } + + + struct HttpContentNegociation::Reference : public boost::noncopyable + { + const Handler& handler_; + uint8_t level_; + float quality_; + + Reference(const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality) : + handler_(handler), + quality_(quality) + { + if (type == "*" && subtype == "*") + { + level_ = 0; + } + else if (subtype == "*") + { + level_ = 1; + } + else + { + level_ = 2; + } + } + + bool operator< (const Reference& other) const + { + if (level_ < other.level_) + { + return true; + } + + if (level_ > other.level_) + { + return false; + } + + return quality_ < other.quality_; + } + }; + + + bool HttpContentNegociation::SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator) + { + size_t pos = source.find(separator); + + if (pos == std::string::npos) + { + return false; + } + else + { + first = Toolbox::StripSpaces(source.substr(0, pos)); + second = Toolbox::StripSpaces(source.substr(pos + 1)); + return true; + } + } + + + float HttpContentNegociation::GetQuality(const Tokens& parameters) + { + for (size_t i = 1; i < parameters.size(); i++) + { + std::string key, value; + if (SplitPair(key, value, parameters[i], '=') && + key == "q") + { + float quality; + bool ok = false; + + try + { + quality = boost::lexical_cast(value); + ok = (quality >= 0.0f && quality <= 1.0f); + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + return quality; + } + else + { + LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value; + throw OrthancException(ErrorCode_BadRequest); + } + } + } + + return 1.0f; // Default quality + } + + + void HttpContentNegociation::SelectBestMatch(std::auto_ptr& best, + const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality) + { + std::auto_ptr match(new Reference(handler, type, subtype, quality)); + + if (best.get() == NULL || + *best < *match) + { + best = match; + } + } + + + void HttpContentNegociation::Register(const std::string& mime, + IHandler& handler) + { + std::string type, subtype; + + if (SplitPair(type, subtype, mime, '/') && + type != "*" && + subtype != "*") + { + handlers_.push_back(Handler(type, subtype, handler)); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool HttpContentNegociation::Apply(const HttpHeaders& headers) + { + HttpHeaders::const_iterator accept = headers.find("accept"); + if (accept != headers.end()) + { + return Apply(accept->second); + } + else + { + return Apply("*/*"); + } + } + + + bool HttpContentNegociation::Apply(const std::string& accept) + { + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + // https://en.wikipedia.org/wiki/Content_negotiation + // http://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers + + Tokens mediaRanges; + Toolbox::TokenizeString(mediaRanges, accept, ','); + + std::auto_ptr bestMatch; + + for (Tokens::const_iterator it = mediaRanges.begin(); + it != mediaRanges.end(); ++it) + { + Tokens parameters; + Toolbox::TokenizeString(parameters, *it, ';'); + + if (parameters.size() > 0) + { + float quality = GetQuality(parameters); + + std::string type, subtype; + if (SplitPair(type, subtype, parameters[0], '/')) + { + for (Handlers::const_iterator it2 = handlers_.begin(); + it2 != handlers_.end(); ++it2) + { + if (it2->IsMatch(type, subtype)) + { + SelectBestMatch(bestMatch, *it2, type, subtype, quality); + } + } + } + } + } + + if (bestMatch.get() == NULL) // No match was found + { + return false; + } + else + { + bestMatch->handler_.Call(); + return true; + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpContentNegociation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpContentNegociation.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,111 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +namespace Orthanc +{ + class HttpContentNegociation : public boost::noncopyable + { + public: + typedef std::map HttpHeaders; + + class IHandler : public boost::noncopyable + { + public: + virtual ~IHandler() + { + } + + virtual void Handle(const std::string& type, + const std::string& subtype) = 0; + }; + + private: + struct Handler + { + std::string type_; + std::string subtype_; + IHandler& handler_; + + Handler(const std::string& type, + const std::string& subtype, + IHandler& handler); + + bool IsMatch(const std::string& type, + const std::string& subtype) const; + + void Call() const + { + handler_.Handle(type_, subtype_); + } + }; + + + struct Reference; + + typedef std::vector Tokens; + typedef std::list Handlers; + + Handlers handlers_; + + + static bool SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator); + + static float GetQuality(const Tokens& parameters); + + static void SelectBestMatch(std::auto_ptr& best, + const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality); + + public: + void Register(const std::string& mime, + IHandler& handler); + + bool Apply(const HttpHeaders& headers); + + bool Apply(const std::string& accept); + }; +} diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -302,15 +302,7 @@ stateMachine_.ClearHeaders(); stateMachine_.SetHttpStatus(status); - - if (describeErrors_) - { - stateMachine_.SendBody(message, messageSize); - } - else - { - stateMachine_.SendBody(NULL, 0); - } + stateMachine_.SendBody(message, messageSize); } diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpOutput.h --- a/Core/HttpServer/HttpOutput.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/HttpOutput.h Wed Nov 18 10:16:21 2015 +0100 @@ -114,7 +114,6 @@ StateMachine stateMachine_; bool isDeflateAllowed_; bool isGzipAllowed_; - bool describeErrors_; HttpCompression GetPreferredCompression(size_t bodySize) const; @@ -123,8 +122,7 @@ bool isKeepAlive) : stateMachine_(stream, isKeepAlive), isDeflateAllowed_(false), - isGzipAllowed_(false), - describeErrors_(true) + isGzipAllowed_(false) { } @@ -148,16 +146,6 @@ return isGzipAllowed_; } - void SetDescribeErrorsEnabled(bool enabled) - { - describeErrors_ = enabled; - } - - bool IsDescribeErrorsEnabled() const - { - return describeErrors_; - } - void SendStatus(HttpStatus status, const char* message, size_t messageSize); diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpToolbox.cpp --- a/Core/HttpServer/HttpToolbox.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/HttpToolbox.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -201,10 +201,9 @@ bool HttpToolbox::SimpleGet(std::string& result, IHttpHandler& handler, RequestOrigin origin, - const std::string& uri) + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders) { - IHttpHandler::Arguments headers; // No HTTP header - UriComponents curi; IHttpHandler::GetArguments getArguments; ParseGetQuery(curi, getArguments, uri.c_str()); @@ -213,7 +212,7 @@ HttpOutput http(stream, false /* no keep alive */); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, - headers, getArguments, NULL /* no body for GET */, 0)) + httpHeaders, getArguments, NULL /* no body for GET */, 0)) { stream.GetOutput(result); return true; @@ -225,6 +224,16 @@ } + bool HttpToolbox::SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri) + { + IHttpHandler::Arguments headers; // No HTTP header + return SimpleGet(result, handler, origin, uri, headers); + } + + static bool SimplePostOrPut(std::string& result, IHttpHandler& handler, RequestOrigin origin, diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/HttpToolbox.h --- a/Core/HttpServer/HttpToolbox.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/HttpToolbox.h Wed Nov 18 10:16:21 2015 +0100 @@ -65,6 +65,12 @@ RequestOrigin origin, const std::string& uri); + static bool SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders); + static bool SimplePost(std::string& result, IHttpHandler& handler, RequestOrigin origin, diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -36,7 +36,6 @@ #include "MongooseServer.h" #include "../Logging.h" -#include "../OrthancException.h" #include "../ChunkedBuffer.h" #include "HttpToolbox.h" #include "mongoose.h" @@ -583,7 +582,6 @@ MongooseOutputStream stream(connection); HttpOutput output(stream, that->IsKeepAliveEnabled()); - output.SetDescribeErrorsEnabled(that->IsDescribeErrorsEnabled()); // Check remote calls if (!that->IsRemoteAccessAllowed() && @@ -735,12 +733,12 @@ } catch (boost::bad_lexical_cast&) { - throw OrthancException(ErrorCode_BadParameterType, HttpStatus_400_BadRequest); + throw OrthancException(ErrorCode_BadParameterType); } catch (std::runtime_error&) { // Presumably an error while parsing the JSON body - throw OrthancException(ErrorCode_BadRequest, HttpStatus_400_BadRequest); + throw OrthancException(ErrorCode_BadRequest); } if (!found) @@ -751,22 +749,17 @@ catch (OrthancException& e) { // Using this candidate handler results in an exception - LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); - - Json::Value message = Json::objectValue; - message["HttpError"] = EnumerationToString(e.GetHttpStatus()); - message["HttpStatus"] = e.GetHttpStatus(); - message["Message"] = e.What(); - message["Method"] = EnumerationToString(method); - message["OrthancError"] = EnumerationToString(e.GetErrorCode()); - message["OrthancStatus"] = e.GetErrorCode(); - message["Uri"] = request->uri; - - std::string info = message.toStyledString(); - try { - output.SendStatus(e.GetHttpStatus(), info); + if (that->GetExceptionFormatter() == NULL) + { + LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); + output.SendStatus(e.GetHttpStatus()); + } + else + { + that->GetExceptionFormatter()->Format(output, e, method, request->uri); + } } catch (OrthancException&) { @@ -832,7 +825,7 @@ filter_ = NULL; keepAlive_ = false; httpCompression_ = true; - describeErrors_ = true; + exceptionFormatter_ = NULL; #if ORTHANC_SSL_ENABLED == 1 // Check for the Heartbleed exploit @@ -938,7 +931,7 @@ #if ORTHANC_SSL_ENABLED == 0 if (enabled) { - throw OrthancException("Orthanc has been built without SSL support"); + throw OrthancException(ErrorCode_SslDisabled); } else { @@ -983,18 +976,20 @@ LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled"); } - void MongooseServer::SetDescribeErrorsEnabled(bool enabled) - { - describeErrors_ = enabled; - LOG(INFO) << "Description of the errors in the HTTP answers is " << (enabled ? "enabled" : "disabled"); - } - void MongooseServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter) { Stop(); filter_ = &filter; } + + void MongooseServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter) + { + Stop(); + exceptionFormatter_ = &formatter; + } + + bool MongooseServer::IsValidBasicHttpAuthentication(const std::string& basic) const { return registeredUsers_.find(basic) != registeredUsers_.end(); diff -r 1b82bb0446d2 -r c131566b8252 Core/HttpServer/MongooseServer.h --- a/Core/HttpServer/MongooseServer.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/HttpServer/MongooseServer.h Wed Nov 18 10:16:21 2015 +0100 @@ -34,6 +34,8 @@ #include "IHttpHandler.h" +#include "../OrthancException.h" + #include #include #include @@ -57,6 +59,21 @@ const char* username) const = 0; }; + + class IHttpExceptionFormatter + { + public: + virtual ~IHttpExceptionFormatter() + { + } + + virtual void Format(HttpOutput& output, + const OrthancException& exception, + HttpMethod method, + const char* uri) = 0; + }; + + class MongooseServer { private: @@ -77,7 +94,7 @@ IIncomingHttpRequestFilter* filter_; bool keepAlive_; bool httpCompression_; - bool describeErrors_; + IHttpExceptionFormatter* exceptionFormatter_; bool IsRunning() const; @@ -144,13 +161,6 @@ void SetHttpCompressionEnabled(bool enabled); - bool IsDescribeErrorsEnabled() const - { - return describeErrors_; - } - - void SetDescribeErrorsEnabled(bool enabled); - const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const { return filter_; @@ -170,5 +180,12 @@ } IHttpHandler& GetHandler() const; + + void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter); + + IHttpExceptionFormatter* GetExceptionFormatter() + { + return exceptionFormatter_; + } }; } diff -r 1b82bb0446d2 -r c131566b8252 Core/Images/Font.cpp --- a/Core/Images/Font.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Images/Font.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -118,7 +118,7 @@ throw OrthancException(ErrorCode_BadFont); } - c->bitmap_[j] = value; + c->bitmap_[j] = static_cast(value); } int index = boost::lexical_cast(characters[i]); @@ -166,7 +166,7 @@ unsigned int width = MyMin(character.width_, target.GetWidth() - x); unsigned int height = MyMin(character.height_, target.GetHeight() - y); - uint8_t bpp = target.GetBytesPerPixel(); + unsigned int bpp = target.GetBytesPerPixel(); // Blit the font bitmap OVER the target image // https://en.wikipedia.org/wiki/Alpha_compositing @@ -184,7 +184,8 @@ for (unsigned int cx = left; cx < width; cx++, pos++, p++) { uint16_t alpha = character.bitmap_[pos]; - *p = (alpha * static_cast(color[0]) + (255 - alpha) * static_cast(*p)) >> 8; + uint16_t value = alpha * static_cast(color[0]) + (255 - alpha) * static_cast(*p); + *p = static_cast(value >> 8); } break; @@ -196,9 +197,11 @@ for (unsigned int cx = left; cx < width; cx++, pos++, p += 3) { uint16_t alpha = character.bitmap_[pos]; - p[0] = (alpha * static_cast(color[0]) + (255 - alpha) * static_cast(p[0])) >> 8; - p[1] = (alpha * static_cast(color[1]) + (255 - alpha) * static_cast(p[1])) >> 8; - p[2] = (alpha * static_cast(color[2]) + (255 - alpha) * static_cast(p[2])) >> 8; + for (uint8_t i = 0; i < 3; i++) + { + uint16_t value = alpha * static_cast(color[i]) + (255 - alpha) * static_cast(p[i]); + p[i] = static_cast(value >> 8); + } } break; diff -r 1b82bb0446d2 -r c131566b8252 Core/Images/JpegWriter.cpp --- a/Core/Images/JpegWriter.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Images/JpegWriter.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -69,7 +69,7 @@ unsigned int width, unsigned int height, PixelFormat format, - int quality) + uint8_t quality) { cinfo.image_width = width; cinfo.image_height = height; diff -r 1b82bb0446d2 -r c131566b8252 Core/Images/JpegWriter.h --- a/Core/Images/JpegWriter.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Images/JpegWriter.h Wed Nov 18 10:16:21 2015 +0100 @@ -42,7 +42,7 @@ class JpegWriter { private: - int quality_; + uint8_t quality_; public: JpegWriter() : quality_(90) diff -r 1b82bb0446d2 -r c131566b8252 Core/Logging.cpp --- a/Core/Logging.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Logging.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -144,9 +144,7 @@ std::ostream* warning_; std::ostream* info_; - std::auto_ptr errorFile_; - std::auto_ptr warningFile_; - std::auto_ptr infoFile_; + std::auto_ptr file_; LoggingState() : infoEnabled_(false), @@ -172,14 +170,14 @@ { static void GetLogPath(boost::filesystem::path& log, boost::filesystem::path& link, - const char* level, + const std::string& suffix, const std::string& directory) { /** From Google Log documentation: Unless otherwise specified, logs will be written to the filename - "...log..", + "...log.", followed by the date, time, and pid (you can't prevent the date, time, and pid from being in the filename). @@ -208,21 +206,17 @@ std::string programName = exe.filename().replace_extension("").string(); - log = (root / (programName + ".log." + - std::string(level) + "." + - std::string(date))); - - link = (root / (programName + "." + std::string(level))); + log = (root / (programName + ".log" + suffix + "." + std::string(date))); + link = (root / (programName + ".log" + suffix)); } - static void PrepareLogFile(std::ostream*& stream, - std::auto_ptr& file, - const char* level, + static void PrepareLogFile(std::auto_ptr& file, + const std::string& suffix, const std::string& directory) { boost::filesystem::path log, link; - GetLogPath(log, link, level, directory); + GetLogPath(log, link, suffix, directory); #if !defined(_WIN32) && (defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))) boost::filesystem::remove(link); @@ -230,7 +224,6 @@ #endif file.reset(new std::ofstream(log.string().c_str())); - stream = file.get(); } @@ -273,9 +266,11 @@ boost::mutex::scoped_lock lock(loggingMutex_); assert(loggingState_.get() != NULL); - PrepareLogFile(loggingState_->error_, loggingState_->errorFile_, "ERROR", path); - PrepareLogFile(loggingState_->warning_, loggingState_->warningFile_, "WARNING", path); - PrepareLogFile(loggingState_->info_, loggingState_->infoFile_, "INFO", path); + PrepareLogFile(loggingState_->file_, "" /* no suffix */, path); + + loggingState_->warning_ = loggingState_->file_.get(); + loggingState_->error_ = loggingState_->file_.get(); + loggingState_->info_ = loggingState_->file_.get(); } InternalLogger::InternalLogger(const char* level, diff -r 1b82bb0446d2 -r c131566b8252 Core/Lua/LuaContext.cpp --- a/Core/Lua/LuaContext.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Lua/LuaContext.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -48,6 +48,20 @@ namespace Orthanc { + static bool OnlyContainsDigits(const std::string& s) + { + for (size_t i = 0; i < s.size(); i++) + { + if (!isdigit(s[i])) + { + return false; + } + } + + return true; + } + + LuaContext& LuaContext::GetLuaContext(lua_State *state) { const void* value = GetGlobalVariable(state, "_LuaContext"); @@ -128,14 +142,21 @@ LuaContext& that = GetLuaContext(state); int nArgs = lua_gettop(state); - if (nArgs != 1) + if ((nArgs != 1 && nArgs != 2) || + (nArgs == 2 && !lua_isboolean(state, 2))) { lua_pushnil(state); return 1; } + bool keepStrings = false; + if (nArgs == 2) + { + keepStrings = lua_toboolean(state, 2) ? true : false; + } + Json::Value json; - that.GetJson(json, 1); + that.GetJson(json, 1, keepStrings); Json::FastWriter writer; std::string s = writer.write(json); @@ -376,7 +397,8 @@ void LuaContext::GetJson(Json::Value& result, - int top) + int top, + bool keepStrings) { if (lua_istable(lua_, top)) { @@ -401,14 +423,15 @@ // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table std::string key(lua_tostring(lua_, -1)); Json::Value v; - GetJson(v, -2); + GetJson(v, -2, keepStrings); tmp[key] = v; size += 1; try { - if (boost::lexical_cast(key) != size) + if (!OnlyContainsDigits(key) || + boost::lexical_cast(key) != size) { isArray = false; } @@ -446,11 +469,13 @@ { result = Json::nullValue; } - else if (lua_isboolean(lua_, top)) + else if (!keepStrings && + lua_isboolean(lua_, top)) { result = lua_toboolean(lua_, top) ? true : false; } - else if (lua_isnumber(lua_, top)) + else if (!keepStrings && + lua_isnumber(lua_, top)) { // Convert to "int" if truncation does not loose precision double value = static_cast(lua_tonumber(lua_, top)); diff -r 1b82bb0446d2 -r c131566b8252 Core/Lua/LuaContext.h --- a/Core/Lua/LuaContext.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Lua/LuaContext.h Wed Nov 18 10:16:21 2015 +0100 @@ -72,7 +72,8 @@ const std::string& command); void GetJson(Json::Value& result, - int top); + int top, + bool keepStrings); public: LuaContext(); diff -r 1b82bb0446d2 -r c131566b8252 Core/Lua/LuaFunctionCall.cpp --- a/Core/Lua/LuaFunctionCall.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Lua/LuaFunctionCall.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -130,10 +130,11 @@ } - void LuaFunctionCall::ExecuteToJson(Json::Value& result) + void LuaFunctionCall::ExecuteToJson(Json::Value& result, + bool keepStrings) { ExecuteInternal(1); - context_.GetJson(result, lua_gettop(context_.lua_)); + context_.GetJson(result, lua_gettop(context_.lua_), keepStrings); } diff -r 1b82bb0446d2 -r c131566b8252 Core/Lua/LuaFunctionCall.h --- a/Core/Lua/LuaFunctionCall.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Lua/LuaFunctionCall.h Wed Nov 18 10:16:21 2015 +0100 @@ -69,7 +69,8 @@ bool ExecutePredicate(); - void ExecuteToJson(Json::Value& result); + void ExecuteToJson(Json::Value& result, + bool keepStrings); void ExecuteToString(std::string& result); }; diff -r 1b82bb0446d2 -r c131566b8252 Core/MultiThreading/BagOfRunnablesBySteps.cpp --- a/Core/MultiThreading/BagOfRunnablesBySteps.cpp Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,185 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeaders.h" -#include "BagOfRunnablesBySteps.h" - -#include "../Logging.h" - -#include -#include - -namespace Orthanc -{ - struct BagOfRunnablesBySteps::PImpl - { - bool continue_; - bool stopFinishListener_; - - boost::mutex mutex_; - boost::condition_variable oneThreadIsStopped_; - boost::condition_variable oneThreadIsJoined_; - - // The list of threads that are waiting to be joined. - typedef std::stack StoppedThreads; - StoppedThreads stoppedThreads_; - - // The set of active runnables, i.e. the runnables that have not - // finished their job yet, plus the runnables that have not been - // joined yet. - typedef std::map ActiveThreads; - ActiveThreads activeThreads_; - - // The thread that joins the runnables after they stop - std::auto_ptr finishListener_; - }; - - - - void BagOfRunnablesBySteps::RunnableThread(BagOfRunnablesBySteps* bag, - IRunnableBySteps* runnable) - { - while (bag->pimpl_->continue_) - { - if (!runnable->Step()) - { - break; - } - } - - { - // Register this runnable as having stopped - boost::mutex::scoped_lock lock(bag->pimpl_->mutex_); - bag->pimpl_->stoppedThreads_.push(runnable); - bag->pimpl_->oneThreadIsStopped_.notify_one(); - } - } - - - void BagOfRunnablesBySteps::FinishListener(BagOfRunnablesBySteps* bag) - { - boost::mutex::scoped_lock lock(bag->pimpl_->mutex_); - - while (!bag->pimpl_->stopFinishListener_) - { - while (!bag->pimpl_->stoppedThreads_.empty()) - { - std::auto_ptr r(bag->pimpl_->stoppedThreads_.top()); - bag->pimpl_->stoppedThreads_.pop(); - - assert(r.get() != NULL); - assert(bag->pimpl_->activeThreads_.find(r.get()) != bag->pimpl_->activeThreads_.end()); - - std::auto_ptr t(bag->pimpl_->activeThreads_[r.get()]); - bag->pimpl_->activeThreads_.erase(r.get()); - - assert(t.get() != NULL); - assert(bag->pimpl_->activeThreads_.find(r.get()) == bag->pimpl_->activeThreads_.end()); - - if (t->joinable()) - { - t->join(); - } - - bag->pimpl_->oneThreadIsJoined_.notify_one(); - } - - bag->pimpl_->oneThreadIsStopped_.wait(lock); - } - } - - - BagOfRunnablesBySteps::BagOfRunnablesBySteps() : pimpl_(new PImpl) - { - pimpl_->continue_ = true; - pimpl_->stopFinishListener_ = false; - - // Everyting is set up, the finish listener can be started - pimpl_->finishListener_.reset(new boost::thread(FinishListener, this)); - } - - - BagOfRunnablesBySteps::~BagOfRunnablesBySteps() - { - if (!pimpl_->stopFinishListener_) - { - LOG(ERROR) << "INTERNAL ERROR: BagOfRunnablesBySteps::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Finalize(); - } - } - - - void BagOfRunnablesBySteps::Add(IRunnableBySteps* runnable) - { - // Make sure the runnable is deleted is something goes wrong - std::auto_ptr runnableRabi(runnable); - - boost::mutex::scoped_lock lock(pimpl_->mutex_); - boost::thread* t(new boost::thread(RunnableThread, this, runnable)); - - pimpl_->activeThreads_.insert(std::make_pair(runnableRabi.release(), t)); - } - - - void BagOfRunnablesBySteps::StopAll() - { - boost::mutex::scoped_lock lock(pimpl_->mutex_); - pimpl_->continue_ = false; - - while (pimpl_->activeThreads_.size() > 0) - { - pimpl_->oneThreadIsJoined_.wait(lock); - } - - pimpl_->continue_ = true; - } - - - - void BagOfRunnablesBySteps::Finalize() - { - if (!pimpl_->stopFinishListener_) - { - StopAll(); - - // Stop the finish listener - pimpl_->stopFinishListener_ = true; - pimpl_->oneThreadIsStopped_.notify_one(); // Awakens the listener - - if (pimpl_->finishListener_->joinable()) - { - pimpl_->finishListener_->join(); - } - } - } - -} diff -r 1b82bb0446d2 -r c131566b8252 Core/MultiThreading/BagOfRunnablesBySteps.h --- a/Core/MultiThreading/BagOfRunnablesBySteps.h Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IRunnableBySteps.h" - -#include -#include - -namespace Orthanc -{ - class BagOfRunnablesBySteps : public boost::noncopyable - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - static void RunnableThread(BagOfRunnablesBySteps* bag, - IRunnableBySteps* runnable); - - static void FinishListener(BagOfRunnablesBySteps* bag); - - public: - BagOfRunnablesBySteps(); - - ~BagOfRunnablesBySteps(); - - void Add(IRunnableBySteps* runnable); - - void StopAll(); - - void Finalize(); - }; -} diff -r 1b82bb0446d2 -r c131566b8252 Core/MultiThreading/IRunnableBySteps.h --- a/Core/MultiThreading/IRunnableBySteps.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/MultiThreading/IRunnableBySteps.h Wed Nov 18 10:16:21 2015 +0100 @@ -32,9 +32,11 @@ #pragma once +#include "../IDynamicObject.h" + namespace Orthanc { - class IRunnableBySteps + class IRunnableBySteps : public IDynamicObject { public: virtual ~IRunnableBySteps() diff -r 1b82bb0446d2 -r c131566b8252 Core/MultiThreading/RunnableWorkersPool.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/RunnableWorkersPool.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,157 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RunnableWorkersPool.h" + +#include "SharedMessageQueue.h" +#include "../OrthancException.h" +#include "../Logging.h" + +namespace Orthanc +{ + struct RunnableWorkersPool::PImpl + { + class Worker + { + private: + const bool& continue_; + SharedMessageQueue& queue_; + boost::thread thread_; + + static void WorkerThread(Worker* that) + { + while (that->continue_) + { + std::auto_ptr obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) + { + try + { + IRunnableBySteps& runnable = *dynamic_cast(obj.get()); + + bool wishToContinue = runnable.Step(); + + if (wishToContinue) + { + // The runnable wishes to continue, reinsert it at the beginning of the queue + that->queue_.Enqueue(obj.release()); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception in a pool of working threads: " << e.What(); + } + } + } + } + + public: + Worker(const bool& globalContinue, + SharedMessageQueue& queue) : + continue_(globalContinue), + queue_(queue) + { + thread_ = boost::thread(WorkerThread, this); + } + + void Join() + { + if (thread_.joinable()) + { + thread_.join(); + } + } + }; + + + bool continue_; + std::vector workers_; + SharedMessageQueue queue_; + }; + + + + RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers) : pimpl_(new PImpl) + { + pimpl_->continue_ = true; + + if (countWorkers <= 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + pimpl_->workers_.resize(countWorkers); + + for (size_t i = 0; i < countWorkers; i++) + { + pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_); + } + } + + + void RunnableWorkersPool::Stop() + { + if (pimpl_->continue_) + { + pimpl_->continue_ = false; + + for (size_t i = 0; i < pimpl_->workers_.size(); i++) + { + PImpl::Worker* worker = pimpl_->workers_[i]; + + if (worker != NULL) + { + worker->Join(); + delete worker; + } + } + } + } + + + RunnableWorkersPool::~RunnableWorkersPool() + { + Stop(); + } + + + void RunnableWorkersPool::Add(IRunnableBySteps* runnable) + { + if (!pimpl_->continue_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->queue_.Enqueue(runnable); + } +} diff -r 1b82bb0446d2 -r c131566b8252 Core/MultiThreading/RunnableWorkersPool.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/RunnableWorkersPool.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IRunnableBySteps.h" + +#include + +namespace Orthanc +{ + class RunnableWorkersPool : public boost::noncopyable + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + void Stop(); + + public: + RunnableWorkersPool(size_t countWorkers); + + ~RunnableWorkersPool(); + + void Add(IRunnableBySteps* runnable); // Takes the ownership + }; +} diff -r 1b82bb0446d2 -r c131566b8252 Core/OrthancException.h --- a/Core/OrthancException.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/OrthancException.h Wed Nov 18 10:16:21 2015 +0100 @@ -32,6 +32,7 @@ #pragma once +#include #include #include "Enumerations.h" @@ -40,7 +41,7 @@ class OrthancException { protected: - ErrorCode errorCode_; + ErrorCode errorCode_; HttpStatus httpStatus_; public: diff -r 1b82bb0446d2 -r c131566b8252 Core/SQLite/Connection.h --- a/Core/SQLite/Connection.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/SQLite/Connection.h Wed Nov 18 10:16:21 2015 +0100 @@ -46,7 +46,7 @@ struct sqlite3; struct sqlite3_stmt; -#define SQLITE_FROM_HERE SQLite::StatementId(__FILE__, __LINE__) +#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__) namespace Orthanc { diff -r 1b82bb0446d2 -r c131566b8252 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Toolbox.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -210,7 +210,7 @@ { if (!boost::filesystem::is_regular_file(path)) { - LOG(ERROR) << "The path does not point to a regular file: " << path; + LOG(ERROR) << std::string("The path does not point to a regular file: ") << path; throw OrthancException(ErrorCode_RegularFileExpected); } @@ -468,9 +468,13 @@ assert(value < 16); if (value < 10) + { return value + '0'; + } else + { return (value - 10) + 'a'; + } } @@ -508,8 +512,8 @@ result.resize(32); for (unsigned int i = 0; i < 16; i++) { - result[2 * i] = GetHexadecimalCharacter(actualHash[i] / 16); - result[2 * i + 1] = GetHexadecimalCharacter(actualHash[i] % 16); + result[2 * i] = GetHexadecimalCharacter(static_cast(actualHash[i] / 16)); + result[2 * i + 1] = GetHexadecimalCharacter(static_cast(actualHash[i] % 16)); } } #endif @@ -527,6 +531,29 @@ { result = base64_decode(data); } + + +# if BOOST_HAS_REGEX == 1 + void Toolbox::DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source) + { + boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)", + boost::regex::icase /* case insensitive search */); + + boost::cmatch what; + if (regex_match(source.c_str(), what, pattern)) + { + mime = what[1]; + DecodeBase64(content, what[2]); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } +# endif + #endif @@ -1009,28 +1036,6 @@ } -#if BOOST_HAS_REGEX == 1 - void Toolbox::DecodeDataUriScheme(std::string& mime, - std::string& content, - const std::string& source) - { - boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)", - boost::regex::icase /* case insensitive search */); - - boost::cmatch what; - if (regex_match(source.c_str(), what, pattern)) - { - mime = what[1]; - content = what[2]; - } - else - { - throw OrthancException(ErrorCode_BadFileFormat); - } - } -#endif - - void Toolbox::MakeDirectory(const std::string& path) { if (boost::filesystem::exists(path)) diff -r 1b82bb0446d2 -r c131566b8252 Core/Toolbox.h --- a/Core/Toolbox.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Core/Toolbox.h Wed Nov 18 10:16:21 2015 +0100 @@ -117,6 +117,12 @@ void EncodeBase64(std::string& result, const std::string& data); + +# if BOOST_HAS_REGEX == 1 + void DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source); +# endif #endif std::string GetPathToExecutable(); @@ -153,12 +159,6 @@ const std::string& source, char separator); -#if BOOST_HAS_REGEX == 1 - void DecodeDataUriScheme(std::string& mime, - std::string& content, - const std::string& source); -#endif - void MakeDirectory(const std::string& path); bool IsExistingFile(const std::string& path); diff -r 1b82bb0446d2 -r c131566b8252 NEWS --- a/NEWS Wed Sep 23 10:29:06 2015 +0200 +++ b/NEWS Wed Nov 18 10:16:21 2015 +0100 @@ -1,13 +1,46 @@ Pending changes in the mainline =============================== +* Full indexation of the patient/study tags to speed up searches and C-Find * Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") +* "/tools/create-dicom": Support of binary tags encoded using data URI scheme +* "/tools/create-dicom": Support of hierarchical structures (creation of sequences) +* "/modify" can insert/modify sequences +* "/series/.../ordered-slices" to order the slices of a 2D+t or 3D image +* New URI "/tools/shutdown" to stop Orthanc from the REST API +* New URIs for attachments: ".../compress", ".../uncompress" and ".../is-compressed" +* New configuration option: "Dictionary" to declare custom DICOM tags +* MIME content type can be associated to custom attachments (cf. "UserContentType") +* New URIs "/tools/create-archive" and "/tools/create-media" to create ZIP/DICOMDIR + from a set of resources +* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so + +Plugins +------- + +* New function "OrthancPluginRegisterErrorCode()" to declare custom error codes +* New function "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags +* New function "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API +* New "OrthancStarted", "OrthancStopped", "UpdatedAttachment" + and "UpdatedMetadata" events in change callbacks +* "/system" URI gives information about the plugins used for storage area and DB back-end +* Plugin callbacks should now return explicit "OrthancPluginErrorCode" instead of integers + +Lua +--- + +* Optional argument "keepStrings" in "DumpJson()" Maintenance ----------- -* "/system" URI gives information about the plugins used for storage area and DB back-end -* Plugin callbacks must now return explicit "OrthancPluginErrorCode" instead of integers +* Full refactoring of the searching features +* C-Move SCP for studies using AccessionNumber tag +* Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change) +* "/tools/create-dicom" can create tags with unknown VR +* "--logdir" flag creates a single log file instead of 3 separate files for errors/warnings/infos +* "--errors" flag lists the error codes that could be returned by Orthanc +* Under Windows, the exit status of Orthanc corresponds to the encountered error code Version 0.9.4 (2015/09/16) diff -r 1b82bb0446d2 -r c131566b8252 OrthancExplorer/explorer.js --- a/OrthancExplorer/explorer.js Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancExplorer/explorer.js Wed Nov 18 10:16:21 2015 +0100 @@ -236,8 +236,8 @@ ).format (patient.MainDicomTags.PatientName, FormatMainDicomTags(patient.MainDicomTags, [ - "PatientName", - "OtherPatientIDs" + "PatientName" + /*"OtherPatientIDs" */ ]), patient.Studies.length ); @@ -288,7 +288,8 @@ "SeriesTime", "Manufacturer", "ImagesInAcquisition", - "SeriesDate" + "SeriesDate", + "ImageOrientationPatient" ]), c ); @@ -305,7 +306,8 @@ "AcquisitionNumber", "InstanceNumber", "InstanceCreationDate", - "InstanceCreationTime" + "InstanceCreationTime", + "ImagePositionPatient" ]) ); diff -r 1b82bb0446d2 -r c131566b8252 OrthancExplorer/libs/images/ajax-loader.gif diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -37,6 +37,7 @@ #include "../Core/Logging.h" #include "../Core/Uuid.h" #include "EmbeddedResources.h" +#include "ServerToolbox.h" #include #include @@ -186,130 +187,6 @@ } - - void DatabaseWrapper::SetGlobalProperty(GlobalProperty property, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); - s.BindInt(0, property); - s.BindString(1, value); - s.Run(); - } - - bool DatabaseWrapper::LookupGlobalProperty(std::string& target, - GlobalProperty property) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM GlobalProperties WHERE property=?"); - s.BindInt(0, property); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - int64_t DatabaseWrapper::CreateResource(const std::string& publicId, - ResourceType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); - s.BindInt(0, type); - s.BindString(1, publicId); - s.Run(); - return db_.GetLastInsertRowId(); - } - - bool DatabaseWrapper::LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); - s.BindString(0, publicId); - - if (!s.Step()) - { - return false; - } - else - { - id = s.ColumnInt(0); - type = static_cast(s.ColumnInt(1)); - - // Check whether there is a single resource with this public id - assert(!s.Step()); - - return true; - } - } - - bool DatabaseWrapper::LookupParent(int64_t& parentId, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT parentId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - if (s.ColumnIsNull(0)) - { - return false; - } - else - { - parentId = s.ColumnInt(0); - return true; - } - } - - std::string DatabaseWrapper::GetPublicId(int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - return s.ColumnString(0); - } - - - ResourceType DatabaseWrapper::GetResourceType(int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT resourceType FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - return static_cast(s.ColumnInt(0)); - } - - - void DatabaseWrapper::AttachChild(int64_t parent, - int64_t child) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); - s.BindInt64(0, parent); - s.BindInt64(1, child); - s.Run(); - } - void DatabaseWrapper::GetChildren(std::list& childrenPublicIds, int64_t id) @@ -341,183 +218,6 @@ } } - void DatabaseWrapper::SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.BindString(2, value); - s.Run(); - } - - void DatabaseWrapper::DeleteMetadata(int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.Run(); - } - - bool DatabaseWrapper::LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - void DatabaseWrapper::ListAvailableMetadata(std::list& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast(s.ColumnInt(0))); - } - } - - - void DatabaseWrapper::AddAttachment(int64_t id, - const FileInfo& attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, attachment.GetContentType()); - s.BindString(2, attachment.GetUuid()); - s.BindInt64(3, attachment.GetCompressedSize()); - s.BindInt64(4, attachment.GetUncompressedSize()); - s.BindInt(5, attachment.GetCompressionType()); - s.BindString(6, attachment.GetUncompressedMD5()); - s.BindString(7, attachment.GetCompressedMD5()); - s.Run(); - } - - - void DatabaseWrapper::DeleteAttachment(int64_t id, - FileContentType attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, attachment); - s.Run(); - } - - - - void DatabaseWrapper::ListAvailableAttachments(std::list& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT fileType FROM AttachedFiles WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast(s.ColumnInt(0))); - } - } - - bool DatabaseWrapper::LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, contentType); - - if (!s.Step()) - { - return false; - } - else - { - attachment = FileInfo(s.ColumnString(0), - contentType, - s.ColumnInt64(1), - s.ColumnString(4), - static_cast(s.ColumnInt(2)), - s.ColumnInt64(3), - s.ColumnString(5)); - return true; - } - } - - - static void SetMainDicomTagsInternal(SQLite::Statement& s, - int64_t id, - const DicomTag& tag, - const std::string& value) - { - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapper::SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - if (tag.IsIdentifier()) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); - SetMainDicomTagsInternal(s, id, tag, value); - } - else - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - SetMainDicomTagsInternal(s, id, tag, value); - } - } - - void DatabaseWrapper::GetMainDicomTags(DicomMap& map, - int64_t id) - { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3)); - } - - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT * FROM DicomIdentifiers WHERE id=?"); - s2.BindInt64(0, id); - while (s2.Step()) - { - map.SetValue(s2.ColumnInt(1), - s2.ColumnInt(2), - s2.ColumnString(3)); - } - } - bool DatabaseWrapper::GetParentPublicId(std::string& target, int64_t id) @@ -538,164 +238,6 @@ } - void DatabaseWrapper::GetChildrenPublicId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapper::GetChildrenInternalId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapper::LogChange(int64_t internalId, - const ServerIndexChange& change) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, change.GetChangeType()); - s.BindInt64(1, internalId); - s.BindInt(2, change.GetResourceType()); - s.BindString(3, change.GetDate()); - s.Run(); - } - - - void DatabaseWrapper::GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ChangeType changeType = static_cast(s.ColumnInt(1)); - ResourceType resourceType = static_cast(s.ColumnInt(3)); - const std::string& date = s.ColumnString(4); - - int64_t internalId = s.ColumnInt64(2); - std::string publicId = GetPublicId(internalId); - - target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); - } - - done = !(target.size() == maxResults && s.Step()); - } - - - void DatabaseWrapper::GetChanges(std::list& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetChangesInternal(target, done, s, maxResults); - } - - void DatabaseWrapper::GetLastChange(std::list& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); - GetChangesInternal(target, done, s, 1); - } - - - void DatabaseWrapper::LogExportedResource(const ExportedResource& resource) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); - - s.BindInt(0, resource.GetResourceType()); - s.BindString(1, resource.GetPublicId()); - s.BindString(2, resource.GetModality()); - s.BindString(3, resource.GetPatientId()); - s.BindString(4, resource.GetStudyInstanceUid()); - s.BindString(5, resource.GetSeriesInstanceUid()); - s.BindString(6, resource.GetSopInstanceUid()); - s.BindString(7, resource.GetDate()); - s.Run(); - } - - - void DatabaseWrapper::GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ResourceType resourceType = static_cast(s.ColumnInt(1)); - std::string publicId = s.ColumnString(2); - - ExportedResource resource(seq, - resourceType, - publicId, - s.ColumnString(3), // modality - s.ColumnString(8), // date - s.ColumnString(4), // patient ID - s.ColumnString(5), // study instance UID - s.ColumnString(6), // series instance UID - s.ColumnString(7)); // sop instance UID - - target.push_back(resource); - } - - done = !(target.size() == maxResults && s.Step()); - } - - - void DatabaseWrapper::GetExportedResources(std::list& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetExportedResourcesInternal(target, done, s, maxResults); - } - - - void DatabaseWrapper::GetLastExportedResource(std::list& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); - GetExportedResourcesInternal(target, done, s, 1); - } - - - - int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table) { char buf[128]; @@ -714,72 +256,20 @@ } - uint64_t DatabaseWrapper::GetTotalCompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapper::GetTotalUncompressedSize() + DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL), base_(db_) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - void DatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } + db_.Open(path); } - void DatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; - } - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?"); - s.BindInt(0, resourceType); - s.BindInt64(1, limit); - s.BindInt64(2, since); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL) - { - db_.Open(path); - Open(); - } - - DatabaseWrapper::DatabaseWrapper() : listener_(NULL) + DatabaseWrapper::DatabaseWrapper() : listener_(NULL), base_(db_) { db_.OpenInMemory(); - Open(); } void DatabaseWrapper::Open() { + db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); + // Performance tuning of SQLite with PRAGMAs // http://www.sqlite.org/pragma.html db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); @@ -839,15 +329,17 @@ void DatabaseWrapper::Upgrade(unsigned int targetVersion, IStorageArea& storageArea) { - if (targetVersion != 5) + if (targetVersion != 6) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } - // This version of Orthanc is only compatible with versions 3, 4 and 5 of the DB schema + // This version of Orthanc is only compatible with versions 3, 4, + // 5 and 6 of the DB schema if (version_ != 3 && version_ != 4 && - version_ != 5) + version_ != 5 && + version_ != 6) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -865,6 +357,23 @@ ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); version_ = 5; } + + if (version_ == 5) + { + LOG(WARNING) << "Upgrading database version from 5 to 6"; + // No change in the DB schema, the step from version 5 to 6 only + // consists in reconstructing the main DICOM tags information + // (as more tags got included). + db_.BeginTransaction(); + Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient); + Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study); + Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series); + Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance); + db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + + boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); + db_.CommitTransaction(); + version_ = 6; + } } @@ -875,90 +384,6 @@ db_.Register(new Internals::SignalResourceDeleted(listener)); } - uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_InternalError); - } - - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - - return c; - } - - bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder " - "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); - s.BindInt64(0, patientIdToAvoid); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - bool DatabaseWrapper::IsProtectedPatient(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); - s.BindInt64(0, internalId); - return !s.Step(); - } - - void DatabaseWrapper::SetProtectedPatient(int64_t internalId, - bool isProtected) - { - if (isProtected) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); - s.BindInt64(0, internalId); - s.Run(); - } - else if (IsProtectedPatient(internalId)) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); - s.BindInt64(0, internalId); - s.Run(); - } - else - { - // Nothing to do: The patient is already unprotected - } - } - - void DatabaseWrapper::ClearTable(const std::string& tableName) { @@ -966,53 +391,75 @@ } - bool DatabaseWrapper::IsExistingResource(int64_t internalId) + bool DatabaseWrapper::LookupParent(int64_t& parentId, + int64_t resourceId) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM Resources WHERE internalId=?"); - s.BindInt64(0, internalId); - return s.Step(); + bool found; + ErrorCode error = base_.LookupParent(found, parentId, resourceId); + + if (error != ErrorCode_Success) + { + throw OrthancException(error); + } + else + { + return found; + } } - void DatabaseWrapper::LookupIdentifier(std::list& target, - const DicomTag& tag, - const std::string& value) + ResourceType DatabaseWrapper::GetResourceType(int64_t resourceId) { - if (!tag.IsIdentifier()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + ResourceType result; + ErrorCode code = base_.GetResourceType(result, resourceId); - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?"); - - s.BindInt(0, tag.GetGroup()); - s.BindInt(1, tag.GetElement()); - s.BindString(2, value); - - target.clear(); - - while (s.Step()) + if (code == ErrorCode_Success) { - target.push_back(s.ColumnInt64(0)); + return result; + } + else + { + throw OrthancException(code); } } - void DatabaseWrapper::LookupIdentifier(std::list& target, - const std::string& value) + std::string DatabaseWrapper::GetPublicId(int64_t resourceId) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM DicomIdentifiers WHERE value=?"); + std::string id; + + if (base_.GetPublicId(id, resourceId)) + { + return id; + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + - s.BindString(0, value); - - target.clear(); + void DatabaseWrapper::GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + ErrorCode code = base_.GetChanges(target, done, since, maxResults); - while (s.Step()) + if (code != ErrorCode_Success) { - target.push_back(s.ColumnInt64(0)); + throw OrthancException(code); + } + } + + + void DatabaseWrapper::GetLastChange(std::list& target /*out*/) + { + ErrorCode code = base_.GetLastChange(target); + + if (code != ErrorCode_Success) + { + throw OrthancException(code); } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DatabaseWrapper.h Wed Nov 18 10:16:21 2015 +0100 @@ -36,6 +36,7 @@ #include "../Core/SQLite/Connection.h" #include "../Core/SQLite/Transaction.h" +#include "DatabaseWrapperBase.h" namespace Orthanc { @@ -54,21 +55,10 @@ private: IDatabaseListener* listener_; SQLite::Connection db_; + DatabaseWrapperBase base_; Internals::SignalRemainingAncestor* signalRemainingAncestor_; unsigned int version_; - void Open(); - - void GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - void ClearTable(const std::string& tableName); public: @@ -76,20 +66,39 @@ DatabaseWrapper(); + virtual void Open(); + + virtual void Close() + { + db_.Close(); + } + virtual void SetListener(IDatabaseListener& listener); virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value); + const std::string& value) + { + base_.SetGlobalProperty(property, value); + } virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property); + GlobalProperty property) + { + return base_.LookupGlobalProperty(target, property); + } virtual int64_t CreateResource(const std::string& publicId, - ResourceType type); + ResourceType type) + { + return base_.CreateResource(publicId, type); + } virtual bool LookupResource(int64_t& id, ResourceType& type, - const std::string& publicId); + const std::string& publicId) + { + return base_.LookupResource(id, type, publicId); + } virtual bool LookupParent(int64_t& parentId, int64_t resourceId); @@ -99,52 +108,106 @@ virtual ResourceType GetResourceType(int64_t resourceId); virtual void AttachChild(int64_t parent, - int64_t child); + int64_t child) + { + base_.AttachChild(parent, child); + } virtual void DeleteResource(int64_t id); virtual void SetMetadata(int64_t id, MetadataType type, - const std::string& value); + const std::string& value) + { + base_.SetMetadata(id, type, value); + } virtual void DeleteMetadata(int64_t id, - MetadataType type); + MetadataType type) + { + base_.DeleteMetadata(id, type); + } virtual bool LookupMetadata(std::string& target, int64_t id, - MetadataType type); + MetadataType type) + { + return base_.LookupMetadata(target, id, type); + } virtual void ListAvailableMetadata(std::list& target, - int64_t id); + int64_t id) + { + base_.ListAvailableMetadata(target, id); + } virtual void AddAttachment(int64_t id, - const FileInfo& attachment); + const FileInfo& attachment) + { + base_.AddAttachment(id, attachment); + } virtual void DeleteAttachment(int64_t id, - FileContentType attachment); + FileContentType attachment) + { + base_.DeleteAttachment(id, attachment); + } virtual void ListAvailableAttachments(std::list& target, - int64_t id); + int64_t id) + { + return base_.ListAvailableAttachments(target, id); + } virtual bool LookupAttachment(FileInfo& attachment, int64_t id, - FileContentType contentType); + FileContentType contentType) + { + return base_.LookupAttachment(attachment, id, contentType); + } + + virtual void ClearMainDicomTags(int64_t id) + { + base_.ClearMainDicomTags(id); + } virtual void SetMainDicomTag(int64_t id, const DicomTag& tag, - const std::string& value); + const std::string& value) + { + base_.SetMainDicomTag(id, tag, value); + } + + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + base_.SetIdentifierTag(id, tag, value); + } virtual void GetMainDicomTags(DicomMap& map, - int64_t id); + int64_t id) + { + base_.GetMainDicomTags(map, id); + } virtual void GetChildrenPublicId(std::list& target, - int64_t id); + int64_t id) + { + base_.GetChildrenPublicId(target, id); + } virtual void GetChildrenInternalId(std::list& target, - int64_t id); + int64_t id) + { + base_.GetChildrenInternalId(target, id); + } virtual void LogChange(int64_t internalId, - const ServerIndexChange& change); + const ServerIndexChange& change) + { + base_.LogChange(internalId, change); + } virtual void GetChanges(std::list& target /*out*/, bool& done /*out*/, @@ -153,38 +216,80 @@ virtual void GetLastChange(std::list& target /*out*/); - virtual void LogExportedResource(const ExportedResource& resource); + virtual void LogExportedResource(const ExportedResource& resource) + { + base_.LogExportedResource(resource); + } virtual void GetExportedResources(std::list& target /*out*/, bool& done /*out*/, int64_t since, - uint32_t maxResults); + uint32_t maxResults) + { + base_.GetExportedResources(target, done, since, maxResults); + } - virtual void GetLastExportedResource(std::list& target /*out*/); + virtual void GetLastExportedResource(std::list& target /*out*/) + { + base_.GetLastExportedResource(target); + } - virtual uint64_t GetTotalCompressedSize(); + virtual uint64_t GetTotalCompressedSize() + { + return base_.GetTotalCompressedSize(); + } - virtual uint64_t GetTotalUncompressedSize(); + virtual uint64_t GetTotalUncompressedSize() + { + return base_.GetTotalUncompressedSize(); + } - virtual uint64_t GetResourceCount(ResourceType resourceType); + virtual uint64_t GetResourceCount(ResourceType resourceType) + { + return base_.GetResourceCount(resourceType); + } + + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + base_.GetAllInternalIds(target, resourceType); + } virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType); + ResourceType resourceType) + { + base_.GetAllPublicIds(target, resourceType); + } virtual void GetAllPublicIds(std::list& target, ResourceType resourceType, size_t since, - size_t limit); + size_t limit) + { + base_.GetAllPublicIds(target, resourceType, since, limit); + } - virtual bool SelectPatientToRecycle(int64_t& internalId); + virtual bool SelectPatientToRecycle(int64_t& internalId) + { + return base_.SelectPatientToRecycle(internalId); + } virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); + int64_t patientIdToAvoid) + { + return base_.SelectPatientToRecycle(internalId, patientIdToAvoid); + } - virtual bool IsProtectedPatient(int64_t internalId); + virtual bool IsProtectedPatient(int64_t internalId) + { + return base_.IsProtectedPatient(internalId); + } virtual void SetProtectedPatient(int64_t internalId, - bool isProtected); + bool isProtected) + { + base_.SetProtectedPatient(internalId, isProtected); + } virtual SQLite::ITransaction* StartTransaction() { @@ -211,14 +316,19 @@ ClearTable("ExportedResources"); } - virtual bool IsExistingResource(int64_t internalId); + virtual bool IsExistingResource(int64_t internalId) + { + return base_.IsExistingResource(internalId); + } - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, - const std::string& value); - - virtual void LookupIdentifier(std::list& target, - const std::string& value); + IdentifierConstraintType type, + const std::string& value) + { + base_.LookupIdentifier(result, level, tag, type, value); + } virtual void GetAllMetadata(std::map& target, int64_t id); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DatabaseWrapperBase.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapperBase.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,730 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeadersServer.h" +#include "DatabaseWrapperBase.h" + +#include +#include + +namespace Orthanc +{ + void DatabaseWrapperBase::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); + s.BindInt(0, property); + s.BindString(1, value); + s.Run(); + } + + bool DatabaseWrapperBase::LookupGlobalProperty(std::string& target, + GlobalProperty property) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM GlobalProperties WHERE property=?"); + s.BindInt(0, property); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + int64_t DatabaseWrapperBase::CreateResource(const std::string& publicId, + ResourceType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); + s.BindInt(0, type); + s.BindString(1, publicId); + s.Run(); + return db_.GetLastInsertRowId(); + } + + bool DatabaseWrapperBase::LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); + s.BindString(0, publicId); + + if (!s.Step()) + { + return false; + } + else + { + id = s.ColumnInt(0); + type = static_cast(s.ColumnInt(1)); + + // Check whether there is a single resource with this public id + assert(!s.Step()); + + return true; + } + } + + ErrorCode DatabaseWrapperBase::LookupParent(bool& found, + int64_t& parentId, + int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT parentId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (!s.Step()) + { + return ErrorCode_UnknownResource; + } + + if (s.ColumnIsNull(0)) + { + found = false; + } + else + { + found = true; + parentId = s.ColumnInt(0); + } + + return ErrorCode_Success; + } + + bool DatabaseWrapperBase::GetPublicId(std::string& result, + int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (!s.Step()) + { + return false; + } + else + { + result = s.ColumnString(0); + return true; + } + } + + + ErrorCode DatabaseWrapperBase::GetResourceType(ResourceType& result, + int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT resourceType FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (s.Step()) + { + result = static_cast(s.ColumnInt(0)); + return ErrorCode_Success; + } + else + { + return ErrorCode_UnknownResource; + } + } + + + void DatabaseWrapperBase::AttachChild(int64_t parent, + int64_t child) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); + s.BindInt64(0, parent); + s.BindInt64(1, child); + s.Run(); + } + + + void DatabaseWrapperBase::SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.BindString(2, value); + s.Run(); + } + + void DatabaseWrapperBase::DeleteMetadata(int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.Run(); + } + + bool DatabaseWrapperBase::LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM Metadata WHERE id=? AND type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + void DatabaseWrapperBase::ListAvailableMetadata(std::list& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.push_back(static_cast(s.ColumnInt(0))); + } + } + + + void DatabaseWrapperBase::AddAttachment(int64_t id, + const FileInfo& attachment) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, attachment.GetContentType()); + s.BindString(2, attachment.GetUuid()); + s.BindInt64(3, attachment.GetCompressedSize()); + s.BindInt64(4, attachment.GetUncompressedSize()); + s.BindInt(5, attachment.GetCompressionType()); + s.BindString(6, attachment.GetUncompressedMD5()); + s.BindString(7, attachment.GetCompressedMD5()); + s.Run(); + } + + + void DatabaseWrapperBase::DeleteAttachment(int64_t id, + FileContentType attachment) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, attachment); + s.Run(); + } + + + + void DatabaseWrapperBase::ListAvailableAttachments(std::list& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT fileType FROM AttachedFiles WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.push_back(static_cast(s.ColumnInt(0))); + } + } + + bool DatabaseWrapperBase::LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, contentType); + + if (!s.Step()) + { + return false; + } + else + { + attachment = FileInfo(s.ColumnString(0), + contentType, + s.ColumnInt64(1), + s.ColumnString(4), + static_cast(s.ColumnInt(2)), + s.ColumnInt64(3), + s.ColumnString(5)); + return true; + } + } + + + void DatabaseWrapperBase::ClearMainDicomTags(int64_t id) + { + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + } + + + void DatabaseWrapperBase::SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + void DatabaseWrapperBase::SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + void DatabaseWrapperBase::GetMainDicomTags(DicomMap& map, + int64_t id) + { + map.Clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + while (s.Step()) + { + map.SetValue(s.ColumnInt(1), + s.ColumnInt(2), + s.ColumnString(3)); + } + } + + + + void DatabaseWrapperBase::GetChildrenPublicId(std::list& target, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + void DatabaseWrapperBase::GetChildrenInternalId(std::list& target, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + + void DatabaseWrapperBase::LogChange(int64_t internalId, + const ServerIndexChange& change) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, change.GetChangeType()); + s.BindInt64(1, internalId); + s.BindInt(2, change.GetResourceType()); + s.BindString(3, change.GetDate()); + s.Run(); + } + + + ErrorCode DatabaseWrapperBase::GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ChangeType changeType = static_cast(s.ColumnInt(1)); + ResourceType resourceType = static_cast(s.ColumnInt(3)); + const std::string& date = s.ColumnString(4); + + int64_t internalId = s.ColumnInt64(2); + std::string publicId; + if (!GetPublicId(publicId, internalId)) + { + return ErrorCode_UnknownResource; + } + + target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); + } + + done = !(target.size() == maxResults && s.Step()); + return ErrorCode_Success; + } + + + ErrorCode DatabaseWrapperBase::GetChanges(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + return GetChangesInternal(target, done, s, maxResults); + } + + ErrorCode DatabaseWrapperBase::GetLastChange(std::list& target) + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); + return GetChangesInternal(target, done, s, 1); + } + + + void DatabaseWrapperBase::LogExportedResource(const ExportedResource& resource) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resource.GetResourceType()); + s.BindString(1, resource.GetPublicId()); + s.BindString(2, resource.GetModality()); + s.BindString(3, resource.GetPatientId()); + s.BindString(4, resource.GetStudyInstanceUid()); + s.BindString(5, resource.GetSeriesInstanceUid()); + s.BindString(6, resource.GetSopInstanceUid()); + s.BindString(7, resource.GetDate()); + s.Run(); + } + + + void DatabaseWrapperBase::GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ResourceType resourceType = static_cast(s.ColumnInt(1)); + std::string publicId = s.ColumnString(2); + + ExportedResource resource(seq, + resourceType, + publicId, + s.ColumnString(3), // modality + s.ColumnString(8), // date + s.ColumnString(4), // patient ID + s.ColumnString(5), // study instance UID + s.ColumnString(6), // series instance UID + s.ColumnString(7)); // sop instance UID + + target.push_back(resource); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void DatabaseWrapperBase::GetExportedResources(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetExportedResourcesInternal(target, done, s, maxResults); + } + + + void DatabaseWrapperBase::GetLastExportedResource(std::list& target) + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); + GetExportedResourcesInternal(target, done, s, 1); + } + + + + uint64_t DatabaseWrapperBase::GetTotalCompressedSize() + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + + uint64_t DatabaseWrapperBase::GetTotalUncompressedSize() + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + void DatabaseWrapperBase::GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + + void DatabaseWrapperBase::GetAllPublicIds(std::list& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + void DatabaseWrapperBase::GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + { + if (limit == 0) + { + target.clear(); + return; + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?"); + s.BindInt(0, resourceType); + s.BindInt64(1, limit); + s.BindInt64(2, since); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + uint64_t DatabaseWrapperBase::GetResourceCount(ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + if (!s.Step()) + { + return 0; + } + else + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + } + + + bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder " + "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); + s.BindInt64(0, patientIdToAvoid); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapperBase::IsProtectedPatient(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); + s.BindInt64(0, internalId); + return !s.Step(); + } + + void DatabaseWrapperBase::SetProtectedPatient(int64_t internalId, + bool isProtected) + { + if (isProtected) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt64(0, internalId); + s.Run(); + } + else if (IsProtectedPatient(internalId)) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt64(0, internalId); + s.Run(); + } + else + { + // Nothing to do: The patient is already unprotected + } + } + + + + bool DatabaseWrapperBase::IsExistingResource(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM Resources WHERE internalId=?"); + s.BindInt64(0, internalId); + return s.Step(); + } + + + + void DatabaseWrapperBase::LookupIdentifier(std::list& target, + ResourceType level, + const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) + { + static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " + "d.id = r.internalId AND r.resourceType=? AND " + "d.tagGroup=? AND d.tagElement=? AND "); + + std::auto_ptr s; + + switch (type) + { + case IdentifierConstraintType_GreaterOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); + break; + + case IdentifierConstraintType_SmallerOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); + break; + + case IdentifierConstraintType_Wildcard: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); + break; + + case IdentifierConstraintType_Equal: + default: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); + break; + } + + assert(s.get() != NULL); + + s->BindInt(0, level); + s->BindInt(1, tag.GetGroup()); + s->BindInt(2, tag.GetElement()); + s->BindString(3, value); + + target.clear(); + + while (s->Step()) + { + target.push_back(s->ColumnInt64(0)); + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DatabaseWrapperBase.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapperBase.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,203 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Core/DicomFormat/DicomMap.h" +#include "../Core/DicomFormat/DicomTag.h" +#include "../Core/Enumerations.h" +#include "../Core/FileStorage/FileInfo.h" +#include "../Core/SQLite/Connection.h" +#include "../OrthancServer/ExportedResource.h" +#include "../OrthancServer/ServerIndexChange.h" +#include "ServerEnumerations.h" + +#include + + +namespace Orthanc +{ + /** + * This class is shared between the Orthanc core and the sample + * database plugin whose code is in + * "../Plugins/Samples/DatabasePlugin". + **/ + class DatabaseWrapperBase + { + private: + SQLite::Connection& db_; + + ErrorCode GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + void GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + public: + DatabaseWrapperBase(SQLite::Connection& db) : db_(db) + { + } + + void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + bool LookupGlobalProperty(std::string& target, + GlobalProperty property); + + int64_t CreateResource(const std::string& publicId, + ResourceType type); + + bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); + + ErrorCode LookupParent(bool& found, + int64_t& parentId, + int64_t resourceId); + + bool GetPublicId(std::string& result, + int64_t resourceId); + + ErrorCode GetResourceType(ResourceType& result, + int64_t resourceId); + + void AttachChild(int64_t parent, + int64_t child); + + void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); + + void DeleteMetadata(int64_t id, + MetadataType type); + + bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type); + + void ListAvailableMetadata(std::list& target, + int64_t id); + + void AddAttachment(int64_t id, + const FileInfo& attachment); + + void DeleteAttachment(int64_t id, + FileContentType attachment); + + void ListAvailableAttachments(std::list& target, + int64_t id); + + bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType); + + + void ClearMainDicomTags(int64_t id); + + + void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value); + + void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value); + + void GetMainDicomTags(DicomMap& map, + int64_t id); + + void GetChildrenPublicId(std::list& target, + int64_t id); + + void GetChildrenInternalId(std::list& target, + int64_t id); + + void LogChange(int64_t internalId, + const ServerIndexChange& change); + + ErrorCode GetChanges(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults); + + ErrorCode GetLastChange(std::list& target); + + void LogExportedResource(const ExportedResource& resource); + + void GetExportedResources(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults); + + void GetLastExportedResource(std::list& target); + + uint64_t GetTotalCompressedSize(); + + uint64_t GetTotalUncompressedSize(); + + void GetAllInternalIds(std::list& target, + ResourceType resourceType); + + void GetAllPublicIds(std::list& target, + ResourceType resourceType); + + void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit); + + uint64_t GetResourceCount(ResourceType resourceType); + + bool SelectPatientToRecycle(int64_t& internalId); + + bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); + + bool IsProtectedPatient(int64_t internalId); + + void SetProtectedPatient(int64_t internalId, + bool isProtected); + + bool IsExistingResource(int64_t internalId); + + void LookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + }; +} + diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomFindQuery.cpp --- a/OrthancServer/DicomFindQuery.cpp Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,389 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - - -#include "PrecompiledHeadersServer.h" -#include "DicomFindQuery.h" - -#include "FromDcmtkBridge.h" - -#include - - -namespace Orthanc -{ - class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint - { - private: - bool isCaseSensitive_; - std::string expected_; - - public: - ValueConstraint(const std::string& value, - bool caseSensitive) : - isCaseSensitive_(caseSensitive), - expected_(value) - { - } - - const std::string& GetValue() const - { - return expected_; - } - - virtual bool IsExactConstraint() const - { - return isCaseSensitive_; - } - - virtual bool Apply(const std::string& value) const - { - if (isCaseSensitive_) - { - return expected_ == value; - } - else - { - std::string v, c; - Toolbox::ToLowerCase(v, value); - Toolbox::ToLowerCase(c, expected_); - return v == c; - } - } - }; - - - class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint - { - private: - std::set values_; - - public: - ListConstraint(const std::string& values) - { - std::vector items; - Toolbox::TokenizeString(items, values, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - std::string lower; - Toolbox::ToLowerCase(lower, items[i]); - values_.insert(lower); - } - } - - virtual bool Apply(const std::string& value) const - { - std::string tmp; - Toolbox::ToLowerCase(tmp, value); - return values_.find(tmp) != values_.end(); - } - }; - - - class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint - { - private: - std::string lower_; - std::string upper_; - - public: - RangeConstraint(const std::string& range) - { - size_t separator = range.find('-'); - Toolbox::ToLowerCase(lower_, range.substr(0, separator)); - Toolbox::ToLowerCase(upper_, range.substr(separator + 1)); - } - - virtual bool Apply(const std::string& value) const - { - std::string v; - Toolbox::ToLowerCase(v, value); - - if (lower_.size() == 0 && - upper_.size() == 0) - { - return false; - } - - if (lower_.size() == 0) - { - return v <= upper_; - } - - if (upper_.size() == 0) - { - return v >= lower_; - } - - return (v >= lower_ && v <= upper_); - } - }; - - - class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint - { - private: - boost::regex pattern_; - - public: - WildcardConstraint(const std::string& wildcard, - bool caseSensitive) - { - std::string re = Toolbox::WildcardToRegularExpression(wildcard); - - if (caseSensitive) - { - pattern_ = boost::regex(re); - } - else - { - pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */); - } - } - - virtual bool Apply(const std::string& value) const - { - return boost::regex_match(value, pattern_); - } - }; - - - void DicomFindQuery::PrepareMainDicomTags(ResourceType level) - { - std::set tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set::const_iterator - it = tags.begin(); it != tags.end(); ++it) - { - mainDicomTags_[*it] = level; - } - } - - - DicomFindQuery::DicomFindQuery() : - level_(ResourceType_Patient), - filterJson_(false) - { - PrepareMainDicomTags(ResourceType_Patient); - PrepareMainDicomTags(ResourceType_Study); - PrepareMainDicomTags(ResourceType_Series); - PrepareMainDicomTags(ResourceType_Instance); - } - - - DicomFindQuery::~DicomFindQuery() - { - for (Constraints::iterator it = constraints_.begin(); - it != constraints_.end(); ++it) - { - delete it->second; - } - } - - - - - void DicomFindQuery::AssignConstraint(const DicomTag& tag, - IConstraint* constraint) - { - Constraints::iterator it = constraints_.find(tag); - - if (it != constraints_.end()) - { - constraints_.erase(it); - } - - constraints_[tag] = constraint; - - MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag); - if (tmp == mainDicomTags_.end()) - { - // The query depends upon a DICOM tag that is not a main tag - // from the point of view of Orthanc, we need to decode the - // JSON file on the disk. - filterJson_ = true; - } - else - { - filteredLevels_.insert(tmp->second); - } - } - - - void DicomFindQuery::SetConstraint(const DicomTag& tag, - const std::string& constraint, - bool caseSensitivePN) - { - ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); - - bool sensitive = true; - if (vr == ValueRepresentation_PatientName) - { - sensitive = caseSensitivePN; - } - - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - - if ((vr == ValueRepresentation_Date || - vr == ValueRepresentation_DateTime || - vr == ValueRepresentation_Time) && - constraint.find('-') != std::string::npos) - { - /** - * Range matching is only defined for TM, DA and DT value - * representations. This code fixes issues 35 and 37. - * - * Reference: "Range matching is not defined for types of - * Attributes other than dates and times", DICOM PS 3.4, - * C.2.2.2.5 ("Range Matching"). - **/ - AssignConstraint(tag, new RangeConstraint(constraint)); - } - else if (constraint.find('\\') != std::string::npos) - { - AssignConstraint(tag, new ListConstraint(constraint)); - } - else if (constraint.find('*') != std::string::npos || - constraint.find('?') != std::string::npos) - { - AssignConstraint(tag, new WildcardConstraint(constraint, sensitive)); - } - else - { - /** - * Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value - * representations. - * - * Reference: DICOM PS 3.4 - * - C.2.2.2.1 ("Single Value Matching") - * - C.2.2.2.4 ("Wild Card Matching") - * http://medical.nema.org/Dicom/2011/11_04pu.pdf - * - * "Except for Attributes with a PN Value Representation, only - * entities with values which match exactly the value specified in the - * request shall match. This matching is case-sensitive, i.e., - * sensitive to the exact encoding of the key attribute value in - * character sets where a letter may have multiple encodings (e.g., - * based on its case, its position in a word, or whether it is - * accented) - * - * For Attributes with a PN Value Representation (e.g., Patient Name - * (0010,0010)), an application may perform literal matching that is - * either case-sensitive, or that is insensitive to some or all - * aspects of case, position, accent, or other character encoding - * variants." - * - * (0008,0018) UI SOPInstanceUID => Case-sensitive - * (0008,0050) SH AccessionNumber => Case-sensitive - * (0010,0020) LO PatientID => Case-sensitive - * (0020,000D) UI StudyInstanceUID => Case-sensitive - * (0020,000E) UI SeriesInstanceUID => Case-sensitive - **/ - - AssignConstraint(tag, new ValueConstraint(constraint, sensitive)); - } - } - - - bool DicomFindQuery::RestrictIdentifier(std::string& value, - DicomTag identifier) const - { - Constraints::const_iterator it = constraints_.find(identifier); - if (it == constraints_.end() || - !it->second->IsExactConstraint()) - { - return false; - } - else - { - value = dynamic_cast(it->second)->GetValue(); - return true; - } - } - - bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const - { - return filteredLevels_.find(level) != filteredLevels_.end(); - } - - bool DicomFindQuery::FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const - { - std::set tags; - mainTags.GetTags(tags); - - for (std::set::const_iterator - it = tags.begin(); it != tags.end(); ++it) - { - Constraints::const_iterator constraint = constraints_.find(*it); - if (constraint != constraints_.end() && - !constraint->second->Apply(mainTags.GetValue(*it).AsString())) - { - return false; - } - } - - return true; - } - - bool DicomFindQuery::HasInstanceFilter() const - { - return filterJson_; - } - - bool DicomFindQuery::FilterInstance(const std::string& instanceId, - const Json::Value& content) const - { - for (Constraints::const_iterator it = constraints_.begin(); - it != constraints_.end(); ++it) - { - std::string tag = it->first.Format(); - std::string value; - if (content.isMember(tag)) - { - value = content.get(tag, Json::arrayValue).get("Value", "").asString(); - } - - if (!it->second->Apply(value)) - { - return false; - } - } - - return true; - } -} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomFindQuery.h --- a/OrthancServer/DicomFindQuery.h Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ResourceFinder.h" - -namespace Orthanc -{ - class DicomFindQuery : public ResourceFinder::IQuery - { - private: - class IConstraint : public boost::noncopyable - { - public: - virtual ~IConstraint() - { - } - - virtual bool IsExactConstraint() const - { - return false; - } - - virtual bool Apply(const std::string& value) const = 0; - }; - - - class ValueConstraint; - class RangeConstraint; - class ListConstraint; - class WildcardConstraint; - - typedef std::map Constraints; - typedef std::map MainDicomTags; - - MainDicomTags mainDicomTags_; - ResourceType level_; - bool filterJson_; - Constraints constraints_; - std::set filteredLevels_; - - void AssignConstraint(const DicomTag& tag, - IConstraint* constraint); - - void PrepareMainDicomTags(ResourceType level); - - - public: - DicomFindQuery(); - - virtual ~DicomFindQuery(); - - void SetLevel(ResourceType level) - { - level_ = level; - } - - virtual ResourceType GetLevel() const - { - return level_; - } - - void SetConstraint(const DicomTag& tag, - const std::string& constraint, - bool caseSensitivePN); - - virtual bool RestrictIdentifier(std::string& value, - DicomTag identifier) const; - - virtual bool HasMainDicomTagsFilter(ResourceType level) const; - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const; - - virtual bool HasInstanceFilter() const; - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const; - }; -} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -109,7 +109,10 @@ if (!json_.HasContent()) { json_.Allocate(); - FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent())); + FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent()), + DicomToJsonFormat_Full, + DicomToJsonFlags_Default, + 256 /* max string length */); } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomModification.cpp --- a/OrthancServer/DicomModification.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomModification.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -44,13 +44,56 @@ namespace Orthanc { + void DicomModification::RemoveInternal(const DicomTag& tag) + { + Replacements::iterator it = replacements_.find(tag); + + if (it != replacements_.end()) + { + delete it->second; + replacements_.erase(it); + } + } + + + void DicomModification::ReplaceInternal(const DicomTag& tag, + const Json::Value& value) + { + Replacements::iterator it = replacements_.find(tag); + + if (it != replacements_.end()) + { + delete it->second; + it->second = NULL; // In the case of an exception during the clone + it->second = new Json::Value(value); // Clone + } + else + { + replacements_[tag] = new Json::Value(value); // Clone + } + } + + + void DicomModification::ClearReplacements() + { + for (Replacements::iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + delete it->second; + } + + replacements_.clear(); + } + + void DicomModification::MarkNotOrthancAnonymization() { Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD); if (it != replacements_.end() && - it->second == ORTHANC_DEIDENTIFICATION_METHOD) + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD) { + delete it->second; replacements_.erase(it); } } @@ -100,7 +143,7 @@ dicom.Replace(*tag, mapped); } - + DicomModification::DicomModification() { removePrivateTags_ = false; @@ -108,10 +151,15 @@ allowManualIdentifiers_ = true; } + DicomModification::~DicomModification() + { + ClearReplacements(); + } + void DicomModification::Keep(const DicomTag& tag) { removals_.erase(tag); - replacements_.erase(tag); + RemoveInternal(tag); if (FromDcmtkBridge::IsPrivateTag(tag)) { @@ -124,7 +172,7 @@ void DicomModification::Remove(const DicomTag& tag) { removals_.insert(tag); - replacements_.erase(tag); + RemoveInternal(tag); privateTagsToKeep_.erase(tag); MarkNotOrthancAnonymization(); @@ -136,12 +184,12 @@ } void DicomModification::Replace(const DicomTag& tag, - const std::string& value, + const Json::Value& value, bool safeForAnonymization) { removals_.erase(tag); privateTagsToKeep_.erase(tag); - replacements_[tag] = value; + ReplaceInternal(tag, value); if (!safeForAnonymization) { @@ -149,12 +197,13 @@ } } + bool DicomModification::IsReplaced(const DicomTag& tag) const { return replacements_.find(tag) != replacements_.end(); } - const std::string& DicomModification::GetReplacement(const DicomTag& tag) const + const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const { Replacements::const_iterator it = replacements_.find(tag); @@ -164,10 +213,26 @@ } else { - return it->second; + return *it->second; } } + + std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const + { + const Json::Value& json = GetReplacement(tag); + + if (json.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return json.asString(); + } + } + + void DicomModification::SetRemovePrivateTags(bool removed) { removePrivateTags_ = removed; @@ -192,7 +257,7 @@ void DicomModification::SetupAnonymization() { removals_.clear(); - replacements_.clear(); + ClearReplacements(); removePrivateTags_ = true; level_ = ResourceType_Patient; uidMap_.clear(); @@ -255,15 +320,15 @@ removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts // Set the DeidentificationMethod tag - replacements_.insert(std::make_pair(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD)); + ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD); // Set the PatientIdentityRemoved tag - replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); + ReplaceInternal(DicomTag(0x0012, 0x0062), "YES"); // (*) Choose a random patient name and ID std::string patientId = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient); - replacements_[DICOM_TAG_PATIENT_ID] = patientId; - replacements_[DICOM_TAG_PATIENT_NAME] = patientId; + ReplaceInternal(DICOM_TAG_PATIENT_ID, patientId); + ReplaceInternal(DICOM_TAG_PATIENT_NAME, patientId); } void DicomModification::Apply(ParsedDicomFile& toModify) @@ -394,7 +459,7 @@ for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it) { - toModify.Replace(it->first, it->second, DicomReplaceMode_InsertIfAbsent); + toModify.Replace(it->first, *it->second, DicomReplaceMode_InsertIfAbsent); } // (4) Update the DICOM identifiers diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomModification.h --- a/OrthancServer/DicomModification.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomModification.h Wed Nov 18 10:16:21 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class DicomModification + class DicomModification : public boost::noncopyable { /** * Process: @@ -47,7 +47,7 @@ private: typedef std::set SetOfTags; - typedef std::map Replacements; + typedef std::map Replacements; typedef std::map< std::pair, std::string> UidMap; SetOfTags removals_; @@ -63,9 +63,18 @@ void MarkNotOrthancAnonymization(); + void ClearReplacements(); + + void RemoveInternal(const DicomTag& tag); + + void ReplaceInternal(const DicomTag& tag, + const Json::Value& value); + public: DicomModification(); + ~DicomModification(); + void Keep(const DicomTag& tag); void Remove(const DicomTag& tag); @@ -73,12 +82,14 @@ bool IsRemoved(const DicomTag& tag) const; void Replace(const DicomTag& tag, - const std::string& value, + const Json::Value& value, // Encoded using UTF-8 bool safeForAnonymization = false); bool IsReplaced(const DicomTag& tag) const; - const std::string& GetReplacement(const DicomTag& tag) const; + const Json::Value& GetReplacement(const DicomTag& tag) const; + + std::string GetReplacementAsString(const DicomTag& tag) const; void SetRemovePrivateTags(bool removed); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomProtocol/DicomServer.cpp --- a/OrthancServer/DicomProtocol/DicomServer.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -40,10 +40,9 @@ #include "../Internals/CommandDispatcher.h" #include "../OrthancInitialization.h" #include "EmbeddedResources.h" +#include "../../Core/MultiThreading/RunnableWorkersPool.h" #include -#include -#include #if defined(__linux) #include @@ -54,155 +53,37 @@ { struct DicomServer::PImpl { - boost::thread thread_; - - //std::set< + boost::thread thread_; + T_ASC_Network *network_; + std::auto_ptr workers_; }; -#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 - static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary, - EmbeddedResources::FileResourceId resource) - { - Toolbox::TemporaryFile tmp; - - FILE* fp = fopen(tmp.GetPath().c_str(), "wb"); - fwrite(EmbeddedResources::GetFileResourceBuffer(resource), - EmbeddedResources::GetFileResourceSize(resource), 1, fp); - fclose(fp); - - if (!dictionary.loadDictionary(tmp.GetPath().c_str())) - { - throw OrthancException(ErrorCode_InternalError); - } - } - -#else - static void LoadExternalDictionary(DcmDataDictionary& dictionary, - const std::string& directory, - const std::string& filename) - { - boost::filesystem::path p = directory; - p = p / filename; - - LOG(WARNING) << "Loading the external DICOM dictionary " << p; - - if (!dictionary.loadDictionary(p.string().c_str())) - { - throw OrthancException(ErrorCode_InternalError); - } - } - -#endif - - - void DicomServer::InitializeDictionary() - { - /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */ - dcmDisableGethostbyaddr.set(OFTrue); - - dcmDataDict.clear(); - DcmDataDictionary& d = dcmDataDict.wrlock(); - -#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 - LOG(WARNING) << "Loading the embedded dictionaries"; - /** - * Do not load DICONDE dictionary, it breaks the other tags. The - * command "strace storescu 2>&1 |grep dic" shows that DICONDE - * dictionary is not loaded by storescu. - **/ - //LoadEmbeddedDictionary(d, EmbeddedResources::DICTIONARY_DICONDE); - - LoadEmbeddedDictionary(d, EmbeddedResources::DICTIONARY_DICOM); - LoadEmbeddedDictionary(d, EmbeddedResources::DICTIONARY_PRIVATE); - -#elif defined(__linux) || defined(__FreeBSD_kernel__) - std::string path = DCMTK_DICTIONARY_DIR; - - const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE); - if (env != NULL) - { - path = std::string(env); - } - - LoadExternalDictionary(d, path, "dicom.dic"); - LoadExternalDictionary(d, path, "private.dic"); - -#else -#error Support your platform here -#endif - - dcmDataDict.unlock(); - - /* 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); - } - - { - // Test the dictionary with a simple DICOM tag - 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); - } - } - } - - void DicomServer::ServerThread(DicomServer* server) { - /* initialize network, i.e. create an instance of T_ASC_Network*. */ - T_ASC_Network *net; - OFCondition cond = ASC_initializeNetwork - (NET_ACCEPTOR, OFstatic_cast(int, server->port_), /*opt_acse_timeout*/ 30, &net); - if (cond.bad()) - { - LOG(ERROR) << "cannot create network: " << cond.text(); - throw OrthancException(ErrorCode_DicomPortInUse); - } - LOG(INFO) << "DICOM server started"; - server->started_ = true; - while (server->continue_) { /* receive an association and acknowledge or reject it. If the association was */ /* acknowledged, offer corresponding services and invoke one or more if required. */ - std::auto_ptr dispatcher(Internals::AcceptAssociation(*server, net)); + std::auto_ptr dispatcher(Internals::AcceptAssociation(*server, server->pimpl_->network_)); - if (dispatcher.get() != NULL) + try { - if (server->isThreaded_) + if (dispatcher.get() != NULL) { - server->bagOfDispatchers_.Add(dispatcher.release()); + server->pimpl_->workers_->Add(dispatcher.release()); } - else - { - IRunnableBySteps::RunUntilDone(*dispatcher); - } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception in the DICOM server thread: " << e.What(); } } LOG(INFO) << "DICOM server stopping"; - - if (server->isThreaded_) - { - server->bagOfDispatchers_.StopAll(); - } - - /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */ - /* is the counterpart of ASC_initializeNetwork(...) which was called above. */ - cond = ASC_dropNetwork(&net); - if (cond.bad()) - { - LOG(ERROR) << "Error while dropping the network: " << cond.text(); - } - } + } DicomServer::DicomServer() : @@ -216,9 +97,7 @@ applicationEntityFilter_ = NULL; checkCalledAet_ = true; clientTimeout_ = 30; - isThreaded_ = true; continue_ = false; - started_ = false; } DicomServer::~DicomServer() @@ -241,17 +120,6 @@ return port_; } - void DicomServer::SetThreaded(bool isThreaded) - { - Stop(); - isThreaded_ = isThreaded; - } - - bool DicomServer::IsThreaded() const - { - return isThreaded_; - } - void DicomServer::SetClientTimeout(uint32_t timeout) { Stop(); @@ -403,14 +271,19 @@ void DicomServer::Start() { Stop(); - continue_ = true; - started_ = false; - pimpl_->thread_ = boost::thread(ServerThread, this); - while (!started_) + /* initialize network, i.e. create an instance of T_ASC_Network*. */ + OFCondition cond = ASC_initializeNetwork + (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_); + if (cond.bad()) { - Toolbox::USleep(50000); // Wait 50ms + LOG(ERROR) << "cannot create network: " << cond.text(); + throw OrthancException(ErrorCode_DicomPortInUse); } + + continue_ = true; + pimpl_->workers_.reset(new RunnableWorkersPool(4)); // Use 4 workers - TODO as a parameter? + pimpl_->thread_ = boost::thread(ServerThread, this); } @@ -425,7 +298,15 @@ pimpl_->thread_.join(); } - bagOfDispatchers_.Finalize(); + pimpl_->workers_.reset(NULL); + + /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */ + /* is the counterpart of ASC_initializeNetwork(...) which was called above. */ + OFCondition cond = ASC_dropNetwork(&pimpl_->network_); + if (cond.bad()) + { + LOG(ERROR) << "Error while dropping the network: " << cond.text(); + } } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomProtocol/DicomServer.h --- a/OrthancServer/DicomProtocol/DicomServer.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.h Wed Nov 18 10:16:21 2015 +0100 @@ -36,11 +36,11 @@ #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" -#include "../../Core/MultiThreading/BagOfRunnablesBySteps.h" #include #include + namespace Orthanc { class DicomServer : public boost::noncopyable @@ -55,19 +55,14 @@ bool continue_; bool started_; uint32_t clientTimeout_; - bool isThreaded_; IFindRequestHandlerFactory* findRequestHandlerFactory_; IMoveRequestHandlerFactory* moveRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; IApplicationEntityFilter* applicationEntityFilter_; - BagOfRunnablesBySteps bagOfDispatchers_; // This is used iff the server is threaded - static void ServerThread(DicomServer* server); public: - static void InitializeDictionary(); - DicomServer(); ~DicomServer(); @@ -75,9 +70,6 @@ void SetPortNumber(uint16_t port); uint16_t GetPortNumber() const; - void SetThreaded(bool isThreaded); - bool IsThreaded() const; - void SetClientTimeout(uint32_t timeout); uint32_t GetClientTimeout() const; diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomProtocol/DicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -272,10 +272,22 @@ const std::string syntax(xfer.getXferID()); bool isGeneric = IsGenericTransferSyntax(syntax); - if (isGeneric ^ IsGenericTransferSyntax(connection.GetPreferredTransferSyntax())) + bool renegociate; + if (isGeneric) { - // Making a generic-to-specific or specific-to-generic change of - // the transfer syntax. Renegotiate the connection. + // Are we making a generic-to-specific or specific-to-generic change of + // the transfer syntax? If this is the case, renegotiate the connection. + renegociate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); + } + else + { + // We are using a specific transfer syntax. Renegociate if the + // current connection does not match this transfer syntax. + renegociate = (syntax != connection.GetPreferredTransferSyntax()); + } + + if (renegociate) + { LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; if (isGeneric) @@ -372,8 +384,9 @@ } - static void CheckFindQuery(ResourceType level, - const DicomMap& fields) + static void FixFindQuery(DicomMap& fixedQuery, + ResourceType level, + const DicomMap& fields) { std::set allowedTags; @@ -410,8 +423,11 @@ const DicomTag& tag = query.GetElement(i).GetTag(); if (allowedTags.find(tag) == allowedTags.end()) { - LOG(ERROR) << "Tag not allowed for this C-Find level: " << tag; - throw OrthancException(ErrorCode_BadRequest); + LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; + } + else + { + fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); } } } @@ -441,7 +457,8 @@ const DicomValue* value = fix->TestAndGetValue(*it); if (value != NULL && - value->AsString() == "*") + !value->IsNull() && + value->GetContent() == "*") { fix->SetValue(*it, ""); } @@ -459,9 +476,10 @@ void DicomUserConnection::Find(DicomFindAnswers& result, ResourceType level, - const DicomMap& fields) + const DicomMap& originalFields) { - CheckFindQuery(level, fields); + DicomMap fields; + FixFindQuery(fields, level, originalFields); CheckIsOpen(); @@ -931,7 +949,7 @@ throw OrthancException(ErrorCode_InternalError); } - const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).AsString(); + const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); DicomMap move; diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomProtocol/RemoteModalityParameters.cpp --- a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -50,7 +50,7 @@ RemoteModalityParameters::RemoteModalityParameters(const std::string& aet, const std::string& host, - int port, + uint16_t port, ModalityManufacturer manufacturer) { SetApplicationEntityTitle(aet); @@ -60,16 +60,6 @@ } - void RemoteModalityParameters::SetPort(int port) - { - if (port <= 0 || port >= 65535) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - port_ = port; - } - void RemoteModalityParameters::FromJson(const Json::Value& modality) { if (!modality.isArray() || @@ -84,13 +74,20 @@ const Json::Value& portValue = modality.get(2u, ""); try { - SetPort(portValue.asInt()); + int tmp = portValue.asInt(); + + if (tmp <= 0 || tmp >= 65535) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + SetPort(static_cast(tmp)); } catch (std::runtime_error /* error inside JsonCpp */) { try { - SetPort(boost::lexical_cast(portValue.asString())); + SetPort(boost::lexical_cast(portValue.asString())); } catch (boost::bad_lexical_cast) { diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/DicomProtocol/RemoteModalityParameters.h --- a/OrthancServer/DicomProtocol/RemoteModalityParameters.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.h Wed Nov 18 10:16:21 2015 +0100 @@ -34,6 +34,7 @@ #include "../ServerEnumerations.h" +#include #include #include @@ -44,7 +45,7 @@ private: std::string aet_; std::string host_; - int port_; + uint16_t port_; ModalityManufacturer manufacturer_; public: @@ -52,7 +53,7 @@ RemoteModalityParameters(const std::string& aet, const std::string& host, - int port, + uint16_t port, ModalityManufacturer manufacturer); const std::string& GetApplicationEntityTitle() const @@ -75,12 +76,15 @@ host_ = host; } - int GetPort() const + uint16_t GetPort() const { return port_; } - void SetPort(int port); + void SetPort(uint16_t port) + { + port_ = port; + } ModalityManufacturer GetManufacturer() const { diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/FromDcmtkBridge.cpp --- a/OrthancServer/FromDcmtkBridge.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -47,14 +47,13 @@ #include "../Core/OrthancException.h" #include "../Core/Images/PngWriter.h" #include "../Core/Uuid.h" -#include "../Core/DicomFormat/DicomString.h" -#include "../Core/DicomFormat/DicomNullValue.h" #include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" #include #include #include +#include #include #include @@ -91,8 +90,10 @@ #include #include +#include #include +#include #include @@ -119,6 +120,175 @@ } +#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 + static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary, + EmbeddedResources::FileResourceId resource) + { + Toolbox::TemporaryFile tmp; + + FILE* fp = fopen(tmp.GetPath().c_str(), "wb"); + fwrite(EmbeddedResources::GetFileResourceBuffer(resource), + EmbeddedResources::GetFileResourceSize(resource), 1, fp); + fclose(fp); + + if (!dictionary.loadDictionary(tmp.GetPath().c_str())) + { + throw OrthancException(ErrorCode_InternalError); + } + } + +#else + static void LoadExternalDictionary(DcmDataDictionary& dictionary, + const std::string& directory, + const std::string& filename) + { + boost::filesystem::path p = directory; + p = p / filename; + + LOG(WARNING) << "Loading the external DICOM dictionary " << p; + + if (!dictionary.loadDictionary(p.string().c_str())) + { + throw OrthancException(ErrorCode_InternalError); + } + } + +#endif + + + namespace + { + class DictionaryLocker + { + private: + DcmDataDictionary& dictionary_; + + public: + DictionaryLocker() : dictionary_(dcmDataDict.wrlock()) + { + } + + ~DictionaryLocker() + { + dcmDataDict.unlock(); + } + + DcmDataDictionary& operator*() + { + return dictionary_; + } + + DcmDataDictionary* operator->() + { + return &dictionary_; + } + }; + } + + + void FromDcmtkBridge::InitializeDictionary() + { + /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */ + dcmDisableGethostbyaddr.set(OFTrue); + + { + DictionaryLocker locker; + + locker->clear(); + +#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 + LOG(WARNING) << "Loading the embedded dictionaries"; + /** + * Do not load DICONDE dictionary, it breaks the other tags. The + * command "strace storescu 2>&1 |grep dic" shows that DICONDE + * dictionary is not loaded by storescu. + **/ + //LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICONDE); + + LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICOM); + LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_PRIVATE); + +#elif defined(__linux) || defined(__FreeBSD_kernel__) + std::string path = DCMTK_DICTIONARY_DIR; + + const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE); + if (env != NULL) + { + path = std::string(env); + } + + LoadExternalDictionary(*locker, path, "dicom.dic"); + LoadExternalDictionary(*locker, path, "private.dic"); + +#else +#error Support your platform here +#endif + } + + /* 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); + } + + { + // Test the dictionary with a simple DICOM tag + 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); + } + } + } + + + void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag, + const DcmEVR& vr, + const std::string& name, + unsigned int minMultiplicity, + unsigned int maxMultiplicity) + { + if (minMultiplicity < 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + bool arbitrary = false; + if (maxMultiplicity == 0) + { + maxMultiplicity = DcmVariableVM; + arbitrary = true; + } + else if (maxMultiplicity < minMultiplicity) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + LOG(INFO) << "Registering tag in dictionary: " << tag << " " << (DcmVR(vr).getValidVRName()) << " " + << name << " (multiplicity: " << minMultiplicity << "-" + << (arbitrary ? "n" : boost::lexical_cast(maxMultiplicity)) << ")"; + + std::auto_ptr entry(new DcmDictEntry(tag.GetGroup(), + tag.GetElement(), + vr, name.c_str(), + static_cast(minMultiplicity), + static_cast(maxMultiplicity), + NULL /* version */, + OFTrue /* doCopyString */, + NULL /* private creator */)); + + entry->setGroupRangeRestriction(DcmDictRange_Unspecified); + entry->setElementRangeRestriction(DcmDictRange_Unspecified); + + { + DictionaryLocker locker; + locker->addEntry(entry.release()); + } + } + + Encoding FromDcmtkBridge::DetectEncoding(DcmDataset& dataset) { // By default, Latin1 encoding is assumed @@ -166,7 +336,7 @@ { target.SetValue(element->getTag().getGTag(), element->getTag().getETag(), - ConvertLeafElement(*element, encoding)); + ConvertLeafElement(*element, DicomToJsonFlags_Default, encoding)); } } } @@ -184,19 +354,6 @@ } - bool FromDcmtkBridge::IsPrivateTag(DcmTag& tag) - { -#if 1 - DcmTagKey tmp(tag.getGTag(), tag.getETag()); - return tmp.isPrivate(); -#else - // Implementation for Orthanc versions <= 0.8.5 - return (tag.getPrivateCreator() != NULL || - !strcmp("PrivateCreator", tag.getTagName())); // TODO - This may change with future versions of DCMTK -#endif - } - - bool FromDcmtkBridge::IsPrivateTag(const DicomTag& tag) { #if 1 @@ -211,6 +368,7 @@ DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element, + DicomToJsonFlags flags, Encoding encoding) { if (!element.isLeaf()) @@ -226,18 +384,18 @@ { if (c == NULL) // This case corresponds to the empty string { - return new DicomString(""); + return new DicomValue("", false); } else { std::string s(c); std::string utf8 = Toolbox::ConvertToUtf8(s, encoding); - return new DicomString(utf8); + return new DicomValue(utf8, false); } } else { - return new DicomNullValue; + return new DicomValue; } } @@ -248,14 +406,26 @@ { /** - * TODO. + * Deal with binary data (including PixelData). **/ case EVR_OB: // other byte case EVR_OF: // other float case EVR_OW: // other word case EVR_UN: // unknown value representation - return new DicomNullValue; + case EVR_ox: // OB or OW depending on context + { + if (!(flags & DicomToJsonFlags_ConvertBinaryToNull)) + { + Uint8* data = NULL; + if (element.getUint8Array(data) == EC_Normal) + { + return new DicomValue(reinterpret_cast(data), element.getLength(), true); + } + } + + return new DicomValue; + } /** * String types, should never happen at this point because of @@ -277,7 +447,7 @@ case EVR_UT: // unlimited text case EVR_PN: // person name case EVR_UI: // unique identifier - return new DicomNullValue; + return new DicomValue; /** @@ -288,54 +458,54 @@ { Sint32 f; if (dynamic_cast(element).getSint32(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } case EVR_SS: // signed short { Sint16 f; if (dynamic_cast(element).getSint16(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } case EVR_UL: // unsigned long { Uint32 f; if (dynamic_cast(element).getUint32(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } case EVR_US: // unsigned short { Uint16 f; if (dynamic_cast(element).getUint16(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } case EVR_FL: // float single-precision { Float32 f; if (dynamic_cast(element).getFloat32(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } case EVR_FD: // float double-precision { Float64 f; if (dynamic_cast(element).getFloat64(f).good()) - return new DicomString(boost::lexical_cast(f)); + return new DicomValue(boost::lexical_cast(f), false); else - return new DicomNullValue; + return new DicomValue; } @@ -349,11 +519,11 @@ if (dynamic_cast(element).getTagVal(tag, 0).good()) { DicomTag t(tag.getGroup(), tag.getElement()); - return new DicomString(t.Format()); + return new DicomValue(t.Format(), false); } else { - return new DicomNullValue; + return new DicomValue; } } @@ -364,14 +534,13 @@ **/ case EVR_SQ: // sequence of items - return new DicomNullValue; + return new DicomValue; /** * Internal to DCMTK. **/ - case EVR_ox: // OB or OW depending on context case EVR_xs: // SS or US depending on context case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) case EVR_na: // na="not applicable", for data which has no VR @@ -388,7 +557,7 @@ case EVR_PixelData: // used internally for uncompressed pixeld data case EVR_OverlayData: // used internally for overlay data case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR - return new DicomNullValue; + return new DicomValue; /** @@ -396,141 +565,271 @@ **/ default: - return new DicomNullValue; + return new DicomValue; } } catch (boost::bad_lexical_cast) { - return new DicomNullValue; + return new DicomValue; } catch (std::bad_cast) { - return new DicomNullValue; + return new DicomValue; } } - static void StoreElement(Json::Value& target, - DcmElement& element, - unsigned int maxStringLength, - Encoding encoding); - - static void StoreItem(Json::Value& target, - DcmItem& item, - unsigned int maxStringLength, - Encoding encoding) + static Json::Value& PrepareNode(Json::Value& parent, + DcmElement& element, + DicomToJsonFormat format) { - target = Json::Value(Json::objectValue); - - for (unsigned long i = 0; i < item.card(); i++) - { - DcmElement* element = item.getElement(i); - StoreElement(target, *element, maxStringLength, encoding); - } - } - - - static void StoreElement(Json::Value& target, - DcmElement& element, - unsigned int maxStringLength, - Encoding encoding) - { - assert(target.type() == Json::objectValue); + assert(parent.type() == Json::objectValue); DicomTag tag(FromDcmtkBridge::GetTag(element)); const std::string formattedTag = tag.Format(); -#if 0 - const std::string tagName = FromDcmtkBridge::GetName(tag); -#else - // This version of the code gives access to the name of the private tags + if (format == DicomToJsonFormat_Short) + { + parent[formattedTag] = Json::nullValue; + return parent[formattedTag]; + } + + // This code gives access to the name of the private tags DcmTag tagbis(element.getTag()); const std::string tagName(tagbis.getTagName()); -#endif + + switch (format) + { + case DicomToJsonFormat_Simple: + parent[tagName] = Json::nullValue; + return parent[tagName]; + + case DicomToJsonFormat_Full: + { + parent[formattedTag] = Json::objectValue; + Json::Value& node = parent[formattedTag]; + + if (element.isLeaf()) + { + node["Name"] = tagName; + + if (tagbis.getPrivateCreator() != NULL) + { + node["PrivateCreator"] = tagbis.getPrivateCreator(); + } + + return node; + } + else + { + node["Name"] = tagName; + node["Type"] = "Sequence"; + node["Value"] = Json::nullValue; + return node["Value"]; + } + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static void LeafValueToJson(Json::Value& target, + const DicomValue& value, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength) + { + Json::Value* targetValue = NULL; + Json::Value* targetType = NULL; + + switch (format) + { + case DicomToJsonFormat_Short: + case DicomToJsonFormat_Simple: + { + assert(target.type() == Json::nullValue); + targetValue = ⌖ + break; + } + + case DicomToJsonFormat_Full: + { + assert(target.type() == Json::objectValue); + target["Value"] = Json::nullValue; + target["Type"] = Json::nullValue; + targetType = &target["Type"]; + targetValue = &target["Value"]; + break; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(targetValue != NULL); + assert(targetValue->type() == Json::nullValue); + assert(targetType == NULL || targetType->type() == Json::nullValue); + + if (value.IsNull()) + { + if (targetType != NULL) + { + *targetType = "Null"; + } + } + else if (value.IsBinary()) + { + if (flags & DicomToJsonFlags_ConvertBinaryToAscii) + { + *targetValue = Toolbox::ConvertToAscii(value.GetContent()); + } + else + { + std::string s; + value.FormatDataUriScheme(s); + *targetValue = s; + } + + if (targetType != NULL) + { + *targetType = "Binary"; + } + } + else if (maxStringLength == 0 || + value.GetContent().size() <= maxStringLength) + { + *targetValue = value.GetContent(); + + if (targetType != NULL) + { + *targetType = "String"; + } + } + else + { + if (targetType != NULL) + { + *targetType = "TooLong"; + } + } + } + + + static void DatasetToJson(Json::Value& parent, + DcmItem& item, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding); + + + void FromDcmtkBridge::ToJson(Json::Value& parent, + DcmElement& element, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding) + { + if (parent.type() == Json::nullValue) + { + parent = Json::objectValue; + } + + assert(parent.type() == Json::objectValue); + Json::Value& target = PrepareNode(parent, element, format); if (element.isLeaf()) { - Json::Value value(Json::objectValue); - value["Name"] = tagName; - - if (tagbis.getPrivateCreator() != NULL) - { - value["PrivateCreator"] = tagbis.getPrivateCreator(); - } - - std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement(element, encoding)); - if (v->IsNull()) - { - value["Type"] = "Null"; - value["Value"] = Json::nullValue; - } - else - { - std::string s = v->AsString(); - if (maxStringLength == 0 || - s.size() <= maxStringLength) - { - value["Type"] = "String"; - value["Value"] = s; - } - else - { - value["Type"] = "TooLong"; - value["Value"] = Json::nullValue; - } - } - - target[formattedTag] = value; + std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement(element, flags, encoding)); + LeafValueToJson(target, *v, format, flags, maxStringLength); } else { - Json::Value children(Json::arrayValue); + assert(target.type() == Json::nullValue); + target = Json::arrayValue; // "All subclasses of DcmElement except for DcmSequenceOfItems // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset - // etc. are not." The following cast is thus OK. + // etc. are not." The following dynamic_cast is thus OK. DcmSequenceOfItems& sequence = dynamic_cast(element); for (unsigned long i = 0; i < sequence.card(); i++) { DcmItem* child = sequence.getItem(i); - Json::Value& v = children.append(Json::objectValue); - StoreItem(v, *child, maxStringLength, encoding); - } - - target[formattedTag]["Name"] = tagName; - target[formattedTag]["Type"] = "Sequence"; - target[formattedTag]["Value"] = children; + Json::Value& v = target.append(Json::objectValue); + DatasetToJson(v, *child, format, flags, maxStringLength, encoding); + } } } - void FromDcmtkBridge::ToJson(Json::Value& root, - DcmDataset& dataset, - unsigned int maxStringLength) + static void DatasetToJson(Json::Value& parent, + DcmItem& item, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding) { - StoreItem(root, dataset, maxStringLength, DetectEncoding(dataset)); + assert(parent.type() == Json::objectValue); + + for (unsigned long i = 0; i < item.card(); i++) + { + DcmElement* element = item.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!(flags & DicomToJsonFlags_IncludePrivateTags) && + element->getTag().isPrivate()) + { + continue; + } + + if (!(flags & DicomToJsonFlags_IncludeUnknownTags)) + { + DictionaryLocker locker; + if (locker->findEntry(element->getTag(), NULL) == NULL) + { + continue; + } + } + + DcmEVR evr = element->getTag().getEVR(); + if (evr == EVR_OB || + evr == EVR_OF || + evr == EVR_OW || + evr == EVR_UN || + evr == EVR_ox) + { + // This is a binary tag + DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + + if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) || + (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary))) + { + continue; + } + } + + FromDcmtkBridge::ToJson(parent, *element, format, flags, maxStringLength, encoding); + } } - void FromDcmtkBridge::ToJson(Json::Value& target, - const std::string& path, + DcmDataset& dataset, + DicomToJsonFormat format, + DicomToJsonFlags flags, unsigned int maxStringLength) { - DcmFileFormat dicom; - if (!dicom.loadFile(path.c_str()).good()) - { - throw OrthancException(ErrorCode_BadFileFormat); - } - else - { - FromDcmtkBridge::ToJson(target, *dicom.getDataset(), maxStringLength); - } + target = Json::objectValue; + DatasetToJson(target, dataset, format, flags, maxStringLength, DetectEncoding(dataset)); } - std::string FromDcmtkBridge::GetName(const DicomTag& t) { // Some patches for important tags because of different DICOM @@ -618,15 +917,10 @@ } - void FromDcmtkBridge::Print(FILE* fp, const DicomMap& m) + bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag) { - for (DicomMap::Map::const_iterator - it = m.map_.begin(); it != m.map_.end(); ++it) - { - DicomTag t = it->first; - std::string s = it->second->AsString(); - fprintf(fp, "0x%04x 0x%04x (%s) [%s]\n", t.GetGroup(), t.GetElement(), GetName(t).c_str(), s.c_str()); - } + DcmTag tmp(tag.GetGroup(), tag.GetElement()); + return tmp.isUnknownVR(); } @@ -646,7 +940,15 @@ { if (simplify) { - result[GetName(it->first)] = it->second->AsString(); + if (it->second->IsNull()) + { + result[GetName(it->first)] = Json::nullValue; + } + else + { + // TODO IsBinary + result[GetName(it->first)] = it->second->GetContent(); + } } else { @@ -661,8 +963,9 @@ } else { + // TODO IsBinary value["Type"] = "String"; - value["Value"] = it->second->AsString(); + value["Value"] = it->second->GetContent(); } result[it->first.Format()] = value; @@ -775,4 +1078,467 @@ } } + + static bool IsBinaryTag(const DcmTag& key) + { + return (key.isPrivate() || + key.isUnknownVR() || + key.getEVR() == EVR_OB || + key.getEVR() == EVR_OF || + key.getEVR() == EVR_OW || + key.getEVR() == EVR_UN || + key.getEVR() == EVR_ox); + } + + + DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag) + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + + if (IsBinaryTag(key)) + { + return new DcmOtherByteOtherWord(key); + } + + switch (key.getEVR()) + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + + /** + * Binary types, handled above + **/ + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_UN: // unknown value representation + case EVR_ox: // OB or OW depending on context + throw OrthancException(ErrorCode_InternalError); + + + /** + * String types. + * http://support.dcmtk.org/docs/classDcmByteString.html + **/ + + case EVR_AS: // age string + return new DcmAgeString(key); + + case EVR_AE: // application entity title + return new DcmApplicationEntity(key); + + case EVR_CS: // code string + return new DcmCodeString(key); + + case EVR_DA: // date string + return new DcmDate(key); + + case EVR_DT: // date time string + return new DcmDateTime(key); + + case EVR_DS: // decimal string + return new DcmDecimalString(key); + + case EVR_IS: // integer string + return new DcmIntegerString(key); + + case EVR_TM: // time string + return new DcmTime(key); + + case EVR_UI: // unique identifier + return new DcmUniqueIdentifier(key); + + case EVR_ST: // short text + return new DcmShortText(key); + + case EVR_LO: // long string + return new DcmLongString(key); + + case EVR_LT: // long text + return new DcmLongText(key); + + case EVR_UT: // unlimited text + return new DcmUnlimitedText(key); + + case EVR_SH: // short string + return new DcmShortString(key); + + case EVR_PN: // person name + return new DcmPersonName(key); + + + /** + * Numerical types + **/ + + case EVR_SL: // signed long + return new DcmSignedLong(key); + + case EVR_SS: // signed short + return new DcmSignedShort(key); + + case EVR_UL: // unsigned long + return new DcmUnsignedLong(key); + + case EVR_US: // unsigned short + return new DcmUnsignedShort(key); + + case EVR_FL: // float single-precision + return new DcmFloatingPointSingle(key); + + case EVR_FD: // float double-precision + return new DcmFloatingPointDouble(key); + + + /** + * Sequence types, should never occur at this point. + **/ + + case EVR_SQ: // sequence of items + throw OrthancException(ErrorCode_ParameterOutOfRange); + + + /** + * TODO + **/ + + case EVR_AT: // attribute tag + throw OrthancException(ErrorCode_NotImplemented); + + + /** + * Internal to DCMTK. + **/ + + case EVR_xs: // SS or US depending on context + case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) + case EVR_na: // na="not applicable", for data which has no VR + case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + default: + break; + } + + throw OrthancException(ErrorCode_InternalError); + } + + + + void FromDcmtkBridge::FillElementWithString(DcmElement& element, + const DicomTag& tag, + const std::string& utf8Value, + bool decodeBinaryTags, + Encoding dicomEncoding) + { + std::string binary; + const std::string* decoded = &utf8Value; + + if (decodeBinaryTags && + boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + { + std::string mime; + Toolbox::DecodeDataUriScheme(mime, binary, utf8Value); + decoded = &binary; + } + else if (dicomEncoding != Encoding_Utf8) + { + binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding); + decoded = &binary; + } + + DcmTag key(tag.GetGroup(), tag.GetElement()); + + if (IsBinaryTag(key)) + { + if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good()) + { + return; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + bool ok = false; + + try + { + switch (key.getEVR()) + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + + /** + * TODO. + **/ + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_AT: // attribute tag + throw OrthancException(ErrorCode_NotImplemented); + + case EVR_UN: // unknown value representation + throw OrthancException(ErrorCode_ParameterOutOfRange); + + + /** + * String types. + **/ + + case EVR_DS: // decimal string + case EVR_IS: // integer string + case EVR_AS: // age string + case EVR_DA: // date string + case EVR_DT: // date time string + case EVR_TM: // time string + case EVR_AE: // application entity title + case EVR_CS: // code string + case EVR_SH: // short string + case EVR_LO: // long string + case EVR_ST: // short text + case EVR_LT: // long text + case EVR_UT: // unlimited text + case EVR_PN: // person name + case EVR_UI: // unique identifier + { + ok = element.putString(decoded->c_str()).good(); + break; + } + + + /** + * Numerical types + **/ + + case EVR_SL: // signed long + { + ok = element.putSint32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_SS: // signed short + { + ok = element.putSint16(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_UL: // unsigned long + { + ok = element.putUint32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_US: // unsigned short + { + ok = element.putUint16(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_FL: // float single-precision + { + ok = element.putFloat32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_FD: // float double-precision + { + ok = element.putFloat64(boost::lexical_cast(*decoded)).good(); + break; + } + + + /** + * Sequence types, should never occur at this point. + **/ + + case EVR_SQ: // sequence of items + { + ok = false; + break; + } + + + /** + * Internal to DCMTK. + **/ + + case EVR_ox: // OB or OW depending on context + case EVR_xs: // SS or US depending on context + case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) + case EVR_na: // na="not applicable", for data which has no VR + case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + default: + break; + } + } + catch (boost::bad_lexical_cast&) + { + ok = false; + } + + if (!ok) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag, + const Json::Value& value, + bool decodeBinaryTags, + Encoding dicomEncoding) + { + std::auto_ptr element; + + switch (value.type()) + { + case Json::stringValue: + element.reset(CreateElementForTag(tag)); + FillElementWithString(*element, tag, value.asString(), decodeBinaryTags, dicomEncoding); + break; + + case Json::arrayValue: + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + if (key.getEVR() != EVR_SQ) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key, value.size()); + element.reset(sequence); + + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + std::auto_ptr item(new DcmItem); + + Json::Value::Members members = value[i].getMemberNames(); + for (Json::Value::ArrayIndex j = 0; j < members.size(); j++) + { + item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeBinaryTags, dicomEncoding)); + } + + sequence->append(item.release()); + } + + break; + } + + default: + throw OrthancException(ErrorCode_BadParameterType); + } + + return element.release(); + } + + + DcmEVR FromDcmtkBridge::ParseValueRepresentation(const std::string& s) + { + if (s == "AE") + return EVR_AE; + + if (s == "AS") + return EVR_AS; + + if (s == "AT") + return EVR_AT; + + if (s == "CS") + return EVR_CS; + + if (s == "DA") + return EVR_DA; + + if (s == "DS") + return EVR_DS; + + if (s == "DT") + return EVR_DT; + + if (s == "FD") + return EVR_FD; + + if (s == "FL") + return EVR_FL; + + if (s == "IS") + return EVR_IS; + + if (s == "LO") + return EVR_LO; + + if (s == "LT") + return EVR_LT; + + if (s == "OB") + return EVR_OB; + + if (s == "OF") + return EVR_OF; + + if (s == "OW") + return EVR_OW; + + if (s == "PN") + return EVR_PN; + + if (s == "SH") + return EVR_SH; + + if (s == "SL") + return EVR_SL; + + if (s == "SQ") + return EVR_SQ; + + if (s == "SS") + return EVR_SS; + + if (s == "ST") + return EVR_ST; + + if (s == "TM") + return EVR_TM; + + if (s == "UI") + return EVR_UI; + + if (s == "UL") + return EVR_UL; + + if (s == "UN") + return EVR_UN; + + if (s == "US") + return EVR_US; + + if (s == "UT") + return EVR_UT; + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/FromDcmtkBridge.h --- a/OrthancServer/FromDcmtkBridge.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Wed Nov 18 10:16:21 2015 +0100 @@ -44,6 +44,14 @@ class FromDcmtkBridge { public: + static void InitializeDictionary(); + + static void RegisterDictionaryTag(const DicomTag& tag, + const DcmEVR& vr, + const std::string& name, + unsigned int minMultiplicity, + unsigned int maxMultiplicity); + static Encoding DetectEncoding(DcmDataset& dataset); static void Convert(DicomMap& target, DcmDataset& dataset); @@ -52,20 +60,26 @@ static DicomTag GetTag(const DcmElement& element); - static bool IsPrivateTag(DcmTag& tag); - static bool IsPrivateTag(const DicomTag& tag); + static bool IsUnknownTag(const DicomTag& tag); + static DicomValue* ConvertLeafElement(DcmElement& element, + DicomToJsonFlags flags, Encoding encoding); + static void ToJson(Json::Value& parent, + DcmElement& element, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding dicomEncoding); + static void ToJson(Json::Value& target, DcmDataset& dataset, - unsigned int maxStringLength = 256); - - static void ToJson(Json::Value& target, - const std::string& path, - unsigned int maxStringLength = 256); + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength); static std::string GetName(const DicomTag& tag); @@ -95,9 +109,6 @@ target.SetValue(ParseTag(tagName), value); } - static void Print(FILE* fp, - const DicomMap& m); - static void ToJson(Json::Value& result, const DicomMap& values, bool simplify); @@ -108,5 +119,20 @@ DcmDataset& dataSet); static ValueRepresentation GetValueRepresentation(const DicomTag& tag); + + static DcmElement* CreateElementForTag(const DicomTag& tag); + + static void FillElementWithString(DcmElement& element, + const DicomTag& tag, + const std::string& utf8alue, // Encoded using UTF-8 + bool interpretBinaryTags, + Encoding dicomEncoding); + + static DcmElement* FromJson(const DicomTag& tag, + const Json::Value& element, // Encoding using UTF-8 + bool interpretBinaryTags, + Encoding dicomEncoding); + + static DcmEVR ParseValueRepresentation(const std::string& s); }; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/IDatabaseWrapper.h --- a/OrthancServer/IDatabaseWrapper.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/IDatabaseWrapper.h Wed Nov 18 10:16:21 2015 +0100 @@ -51,6 +51,10 @@ { } + virtual void Open() = 0; + + virtual void Close() = 0; + virtual void AddAttachment(int64_t id, const FileInfo& attachment) = 0; @@ -79,6 +83,9 @@ virtual void GetAllMetadata(std::map& target, int64_t id) = 0; + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) = 0; + virtual void GetAllPublicIds(std::list& target, ResourceType resourceType) = 0; @@ -142,11 +149,10 @@ virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property) = 0; - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, - const std::string& value) = 0; - - virtual void LookupIdentifier(std::list& target, + IdentifierConstraintType type, const std::string& value) = 0; virtual bool LookupMetadata(std::string& target, @@ -168,10 +174,16 @@ virtual void SetGlobalProperty(GlobalProperty property, const std::string& value) = 0; + virtual void ClearMainDicomTags(int64_t id) = 0; + virtual void SetMainDicomTag(int64_t id, const DicomTag& tag, const std::string& value) = 0; + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) = 0; + virtual void SetMetadata(int64_t id, MetadataType type, const std::string& value) = 0; diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Internals/StoreScp.cpp --- a/OrthancServer/Internals/StoreScp.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/Internals/StoreScp.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -168,7 +168,10 @@ try { FromDcmtkBridge::Convert(summary, **imageDataSet); - FromDcmtkBridge::ToJson(dicomJson, **imageDataSet); + FromDcmtkBridge::ToJson(dicomJson, **imageDataSet, + DicomToJsonFormat_Full, + DicomToJsonFlags_Default, + 256 /* max string length */); if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet)) { @@ -211,7 +214,7 @@ if (e.GetErrorCode() == ErrorCode_InexistentTag) { - LogMissingRequiredTag(summary); + Toolbox::LogMissingRequiredTag(summary); } else { diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/LuaScripting.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -83,18 +83,23 @@ const char* uri = lua_tostring(state, 1); bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false); - std::string result; - if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri)) + try { - lua_pushlstring(state, result.c_str(), result.size()); + std::string result; + if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + RequestOrigin_Lua, uri)) + { + lua_pushlstring(state, result.c_str(), result.size()); + return 1; + } } - else + catch (OrthancException& e) { - LOG(ERROR) << "Lua: Error in RestApiGet() for URI: " << uri; - lua_pushnil(state); + LOG(ERROR) << "Lua: " << e.What(); } + LOG(ERROR) << "Lua: Error in RestApiGet() for URI: " << uri; + lua_pushnil(state); return 1; } @@ -127,21 +132,26 @@ const char* bodyData = lua_tolstring(state, 2, &bodySize); bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false); - std::string result; - if (isPost ? - HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri, bodyData, bodySize) : - HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri, bodyData, bodySize)) + try { - lua_pushlstring(state, result.c_str(), result.size()); + std::string result; + if (isPost ? + HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + RequestOrigin_Lua, uri, bodyData, bodySize) : + HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + RequestOrigin_Lua, uri, bodyData, bodySize)) + { + lua_pushlstring(state, result.c_str(), result.size()); + return 1; + } } - else + catch (OrthancException& e) { - LOG(ERROR) << "Lua: Error in " << (isPost ? "RestApiPost()" : "RestApiPut()") << " for URI: " << uri; - lua_pushnil(state); + LOG(ERROR) << "Lua: " << e.What(); } + LOG(ERROR) << "Lua: Error in " << (isPost ? "RestApiPost()" : "RestApiPut()") << " for URI: " << uri; + lua_pushnil(state); return 1; } @@ -185,16 +195,22 @@ const char* uri = lua_tostring(state, 1); bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false); - if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri)) + try { - lua_pushboolean(state, 1); + if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + RequestOrigin_Lua, uri)) + { + lua_pushboolean(state, 1); + return 1; + } } - else + catch (OrthancException& e) { - LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri; + LOG(ERROR) << "Lua: " << e.What(); + } + + LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri; lua_pushnil(state); - } return 1; } @@ -254,10 +270,12 @@ if (operation == "modify") { LOG(INFO) << "Lua script to modify resource " << parameters["Resource"].asString(); - DicomModification modification; - OrthancRestApi::ParseModifyRequest(modification, parameters); + std::auto_ptr modification(new DicomModification); + OrthancRestApi::ParseModifyRequest(*modification, parameters); - std::auto_ptr command(new ModifyInstanceCommand(context_, RequestOrigin_Lua, modification)); + std::auto_ptr command + (new ModifyInstanceCommand(context_, RequestOrigin_Lua, modification.release())); + return command.release(); } @@ -316,7 +334,7 @@ { Json::Value operations; LuaFunctionCall call2(lua_, "_AccessJob"); - call2.ExecuteToJson(operations); + call2.ExecuteToJson(operations, false); if (operations.type() != Json::arrayValue) { diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -33,14 +33,12 @@ #include "PrecompiledHeadersServer.h" #include "OrthancFindRequestHandler.h" +#include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" -#include "../Core/DicomFormat/DicomArray.h" -#include "ServerToolbox.h" +#include "FromDcmtkBridge.h" #include "OrthancInitialization.h" -#include "FromDcmtkBridge.h" - -#include "ResourceFinder.h" -#include "DicomFindQuery.h" +#include "Search/LookupResource.h" +#include "ServerToolbox.h" #include @@ -90,130 +88,6 @@ } - namespace - { - class CFindQuery : public DicomFindQuery - { - private: - DicomFindAnswers& answers_; - ServerIndex& index_; - const DicomArray& query_; - bool hasModalitiesInStudy_; - std::set modalitiesInStudy_; - - public: - CFindQuery(DicomFindAnswers& answers, - ServerIndex& index, - const DicomArray& query) : - answers_(answers), - index_(index), - query_(query), - hasModalitiesInStudy_(false) - { - } - - void SetModalitiesInStudy(const std::string& value) - { - hasModalitiesInStudy_ = true; - - std::vector tmp; - Toolbox::TokenizeString(tmp, value, '\\'); - - for (size_t i = 0; i < tmp.size(); i++) - { - modalitiesInStudy_.insert(tmp[i]); - } - } - - virtual bool HasMainDicomTagsFilter(ResourceType level) const - { - if (DicomFindQuery::HasMainDicomTagsFilter(level)) - { - return true; - } - - return (level == ResourceType_Study && - hasModalitiesInStudy_); - } - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const - { - if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags)) - { - return false; - } - - if (level != ResourceType_Study || - !hasModalitiesInStudy_) - { - return true; - } - - try - { - // We are considering a single study, and the - // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check - // whether one of its child series matches one of the - // modalities. - - Json::Value study; - if (index_.LookupResource(study, resourceId, ResourceType_Study)) - { - // Loop over the series of the considered study. - for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) - { - Json::Value series; - if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) - { - // Get the modality of this series - if (series["MainDicomTags"].isMember("Modality")) - { - std::string modality = series["MainDicomTags"]["Modality"].asString(); - if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end()) - { - // This series of the considered study matches one - // of the required modalities. Take the study into - // consideration for future filtering. - return true; - } - } - } - } - } - } - catch (OrthancException&) - { - // This resource has probably been deleted during the find request - } - - return false; - } - - virtual bool HasInstanceFilter() const - { - return true; - } - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const - { - bool ok = DicomFindQuery::FilterInstance(instanceId, content); - - if (ok) - { - // Add this resource to the answers - AddAnswer(answers_, content, query_); - } - - return ok; - } - }; - } - - - bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, @@ -240,12 +114,14 @@ **/ const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); - if (levelTmp == NULL) + if (levelTmp == NULL || + levelTmp->IsNull() || + levelTmp->IsBinary()) { throw OrthancException(ErrorCode_BadRequest); } - ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); + ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); if (level != ResourceType_Patient && level != ResourceType_Study && @@ -265,7 +141,7 @@ { LOG(INFO) << " " << query.GetElement(i).GetTag() << " " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag()) - << " = " << query.GetElement(i).GetValue().AsString(); + << " = " << query.GetElement(i).GetValue().GetContent(); } } @@ -274,9 +150,8 @@ * Build up the query object. **/ - CFindQuery findQuery(answers, context_.GetIndex(), query); - findQuery.SetLevel(level); - + LookupResource finder(level); + for (size_t i = 0; i < query.GetSize(); i++) { const DicomTag tag = query.GetElement(i).GetTag(); @@ -288,21 +163,24 @@ continue; } - std::string value = query.GetElement(i).GetValue().AsString(); + std::string value = query.GetElement(i).GetValue().GetContent(); if (value.size() == 0) { // An empty string corresponds to a "*" wildcard constraint, so we ignore it continue; } - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PatientName) { - findQuery.SetModalitiesInStudy(value); + sensitive = caseSensitivePN; } - else - { - findQuery.SetConstraint(tag, value, caseSensitivePN); - } + + finder.AddDicomConstraint(tag, value, sensitive); } @@ -310,28 +188,35 @@ * Run the query. **/ - ResourceFinder finder(context_); + size_t maxResults = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; + + std::vector resources, instances; + context_.GetIndex().FindCandidates(resources, instances, finder); - switch (level) + assert(resources.size() == instances.size()); + bool finished = true; + + for (size_t i = 0; i < instances.size(); i++) { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: - finder.SetMaxResults(maxResults_); - break; - - case ResourceType_Instance: - finder.SetMaxResults(maxInstances_); - break; - - default: - throw OrthancException(ErrorCode_InternalError); + Json::Value dicom; + context_.ReadJson(dicom, instances[i]); + + if (finder.IsMatch(dicom)) + { + if (maxResults != 0 && + answers.GetSize() >= maxResults) + { + finished = false; + break; + } + else + { + AddAnswer(answers, dicom, query); + } + } } - std::list tmp; - bool finished = finder.Apply(tmp, findQuery); - - LOG(INFO) << "Number of matching resources: " << tmp.size(); + LOG(INFO) << "Number of matching resources: " << answers.GetSize(); return finished; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -39,9 +39,9 @@ #include "../Core/Toolbox.h" #include "../Core/FileStorage/FilesystemStorage.h" -#include "DicomProtocol/DicomServer.h" #include "ServerEnumerations.h" #include "DatabaseWrapper.h" +#include "FromDcmtkBridge.h" #include #include @@ -252,24 +252,26 @@ Json::Value::Members members = parameter.getMemberNames(); for (size_t i = 0; i < members.size(); i++) { - std::string info = "\"" + members[i] + "\" = " + parameter[members[i]].toStyledString(); - LOG(INFO) << "Registering user-defined metadata: " << info; + const std::string& name = members[i]; - if (!parameter[members[i]].asBool()) + if (!parameter[name].isInt()) { - LOG(ERROR) << "Not a number in this user-defined metadata: " << info; + LOG(ERROR) << "Not a number in this user-defined metadata: " << name; throw OrthancException(ErrorCode_BadParameterType); } - int metadata = parameter[members[i]].asInt(); + int metadata = parameter[name].asInt(); + + LOG(INFO) << "Registering user-defined metadata: " << name << " (index " + << metadata << ")"; try { - RegisterUserMetadata(metadata, members[i]); + RegisterUserMetadata(metadata, name); } catch (OrthancException&) { - LOG(ERROR) << "Cannot register this user-defined metadata: " << info; + LOG(ERROR) << "Cannot register this user-defined metadata: " << name; throw; } } @@ -286,24 +288,39 @@ Json::Value::Members members = parameter.getMemberNames(); for (size_t i = 0; i < members.size(); i++) { - std::string info = "\"" + members[i] + "\" = " + parameter[members[i]].toStyledString(); - LOG(INFO) << "Registering user-defined attachment type: " << info; + const std::string& name = members[i]; + std::string mime = "application/octet-stream"; + + const Json::Value& value = parameter[name]; + int contentType; - if (!parameter[members[i]].asBool()) + if (value.isArray() && + value.size() == 2 && + value[0].isInt() && + value[1].isString()) { - LOG(ERROR) << "Not a number in this user-defined attachment type: " << info; + contentType = value[0].asInt(); + mime = value[1].asString(); + } + else if (value.isInt()) + { + contentType = value.asInt(); + } + else + { + LOG(ERROR) << "Not a number in this user-defined attachment type: " << name; throw OrthancException(ErrorCode_BadParameterType); } - int contentType = parameter[members[i]].asInt(); + LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " + << contentType << ") with MIME type \"" << mime << "\""; try { - RegisterUserContentType(contentType, members[i]); + RegisterUserContentType(contentType, name, mime); } catch (OrthancException&) { - LOG(ERROR) << "Cannot register this user-defined attachment type: " << info; throw; } } @@ -311,6 +328,43 @@ } + static void LoadCustomDictionary(const Json::Value& configuration) + { + if (configuration.type() != Json::objectValue || + !configuration.isMember("Dictionary") || + configuration["Dictionary"].type() != Json::objectValue) + { + return; + } + + Json::Value::Members tags(configuration["Dictionary"].getMemberNames()); + + for (Json::Value::ArrayIndex i = 0; i < tags.size(); i++) + { + const Json::Value& content = configuration["Dictionary"][tags[i]]; + if (content.type() != Json::arrayValue || + content.size() < 2 || + content.size() > 4 || + content[0].type() != Json::stringValue || + content[1].type() != Json::stringValue || + (content.size() >= 3 && content[2].type() != Json::intValue) || + (content.size() >= 4 && content[3].type() != Json::intValue)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + DicomTag tag(FromDcmtkBridge::ParseTag(tags[i])); + DcmEVR vr = FromDcmtkBridge::ParseValueRepresentation(content[0].asString()); + std::string name = content[1].asString(); + unsigned int minMultiplicity = (content.size() >= 2) ? content[2].asUInt() : 1; + unsigned int maxMultiplicity = (content.size() >= 3) ? content[3].asUInt() : 1; + + FromDcmtkBridge::RegisterDictionaryTag(tag, vr, name, minMultiplicity, maxMultiplicity); + } + } + + + void OrthancInitialize(const char* configurationFile) { boost::mutex::scoped_lock lock(globalMutex_); @@ -338,7 +392,8 @@ RegisterUserMetadata(); RegisterUserContentType(); - DicomServer::InitializeDictionary(); + FromDcmtkBridge::InitializeDictionary(); + LoadCustomDictionary(configuration_); #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 LOG(WARNING) << "Registering JPEG Lossless codecs"; diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -105,18 +105,50 @@ bool OrthancMoveRequestHandler::LookupIdentifier(std::string& publicId, - DicomTag tag, + ResourceType level, const DicomMap& input) { + DicomTag tag(0, 0); // Dummy initialization + + switch (level) + { + case ResourceType_Patient: + tag = DICOM_TAG_PATIENT_ID; + break; + + case ResourceType_Study: + tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ? + DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + tag = DICOM_TAG_SERIES_INSTANCE_UID; + break; + + case ResourceType_Instance: + tag = DICOM_TAG_SOP_INSTANCE_UID; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + if (!input.HasTag(tag)) { return false; } - std::string value = input.GetValue(tag).AsString(); + const DicomValue& value = input.GetValue(tag); + if (value.IsNull() || + value.IsBinary()) + { + return false; + } + + const std::string& content = value.GetContent(); std::list ids; - context_.GetIndex().LookupIdentifier(ids, tag, value); + context_.GetIndex().LookupIdentifierExact(ids, level, tag, content); if (ids.size() != 1) { @@ -145,7 +177,7 @@ { LOG(INFO) << " " << query.GetElement(i).GetTag() << " " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag()) - << " = " << query.GetElement(i).GetValue().AsString(); + << " = " << query.GetElement(i).GetValue().GetContent(); } } } @@ -156,14 +188,11 @@ * Retrieve the query level. **/ - ResourceType level; const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); - if (levelTmp != NULL) - { - level = StringToResourceType(levelTmp->AsString().c_str()); - } - else + if (levelTmp == NULL || + levelTmp->IsNull() || + levelTmp->IsBinary()) { // The query level is not present in the C-Move request, which // does not follow the DICOM standard. This is for instance the @@ -173,10 +202,10 @@ std::string publicId; - if (LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input) || - LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input) || - LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input) || - LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input)) + if (LookupIdentifier(publicId, ResourceType_Instance, input) || + LookupIdentifier(publicId, ResourceType_Series, input) || + LookupIdentifier(publicId, ResourceType_Study, input) || + LookupIdentifier(publicId, ResourceType_Patient, input)) { return new OrthancMoveRequestIterator(context_, targetAet, publicId); } @@ -187,42 +216,23 @@ } } + assert(levelTmp != NULL); + ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); /** * Lookup for the resource to be sent. **/ - bool ok; std::string publicId; - switch (level) + if (LookupIdentifier(publicId, level, input)) { - case ResourceType_Patient: - ok = LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input); - break; - - case ResourceType_Study: - ok = LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); - break; - - case ResourceType_Series: - ok = LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); - break; - - case ResourceType_Instance: - ok = LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); - break; - - default: - ok = false; + return new OrthancMoveRequestIterator(context_, targetAet, publicId); } - - if (!ok) + else { throw OrthancException(ErrorCode_BadRequest); } - - return new OrthancMoveRequestIterator(context_, targetAet, publicId); } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancMoveRequestHandler.h --- a/OrthancServer/OrthancMoveRequestHandler.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 @@ -42,7 +42,7 @@ ServerContext& context_; bool LookupIdentifier(std::string& publicId, - DicomTag tag, + ResourceType level, const DicomMap& input); public: diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -40,6 +40,7 @@ #include "../OrthancInitialization.h" #include +#include namespace Orthanc { @@ -97,12 +98,13 @@ for (size_t i = 0; i < members.size(); i++) { const std::string& name = members[i]; - std::string value = replacements[name].asString(); + const Json::Value& value = replacements[name]; DicomTag tag = FromDcmtkBridge::ParseTag(name); target.Replace(tag, value); - VLOG(1) << "Replace: " << name << " " << tag << " == " << value << std::endl; + VLOG(1) << "Replace: " << name << " " << tag + << " == " << value.toStyledString() << std::endl; } } @@ -167,7 +169,7 @@ // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": null,"Remove":["Modality"]}' > Anonymized.dcm target.SetupAnonymization(); - std::string patientName = target.GetReplacement(DICOM_TAG_PATIENT_NAME); + std::string patientName = target.GetReplacementAsString(DICOM_TAG_PATIENT_NAME); Json::Value request; if (call.ParseJsonRequest(request) && request.isObject()) @@ -485,7 +487,8 @@ static void InjectTags(ParsedDicomFile& dicom, - const Json::Value& tags) + const Json::Value& tags, + bool decodeBinaryTags) { if (tags.type() != Json::objectValue) { @@ -497,17 +500,21 @@ for (size_t i = 0; i < members.size(); i++) { const std::string& name = members[i]; - if (tags[name].type() != Json::stringValue) - { - throw OrthancException(ErrorCode_CreateDicomNotString); - } + DicomTag tag = FromDcmtkBridge::ParseTag(name); - std::string value = tags[name].asString(); - - DicomTag tag = FromDcmtkBridge::ParseTag(name); if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) { if (tag != DICOM_TAG_PATIENT_ID && + tag != DICOM_TAG_ACQUISITION_DATE && + tag != DICOM_TAG_ACQUISITION_TIME && + tag != DICOM_TAG_CONTENT_DATE && + tag != DICOM_TAG_CONTENT_TIME && + tag != DICOM_TAG_INSTANCE_CREATION_DATE && + tag != DICOM_TAG_INSTANCE_CREATION_TIME && + tag != DICOM_TAG_SERIES_DATE && + tag != DICOM_TAG_SERIES_TIME && + tag != DICOM_TAG_STUDY_DATE && + tag != DICOM_TAG_STUDY_TIME && dicom.HasTag(tag)) { throw OrthancException(ErrorCode_CreateDicomOverrideTag); @@ -519,7 +526,7 @@ } else { - dicom.Replace(tag, Toolbox::ConvertFromUtf8(value, dicom.GetEncoding())); + dicom.Replace(tag, tags[name], decodeBinaryTags); } } } @@ -528,7 +535,8 @@ static void CreateSeries(RestApiPostCall& call, ParsedDicomFile& base /* in */, - const Json::Value& content) + const Json::Value& content, + bool decodeBinaryTags) { assert(content.isArray()); assert(content.size() > 0); @@ -561,7 +569,7 @@ if (content[i].isMember("Tags")) { - InjectTags(*dicom, content[i]["Tags"]); + InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags); } } @@ -734,6 +742,19 @@ } } + + bool decodeBinaryTags = true; + if (request.isMember("InterpretBinaryTags")) + { + const Json::Value& v = request["InterpretBinaryTags"]; + if (v.type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + + decodeBinaryTags = v.asBool(); + } + // Inject time-related information std::string date, time; @@ -761,7 +782,7 @@ } - InjectTags(dicom, request["Tags"]); + InjectTags(dicom, request["Tags"], decodeBinaryTags); // Inject the content (either an image, or a PDF file) @@ -779,7 +800,7 @@ if (content.size() > 0) { // Let's create a series instead of a single instance - CreateSeries(call, dicom, content); + CreateSeries(call, dicom, content, decodeBinaryTags); return; } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -59,11 +59,20 @@ void OrthancRestApi::ResetOrthanc(RestApiPostCall& call) { + OrthancRestApi::GetApi(call).leaveBarrier_ = true; OrthancRestApi::GetApi(call).resetRequestReceived_ = true; call.GetOutput().AnswerBuffer("{}", "application/json"); } + void OrthancRestApi::ShutdownOrthanc(RestApiPostCall& call) + { + OrthancRestApi::GetApi(call).leaveBarrier_ = true; + call.GetOutput().AnswerBuffer("{}", "application/json"); + LOG(WARNING) << "Shutdown request received"; + } + + @@ -99,6 +108,7 @@ OrthancRestApi::OrthancRestApi(ServerContext& context) : context_(context), + leaveBarrier_(false), resetRequestReceived_(false) { RegisterSystem(); @@ -114,6 +124,7 @@ // Auto-generated directories Register("/tools", RestApi::AutoListChildren); Register("/tools/reset", ResetOrthanc); + Register("/tools/shutdown", ShutdownOrthanc); Register("/instances/{id}/frames/{frame}", RestApi::AutoListChildren); } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Wed Nov 18 10:16:21 2015 +0100 @@ -49,6 +49,7 @@ private: ServerContext& context_; + bool leaveBarrier_; bool resetRequestReceived_; void RegisterSystem(); @@ -65,10 +66,17 @@ static void ResetOrthanc(RestApiPostCall& call); + static void ShutdownOrthanc(RestApiPostCall& call); + public: OrthancRestApi(ServerContext& context); - const bool& ResetRequestReceivedFlag() const + const bool& LeaveBarrierFlag() const + { + return leaveBarrier_; + } + + bool IsResetRequestReceived() const { return resetRequestReceived_; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -34,6 +34,7 @@ #include "OrthancRestApi.h" #include "../DicomDirWriter.h" +#include "../../Core/FileStorage/StorageAccessor.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" #include "../../Core/Logging.h" @@ -53,200 +54,11 @@ { // Download of ZIP files ---------------------------------------------------- - static std::string GetDirectoryNameInArchive(const Json::Value& resource, - ResourceType resourceType) + static bool IsZip64Required(uint64_t uncompressedSize, + unsigned int countInstances) { - std::string s; - const Json::Value& tags = resource["MainDicomTags"]; - - switch (resourceType) - { - case ResourceType_Patient: - { - std::string p = tags["PatientID"].asString(); - std::string n = tags["PatientName"].asString(); - s = p + " " + n; - break; - } - - case ResourceType_Study: - { - std::string p; - if (tags.isMember("AccessionNumber")) - { - p = tags["AccessionNumber"].asString() + " "; - } - - s = p + tags["StudyDescription"].asString(); - break; - } - - case ResourceType_Series: - { - std::string d = tags["SeriesDescription"].asString(); - std::string m = tags["Modality"].asString(); - s = m + " " + d; - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - // Get rid of special characters - return Toolbox::ConvertToAscii(s); - } - - static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, - ServerContext& context, - const Json::Value& resource, - ResourceType resourceType) - { - if (resourceType == ResourceType_Patient) - { - return true; - } - - ResourceType parentType = GetParentResourceType(resourceType); - Json::Value parent; - - switch (resourceType) - { - case ResourceType_Study: - { - if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType)) - { - return false; - } - - break; - } - - case ResourceType_Series: - if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) || - !CreateRootDirectoryInArchive(writer, context, parent, parentType)) - { - return false; - } - break; - - default: - throw OrthancException(ErrorCode_NotImplemented); - } - - writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str()); - return true; - } - - static bool ArchiveInstance(HierarchicalZipWriter& writer, - ServerContext& context, - const std::string& instancePublicId, - const char* filename) - { - writer.OpenFile(filename); - - std::string dicom; - context.ReadFile(dicom, instancePublicId, FileContentType_Dicom); - writer.Write(dicom); + static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; - return true; - } - - static bool ArchiveInternal(HierarchicalZipWriter& writer, - ServerContext& context, - const std::string& publicId, - ResourceType resourceType, - bool isFirstLevel) - { - Json::Value resource; - if (!context.GetIndex().LookupResource(resource, publicId, resourceType)) - { - return false; - } - - if (isFirstLevel && - !CreateRootDirectoryInArchive(writer, context, resource, resourceType)) - { - return false; - } - - writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str()); - - switch (resourceType) - { - case ResourceType_Patient: - for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++) - { - std::string studyId = resource["Studies"][i].asString(); - if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false)) - { - return false; - } - } - break; - - case ResourceType_Study: - for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++) - { - std::string seriesId = resource["Series"][i].asString(); - if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false)) - { - return false; - } - } - break; - - case ResourceType_Series: - { - // Create a filename prefix, depending on the modality - char format[24] = "%08d.dcm"; - - if (resource["MainDicomTags"].isMember("Modality")) - { - std::string modality = resource["MainDicomTags"]["Modality"].asString(); - - if (modality.size() == 1) - { - snprintf(format, sizeof(format) - 1, "%c%%07d.dcm", toupper(modality[0])); - } - else if (modality.size() >= 2) - { - snprintf(format, sizeof(format) - 1, "%c%c%%06d.dcm", toupper(modality[0]), toupper(modality[1])); - } - } - - char filename[24]; - - for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++) - { - snprintf(filename, sizeof(filename) - 1, format, i); - - std::string publicId = resource["Instances"][i].asString(); - - // This was the implementation up to Orthanc 0.7.0: - // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm"; - - if (!ArchiveInstance(writer, context, publicId, filename)) - { - return false; - } - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - writer.CloseDirectory(); - return true; - } - - - static bool IsZip64Required(ServerIndex& index, - const std::string& id) - { /** * Determine whether ZIP64 is required. Original ZIP format can * store up to 2GB of data (some implementation supporting up to @@ -254,14 +66,7 @@ * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64 **/ - uint64_t uncompressedSize; - uint64_t compressedSize; - unsigned int countStudies; - unsigned int countSeries; - unsigned int countInstances; - index.GetStatistics(compressedSize, uncompressedSize, - countStudies, countSeries, countInstances, id); - const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES || + const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN || countInstances >= 65535); LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size " @@ -270,112 +75,701 @@ return isZip64; } - + + + namespace + { + class ResourceIdentifiers + { + private: + ResourceType level_; + std::string patient_; + std::string study_; + std::string series_; + std::string instance_; + + static void GoToParent(ServerIndex& index, + std::string& current) + { + std::string tmp; + + if (index.LookupParent(tmp, current)) + { + current = tmp; + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } - template - static void GetArchive(RestApiGetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); + + public: + ResourceIdentifiers(ServerIndex& index, + const std::string& publicId) + { + if (!index.LookupResourceType(level_, publicId)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + std::string current = publicId;; + switch (level_) // Do not add "break" below! + { + case ResourceType_Instance: + instance_ = current; + GoToParent(index, current); + + case ResourceType_Series: + series_ = current; + GoToParent(index, current); + + case ResourceType_Study: + study_ = current; + GoToParent(index, current); + + case ResourceType_Patient: + patient_ = current; + break; - std::string id = call.GetUriComponent("id", ""); - bool isZip64 = IsZip64Required(context.GetIndex(), id); + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + ResourceType GetLevel() const + { + return level_; + } + + const std::string& GetIdentifier(ResourceType level) const + { + // Some sanity check to ensure enumerations are not altered + assert(ResourceType_Patient < ResourceType_Study); + assert(ResourceType_Study < ResourceType_Series); + assert(ResourceType_Series < ResourceType_Instance); + + if (level > level_) + { + throw OrthancException(ErrorCode_InternalError); + } + + switch (level) + { + case ResourceType_Patient: + return patient_; + + case ResourceType_Study: + return study_; - // Create a RAII for the temporary file to manage the ZIP file - Toolbox::TemporaryFile tmp; + case ResourceType_Series: + return series_; + + case ResourceType_Instance: + return instance_; + default: + throw OrthancException(ErrorCode_InternalError); + } + } + }; + + + class IArchiveVisitor : public boost::noncopyable { - // Create a ZIP writer - HierarchicalZipWriter writer(tmp.GetPath().c_str()); - writer.SetZip64(isZip64); + public: + virtual ~IArchiveVisitor() + { + } + + virtual void Open(ResourceType level, + const std::string& publicId) = 0; + + virtual void Close() = 0; + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) = 0; + }; + - // Store the requested resource into the ZIP - if (!ArchiveInternal(writer, context, id, resourceType, true)) + class ArchiveIndex + { + private: + struct Instance + { + std::string id_; + FileInfo dicom_; + + Instance(const std::string& id, + const FileInfo& dicom) : + id_(id), dicom_(dicom) + { + } + }; + + // A "NULL" value for ArchiveIndex indicates a non-expanded node + typedef std::map Resources; + + ResourceType level_; + Resources resources_; // Only at patient/study/series level + std::list instances_; // Only at instance level + + + void AddResourceToExpand(ServerIndex& index, + const std::string& id) { - return; + if (level_ == ResourceType_Instance) + { + FileInfo tmp; + if (index.LookupAttachment(tmp, id, FileContentType_Dicom)) + { + instances_.push_back(Instance(id, tmp)); + } + } + else + { + resources_[id] = NULL; + } } - } + + + public: + ArchiveIndex(ResourceType level) : + level_(level) + { + } + + ~ArchiveIndex() + { + for (Resources::iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + delete it->second; + } + } + + + void Add(ServerIndex& index, + const ResourceIdentifiers& resource) + { + const std::string& id = resource.GetIdentifier(level_); + Resources::iterator previous = resources_.find(id); - // Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp.GetPath()); - sender.SetContentType("application/zip"); - sender.SetContentFilename(id + ".zip"); + if (level_ == ResourceType_Instance) + { + AddResourceToExpand(index, id); + } + else if (resource.GetLevel() == level_) + { + // Mark this resource for further expansion + if (previous != resources_.end()) + { + delete previous->second; + } + + resources_[id] = NULL; + } + else if (previous == resources_.end()) + { + // This is the first time we meet this resource + std::auto_ptr child(new ArchiveIndex(GetChildResourceType(level_))); + child->Add(index, resource); + resources_[id] = child.release(); + } + else if (previous->second != NULL) + { + previous->second->Add(index, resource); + } + else + { + // Nothing to do: This item is marked for further expansion + } + } + - // Send the ZIP - call.GetOutput().AnswerStream(sender); + void Expand(ServerIndex& index) + { + if (level_ == ResourceType_Instance) + { + // Expanding an instance node makes no sense + return; + } - // The temporary file is automatically removed thanks to the RAII - } + for (Resources::iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + if (it->second == NULL) + { + // This is resource is marked for expansion + std::list children; + index.GetChildren(children, it->first); + + std::auto_ptr child(new ArchiveIndex(GetChildResourceType(level_))); + + for (std::list::const_iterator + it2 = children.begin(); it2 != children.end(); ++it2) + { + child->AddResourceToExpand(index, *it2); + } + + it->second = child.release(); + } + + assert(it->second != NULL); + it->second->Expand(index); + } + } - static void GetMediaArchive(RestApiGetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); + void Apply(IArchiveVisitor& visitor) const + { + if (level_ == ResourceType_Instance) + { + for (std::list::const_iterator + it = instances_.begin(); it != instances_.end(); ++it) + { + visitor.AddInstance(it->id_, it->dicom_); + } + } + else + { + for (Resources::const_iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + assert(it->second != NULL); // There must have been a call to "Expand()" + visitor.Open(level_, it->first); + it->second->Apply(visitor); + visitor.Close(); + } + } + } + }; + + + class StatisticsVisitor : public IArchiveVisitor + { + private: + uint64_t size_; + unsigned int instances_; + + public: + StatisticsVisitor() : size_(0), instances_(0) + { + } + + uint64_t GetUncompressedSize() const + { + return size_; + } + + unsigned int GetInstancesCount() const + { + return instances_; + } + + virtual void Open(ResourceType level, + const std::string& publicId) + { + } - std::string id = call.GetUriComponent("id", ""); - bool isZip64 = IsZip64Required(context.GetIndex(), id); + virtual void Close() + { + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) + { + instances_ ++; + size_ += dicom.GetUncompressedSize(); + } + }; + + + class PrintVisitor : public IArchiveVisitor + { + private: + std::ostream& out_; + std::string indent_; + + public: + PrintVisitor(std::ostream& out) : out_(out) + { + } - // Create a RAII for the temporary file to manage the ZIP file - Toolbox::TemporaryFile tmp; + virtual void Open(ResourceType level, + const std::string& publicId) + { + switch (level) + { + case ResourceType_Patient: indent_ = ""; break; + case ResourceType_Study: indent_ = " "; break; + case ResourceType_Series: indent_ = " "; break; + default: + throw OrthancException(ErrorCode_InternalError); + } + out_ << indent_ << publicId << std::endl; + } + + virtual void Close() + { + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) + { + out_ << " " << instanceId << std::endl; + } + }; + + + class ArchiveWriterVisitor : public IArchiveVisitor { - // Create a ZIP writer - HierarchicalZipWriter writer(tmp.GetPath().c_str()); - writer.SetZip64(isZip64); - writer.OpenDirectory("IMAGES"); + private: + HierarchicalZipWriter& writer_; + ServerContext& context_; + char instanceFormat_[24]; + unsigned int countInstances_; + + static std::string GetTag(const DicomMap& tags, + const DicomTag& tag) + { + const DicomValue* v = tags.TestAndGetValue(tag); + if (v != NULL && + !v->IsBinary() && + !v->IsNull()) + { + return v->GetContent(); + } + else + { + return ""; + } + } + + public: + ArchiveWriterVisitor(HierarchicalZipWriter& writer, + ServerContext& context) : + writer_(writer), + context_(context) + { + } + + virtual void Open(ResourceType level, + const std::string& publicId) + { + std::string path; + + DicomMap tags; + if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level)) + { + switch (level) + { + case ResourceType_Patient: + path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME); + break; - // Create the DICOMDIR writer - DicomDirWriter dicomDir; + case ResourceType_Study: + path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION); + break; + + case ResourceType_Series: + { + std::string modality = GetTag(tags, DICOM_TAG_MODALITY); + path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION); + + if (modality.size() == 0) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); + } + else if (modality.size() == 1) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", + toupper(modality[0])); + } + else if (modality.size() >= 2) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", + toupper(modality[0]), toupper(modality[1])); + } + + countInstances_ = 0; + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path)); + + if (path.empty()) + { + path = std::string("Unknown ") + EnumerationToString(level); + } + + writer_.OpenDirectory(path.c_str()); + } - // Retrieve the list of the instances - std::list instances; - context.GetIndex().GetChildInstances(instances, id); + virtual void Close() + { + writer_.CloseDirectory(); + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) + { + std::string content; + context_.ReadFile(content, dicom); + + char filename[24]; + snprintf(filename, sizeof(filename) - 1, instanceFormat_, countInstances_); + countInstances_ ++; + + writer_.OpenFile(filename); + writer_.Write(content); + } + + static void Apply(RestApiOutput& output, + ServerContext& context, + ArchiveIndex& archive, + const std::string& filename) + { + archive.Expand(context.GetIndex()); + + StatisticsVisitor stats; + archive.Apply(stats); + + const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount()); + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + + { + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + writer.SetZip64(isZip64); + + ArchiveWriterVisitor v(writer, context); + archive.Apply(v); + } - size_t pos = 0; - for (std::list::const_iterator - it = instances.begin(); it != instances.end(); ++it, ++pos) + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath()); + sender.SetContentType("application/zip"); + sender.SetContentFilename(filename); + + // Send the ZIP + output.AnswerStream(sender); + + // The temporary file is automatically removed thanks to the RAII + } + }; + + + class MediaWriterVisitor : public IArchiveVisitor + { + private: + HierarchicalZipWriter& writer_; + DicomDirWriter dicomDir_; + ServerContext& context_; + unsigned int countInstances_; + + public: + MediaWriterVisitor(HierarchicalZipWriter& writer, + ServerContext& context) : + writer_(writer), + context_(context), + countInstances_(0) + { + } + + void EncodeDicomDir(std::string& result) + { + dicomDir_.Encode(result); + } + + virtual void Open(ResourceType level, + const std::string& publicId) + { + } + + virtual void Close() + { + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) { // "DICOM restricts the filenames on DICOM media to 8 // characters (some systems wrongly use 8.3, but this does not // conform to the standard)." - std::string filename = "IM" + boost::lexical_cast(pos); - writer.OpenFile(filename.c_str()); + std::string filename = "IM" + boost::lexical_cast(countInstances_); + writer_.OpenFile(filename.c_str()); - std::string dicom; - context.ReadFile(dicom, *it, FileContentType_Dicom); - writer.Write(dicom); + std::string content; + context_.ReadFile(content, dicom); + writer_.Write(content); - ParsedDicomFile parsed(dicom); - dicomDir.Add("IMAGES", filename, parsed); + ParsedDicomFile parsed(content); + dicomDir_.Add("IMAGES", filename, parsed); + + countInstances_ ++; } - // Add the DICOMDIR - writer.CloseDirectory(); - writer.OpenFile("DICOMDIR"); - std::string s; - dicomDir.Encode(s); - writer.Write(s); - } + static void Apply(RestApiOutput& output, + ServerContext& context, + ArchiveIndex& archive, + const std::string& filename) + { + archive.Expand(context.GetIndex()); + + StatisticsVisitor stats; + archive.Apply(stats); + + const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount()); + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + + { + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + writer.SetZip64(isZip64); + writer.OpenDirectory("IMAGES"); + + // Create the DICOMDIR writer + DicomDirWriter dicomDir; + + MediaWriterVisitor v(writer, context); + archive.Apply(v); + + // Add the DICOMDIR + writer.CloseDirectory(); + writer.OpenFile("DICOMDIR"); + std::string s; + v.EncodeDicomDir(s); + writer.Write(s); + } + + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath()); + sender.SetContentType("application/zip"); + sender.SetContentFilename(filename); + + // Send the ZIP + output.AnswerStream(sender); + + // The temporary file is automatically removed thanks to the RAII + } + }; + } + + + static bool AddResourcesOfInterest(ArchiveIndex& archive, + RestApiPostCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + Json::Value resources; + if (call.ParseJsonRequest(resources) && + resources.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + { + if (resources[i].type() != Json::stringValue) + { + return false; // Bad request + } + + ResourceIdentifiers resource(index, resources[i].asString()); + archive.Add(index, resource); + } - // Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp.GetPath()); - sender.SetContentType("application/zip"); - sender.SetContentFilename(id + ".zip"); + return true; + } + else + { + return false; + } + } + + + static void CreateBatchArchive(RestApiPostCall& call) + { + ArchiveIndex archive(ResourceType_Patient); // root + + if (AddResourcesOfInterest(archive, call)) + { + ArchiveWriterVisitor::Apply(call.GetOutput(), + OrthancRestApi::GetContext(call), + archive, + "Archive.zip"); + } + } + + + static void CreateBatchMedia(RestApiPostCall& call) + { + ArchiveIndex archive(ResourceType_Patient); // root - // Send the ZIP - call.GetOutput().AnswerStream(sender); + if (AddResourcesOfInterest(archive, call)) + { + MediaWriterVisitor::Apply(call.GetOutput(), + OrthancRestApi::GetContext(call), + archive, + "Archive.zip"); + } + } + + + static void CreateArchive(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::string id = call.GetUriComponent("id", ""); + ResourceIdentifiers resource(index, id); + + ArchiveIndex archive(ResourceType_Patient); // root + archive.Add(OrthancRestApi::GetIndex(call), resource); - // The temporary file is automatically removed thanks to the RAII + ArchiveWriterVisitor::Apply(call.GetOutput(), + OrthancRestApi::GetContext(call), + archive, + id + ".zip"); + } + + + static void CreateMedia(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::string id = call.GetUriComponent("id", ""); + ResourceIdentifiers resource(index, id); + + ArchiveIndex archive(ResourceType_Patient); // root + archive.Add(OrthancRestApi::GetIndex(call), resource); + + MediaWriterVisitor::Apply(call.GetOutput(), + OrthancRestApi::GetContext(call), + archive, + id + ".zip"); } void OrthancRestApi::RegisterArchive() { - Register("/patients/{id}/archive", GetArchive); - Register("/studies/{id}/archive", GetArchive); - Register("/series/{id}/archive", GetArchive); + Register("/patients/{id}/archive", CreateArchive); + Register("/studies/{id}/archive", CreateArchive); + Register("/series/{id}/archive", CreateArchive); - Register("/patients/{id}/media", GetMediaArchive); - Register("/studies/{id}/media", GetMediaArchive); - Register("/series/{id}/media", GetMediaArchive); + Register("/patients/{id}/media", CreateMedia); + Register("/studies/{id}/media", CreateMedia); + Register("/series/{id}/media", CreateMedia); + + Register("/tools/create-archive", CreateBatchArchive); + Register("/tools/create-media", CreateBatchMedia); } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -198,8 +198,8 @@ return; } - if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) + if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) { return; } @@ -228,9 +228,9 @@ return; } - if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) + if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) || + fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2) { return; } @@ -259,10 +259,10 @@ return; } - if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || - fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) + if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) || + fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2 || + fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent().size() <= 2) { return; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -34,11 +34,11 @@ #include "OrthancRestApi.h" #include "../../Core/Logging.h" +#include "../../Core/HttpServer/HttpContentNegociation.h" #include "../ServerToolbox.h" #include "../FromDcmtkBridge.h" -#include "../ResourceFinder.h" -#include "../DicomFindQuery.h" #include "../ServerContext.h" +#include "../SliceOrdering.h" namespace Orthanc @@ -208,7 +208,7 @@ context.ReadJson(full, publicId); Json::Value simplified; - SimplifyTags(simplified, full); + Toolbox::SimplifyTags(simplified, full); call.GetOutput().AnswerJson(simplified); } else @@ -260,6 +260,120 @@ } + namespace + { + class ImageToEncode + { + private: + std::string format_; + std::string encoded_; + ParsedDicomFile& dicom_; + unsigned int frame_; + ImageExtractionMode mode_; + + public: + ImageToEncode(ParsedDicomFile& dicom, + unsigned int frame, + ImageExtractionMode mode) : + dicom_(dicom), + frame_(frame), + mode_(mode) + { + } + + ParsedDicomFile& GetDicom() const + { + return dicom_; + } + + unsigned int GetFrame() const + { + return frame_; + } + + ImageExtractionMode GetMode() const + { + return mode_; + } + + void SetFormat(const std::string& format) + { + format_ = format; + } + + std::string& GetTarget() + { + return encoded_; + } + + void Answer(RestApiOutput& output) + { + output.AnswerBuffer(encoded_, format_); + } + }; + + class EncodePng : public HttpContentNegociation::IHandler + { + private: + ImageToEncode& image_; + + public: + EncodePng(ImageToEncode& image) : image_(image) + { + } + + virtual void Handle(const std::string& type, + const std::string& subtype) + { + assert(type == "image"); + assert(subtype == "png"); + image_.GetDicom().ExtractPngImage(image_.GetTarget(), image_.GetFrame(), image_.GetMode()); + image_.SetFormat("image/png"); + } + }; + + class EncodeJpeg : public HttpContentNegociation::IHandler + { + private: + ImageToEncode& image_; + unsigned int quality_; + + public: + EncodeJpeg(ImageToEncode& image, + const RestApiGetCall& call) : + image_(image) + { + std::string v = call.GetArgument("quality", "90" /* default JPEG quality */); + bool ok = false; + + try + { + quality_ = boost::lexical_cast(v); + ok = (quality_ >= 0 && quality_ <= 100); + } + catch (boost::bad_lexical_cast&) + { + } + + if (!ok) + { + LOG(ERROR) << "Bad quality for a JPEG encoding (must be a number between 0 and 100): " << v; + throw OrthancException(ErrorCode_BadRequest); + } + } + + virtual void Handle(const std::string& type, + const std::string& subtype) + { + assert(type == "image"); + assert(subtype == "jpeg"); + image_.GetDicom().ExtractJpegImage(image_.GetTarget(), image_.GetFrame(), image_.GetMode(), quality_); + image_.SetFormat("image/jpeg"); + } + }; + } + + template static void GetImage(RestApiGetCall& call) { @@ -278,15 +392,23 @@ } std::string publicId = call.GetUriComponent("id", ""); - std::string dicomContent, png; + std::string dicomContent; context.ReadFile(dicomContent, publicId, FileContentType_Dicom); ParsedDicomFile dicom(dicomContent); try { - dicom.ExtractPngImage(png, frame, mode); - call.GetOutput().AnswerBuffer(png, "image/png"); + ImageToEncode image(dicom, frame, mode); + + HttpContentNegociation negociation; + EncodePng png(image); negociation.Register("image/png", png); + EncodeJpeg jpeg(image, call); negociation.Register("image/jpeg", jpeg); + + if (negociation.Apply(call.GetHttpHeaders())) + { + image.Answer(call.GetOutput()); + } } catch (OrthancException& e) { @@ -406,10 +528,8 @@ std::string name = call.GetUriComponent("name", ""); MetadataType metadata = StringToMetadata(name); - if (metadata >= MetadataType_StartUser && - metadata <= MetadataType_EndUser) - { - // It is forbidden to modify internal metadata + if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata + { OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); call.GetOutput().AnswerBuffer("", "text/plain"); } @@ -427,8 +547,7 @@ std::string value; call.BodyToString(value); - if (metadata >= MetadataType_StartUser && - metadata <= MetadataType_EndUser) + if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata { // It is forbidden to modify internal metadata OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); @@ -479,6 +598,7 @@ { Json::Value operations = Json::arrayValue; + operations.append("compress"); operations.append("compressed-data"); if (info.GetCompressedMD5() != "") @@ -488,6 +608,7 @@ operations.append("compressed-size"); operations.append("data"); + operations.append("is-compressed"); if (info.GetUncompressedMD5() != "") { @@ -495,6 +616,7 @@ } operations.append("size"); + operations.append("uncompress"); if (info.GetCompressedMD5() != "" && info.GetUncompressedMD5() != "") @@ -636,8 +758,7 @@ std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - if (contentType >= FileContentType_StartUser && // It is forbidden to modify internal attachments - contentType <= FileContentType_EndUser && + if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize())) { call.GetOutput().AnswerBuffer("{}", "application/json"); @@ -653,16 +774,39 @@ std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - if (contentType >= FileContentType_StartUser && - contentType <= FileContentType_EndUser) + if (IsUserContentType(contentType)) // It is forbidden to delete internal attachments { - // It is forbidden to delete internal attachments OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); call.GetOutput().AnswerBuffer("{}", "application/json"); } } + template + static void ChangeAttachmentCompression(RestApiPostCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression); + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + static void IsAttachmentCompressed(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; + call.GetOutput().AnswerBuffer(answer, "text/plain"); + } + } + + // Raw access to the DICOM tags of an instance ------------------------------ static void GetRawContent(RestApiGetCall& call) @@ -764,7 +908,7 @@ if (simplify) { Json::Value simplified; - SimplifyTags(simplified, sharedTags); + Toolbox::SimplifyTags(simplified, sharedTags); call.GetOutput().AnswerJson(simplified); } else @@ -831,7 +975,7 @@ if (simplify) { Json::Value simplified; - SimplifyTags(simplified, result); + Toolbox::SimplifyTags(simplified, result); call.GetOutput().AnswerJson(simplified); } else @@ -850,20 +994,44 @@ } + namespace + { + typedef std::list< std::pair > LookupResults; + } + + + static void AccumulateLookupResults(LookupResults& result, + ServerIndex& index, + const DicomTag& tag, + const std::string& value, + ResourceType level) + { + std::list tmp; + index.LookupIdentifierExact(tmp, level, tag, value); + + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + result.push_back(std::make_pair(level, *it)); + } + } + + static void Lookup(RestApiPostCall& call) { - typedef std::list< std::pair > Resources; - std::string tag; call.BodyToString(tag); - Resources resources; - - OrthancRestApi::GetIndex(call).LookupIdentifier(resources, tag); - Json::Value result = Json::arrayValue; - - for (Resources::const_iterator it = resources.begin(); - it != resources.end(); ++it) + LookupResults resources; + ServerIndex& index = OrthancRestApi::GetIndex(call); + AccumulateLookupResults(resources, index, DICOM_TAG_PATIENT_ID, tag, ResourceType_Patient); + AccumulateLookupResults(resources, index, DICOM_TAG_STUDY_INSTANCE_UID, tag, ResourceType_Study); + AccumulateLookupResults(resources, index, DICOM_TAG_SERIES_INSTANCE_UID, tag, ResourceType_Series); + AccumulateLookupResults(resources, index, DICOM_TAG_SOP_INSTANCE_UID, tag, ResourceType_Instance); + + Json::Value result = Json::arrayValue; + for (LookupResults::const_iterator + it = resources.begin(); it != resources.end(); ++it) { ResourceType type = it->first; const std::string& id = it->second; @@ -891,7 +1059,8 @@ request.isMember("Query") && request["Level"].type() == Json::stringValue && request["Query"].type() == Json::objectValue && - (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue)) + (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) && + (!request.isMember("Limit") || request["Limit"].type() == Json::intValue)) { bool expand = false; if (request.isMember("Expand")) @@ -905,10 +1074,19 @@ caseSensitive = request["CaseSensitive"].asBool(); } + size_t limit = 0; + if (request.isMember("Limit")) + { + limit = request["CaseSensitive"].asInt(); + if (limit < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + std::string level = request["Level"].asString(); - DicomFindQuery query; - query.SetLevel(StringToResourceType(level.c_str())); + LookupResource query(StringToResourceType(level.c_str())); Json::Value::Members members = request["Query"].getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -918,14 +1096,13 @@ throw OrthancException(ErrorCode_BadRequest); } - query.SetConstraint(FromDcmtkBridge::ParseTag(members[i]), - request["Query"][members[i]].asString(), - caseSensitive); + query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), + request["Query"][members[i]].asString(), + caseSensitive); } std::list resources; - ResourceFinder finder(context); - finder.Apply(resources, query); + context.Apply(resources, query, limit); AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand); } else @@ -1002,7 +1179,7 @@ if (simplify) { Json::Value simplified; - SimplifyTags(simplified, full); + Toolbox::SimplifyTags(simplified, full); result[*it] = simplified; } else @@ -1065,6 +1242,19 @@ } + static void OrderSlices(RestApiGetCall& call) + { + const std::string id = call.GetUriComponent("id", ""); + + ServerIndex& index = OrthancRestApi::GetIndex(call); + SliceOrdering ordering(index, id); + + Json::Value result; + ordering.Format(result); + call.GetOutput().AnswerJson(result); + } + + void OrthancRestApi::RegisterResources() { Register("/instances", ListResources); @@ -1125,14 +1315,17 @@ Register("/{resourceType}/{id}/attachments", ListAttachments); Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment); Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations); + Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); + Register("/{resourceType}/{id}/attachments/{name}/compress", ChangeAttachmentCompression); Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>); + Register("/{resourceType}/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed); Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5); Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize); + Register("/{resourceType}/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression); Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); - Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); Register("/tools/lookup", Lookup); Register("/tools/find", Find); @@ -1156,5 +1349,7 @@ Register("/series/{id}/instances-tags", GetChildInstancesTags); Register("/instances/{id}/content/*", GetRawContent); + + Register("/series/{id}/ordered-slices", OrderSlices); } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -53,9 +53,7 @@ { Json::Value result = Json::objectValue; - std::string dbVersion = OrthancRestApi::GetIndex(call).GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "0"); - - result["DatabaseVersion"] = boost::lexical_cast(dbVersion); + result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion(); result["DicomAet"] = Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"); result["DicomPort"] = Configuration::GetGlobalIntegerParameter("DicomPort", 4242); result["HttpPort"] = Configuration::GetGlobalIntegerParameter("HttpPort", 8042); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -84,16 +84,16 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" #include "Internals/DicomImageDecoder.h" -#include "../Core/Logging.h" -#include "../Core/Toolbox.h" -#include "../Core/OrthancException.h" +#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" #include "../Core/Images/ImageBuffer.h" +#include "../Core/Images/JpegWriter.h" +#include "../Core/Images/JpegReader.h" +#include "../Core/Images/PngReader.h" #include "../Core/Images/PngWriter.h" +#include "../Core/Logging.h" +#include "../Core/OrthancException.h" +#include "../Core/Toolbox.h" #include "../Core/Uuid.h" -#include "../Core/DicomFormat/DicomString.h" -#include "../Core/DicomFormat/DicomNullValue.h" -#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" -#include "../Core/Images/PngReader.h" #include #include @@ -137,6 +137,7 @@ #include #include +#include static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; @@ -148,7 +149,6 @@ struct ParsedDicomFile::PImpl { std::auto_ptr file_; - Encoding encoding_; }; @@ -173,8 +173,6 @@ } pimpl_->file_->loadAllDataIntoMemory(); pimpl_->file_->transferEnd(); - - pimpl_->encoding_ = FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset()); } @@ -519,284 +517,6 @@ } - - - - static DcmElement* CreateElementForTag(const DicomTag& tag) - { - DcmTag key(tag.GetGroup(), tag.GetElement()); - - switch (key.getEVR()) - { - // http://support.dcmtk.org/docs/dcvr_8h-source.html - - /** - * TODO. - **/ - - case EVR_OB: // other byte - case EVR_OF: // other float - case EVR_OW: // other word - case EVR_AT: // attribute tag - throw OrthancException(ErrorCode_NotImplemented); - - case EVR_UN: // unknown value representation - throw OrthancException(ErrorCode_ParameterOutOfRange); - - - /** - * String types. - * http://support.dcmtk.org/docs/classDcmByteString.html - **/ - - case EVR_AS: // age string - return new DcmAgeString(key); - - case EVR_AE: // application entity title - return new DcmApplicationEntity(key); - - case EVR_CS: // code string - return new DcmCodeString(key); - - case EVR_DA: // date string - return new DcmDate(key); - - case EVR_DT: // date time string - return new DcmDateTime(key); - - case EVR_DS: // decimal string - return new DcmDecimalString(key); - - case EVR_IS: // integer string - return new DcmIntegerString(key); - - case EVR_TM: // time string - return new DcmTime(key); - - case EVR_UI: // unique identifier - return new DcmUniqueIdentifier(key); - - case EVR_ST: // short text - return new DcmShortText(key); - - case EVR_LO: // long string - return new DcmLongString(key); - - case EVR_LT: // long text - return new DcmLongText(key); - - case EVR_UT: // unlimited text - return new DcmUnlimitedText(key); - - case EVR_SH: // short string - return new DcmShortString(key); - - case EVR_PN: // person name - return new DcmPersonName(key); - - - /** - * Numerical types - **/ - - case EVR_SL: // signed long - return new DcmSignedLong(key); - - case EVR_SS: // signed short - return new DcmSignedShort(key); - - case EVR_UL: // unsigned long - return new DcmUnsignedLong(key); - - case EVR_US: // unsigned short - return new DcmUnsignedShort(key); - - case EVR_FL: // float single-precision - return new DcmFloatingPointSingle(key); - - case EVR_FD: // float double-precision - return new DcmFloatingPointDouble(key); - - - /** - * Sequence types, should never occur at this point. - **/ - - case EVR_SQ: // sequence of items - throw OrthancException(ErrorCode_ParameterOutOfRange); - - - /** - * Internal to DCMTK. - **/ - - case EVR_ox: // OB or OW depending on context - case EVR_xs: // SS or US depending on context - case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) - case EVR_na: // na="not applicable", for data which has no VR - case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor - case EVR_item: // used internally for items - case EVR_metainfo: // used internally for meta info datasets - case EVR_dataset: // used internally for datasets - case EVR_fileFormat: // used internally for DICOM files - case EVR_dicomDir: // used internally for DICOMDIR objects - case EVR_dirRecord: // used internally for DICOMDIR records - case EVR_pixelSQ: // used internally for pixel sequences in a compressed image - case EVR_pixelItem: // used internally for pixel items in a compressed image - case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) - case EVR_PixelData: // used internally for uncompressed pixeld data - case EVR_OverlayData: // used internally for overlay data - case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR - default: - break; - } - - throw OrthancException(ErrorCode_InternalError); - } - - - - static void FillElementWithString(DcmElement& element, - const DicomTag& tag, - const std::string& value) - { - DcmTag key(tag.GetGroup(), tag.GetElement()); - bool ok = false; - - try - { - switch (key.getEVR()) - { - // http://support.dcmtk.org/docs/dcvr_8h-source.html - - /** - * TODO. - **/ - - case EVR_OB: // other byte - case EVR_OF: // other float - case EVR_OW: // other word - case EVR_AT: // attribute tag - throw OrthancException(ErrorCode_NotImplemented); - - case EVR_UN: // unknown value representation - throw OrthancException(ErrorCode_ParameterOutOfRange); - - - /** - * String types. - **/ - - case EVR_DS: // decimal string - case EVR_IS: // integer string - case EVR_AS: // age string - case EVR_DA: // date string - case EVR_DT: // date time string - case EVR_TM: // time string - case EVR_AE: // application entity title - case EVR_CS: // code string - case EVR_SH: // short string - case EVR_LO: // long string - case EVR_ST: // short text - case EVR_LT: // long text - case EVR_UT: // unlimited text - case EVR_PN: // person name - case EVR_UI: // unique identifier - { - ok = element.putString(value.c_str()).good(); - break; - } - - - /** - * Numerical types - **/ - - case EVR_SL: // signed long - { - ok = element.putSint32(boost::lexical_cast(value)).good(); - break; - } - - case EVR_SS: // signed short - { - ok = element.putSint16(boost::lexical_cast(value)).good(); - break; - } - - case EVR_UL: // unsigned long - { - ok = element.putUint32(boost::lexical_cast(value)).good(); - break; - } - - case EVR_US: // unsigned short - { - ok = element.putUint16(boost::lexical_cast(value)).good(); - break; - } - - case EVR_FL: // float single-precision - { - ok = element.putFloat32(boost::lexical_cast(value)).good(); - break; - } - - case EVR_FD: // float double-precision - { - ok = element.putFloat64(boost::lexical_cast(value)).good(); - break; - } - - - /** - * Sequence types, should never occur at this point. - **/ - - case EVR_SQ: // sequence of items - { - ok = false; - break; - } - - - /** - * Internal to DCMTK. - **/ - - case EVR_ox: // OB or OW depending on context - case EVR_xs: // SS or US depending on context - case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) - case EVR_na: // na="not applicable", for data which has no VR - case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor - case EVR_item: // used internally for items - case EVR_metainfo: // used internally for meta info datasets - case EVR_dataset: // used internally for datasets - case EVR_fileFormat: // used internally for DICOM files - case EVR_dicomDir: // used internally for DICOMDIR objects - case EVR_dirRecord: // used internally for DICOMDIR records - case EVR_pixelSQ: // used internally for pixel sequences in a compressed image - case EVR_pixelItem: // used internally for pixel items in a compressed image - case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) - case EVR_PixelData: // used internally for uncompressed pixeld data - case EVR_OverlayData: // used internally for overlay data - case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR - default: - break; - } - } - catch (boost::bad_lexical_cast&) - { - ok = false; - } - - if (!ok) - { - throw OrthancException(ErrorCode_InternalError); - } - } - - void ParsedDicomFile::Remove(const DicomTag& tag) { DcmTagKey key(tag.GetGroup(), tag.GetElement()); @@ -823,7 +543,7 @@ DcmTag tag(element->getTag()); // Is this a private tag? - if (FromDcmtkBridge::IsPrivateTag(tag)) + if (tag.isPrivate()) { bool remove = true; @@ -857,53 +577,40 @@ } - - - void ParsedDicomFile::Insert(const DicomTag& tag, - const std::string& value) + static void InsertInternal(DcmDataset& dicom, + DcmElement* element) { - OFCondition cond; - - if (FromDcmtkBridge::IsPrivateTag(tag)) - { - // This is a private tag - // http://support.dcmtk.org/redmine/projects/dcmtk/wiki/howto_addprivatedata - - DcmTag key(tag.GetGroup(), tag.GetElement(), EVR_OB); - cond = pimpl_->file_->getDataset()->putAndInsertUint8Array - (key, (const Uint8*) value.c_str(), value.size(), false); - } - else - { - std::auto_ptr element(CreateElementForTag(tag)); - FillElementWithString(*element, tag, value); - - cond = pimpl_->file_->getDataset()->insert(element.release(), false, false); - } - + OFCondition cond = dicom.insert(element, false, false); if (!cond.good()) { // This field already exists + delete element; throw OrthancException(ErrorCode_InternalError); } } - void ParsedDicomFile::Replace(const DicomTag& tag, - const std::string& value, - DicomReplaceMode mode) + void ParsedDicomFile::Insert(const DicomTag& tag, + const Json::Value& value, + bool decodeBinaryTags) { - DcmTagKey key(tag.GetGroup(), tag.GetElement()); - DcmElement* element = NULL; + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding())); + InsertInternal(*pimpl_->file_->getDataset(), element.release()); + } + - if (!pimpl_->file_->getDataset()->findAndGetElement(key, element).good() || - element == NULL) + static void ReplaceInternal(DcmDataset& dicom, + std::auto_ptr& element, + DicomReplaceMode mode) + { + const DcmTagKey& tag = element->getTag(); + + if (!dicom.findAndDeleteElement(tag).good()) { // This field does not exist, act wrt. the specified "mode" switch (mode) { case DicomReplaceMode_InsertIfAbsent: - Insert(tag, value); break; case DicomReplaceMode_ThrowIfAbsent: @@ -913,22 +620,43 @@ return; } } + + // Either the tag was not existing, or the replace mode was set to + // "InsertIfAbsent" + InsertInternal(dicom, element.release()); + } + + + void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag, + const std::string& utf8Value, + bool decodeBinaryTags) + { + if (tag != DICOM_TAG_SOP_CLASS_UID && + tag != DICOM_TAG_SOP_INSTANCE_UID) + { + return; + } + + std::string binary; + const std::string* decoded = &utf8Value; + + if (decodeBinaryTags && + boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + { + std::string mime; + Toolbox::DecodeDataUriScheme(mime, binary, utf8Value); + decoded = &binary; + } else { - if (FromDcmtkBridge::IsPrivateTag(tag)) + Encoding encoding = GetEncoding(); + if (GetEncoding() != Encoding_Utf8) { - if (!element->putUint8Array((const Uint8*) value.c_str(), value.size()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - else - { - FillElementWithString(*element, tag, value); + binary = Toolbox::ConvertFromUtf8(utf8Value, encoding); + decoded = &binary; } } - /** * dcmodify will automatically correct 'Media Storage SOP Class * UID' and 'Media Storage SOP Instance UID' in the metaheader, if @@ -940,12 +668,44 @@ if (tag == DICOM_TAG_SOP_CLASS_UID) { - Replace(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, value, DicomReplaceMode_InsertIfAbsent); + Replace(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, *decoded, DicomReplaceMode_InsertIfAbsent); } if (tag == DICOM_TAG_SOP_INSTANCE_UID) { - Replace(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, value, DicomReplaceMode_InsertIfAbsent); + Replace(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, *decoded, DicomReplaceMode_InsertIfAbsent); + } + } + + + void ParsedDicomFile::Replace(const DicomTag& tag, + const std::string& utf8Value, + DicomReplaceMode mode) + { + std::auto_ptr element(FromDcmtkBridge::CreateElementForTag(tag)); + FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, false, GetEncoding()); + ReplaceInternal(*pimpl_->file_->getDataset(), element, mode); + UpdateStorageUid(tag, utf8Value, false); + } + + + void ParsedDicomFile::Replace(const DicomTag& tag, + const Json::Value& value, + bool decodeBinaryTags, + DicomReplaceMode mode) + { + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding())); + ReplaceInternal(*pimpl_->file_->getDataset(), element, mode); + + if (tag == DICOM_TAG_SOP_CLASS_UID || + tag == DICOM_TAG_SOP_INSTANCE_UID) + { + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + UpdateStorageUid(tag, value.asString(), decodeBinaryTags); } } @@ -968,6 +728,7 @@ DcmDataset& dataset = *pimpl_->file_->getDataset(); if (FromDcmtkBridge::IsPrivateTag(tag) || + FromDcmtkBridge::IsUnknownTag(tag) || tag == DICOM_TAG_PIXEL_DATA || tag == DICOM_TAG_ENCAPSULATED_DOCUMENT) { @@ -1002,15 +763,18 @@ return false; } - std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement(*element, pimpl_->encoding_)); + std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_Default, GetEncoding())); - if (v.get() == NULL) + if (v.get() == NULL || + v->IsNull()) { value = ""; } else { - value = v->AsString(); + // TODO v->IsBinary() + value = v->GetContent(); } return true; @@ -1034,36 +798,6 @@ } - template - static void ExtractPngImageTruncate(std::string& result, - DicomIntegerPixelAccessor& accessor, - PixelFormat format) - { - assert(accessor.GetInformation().GetChannelCount() == 1); - - PngWriter w; - - std::vector image(accessor.GetInformation().GetWidth() * accessor.GetInformation().GetHeight(), 0); - T* pixel = &image[0]; - for (unsigned int y = 0; y < accessor.GetInformation().GetHeight(); y++) - { - for (unsigned int x = 0; x < accessor.GetInformation().GetWidth(); x++, pixel++) - { - int32_t v = accessor.GetValue(x, y); - if (v < static_cast(std::numeric_limits::min())) - *pixel = std::numeric_limits::min(); - else if (v > static_cast(std::numeric_limits::max())) - *pixel = std::numeric_limits::max(); - else - *pixel = static_cast(v); - } - } - - w.WriteToMemory(result, accessor.GetInformation().GetWidth(), accessor.GetInformation().GetHeight(), - accessor.GetInformation().GetWidth() * sizeof(T), format, &image[0]); - } - - void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); @@ -1082,7 +816,6 @@ ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl) { pimpl_->file_.reset(new DcmFileFormat); - pimpl_->encoding_ = Encoding_Ascii; Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient)); Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)); Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series)); @@ -1112,7 +845,6 @@ pimpl_(new PImpl) { pimpl_->file_.reset(dynamic_cast(other.pimpl_->file_->clone())); - pimpl_->encoding_ = other.pimpl_->encoding_; // Create a new instance-level identifier Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance)); @@ -1139,14 +871,12 @@ void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme) { - std::string mime, base64; - Toolbox::DecodeDataUriScheme(mime, base64, dataUriScheme); + std::string mime, content; + Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme); Toolbox::ToLowerCase(mime); - std::string content; - Toolbox::DecodeBase64(content, base64); - - if (mime == "image/png") + if (mime == "image/png" || + mime == "image/jpeg") { EmbedImage(mime, content); } @@ -1156,7 +886,7 @@ } else { - LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file"; + LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime; throw OrthancException(ErrorCode_NotImplemented); } } @@ -1171,6 +901,12 @@ reader.ReadFromMemory(content); EmbedImage(reader); } + else if (mime == "image/jpeg") + { + JpegReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + } else { throw OrthancException(ErrorCode_NotImplemented); @@ -1343,9 +1079,30 @@ } + void ParsedDicomFile::ExtractJpegImage(std::string& result, + unsigned int frame, + ImageExtractionMode mode, + uint8_t quality) + { + if (mode != ImageExtractionMode_UInt8 && + mode != ImageExtractionMode_Preview) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + ImageBuffer buffer; + ExtractImage(buffer, frame, mode); + + ImageAccessor accessor(buffer.GetConstAccessor()); + JpegWriter writer; + writer.SetQuality(quality); + writer.WriteToMemory(result, accessor); + } + + Encoding ParsedDicomFile::GetEncoding() const { - return pimpl_->encoding_; + return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset()); } @@ -1358,24 +1115,16 @@ return; } - pimpl_->encoding_ = encoding; - std::string s = GetDicomSpecificCharacterSet(encoding); Replace(DICOM_TAG_SPECIFIC_CHARACTER_SET, s, DicomReplaceMode_InsertIfAbsent); } - void ParsedDicomFile::ToJson(Json::Value& target, bool simplify) + void ParsedDicomFile::ToJson(Json::Value& target, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength) { - if (simplify) - { - Json::Value tmp; - FromDcmtkBridge::ToJson(tmp, *pimpl_->file_->getDataset()); - SimplifyTags(target, tmp); - } - else - { - FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset()); - } + FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset(), format, flags, maxStringLength); } @@ -1467,4 +1216,10 @@ return true; } + + + void ParsedDicomFile::Convert(DicomMap& tags) + { + FromDcmtkBridge::Convert(tags, *pimpl_->file_->getDataset()); + } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ParsedDicomFile.h --- a/OrthancServer/ParsedDicomFile.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ParsedDicomFile.h Wed Nov 18 10:16:21 2015 +0100 @@ -37,6 +37,7 @@ #include "ServerEnumerations.h" #include "../Core/Images/ImageAccessor.h" #include "../Core/Images/ImageBuffer.h" +#include "../Core/IDynamicObject.h" namespace Orthanc { @@ -53,6 +54,10 @@ void RemovePrivateTagsInternal(const std::set* toKeep); + void UpdateStorageUid(const DicomTag& tag, + const std::string& value, + bool decodeBinaryTags); + public: ParsedDicomFile(); // Create a minimal DICOM instance @@ -74,13 +79,19 @@ void Remove(const DicomTag& tag); - void Insert(const DicomTag& tag, - const std::string& value); + void Replace(const DicomTag& tag, + const std::string& utf8Value, + DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent); void Replace(const DicomTag& tag, - const std::string& value, + const Json::Value& value, // Assumed to be encoded with UTF-8 + bool decodeBinaryTags, DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent); + void Insert(const DicomTag& tag, + const Json::Value& value, // Assumed to be encoded with UTF-8 + bool decodeBinaryTags); + void RemovePrivateTags() { RemovePrivateTagsInternal(NULL); @@ -118,18 +129,27 @@ unsigned int frame, ImageExtractionMode mode); + void ExtractJpegImage(std::string& result, + unsigned int frame, + ImageExtractionMode mode, + uint8_t quality); + Encoding GetEncoding() const; void SetEncoding(Encoding encoding); void ToJson(Json::Value& target, - bool simplify); + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength); bool HasTag(const DicomTag& tag) const; void EmbedPdf(const std::string& pdf); bool ExtractPdf(std::string& pdf); + + void Convert(DicomMap& tags); }; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/PrepareDatabase.sql Wed Nov 18 10:16:21 2015 +0100 @@ -123,4 +123,4 @@ -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "5"); +INSERT INTO GlobalProperties VALUES (1, "6"); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ResourceFinder.cpp --- a/OrthancServer/ResourceFinder.cpp Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,383 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "PrecompiledHeadersServer.h" -#include "ResourceFinder.h" - -#include "../Core/Logging.h" -#include "FromDcmtkBridge.h" -#include "ServerContext.h" - -#include - -namespace Orthanc -{ - class ResourceFinder::CandidateResources - { - private: - typedef std::map Query; - - ResourceFinder& finder_; - ServerIndex& index_; - ResourceType level_; - bool isFilterApplied_; - std::set filtered_; - - - static void ListToSet(std::set& target, - const std::list& source) - { - for (std::list::const_iterator - it = source.begin(); it != source.end(); ++it) - { - target.insert(*it); - } - } - - - public: - CandidateResources(ResourceFinder& finder) : - finder_(finder), - index_(finder.context_.GetIndex()), - level_(ResourceType_Patient), - isFilterApplied_(false) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - void GoDown() - { - assert(level_ != ResourceType_Instance); - - if (isFilterApplied_) - { - std::set tmp = filtered_; - - filtered_.clear(); - - for (std::set::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - std::list children; - try - { - index_.GetChildren(children, *it); - ListToSet(filtered_, children); - } - catch (OrthancException&) - { - // The resource was removed in the meantime - } - } - } - - switch (level_) - { - case ResourceType_Patient: - level_ = ResourceType_Study; - break; - - case ResourceType_Study: - level_ = ResourceType_Series; - break; - - case ResourceType_Series: - level_ = ResourceType_Instance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - void Flatten(std::list& resources) const - { - resources.clear(); - - if (isFilterApplied_) - { - for (std::set::const_iterator - it = filtered_.begin(); it != filtered_.end(); ++it) - { - resources.push_back(*it); - } - } - else - { - index_.GetAllUuids(resources, level_); - } - } - - - void RestrictIdentifier(const IQuery& query, - const DicomTag& tag) - { - assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || - (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || - (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || - (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || - (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); - - std::string value; - if (!query.RestrictIdentifier(value, tag)) - { - return; - } - - LOG(INFO) << "Lookup for identifier tag " - << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")"; - - std::list resources; - index_.LookupIdentifier(resources, tag, value, level_); - - if (isFilterApplied_) - { - std::set s; - ListToSet(s, resources); - - std::set tmp = filtered_; - filtered_.clear(); - - for (std::set::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - if (s.find(*it) != s.end()) - { - filtered_.insert(*it); - } - } - } - else - { - assert(filtered_.empty()); - isFilterApplied_ = true; - ListToSet(filtered_, resources); - } - } - - - void RestrictMainDicomTags(const IQuery& query) - { - if (!query.HasMainDicomTagsFilter(level_)) - { - return; - } - - std::list resources; - Flatten(resources); - - isFilterApplied_ = true; - filtered_.clear(); - - for (std::list::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - DicomMap mainTags; - if (index_.GetMainDicomTags(mainTags, *it, level_)) - { - if (query.FilterMainDicomTags(*it, level_, mainTags)) - { - filtered_.insert(*it); - } - } - } - } - }; - - - ResourceFinder::ResourceFinder(ServerContext& context) : - context_(context), - maxResults_(0) - { - } - - - void ResourceFinder::ApplyAtLevel(CandidateResources& candidates, - const IQuery& query, - ResourceType level) - { - if (level != ResourceType_Patient) - { - candidates.GoDown(); - } - - switch (level) - { - case ResourceType_Patient: - { - candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID); - break; - } - - case ResourceType_Study: - { - candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID); - candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER); - break; - } - - case ResourceType_Series: - { - candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID); - break; - } - - case ResourceType_Instance: - { - candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - candidates.RestrictMainDicomTags(query); - } - - - - static bool LookupOneInstance(std::string& result, - ServerIndex& index, - const std::string& id, - ResourceType type) - { - if (type == ResourceType_Instance) - { - result = id; - return true; - } - - std::string childId; - - { - std::list children; - index.GetChildInstances(children, id); - - if (children.empty()) - { - return false; - } - - childId = children.front(); - } - - return LookupOneInstance(result, index, childId, GetChildResourceType(type)); - } - - - bool ResourceFinder::Apply(std::list& result, - const IQuery& query) - { - CandidateResources candidates(*this); - - ApplyAtLevel(candidates, query, ResourceType_Patient); - - const ResourceType level = query.GetLevel(); - - if (level == ResourceType_Study || - level == ResourceType_Series || - level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Study); - } - - if (level == ResourceType_Series || - level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Series); - } - - if (level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Instance); - } - - if (!query.HasInstanceFilter()) - { - candidates.Flatten(result); - - if (maxResults_ != 0 && - result.size() >= maxResults_) - { - result.resize(maxResults_); - return false; - } - else - { - return true; - } - } - else - { - std::list tmp; - candidates.Flatten(tmp); - - result.clear(); - for (std::list::const_iterator - resource = tmp.begin(); resource != tmp.end(); ++resource) - { - try - { - std::string instance; - if (LookupOneInstance(instance, context_.GetIndex(), *resource, level)) - { - Json::Value content; - context_.ReadJson(content, instance); - if (query.FilterInstance(*resource, content)) - { - result.push_back(*resource); - - if (maxResults_ != 0 && - result.size() >= maxResults_) - { - // Too many results, stop before recording this new match - return false; - } - } - } - } - catch (OrthancException&) - { - // This resource has been deleted since the search was started - } - } - } - - return true; // All the matching resources have been returned - } -} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ResourceFinder.h --- a/OrthancServer/ResourceFinder.h Wed Sep 23 10:29:06 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerIndex.h" - -#include - -namespace Orthanc -{ - class ResourceFinder : public boost::noncopyable - { - public: - class IQuery : public boost::noncopyable - { - public: - virtual ~IQuery() - { - } - - virtual ResourceType GetLevel() const = 0; - - virtual bool RestrictIdentifier(std::string& value, - DicomTag identifier) const = 0; - - virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0; - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const = 0; - - virtual bool HasInstanceFilter() const = 0; - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const = 0; - }; - - - private: - typedef std::map Identifiers; - - class CandidateResources; - - ServerContext& context_; - size_t maxResults_; - - void ApplyAtLevel(CandidateResources& candidates, - const IQuery& query, - ResourceType level); - - public: - ResourceFinder(ServerContext& context); - - void SetMaxResults(size_t value) - { - maxResults_ = value; - } - - size_t GetMaxResults() const - { - return maxResults_; - } - - // Returns "true" iff. all the matching resources have been - // returned. Will be "false" if the results were truncated by - // "SetMaxResults()". - bool Apply(std::list& result, - const IQuery& query); - }; - -} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Scheduler/ModifyInstanceCommand.cpp --- a/OrthancServer/Scheduler/ModifyInstanceCommand.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -39,28 +39,28 @@ { ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, RequestOrigin origin, - const DicomModification& modification) : + DicomModification* modification) : context_(context), origin_(origin), modification_(modification) { - modification_.SetAllowManualIdentifiers(true); + modification_->SetAllowManualIdentifiers(true); - if (modification_.IsReplaced(DICOM_TAG_PATIENT_ID)) + if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) { - modification_.SetLevel(ResourceType_Patient); + modification_->SetLevel(ResourceType_Patient); } - else if (modification_.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { - modification_.SetLevel(ResourceType_Study); + modification_->SetLevel(ResourceType_Study); } - else if (modification_.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) { - modification_.SetLevel(ResourceType_Series); + modification_->SetLevel(ResourceType_Series); } else { - modification_.SetLevel(ResourceType_Instance); + modification_->SetLevel(ResourceType_Instance); } if (origin_ != RequestOrigin_Lua) @@ -71,6 +71,15 @@ } + ModifyInstanceCommand::~ModifyInstanceCommand() + { + if (modification_) + { + delete modification_; + } + } + + bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, const ListOfStrings& inputs) { @@ -88,7 +97,7 @@ modified.reset(lock.GetDicom().Clone()); } - modification_.Apply(*modified); + modification_->Apply(*modified); DicomInstanceToStore toStore; assert(origin_ == RequestOrigin_Lua); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Scheduler/ModifyInstanceCommand.h --- a/OrthancServer/Scheduler/ModifyInstanceCommand.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.h Wed Nov 18 10:16:21 2015 +0100 @@ -43,16 +43,18 @@ private: ServerContext& context_; RequestOrigin origin_; - DicomModification modification_; + DicomModification* modification_; public: ModifyInstanceCommand(ServerContext& context, RequestOrigin origin, - const DicomModification& modification); + DicomModification* modification); // takes the ownership + + virtual ~ModifyInstanceCommand(); const DicomModification& GetModification() const { - return modification_; + return *modification_; } virtual bool Apply(ListOfStrings& outputs, diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/IFindConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/IFindConstraint.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "LookupIdentifierQuery.h" + +namespace Orthanc +{ + class IFindConstraint : public boost::noncopyable + { + public: + virtual ~IFindConstraint() + { + } + + virtual IFindConstraint* Clone() const = 0; + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const = 0; + + virtual bool Match(const std::string& value) const = 0; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/ListConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ListConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "ListConstraint.h" + + +namespace Orthanc +{ + void ListConstraint::AddAllowedValue(const std::string& value) + { + if (isCaseSensitive_) + { + allowedValues_.insert(value); + } + else + { + std::string s = value; + Toolbox::ToUpperCase(s); + allowedValues_.insert(s); + } + } + + + void ListConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + LookupIdentifierQuery::Disjunction& target = lookup.AddDisjunction(); + + for (std::set::const_iterator + it = allowedValues_.begin(); it != allowedValues_.end(); ++it) + { + target.Add(tag, IdentifierConstraintType_Equal, *it); + } + } + + + bool ListConstraint::Match(const std::string& value) const + { + std::string v = value; + + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(v); + } + + return allowedValues_.find(v) != allowedValues_.end(); + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/ListConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ListConstraint.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,71 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +#include + +namespace Orthanc +{ + class ListConstraint : public IFindConstraint + { + private: + std::set allowedValues_; + bool isCaseSensitive_; + + ListConstraint(const ListConstraint& other) : + allowedValues_(other.allowedValues_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + ListConstraint(bool isCaseSensitive) : + isCaseSensitive_(isCaseSensitive) + { + } + + void AddAllowedValue(const std::string& value); + + virtual IFindConstraint* Clone() const + { + return new ListConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/LookupIdentifierQuery.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupIdentifierQuery.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,283 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "LookupIdentifierQuery.h" + +#include "../../Core/OrthancException.h" +#include "SetOfResources.h" +#include "../FromDcmtkBridge.h" + +#include + + + +namespace Orthanc +{ + static const DicomTag patientIdentifiers[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE + }; + + static const DicomTag studyIdentifiers[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE, + DICOM_TAG_STUDY_INSTANCE_UID, + DICOM_TAG_ACCESSION_NUMBER, + DICOM_TAG_STUDY_DESCRIPTION, + DICOM_TAG_STUDY_DATE + }; + + static const DicomTag seriesIdentifiers[] = + { + DICOM_TAG_SERIES_INSTANCE_UID + }; + + static const DicomTag instanceIdentifiers[] = + { + DICOM_TAG_SOP_INSTANCE_UID + }; + + + void LookupIdentifierQuery::LoadIdentifiers(const DicomTag*& tags, + size_t& size, + ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + tags = patientIdentifiers; + size = sizeof(patientIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyIdentifiers; + size = sizeof(studyIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesIdentifiers; + size = sizeof(seriesIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceIdentifiers; + size = sizeof(instanceIdentifiers) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + LookupIdentifierQuery::Disjunction::~Disjunction() + { + for (size_t i = 0; i < disjunction_.size(); i++) + { + delete disjunction_[i]; + } + } + + + void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) + { + disjunction_.push_back(new Constraint(tag, type, value)); + } + + + LookupIdentifierQuery::~LookupIdentifierQuery() + { + for (Constraints::iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + delete *it; + } + } + + + + bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag, + ResourceType level) + { + const DicomTag* tags; + size_t size; + + LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (tag == tags[i]) + { + return true; + } + } + + return false; + } + + + void LookupIdentifierQuery::AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value) + { + assert(IsIdentifier(tag)); + constraints_.push_back(new Disjunction); + constraints_.back()->Add(tag, type, value); + } + + + LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction() + { + constraints_.push_back(new Disjunction); + return *constraints_.back(); + } + + + std::string LookupIdentifierQuery::NormalizeIdentifier(const std::string& value) + { + std::string t; + t.reserve(value.size()); + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '%' || + value[i] == '_') + { + t.push_back(' '); // These characters might break wildcard queries in SQL + } + else if (isascii(value[i]) && + !iscntrl(value[i]) && + (!isspace(value[i]) || value[i] == ' ')) + { + t.push_back(value[i]); + } + } + + Toolbox::ToUpperCase(t); + + return Toolbox::StripSpaces(t); + } + + + void LookupIdentifierQuery::StoreIdentifiers(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& map) + { + const DicomTag* tags; + size_t size; + + LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + const DicomValue* value = map.TestAndGetValue(tags[i]); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + std::string s = NormalizeIdentifier(value->GetContent()); + database.SetIdentifierTag(resource, tags[i], s); + } + } + } + + + void LookupIdentifierQuery::Apply(std::list& result, + IDatabaseWrapper& database) + { + SetOfResources resources(database, level_); + Apply(resources, database); + + resources.Flatten(result); + } + + + void LookupIdentifierQuery::Apply(SetOfResources& result, + IDatabaseWrapper& database) + { + for (size_t i = 0; i < GetSize(); i++) + { + std::list a; + + for (size_t j = 0; j < constraints_[i]->GetSize(); j++) + { + const Constraint& constraint = constraints_[i]->GetConstraint(j); + std::list b; + database.LookupIdentifier(b, level_, constraint.GetTag(), constraint.GetType(), constraint.GetValue()); + + a.splice(a.end(), b); + } + + result.Intersect(a); + } + } + + + void LookupIdentifierQuery::Print(std::ostream& s) const + { + s << "Constraint: " << std::endl; + for (Constraints::const_iterator + it = constraints_.begin(); it != constraints_.end(); ++it) + { + if (it == constraints_.begin()) + s << " "; + else + s << "OR "; + + for (size_t j = 0; j < (*it)->GetSize(); j++) + { + const Constraint& c = (*it)->GetConstraint(j); + s << FromDcmtkBridge::GetName(c.GetTag()); + + switch (c.GetType()) + { + case IdentifierConstraintType_Equal: s << " == "; break; + case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break; + case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break; + case IdentifierConstraintType_Wildcard: s << " ~= "; break; + default: + s << " ? "; + } + + s << c.GetValue() << std::endl; + } + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/LookupIdentifierQuery.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupIdentifierQuery.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,183 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../ServerEnumerations.h" +#include "../IDatabaseWrapper.h" + +#include "SetOfResources.h" + +#include +#include + +namespace Orthanc +{ + /** + * Primitive for wildcard matching, as defined in DICOM: + * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4 + * + * "Any occurrence of an "*" or a "?", then "*" shall match any + * sequence of characters (including a zero length value) and "?" + * shall match any single character. This matching is case + * sensitive, except for Attributes with an PN Value + * Representation (e.g., Patient Name (0010,0010))." + * + * Pay attention to the fact that "*" (resp. "?") generally + * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The + * values "%", "_", "\" should in the user request should + * respectively be escaped as "\%", "\_" and "\\". + * + * This matching must be case sensitive: The special case of PN VR + * is taken into consideration by normalizing the query string in + * method "NormalizeIdentifier()". + **/ + + class LookupIdentifierQuery : public boost::noncopyable + { + public: + class Constraint + { + private: + DicomTag tag_; + IdentifierConstraintType type_; + std::string value_; + + public: + Constraint(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) : + tag_(tag), + type_(type), + value_(NormalizeIdentifier(value)) + { + } + + const DicomTag& GetTag() const + { + return tag_; + } + + IdentifierConstraintType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + return value_; + } + }; + + + class Disjunction : public boost::noncopyable + { + private: + std::vector disjunction_; + + public: + ~Disjunction(); + + void Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + + size_t GetSize() const + { + return disjunction_.size(); + } + + const Constraint& GetConstraint(size_t i) const + { + return *disjunction_[i]; + } + }; + + + private: + typedef std::vector Constraints; + + ResourceType level_; + Constraints constraints_; + + public: + LookupIdentifierQuery(ResourceType level) : level_(level) + { + } + + ~LookupIdentifierQuery(); + + bool IsIdentifier(const DicomTag& tag) + { + return IsIdentifier(tag, level_); + } + + void AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value); + + Disjunction& AddDisjunction(); + + ResourceType GetLevel() const + { + return level_; + } + + size_t GetSize() const + { + return constraints_.size(); + } + + // The database must be locked + void Apply(std::list& result, + IDatabaseWrapper& database); + + void Apply(SetOfResources& result, + IDatabaseWrapper& database); + + static void LoadIdentifiers(const DicomTag*& tags, + size_t& size, + ResourceType level); + + static bool IsIdentifier(const DicomTag& tag, + ResourceType level); + + static void StoreIdentifiers(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& map); + + static std::string NormalizeIdentifier(const std::string& value); + + void Print(std::ostream& s) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/LookupResource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupResource.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,510 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "LookupResource.h" + +#include "ListConstraint.h" +#include "RangeConstraint.h" +#include "ValueConstraint.h" +#include "WildcardConstraint.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/FileStorage/StorageAccessor.h" +#include "../ServerToolbox.h" +#include "../FromDcmtkBridge.h" + + +namespace Orthanc +{ + LookupResource::Level::Level(ResourceType level) : level_(level) + { + const DicomTag* tags = NULL; + size_t size; + + LookupIdentifierQuery::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + identifiers_.insert(tags[i]); + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (identifiers_.find(tags[i]) == identifiers_.end()) + { + mainTags_.insert(tags[i]); + } + } + } + + LookupResource::Level::~Level() + { + for (Constraints::iterator it = mainTagsConstraints_.begin(); + it != mainTagsConstraints_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + delete it->second; + } + } + + bool LookupResource::Level::Add(const DicomTag& tag, + std::auto_ptr& constraint) + { + if (identifiers_.find(tag) != identifiers_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + identifiersConstraints_[tag] = constraint->Clone(); + } + else + { + identifiersConstraints_[tag] = constraint.release(); + } + + return true; + } + else if (mainTags_.find(tag) != mainTags_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + mainTagsConstraints_[tag] = constraint->Clone(); + } + else + { + mainTagsConstraints_[tag] = constraint.release(); + } + + return true; + } + else + { + return false; + } + } + + + LookupResource::LookupResource(ResourceType level) : level_(level) + { + switch (level) + { + case ResourceType_Patient: + levels_[ResourceType_Patient] = new Level(ResourceType_Patient); + break; + + case ResourceType_Study: + levels_[ResourceType_Study] = new Level(ResourceType_Study); + // Do not add "break" here + + case ResourceType_Series: + levels_[ResourceType_Series] = new Level(ResourceType_Series); + // Do not add "break" here + + case ResourceType_Instance: + levels_[ResourceType_Instance] = new Level(ResourceType_Instance); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + LookupResource::~LookupResource() + { + for (Levels::iterator it = levels_.begin(); + it != levels_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + delete it->second; + } + } + + + + bool LookupResource::AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint) + { + Levels::iterator it = levels_.find(level); + if (it != levels_.end()) + { + if (it->second->Add(tag, constraint)) + { + return true; + } + } + + return false; + } + + + void LookupResource::Add(const DicomTag& tag, + IFindConstraint* constraint) + { + std::auto_ptr c(constraint); + + if (!AddInternal(ResourceType_Patient, tag, c) && + !AddInternal(ResourceType_Study, tag, c) && + !AddInternal(ResourceType_Series, tag, c) && + !AddInternal(ResourceType_Instance, tag, c)) + { + unoptimizedConstraints_[tag] = c.release(); + } + } + + + static bool Match(const DicomMap& tags, + const DicomTag& tag, + const IFindConstraint& constraint) + { + const DicomValue* value = tags.TestAndGetValue(tag); + + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + return false; + } + else + { + return constraint.Match(value->GetContent()); + } + } + + + void LookupResource::Level::Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const + { + // First, use the indexed identifiers + LookupIdentifierQuery query(level_); + + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + it->second->Setup(query, it->first); + } + + query.Apply(candidates, database); + + /*{ + query.Print(std::cout); + std::list source; + candidates.Flatten(source); + printf("=> %d\n", source.size()); + }*/ + + // Secondly, filter using the main DICOM tags + if (!identifiersConstraints_.empty() || + !mainTagsConstraints_.empty()) + { + std::list source; + candidates.Flatten(source); + candidates.Clear(); + + std::list filtered; + for (std::list::const_iterator candidate = source.begin(); + candidate != source.end(); ++candidate) + { + DicomMap tags; + database.GetMainDicomTags(tags, *candidate); + + bool match = true; + + // Re-apply the identifier constraints, as their "Setup" + // method is less restrictive than their "Match" method + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + match && it != identifiersConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + for (Constraints::const_iterator it = mainTagsConstraints_.begin(); + match && it != mainTagsConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + if (match) + { + filtered.push_back(*candidate); + } + } + + candidates.Intersect(filtered); + } + } + + + + bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const + { + for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + std::string tag = it->first.Format(); + if (dicomAsJson.isMember(tag) && + dicomAsJson[tag]["Type"] == "String") + { + std::string value = dicomAsJson[tag]["Value"].asString(); + if (!it->second->Match(value)) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + + + void LookupResource::ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const + { + Levels::const_iterator it = levels_.find(level); + if (it != levels_.end()) + { + it->second->Apply(candidates, database); + } + + if (level == ResourceType_Study && + modalitiesInStudy_.get() != NULL) + { + // There is a constraint on the "ModalitiesInStudy" DICOM + // extension. Check out whether one child series has one of the + // allowed modalities + std::list allStudies, matchingStudies; + candidates.Flatten(allStudies); + + for (std::list::const_iterator + study = allStudies.begin(); study != allStudies.end(); ++study) + { + std::list childrenSeries; + database.GetChildrenInternalId(childrenSeries, *study); + + for (std::list::const_iterator + series = childrenSeries.begin(); series != childrenSeries.end(); ++series) + { + DicomMap tags; + database.GetMainDicomTags(tags, *series); + + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + if (modalitiesInStudy_->Match(value->GetContent())) + { + matchingStudies.push_back(*study); + break; + } + } + } + } + + candidates.Intersect(matchingStudies); + } + } + + + void LookupResource::FindCandidates(std::list& result, + IDatabaseWrapper& database) const + { + ResourceType startingLevel; + if (level_ == ResourceType_Patient) + { + startingLevel = ResourceType_Patient; + } + else + { + startingLevel = ResourceType_Study; + } + + SetOfResources candidates(database, startingLevel); + + switch (level_) + { + case ResourceType_Patient: + ApplyLevel(candidates, ResourceType_Patient, database); + break; + + case ResourceType_Study: + ApplyLevel(candidates, ResourceType_Study, database); + break; + + case ResourceType_Series: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + break; + + case ResourceType_Instance: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Instance, database); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + candidates.Flatten(result); + } + + + void LookupResource::SetModalitiesInStudy(const std::string& modalities) + { + modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */)); + + std::vector items; + Toolbox::TokenizeString(items, modalities, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + modalitiesInStudy_->AddAllowedValue(items[i]); + } + } + + + void LookupResource::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive) + { + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + SetModalitiesInStudy(dicomQuery); + } + else if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + Add(tag, new RangeConstraint(lower, upper, caseSensitive)); + } + else if (dicomQuery.find('\\') != std::string::npos) + { + std::auto_ptr constraint(new ListConstraint(caseSensitive)); + + std::vector items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddAllowedValue(items[i]); + } + + Add(tag, constraint.release()); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + Add(tag, new WildcardConstraint(dicomQuery, caseSensitive)); + } + else + { + /** + * Case-insensitive match for PN value representation (Patient + * Name). Case-senstive match for all the other value + * representations. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + + Add(tag, new ValueConstraint(dicomQuery, caseSensitive)); + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/LookupResource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupResource.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,107 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ListConstraint.h" +#include "SetOfResources.h" + +#include + +namespace Orthanc +{ + class LookupResource : public boost::noncopyable + { + private: + typedef std::map Constraints; + + class Level + { + private: + ResourceType level_; + std::set identifiers_; + std::set mainTags_; + Constraints identifiersConstraints_; + Constraints mainTagsConstraints_; + + public: + Level(ResourceType level); + + ~Level(); + + bool Add(const DicomTag& tag, + std::auto_ptr& constraint); + + void Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const; + }; + + typedef std::map Levels; + + ResourceType level_; + Levels levels_; + Constraints unoptimizedConstraints_; + std::auto_ptr modalitiesInStudy_; + + bool AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint); + + void ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const; + + public: + LookupResource(ResourceType level); + + ~LookupResource(); + + ResourceType GetLevel() const + { + return level_; + } + + void SetModalitiesInStudy(const std::string& modalities); + + void Add(const DicomTag& tag, + IFindConstraint* constraint); // Takes ownership + + void AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); + + void FindCandidates(std::list& result, + IDatabaseWrapper& database) const; + + bool IsMatch(const Json::Value& dicomAsJson) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/RangeConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/RangeConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,90 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "RangeConstraint.h" + +#include "../../Core/Toolbox.h" + +namespace Orthanc +{ + RangeConstraint::RangeConstraint(const std::string& lower, + const std::string& upper, + bool isCaseSensitive) : + lower_(lower), + upper_(upper), + isCaseSensitive_(isCaseSensitive) + { + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(lower_); + Toolbox::ToUpperCase(upper_); + } + } + + + void RangeConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_); + lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_); + } + + + bool RangeConstraint::Match(const std::string& value) const + { + std::string v = value; + + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(v); + } + + if (lower_.size() == 0 && + upper_.size() == 0) + { + return false; + } + + if (lower_.size() == 0) + { + return v <= upper_; + } + + if (upper_.size() == 0) + { + return v >= lower_; + } + + return (v >= lower_ && v <= upper_); + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/RangeConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/RangeConstraint.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,68 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +namespace Orthanc +{ + class RangeConstraint : public IFindConstraint + { + private: + std::string lower_; + std::string upper_; + bool isCaseSensitive_; + + RangeConstraint(const RangeConstraint& other) : + lower_(other.lower_), + upper_(other.upper_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + RangeConstraint(const std::string& lower, + const std::string& upper, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new RangeConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/SetOfResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/SetOfResources.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,156 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "SetOfResources.h" + +#include "../../Core/OrthancException.h" + + +namespace Orthanc +{ + void SetOfResources::Intersect(const std::list& resources) + { + if (resources_.get() == NULL) + { + resources_.reset(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + resources_->insert(*it); + } + } + else + { + std::auto_ptr filtered(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + if (resources_->find(*it) != resources_->end()) + { + filtered->insert(*it); + } + } + + resources_ = filtered; + } + } + + + void SetOfResources::GoDown() + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (resources_.get() != NULL) + { + std::auto_ptr children(new Resources); + + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + std::list tmp; + database_.GetChildrenInternalId(tmp, *it); + + for (std::list::const_iterator + child = tmp.begin(); child != tmp.end(); ++child) + { + children->insert(*child); + } + } + + resources_ = children; + } + + switch (level_) + { + case ResourceType_Patient: + level_ = ResourceType_Study; + break; + + case ResourceType_Study: + level_ = ResourceType_Series; + break; + + case ResourceType_Series: + level_ = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void SetOfResources::Flatten(std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + database_.GetAllPublicIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(database_.GetPublicId(*it)); + } + } + } + + + void SetOfResources::Flatten(std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + database_.GetAllInternalIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(*it); + } + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/SetOfResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/SetOfResources.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IDatabaseWrapper.h" + +#include +#include +#include + +namespace Orthanc +{ + class SetOfResources : public boost::noncopyable + { + private: + typedef std::set Resources; + + IDatabaseWrapper& database_; + ResourceType level_; + std::auto_ptr resources_; + + public: + SetOfResources(IDatabaseWrapper& database, + ResourceType level) : + database_(database), + level_(level) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + void Intersect(const std::list& resources); + + void GoDown(); + + void Flatten(std::list& result); + + void Flatten(std::list& result); + + void Clear() + { + resources_.reset(NULL); + } + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/ValueConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ValueConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "ValueConstraint.h" + +#include "../../Core/Toolbox.h" + +#include + +namespace Orthanc +{ + ValueConstraint::ValueConstraint(const std::string& value, + bool isCaseSensitive) : + value_(value), + isCaseSensitive_(isCaseSensitive) + { + if (!isCaseSensitive) + { + Toolbox::ToUpperCase(value_); + } + } + + + void ValueConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_Equal, value_); + } + + bool ValueConstraint::Match(const std::string& value) const + { + if (isCaseSensitive_) + { + return value_ == value; + } + else + { + std::string v; + Toolbox::ToUpperCase(v, value); + return value_ == v; + } + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/ValueConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ValueConstraint.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +namespace Orthanc +{ + class ValueConstraint : public IFindConstraint + { + private: + std::string value_; + bool isCaseSensitive_; + + ValueConstraint(const ValueConstraint& other) : + value_(other.value_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + ValueConstraint(const std::string& value, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new ValueConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/WildcardConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/WildcardConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "WildcardConstraint.h" + +#include + +namespace Orthanc +{ + struct WildcardConstraint::PImpl + { + boost::regex pattern_; + std::string wildcard_; + }; + + + WildcardConstraint::WildcardConstraint(const WildcardConstraint& other) : + pimpl_(new PImpl(*other.pimpl_)) + { + } + + + WildcardConstraint::WildcardConstraint(const std::string& wildcard, + bool isCaseSensitive) : + pimpl_(new PImpl) + { + pimpl_->wildcard_ = wildcard; + + std::string re = Toolbox::WildcardToRegularExpression(wildcard); + + if (isCaseSensitive) + { + pimpl_->pattern_ = boost::regex(re); + } + else + { + pimpl_->pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */); + } + } + + bool WildcardConstraint::Match(const std::string& value) const + { + return boost::regex_match(value, pimpl_->pattern_); + } + + void WildcardConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_); + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/Search/WildcardConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/WildcardConstraint.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,63 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +#include + +namespace Orthanc +{ + class WildcardConstraint : public IFindConstraint + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + WildcardConstraint(const WildcardConstraint& other); + + public: + WildcardConstraint(const std::string& wildcard, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new WildcardConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -189,7 +189,7 @@ resultPublicId = hasher.HashInstance(); Json::Value simplifiedTags; - SimplifyTags(simplifiedTags, dicom.GetJson()); + Toolbox::SimplifyTags(simplifiedTags, dicom.GetJson()); // Test if the instance must be filtered out bool accepted = true; @@ -299,7 +299,7 @@ { if (e.GetErrorCode() == ErrorCode_InexistentTag) { - LogMissingRequiredTag(dicom.GetSummary()); + Toolbox::LogMissingRequiredTag(dicom.GetSummary()); } throw; @@ -307,19 +307,64 @@ } - void ServerContext::AnswerAttachment(RestApiOutput& output, - const std::string& instancePublicId, + const std::string& resourceId, FileContentType content) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, instancePublicId, content)) + if (!index_.LookupAttachment(attachment, resourceId, content)) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_UnknownResource); } StorageAccessor accessor(area_); - accessor.AnswerFile(output, attachment); + accessor.AnswerFile(output, attachment, GetFileContentMime(content)); + } + + + void ServerContext::ChangeAttachmentCompression(const std::string& resourceId, + FileContentType attachmentType, + CompressionType compression) + { + LOG(INFO) << "Changing compression type for attachment " + << EnumerationToString(attachmentType) + << " of resource " << resourceId << " to " + << compression; + + FileInfo attachment; + if (!index_.LookupAttachment(attachment, resourceId, attachmentType)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + if (attachment.GetCompressionType() == compression) + { + // Nothing to do + return; + } + + std::string content; + + StorageAccessor accessor(area_); + accessor.Read(content, attachment); + + FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), + content.size(), attachmentType, compression, storeMD5_); + + try + { + StoreStatus status = index_.AddAttachment(modified, resourceId); + if (status != StoreStatus_Success) + { + accessor.Remove(modified); + throw OrthancException(ErrorCode_Database); + } + } + catch (OrthancException&) + { + accessor.Remove(modified); + throw; + } } @@ -362,6 +407,14 @@ } + void ServerContext::ReadFile(std::string& result, + const FileInfo& file) + { + StorageAccessor accessor(area_); + accessor.Read(result, file); + } + + IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId) { std::string content; @@ -475,6 +528,18 @@ } } + OrthancPlugins& ServerContext::GetPlugins() + { + if (HasPlugins()) + { + return *plugins_; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + #endif @@ -486,4 +551,39 @@ return false; #endif } + + + bool ServerContext::Apply(std::list& result, + const ::Orthanc::LookupResource& lookup, + size_t maxResults) + { + result.clear(); + + std::vector resources, instances; + GetIndex().FindCandidates(resources, instances, lookup); + + assert(resources.size() == instances.size()); + + for (size_t i = 0; i < instances.size(); i++) + { + Json::Value dicom; + ReadJson(dicom, instances[i]); + + if (lookup.IsMatch(dicom)) + { + if (maxResults != 0 && + result.size() >= maxResults) + { + return false; // too many results + } + else + { + result.push_back(resources[i]); + } + } + } + + return true; // finished + } + } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerContext.h Wed Nov 18 10:16:21 2015 +0100 @@ -47,6 +47,7 @@ #include "Scheduler/ServerScheduler.h" #include "ServerIndex.h" #include "OrthancHttpHandler.h" +#include "Search/LookupResource.h" #include #include @@ -184,9 +185,13 @@ DicomInstanceToStore& dicom); void AnswerAttachment(RestApiOutput& output, - const std::string& instancePublicId, + const std::string& resourceId, FileContentType content); + void ChangeAttachmentCompression(const std::string& resourceId, + FileContentType attachmentType, + CompressionType compression); + void ReadJson(Json::Value& result, const std::string& instancePublicId); @@ -196,6 +201,9 @@ FileContentType content, bool uncompressIfNeeded = true); + void ReadFile(std::string& result, + const FileInfo& file); + void SetStoreMD5ForAttachments(bool storeMD5); bool IsStoreMD5ForAttachments() const @@ -241,6 +249,10 @@ void Stop(); + bool Apply(std::list& result, + const ::Orthanc::LookupResource& lookup, + size_t maxResults); + /** * Management of the plugins @@ -252,9 +264,10 @@ void ResetPlugins(); const OrthancPlugins& GetPlugins() const; + + OrthancPlugins& GetPlugins(); #endif bool HasPlugins() const; - }; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -35,15 +35,19 @@ #include "../Core/OrthancException.h" #include "../Core/EnumerationDictionary.h" +#include "../Core/Logging.h" #include "../Core/Toolbox.h" #include namespace Orthanc { + typedef std::map MimeTypes; + static boost::mutex enumerationsMutex_; static Toolbox::EnumerationDictionary dictMetadataType_; static Toolbox::EnumerationDictionary dictContentType_; + static MimeTypes mimeTypes_; void InitializeServerEnumerations() { @@ -69,13 +73,29 @@ { boost::mutex::scoped_lock lock(enumerationsMutex_); - if (metadata < static_cast(MetadataType_StartUser) || - metadata > static_cast(MetadataType_EndUser)) + MetadataType type = static_cast(metadata); + + if (metadata < 0 || + !IsUserMetadata(type)) { + LOG(ERROR) << "A user content type must have index between " + << static_cast(MetadataType_StartUser) << " and " + << static_cast(MetadataType_EndUser) << ", but \"" + << name << "\" has index " << metadata; + throw OrthancException(ErrorCode_ParameterOutOfRange); } - dictMetadataType_.Add(static_cast(metadata), name); + if (dictMetadataType_.Contains(type)) + { + LOG(ERROR) << "Cannot associate user content type \"" + << name << "\" with index " << metadata + << ", as this index is already used"; + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + dictMetadataType_.Add(type, name); } std::string EnumerationToString(MetadataType type) @@ -93,17 +113,35 @@ } void RegisterUserContentType(int contentType, - const std::string& name) + const std::string& name, + const std::string& mime) { boost::mutex::scoped_lock lock(enumerationsMutex_); - if (contentType < static_cast(FileContentType_StartUser) || - contentType > static_cast(FileContentType_EndUser)) + FileContentType type = static_cast(contentType); + + if (contentType < 0 || + !IsUserContentType(type)) { + LOG(ERROR) << "A user content type must have index between " + << static_cast(FileContentType_StartUser) << " and " + << static_cast(FileContentType_EndUser) << ", but \"" + << name << "\" has index " << contentType; + throw OrthancException(ErrorCode_ParameterOutOfRange); } - dictContentType_.Add(static_cast(contentType), name); + if (dictContentType_.Contains(type)) + { + LOG(ERROR) << "Cannot associate user content type \"" + << name << "\" with index " << contentType + << ", as this index is already used"; + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + dictContentType_.Add(type, name); + mimeTypes_[type] = mime; } std::string EnumerationToString(FileContentType type) @@ -114,6 +152,33 @@ return dictContentType_.Translate(type); } + std::string GetFileContentMime(FileContentType type) + { + if (type >= FileContentType_StartUser && + type <= FileContentType_EndUser) + { + boost::mutex::scoped_lock lock(enumerationsMutex_); + + MimeTypes::const_iterator it = mimeTypes_.find(type); + if (it != mimeTypes_.end()) + { + return it->second; + } + } + + switch (type) + { + case FileContentType_Dicom: + return "application/dicom"; + + case FileContentType_DicomAsJson: + return "application/json"; + + default: + return "application/octet-stream"; + } + } + FileContentType StringToContentType(const std::string& str) { boost::mutex::scoped_lock lock(enumerationsMutex_); @@ -237,6 +302,12 @@ case ChangeType_NewChildInstance: return "NewChildInstance"; + case ChangeType_UpdatedAttachment: + return "UpdatedAttachment"; + + case ChangeType_UpdatedMetadata: + return "UpdatedMetadata"; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -365,4 +436,10 @@ } } + + bool IsUserMetadata(MetadataType metadata) + { + return (metadata >= MetadataType_StartUser && + metadata <= MetadataType_EndUser); + } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerEnumerations.h Wed Nov 18 10:16:21 2015 +0100 @@ -35,6 +35,7 @@ #include #include "../Core/Enumerations.h" +#include "../Core/DicomFormat/DicomTag.h" namespace Orthanc { @@ -100,6 +101,38 @@ ValueRepresentation_Time }; + enum DicomToJsonFormat + { + DicomToJsonFormat_Full, + DicomToJsonFormat_Short, + DicomToJsonFormat_Simple + }; + + enum DicomToJsonFlags + { + DicomToJsonFlags_IncludeBinary = (1 << 0), + DicomToJsonFlags_IncludePrivateTags = (1 << 1), + DicomToJsonFlags_IncludeUnknownTags = (1 << 2), + DicomToJsonFlags_IncludePixelData = (1 << 3), + DicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), + DicomToJsonFlags_ConvertBinaryToNull = (1 << 5), + + // Some predefined combinations + DicomToJsonFlags_None = 0, + DicomToJsonFlags_Default = (DicomToJsonFlags_IncludePrivateTags | + DicomToJsonFlags_IncludeUnknownTags | + DicomToJsonFlags_IncludePixelData | + DicomToJsonFlags_ConvertBinaryToNull) + }; + + enum IdentifierConstraintType + { + IdentifierConstraintType_Equal, + IdentifierConstraintType_SmallerOrEqual, + IdentifierConstraintType_GreaterOrEqual, + IdentifierConstraintType_Wildcard /* Case sensitive, "*" or "?" are the only allowed wildcards */ + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -109,7 +142,7 @@ enum GlobalProperty { - GlobalProperty_DatabaseSchemaVersion = 1, + GlobalProperty_DatabaseSchemaVersion = 1, // Unused in the Orthanc core as of Orthanc 0.9.5 GlobalProperty_FlushSleep = 2, GlobalProperty_AnonymizationSequence = 3 }; @@ -145,6 +178,8 @@ ChangeType_StablePatient = 12, ChangeType_StableStudy = 13, ChangeType_StableSeries = 14, + ChangeType_UpdatedAttachment = 15, + ChangeType_UpdatedMetadata = 16, ChangeType_INTERNAL_LastLogged = 4095, @@ -165,12 +200,15 @@ std::string EnumerationToString(MetadataType type); void RegisterUserContentType(int contentType, - const std::string& name); + const std::string& name, + const std::string& mime); FileContentType StringToContentType(const std::string& str); std::string EnumerationToString(FileContentType type); + std::string GetFileContentMime(FileContentType type); + std::string GetBasePath(ResourceType type, const std::string& publicId); @@ -187,4 +225,6 @@ const char* EnumerationToString(TransferSyntax syntax); ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); + + bool IsUserMetadata(MetadataType type); } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -40,10 +40,13 @@ #include "ServerIndexChange.h" #include "EmbeddedResources.h" #include "OrthancInitialization.h" +#include "ServerToolbox.h" #include "../Core/Toolbox.h" #include "../Core/Logging.h" #include "../Core/Uuid.h" #include "../Core/DicomFormat/DicomArray.h" +#include "Search/LookupIdentifierQuery.h" +#include "Search/LookupResource.h" #include "FromDcmtkBridge.h" #include "ServerContext.h" @@ -393,8 +396,8 @@ (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL) { // Patch for series with temporal positions thanks to Will Ryder - int64_t imagesInAcquisition = boost::lexical_cast(value->AsString()); - int64_t countTemporalPositions = boost::lexical_cast(value2->AsString()); + int64_t imagesInAcquisition = boost::lexical_cast(value->GetContent()); + int64_t countTemporalPositions = boost::lexical_cast(value2->GetContent()); std::string expected = boost::lexical_cast(imagesInAcquisition * countTemporalPositions); db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); } @@ -403,18 +406,21 @@ (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL) { // Support of Cardio-PET images - int64_t numberOfSlices = boost::lexical_cast(value->AsString()); - int64_t numberOfTimeSlices = boost::lexical_cast(value2->AsString()); + int64_t numberOfSlices = boost::lexical_cast(value->GetContent()); + int64_t numberOfTimeSlices = boost::lexical_cast(value2->GetContent()); std::string expected = boost::lexical_cast(numberOfSlices * numberOfTimeSlices); db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); } else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL) { - db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->AsString()); + db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->GetContent()); } } - catch (boost::bad_lexical_cast) + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) { } } @@ -490,18 +496,6 @@ - void ServerIndex::SetMainDicomTags(int64_t resource, - const DicomMap& tags) - { - DicomArray flattened(tags); - for (size_t i = 0; i < flattened.GetSize(); i++) - { - const DicomElement& element = flattened.GetElement(i); - db_.SetMainDicomTag(resource, element.GetTag(), element.GetValue().AsString()); - } - } - - int64_t ServerIndex::CreateResource(const std::string& publicId, ResourceType type) { @@ -638,10 +632,7 @@ // Create the instance int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance); - - DicomMap dicom; - dicomSummary.ExtractInstanceInformation(dicom); - SetMainDicomTags(instance, dicom); + Toolbox::SetMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary); // Detect up to which level the patient/study/series/instance // hierarchy must be created @@ -693,24 +684,21 @@ if (isNewSeries) { series = CreateResource(hasher.HashSeries(), ResourceType_Series); - dicomSummary.ExtractSeriesInformation(dicom); - SetMainDicomTags(series, dicom); + Toolbox::SetMainDicomTags(db_, series, ResourceType_Series, dicomSummary); } // Create the study if needed if (isNewStudy) { study = CreateResource(hasher.HashStudy(), ResourceType_Study); - dicomSummary.ExtractStudyInformation(dicom); - SetMainDicomTags(study, dicom); + Toolbox::SetMainDicomTags(db_, study, ResourceType_Study, dicomSummary); } // Create the patient if needed if (isNewPatient) { patient = CreateResource(hasher.HashPatient(), ResourceType_Patient); - dicomSummary.ExtractPatientInformation(dicom); - SetMainDicomTags(patient, dicom); + Toolbox::SetMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary); } // Create the parent-to-child links @@ -785,8 +773,12 @@ if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) { - db_.SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString()); - instanceMetadata[MetadataType_Instance_IndexInSeries] = value->AsString(); + if (!value->IsNull() && + !value->IsBinary()) + { + db_.SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->GetContent()); + instanceMetadata[MetadataType_Instance_IndexInSeries] = value->GetContent(); + } } // Check whether the series of this new instance is now completed @@ -890,14 +882,30 @@ } - void ServerIndex::MainDicomTagsToJson(Json::Value& target, - int64_t resourceId) + int64_t resourceId, + ResourceType resourceType) { DicomMap tags; db_.GetMainDicomTags(tags, resourceId); - target["MainDicomTags"] = Json::objectValue; - FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); + + if (resourceType == ResourceType_Study) + { + DicomMap t1, t2; + tags.ExtractStudyInformation(t1); + tags.ExtractPatientInformation(t2); + + target["MainDicomTags"] = Json::objectValue; + FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true); + + target["PatientMainDicomTags"] = Json::objectValue; + FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true); + } + else + { + target["MainDicomTags"] = Json::objectValue; + FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); + } } bool ServerIndex::LookupResource(Json::Value& result, @@ -1033,7 +1041,7 @@ // Record the remaining information result["ID"] = publicId; - MainDicomTagsToJson(result, id); + MainDicomTagsToJson(result, id, type); std::string tmp; @@ -1198,22 +1206,22 @@ switch (currentType) { case ResourceType_Patient: - patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString(); + patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent(); done = true; break; case ResourceType_Study: - studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); + studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(); currentType = ResourceType_Patient; break; case ResourceType_Series: - seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); + seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(); currentType = ResourceType_Study; break; case ResourceType_Instance: - sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); + sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent(); currentType = ResourceType_Series; break; @@ -1517,6 +1525,7 @@ const std::string& value) { boost::mutex::scoped_lock lock(mutex_); + Transaction t(*this); ResourceType rtype; int64_t id; @@ -1526,6 +1535,13 @@ } db_.SetMetadata(id, type, value); + + if (IsUserMetadata(type)) + { + LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); + } + + t.Commit(0); } @@ -1533,6 +1549,7 @@ MetadataType type) { boost::mutex::scoped_lock lock(mutex_); + Transaction t(*this); ResourceType rtype; int64_t id; @@ -1542,6 +1559,13 @@ } db_.DeleteMetadata(id, type); + + if (IsUserMetadata(type)) + { + LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); + } + + t.Commit(0); } @@ -1889,64 +1913,24 @@ - void ServerIndex::LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value, - ResourceType type) + void ServerIndex::LookupIdentifierExact(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value) { + assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || + (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || + (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || + (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || + (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); + result.clear(); boost::mutex::scoped_lock lock(mutex_); - std::list id; - db_.LookupIdentifier(id, tag, value); - - for (std::list::const_iterator - it = id.begin(); it != id.end(); ++it) - { - if (db_.GetResourceType(*it) == type) - { - result.push_back(db_.GetPublicId(*it)); - } - } - } - - - void ServerIndex::LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value) - { - result.clear(); - - boost::mutex::scoped_lock lock(mutex_); - - std::list id; - db_.LookupIdentifier(id, tag, value); - - for (std::list::const_iterator - it = id.begin(); it != id.end(); ++it) - { - result.push_back(db_.GetPublicId(*it)); - } - } - - - void ServerIndex::LookupIdentifier(std::list< std::pair >& result, - const std::string& value) - { - result.clear(); - - boost::mutex::scoped_lock lock(mutex_); - - std::list id; - db_.LookupIdentifier(id, value); - - for (std::list::const_iterator - it = id.begin(); it != id.end(); ++it) - { - result.push_back(std::make_pair(db_.GetResourceType(*it), - db_.GetPublicId(*it))); - } + LookupIdentifierQuery query(level); + query.AddConstraint(tag, IdentifierConstraintType_Equal, value); + query.Apply(result, db_); } @@ -1990,6 +1974,11 @@ db_.AddAttachment(resourceId, attachment); + if (IsUserContentType(attachment.GetContentType())) + { + LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId); + } + t.Commit(attachment.GetCompressedSize()); return StoreStatus_Success; @@ -2011,6 +2000,11 @@ db_.DeleteAttachment(id, type); + if (IsUserContentType(type)) + { + LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId); + } + t.Commit(0); } @@ -2077,8 +2071,19 @@ bool ServerIndex::GetMainDicomTags(DicomMap& result, const std::string& publicId, - ResourceType expectedType) + ResourceType expectedType, + ResourceType levelOfInterest) { + // Yes, the following test could be shortened, but we wish to make it as clear as possible + if (!(expectedType == ResourceType_Patient && levelOfInterest == ResourceType_Patient) && + !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Patient) && + !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Study) && + !(expectedType == ResourceType_Series && levelOfInterest == ResourceType_Series) && + !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + result.Clear(); boost::mutex::scoped_lock lock(mutex_); @@ -2091,6 +2096,26 @@ { return false; } + + if (type == ResourceType_Study) + { + DicomMap tmp; + db_.GetMainDicomTags(tmp, id); + + switch (levelOfInterest) + { + case ResourceType_Patient: + tmp.ExtractPatientInformation(result); + return true; + + case ResourceType_Study: + tmp.ExtractStudyInformation(result); + return true; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } else { db_.GetMainDicomTags(result, id); @@ -2108,4 +2133,40 @@ return db_.LookupResource(id, type, publicId); } + + unsigned int ServerIndex::GetDatabaseVersion() + { + boost::mutex::scoped_lock lock(mutex_); + return db_.GetDatabaseVersion(); + } + + + void ServerIndex::FindCandidates(std::vector& resources, + std::vector& instances, + const ::Orthanc::LookupResource& lookup) + { + boost::mutex::scoped_lock lock(mutex_); + + std::list tmp; + lookup.FindCandidates(tmp, db_); + + resources.resize(tmp.size()); + instances.resize(tmp.size()); + + size_t pos = 0; + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it, pos++) + { + assert(db_.GetResourceType(*it) == lookup.GetLevel()); + + int64_t instance; + if (!Toolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel())) + { + throw OrthancException(ErrorCode_InternalError); + } + + resources[pos] = db_.GetPublicId(*it); + instances[pos] = db_.GetPublicId(instance); + } + } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerIndex.h Wed Nov 18 10:16:21 2015 +0100 @@ -45,6 +45,7 @@ namespace Orthanc { + class LookupResource; class ServerContext; class ServerIndex : public boost::noncopyable @@ -76,7 +77,8 @@ static void UnstableResourcesMonitorThread(ServerIndex* that); void MainDicomTagsToJson(Json::Value& result, - int64_t resourceId); + int64_t resourceId, + ResourceType resourceType); SeriesStatus GetSeriesStatus(int64_t id); @@ -110,9 +112,6 @@ uint64_t IncrementGlobalSequenceInternal(GlobalProperty property); - void SetMainDicomTags(int64_t resource, - const DicomMap& tags); - int64_t CreateResource(const std::string& publicId, ResourceType type); @@ -237,17 +236,10 @@ /* out */ unsigned int& countInstances, const std::string& publicId); - void LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value, - ResourceType type); - - void LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value); - - void LookupIdentifier(std::list< std::pair >& result, - const std::string& value); + void LookupIdentifierExact(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value); StoreStatus AddAttachment(const FileInfo& attachment, const std::string& publicId); @@ -263,9 +255,16 @@ bool GetMainDicomTags(DicomMap& result, const std::string& publicId, - ResourceType expectedType); + ResourceType expectedType, + ResourceType levelOfInterest); bool LookupResourceType(ResourceType& type, const std::string& publicId); + + unsigned int GetDatabaseVersion(); + + void FindCandidates(std::vector& resources, + std::vector& instances, + const ::Orthanc::LookupResource& lookup); }; } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerIndexChange.h --- a/OrthancServer/ServerIndexChange.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerIndexChange.h Wed Nov 18 10:16:21 2015 +0100 @@ -33,6 +33,7 @@ #pragma once #include "ServerEnumerations.h" +#include "../Core/IDynamicObject.h" #include "../Core/Toolbox.h" #include diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerToolbox.cpp --- a/OrthancServer/ServerToolbox.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerToolbox.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -33,122 +33,305 @@ #include "PrecompiledHeadersServer.h" #include "ServerToolbox.h" +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/FileStorage/StorageAccessor.h" #include "../Core/Logging.h" #include "../Core/OrthancException.h" +#include "ParsedDicomFile.h" +#include "Search/LookupIdentifierQuery.h" #include namespace Orthanc { - void SimplifyTags(Json::Value& target, - const Json::Value& source) + namespace Toolbox { - assert(source.isObject()); + void SimplifyTags(Json::Value& target, + const Json::Value& source) + { + assert(source.isObject()); + + target = Json::objectValue; + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& v = source[members[i]]; + const std::string& name = v["Name"].asString(); + const std::string& type = v["Type"].asString(); - target = Json::objectValue; - Json::Value::Members members = source.getMemberNames(); + if (type == "String") + { + target[name] = v["Value"].asString(); + } + else if (type == "TooLong" || + type == "Null") + { + target[name] = Json::nullValue; + } + else if (type == "Sequence") + { + const Json::Value& array = v["Value"]; + assert(array.isArray()); - for (size_t i = 0; i < members.size(); i++) + Json::Value children = Json::arrayValue; + for (Json::Value::ArrayIndex i = 0; i < array.size(); i++) + { + Json::Value c; + SimplifyTags(c, array[i]); + children.append(c); + } + + target[name] = children; + } + else + { + assert(0); + } + } + } + + + static std::string ValueAsString(const DicomMap& summary, + const DicomTag& tag) { - const Json::Value& v = source[members[i]]; - const std::string& name = v["Name"].asString(); - const std::string& type = v["Type"].asString(); - - if (type == "String") + const DicomValue& value = summary.GetValue(tag); + if (value.IsNull()) { - target[name] = v["Value"].asString(); + return "(null)"; + } + else + { + return value.GetContent(); } - else if (type == "TooLong" || - type == "Null") + } + + + void LogMissingRequiredTag(const DicomMap& summary) + { + std::string s, t; + + if (summary.HasTag(DICOM_TAG_PATIENT_ID)) { - target[name] = Json::nullValue; + if (t.size() > 0) + t += ", "; + t += "PatientID=" + ValueAsString(summary, DICOM_TAG_PATIENT_ID); } - else if (type == "Sequence") + else { - const Json::Value& array = v["Value"]; - assert(array.isArray()); + if (s.size() > 0) + s += ", "; + s += "PatientID"; + } - Json::Value children = Json::arrayValue; - for (Json::Value::ArrayIndex i = 0; i < array.size(); i++) - { - Json::Value c; - SimplifyTags(c, array[i]); - children.append(c); - } + if (summary.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "StudyInstanceUID=" + ValueAsString(summary, DICOM_TAG_STUDY_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "StudyInstanceUID"; + } - target[name] = children; + if (summary.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "SeriesInstanceUID=" + ValueAsString(summary, DICOM_TAG_SERIES_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "SeriesInstanceUID"; + } + + if (summary.HasTag(DICOM_TAG_SOP_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "SOPInstanceUID=" + ValueAsString(summary, DICOM_TAG_SOP_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "SOPInstanceUID"; + } + + if (t.size() == 0) + { + LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; } else { - assert(0); + LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; + } + } + + + static void SetMainDicomTagsInternal(IDatabaseWrapper& database, + int64_t resource, + const DicomMap& tags) + { + DicomArray flattened(tags); + + for (size_t i = 0; i < flattened.GetSize(); i++) + { + const DicomElement& element = flattened.GetElement(i); + const DicomTag& tag = element.GetTag(); + const DicomValue& value = element.GetValue(); + if (!value.IsNull() && + !value.IsBinary()) + { + database.SetMainDicomTag(resource, tag, element.GetValue().GetContent()); + } + } + } + + + void SetMainDicomTags(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& dicomSummary) + { + // WARNING: The database should be locked with a transaction! + + LookupIdentifierQuery::StoreIdentifiers(database, resource, level, dicomSummary); + + DicomMap tags; + + switch (level) + { + case ResourceType_Patient: + dicomSummary.ExtractPatientInformation(tags); + break; + + case ResourceType_Study: + // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) + dicomSummary.ExtractPatientInformation(tags); + SetMainDicomTagsInternal(database, resource, tags); + + dicomSummary.ExtractStudyInformation(tags); + break; + + case ResourceType_Series: + dicomSummary.ExtractSeriesInformation(tags); + break; + + case ResourceType_Instance: + dicomSummary.ExtractInstanceInformation(tags); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + SetMainDicomTagsInternal(database, resource, tags); + } + + + bool FindOneChildInstance(int64_t& result, + IDatabaseWrapper& database, + int64_t resource, + ResourceType type) + { + for (;;) + { + if (type == ResourceType_Instance) + { + result = resource; + return true; + } + + std::list children; + database.GetChildrenInternalId(children, resource); + if (children.empty()) + { + return false; + } + + resource = children.front(); + type = GetChildResourceType(type); + } + } + + + void ReconstructMainDicomTags(IDatabaseWrapper& database, + IStorageArea& storageArea, + ResourceType level) + { + // WARNING: The database should be locked with a transaction! + + const char* plural = NULL; + + switch (level) + { + case ResourceType_Patient: + plural = "patients"; + break; + + case ResourceType_Study: + plural = "studies"; + break; + + case ResourceType_Series: + plural = "series"; + break; + + case ResourceType_Instance: + plural = "instances"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "..."; + + std::list resources; + database.GetAllPublicIds(resources, level); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); it++) + { + // Locate the resource and one of its child instances + int64_t resource, instance; + ResourceType tmp; + + if (!database.LookupResource(resource, tmp, *it) || + tmp != level || + !FindOneChildInstance(instance, database, resource, level)) + { + throw OrthancException(ErrorCode_InternalError); + } + + // Get the DICOM file attached to some instances in the resource + FileInfo attachment; + if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError); + } + + // Read and parse the content of the DICOM file + StorageAccessor accessor(storageArea); + + std::string content; + accessor.Read(content, attachment); + + ParsedDicomFile dicom(content); + + // Update the tags of this resource + DicomMap dicomSummary; + dicom.Convert(dicomSummary); + + database.ClearMainDicomTags(resource); + Toolbox::SetMainDicomTags(database, resource, level, dicomSummary); } } } - - - void LogMissingRequiredTag(const DicomMap& summary) - { - std::string s, t; - - if (summary.HasTag(DICOM_TAG_PATIENT_ID)) - { - if (t.size() > 0) - t += ", "; - t += "PatientID=" + summary.GetValue(DICOM_TAG_PATIENT_ID).AsString(); - } - else - { - if (s.size() > 0) - s += ", "; - s += "PatientID"; - } - - if (summary.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)) - { - if (t.size() > 0) - t += ", "; - t += "StudyInstanceUID=" + summary.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); - } - else - { - if (s.size() > 0) - s += ", "; - s += "StudyInstanceUID"; - } - - if (summary.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)) - { - if (t.size() > 0) - t += ", "; - t += "SeriesInstanceUID=" + summary.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); - } - else - { - if (s.size() > 0) - s += ", "; - s += "SeriesInstanceUID"; - } - - if (summary.HasTag(DICOM_TAG_SOP_INSTANCE_UID)) - { - if (t.size() > 0) - t += ", "; - t += "SOPInstanceUID=" + summary.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); - } - else - { - if (s.size() > 0) - s += ", "; - s += "SOPInstanceUID"; - } - - if (t.size() == 0) - { - LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; - } - else - { - LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; - } - } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ServerToolbox.h --- a/OrthancServer/ServerToolbox.h Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ServerToolbox.h Wed Nov 18 10:16:21 2015 +0100 @@ -33,13 +33,31 @@ #pragma once #include "../Core/DicomFormat/DicomMap.h" +#include "IDatabaseWrapper.h" #include namespace Orthanc { - void SimplifyTags(Json::Value& target, - const Json::Value& source); + namespace Toolbox + { + void SimplifyTags(Json::Value& target, + const Json::Value& source); + + void LogMissingRequiredTag(const DicomMap& summary); - void LogMissingRequiredTag(const DicomMap& summary); + void SetMainDicomTags(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& dicomSummary); + + bool FindOneChildInstance(int64_t& result, + IDatabaseWrapper& database, + int64_t resource, + ResourceType type); + + void ReconstructMainDicomTags(IDatabaseWrapper& database, + IStorageArea& storageArea, + ResourceType level); + } } diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/SliceOrdering.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/SliceOrdering.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,411 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeadersServer.h" +#include "SliceOrdering.h" + +#include "../Core/Logging.h" +#include "../Core/Toolbox.h" +#include "ServerEnumerations.h" + +#include +#include +#include + + +namespace Orthanc +{ + static bool TokenizeVector(std::vector& result, + const std::string& value, + unsigned int expectedSize) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, value, '\\'); + + if (tokens.size() != expectedSize) + { + return false; + } + + result.resize(tokens.size()); + + for (size_t i = 0; i < tokens.size(); i++) + { + try + { + result[i] = boost::lexical_cast(tokens[i]); + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + return true; + } + + + static bool TokenizeVector(std::vector& result, + const DicomMap& map, + const DicomTag& tag, + unsigned int expectedSize) + { + const DicomValue* value = map.TestAndGetValue(tag); + + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + return false; + } + else + { + return TokenizeVector(result, value->GetContent(), expectedSize); + } + } + + + struct SliceOrdering::Instance : public boost::noncopyable + { + private: + std::string instanceId_; + bool hasPosition_; + Vector position_; + bool hasIndexInSeries_; + size_t indexInSeries_; + unsigned int framesCount_; + + public: + Instance(ServerIndex& index, + const std::string& instanceId) : + instanceId_(instanceId), + framesCount_(1) + { + DicomMap instance; + if (!index.GetMainDicomTags(instance, instanceId, ResourceType_Instance, ResourceType_Instance)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + const DicomValue* frames = instance.TestAndGetValue(DICOM_TAG_NUMBER_OF_FRAMES); + if (frames != NULL && + !frames->IsNull() && + !frames->IsBinary()) + { + try + { + framesCount_ = boost::lexical_cast(frames->GetContent()); + } + catch (boost::bad_lexical_cast&) + { + } + } + + std::vector tmp; + hasPosition_ = TokenizeVector(tmp, instance, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); + + if (hasPosition_) + { + position_[0] = tmp[0]; + position_[1] = tmp[1]; + position_[2] = tmp[2]; + } + + std::string s; + hasIndexInSeries_ = false; + + try + { + if (index.LookupMetadata(s, instanceId, MetadataType_Instance_IndexInSeries)) + { + indexInSeries_ = boost::lexical_cast(s); + hasIndexInSeries_ = true; + } + } + catch (boost::bad_lexical_cast&) + { + } + } + + const std::string& GetIdentifier() const + { + return instanceId_; + } + + bool HasPosition() const + { + return hasPosition_; + } + + float ComputeRelativePosition(const Vector& normal) const + { + assert(HasPosition()); + return (normal[0] * position_[0] + + normal[1] * position_[1] + + normal[2] * position_[2]); + } + + bool HasIndexInSeries() const + { + return hasIndexInSeries_; + } + + size_t GetIndexInSeries() const + { + assert(HasIndexInSeries()); + return indexInSeries_; + } + + unsigned int GetFramesCount() const + { + return framesCount_; + } + }; + + + class SliceOrdering::PositionComparator + { + private: + const Vector& normal_; + + public: + PositionComparator(const Vector& normal) : normal_(normal) + { + } + + int operator() (const Instance* a, + const Instance* b) const + { + return a->ComputeRelativePosition(normal_) < b->ComputeRelativePosition(normal_); + } + }; + + + bool SliceOrdering::IndexInSeriesComparator(const SliceOrdering::Instance* a, + const SliceOrdering::Instance* b) + { + return a->GetIndexInSeries() < b->GetIndexInSeries(); + } + + + void SliceOrdering::ComputeNormal() + { + DicomMap series; + if (!index_.GetMainDicomTags(series, seriesId_, ResourceType_Series, ResourceType_Series)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + std::vector cosines; + hasNormal_ = TokenizeVector(cosines, series, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + + if (hasNormal_) + { + normal_[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4]; + normal_[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5]; + normal_[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3]; + } + } + + + void SliceOrdering::CreateInstances() + { + std::list instancesId; + index_.GetChildren(instancesId, seriesId_); + + instances_.reserve(instancesId.size()); + for (std::list::const_iterator + it = instancesId.begin(); it != instancesId.end(); ++it) + { + instances_.push_back(new Instance(index_, *it)); + } + } + + + bool SliceOrdering::SortUsingPositions() + { + if (instances_.size() <= 1) + { + // One single instance: It is sorted by default + return true; + } + + if (!hasNormal_) + { + return false; + } + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + if (!instances_[i]->HasPosition()) + { + return false; + } + } + + PositionComparator comparator(normal_); + std::sort(instances_.begin(), instances_.end(), comparator); + + float a = instances_.front()->ComputeRelativePosition(normal_); + float b = instances_.back()->ComputeRelativePosition(normal_); + + if (std::fabs(b - a) <= 10.0f * std::numeric_limits::epsilon()) + { + // Not enough difference between the minimum and maximum + // positions along the normal of the volume + return false; + } + else + { + // This is a 3D volume + isVolume_ = true; + return true; + } + } + + + bool SliceOrdering::SortUsingIndexInSeries() + { + if (instances_.size() <= 1) + { + // One single instance: It is sorted by default + return true; + } + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + if (!instances_[i]->HasIndexInSeries()) + { + return false; + } + } + + std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator); + + for (size_t i = 1; i < instances_.size(); i++) + { + if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries()) + { + // The current "IndexInSeries" occurs 2 times: Not a proper ordering + return false; + } + } + + return true; + } + + + SliceOrdering::SliceOrdering(ServerIndex& index, + const std::string& seriesId) : + index_(index), + seriesId_(seriesId), + isVolume_(false) + { + ComputeNormal(); + CreateInstances(); + + if (!SortUsingPositions() && + !SortUsingIndexInSeries()) + { + LOG(ERROR) << "Unable to order the slices of the series " << seriesId; + throw OrthancException(ErrorCode_CannotOrderSlices); + } + } + + + SliceOrdering::~SliceOrdering() + { + for (std::vector::iterator + it = instances_.begin(); it != instances_.end(); ++it) + { + if (*it != NULL) + { + delete *it; + } + } + } + + + const std::string& SliceOrdering::GetInstanceId(size_t index) const + { + if (index >= instances_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return instances_[index]->GetIdentifier(); + } + } + + + unsigned int SliceOrdering::GetFramesCount(size_t index) const + { + if (index >= instances_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return instances_[index]->GetFramesCount(); + } + } + + + void SliceOrdering::Format(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = (isVolume_ ? "Volume" : "Sequence"); + + Json::Value tmp = Json::arrayValue; + for (size_t i = 0; i < GetInstancesCount(); i++) + { + tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file"); + } + + result["Dicom"] = tmp; + + tmp.clear(); + for (size_t i = 0; i < GetInstancesCount(); i++) + { + std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i)); + for (size_t j = 0; j < GetFramesCount(i); j++) + { + tmp.append(base + "/frames/" + boost::lexical_cast(j)); + } + } + + result["Slices"] = tmp; + } +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/SliceOrdering.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/SliceOrdering.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ServerIndex.h" + +namespace Orthanc +{ + class SliceOrdering + { + private: + typedef float Vector[3]; + + struct Instance; + struct PositionComparator; + + ServerIndex& index_; + std::string seriesId_; + bool hasNormal_; + Vector normal_; + std::vector instances_; + bool isVolume_; + + static bool IndexInSeriesComparator(const SliceOrdering::Instance* a, + const SliceOrdering::Instance* b); + + void ComputeNormal(); + + void CreateInstances(); + + bool SortUsingPositions(); + + bool SortUsingIndexInSeries(); + + public: + SliceOrdering(ServerIndex& index, + const std::string& seriesId); + + ~SliceOrdering(); + + size_t GetInstancesCount() const + { + return instances_.size(); + } + + const std::string& GetInstanceId(size_t index) const; + + unsigned int GetFramesCount(size_t index) const; + + void Format(Json::Value& result) const; + }; +} diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/ToDcmtkBridge.cpp --- a/OrthancServer/ToDcmtkBridge.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/ToDcmtkBridge.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -52,8 +52,11 @@ for (DicomMap::Map::const_iterator it = map.map_.begin(); it != map.map_.end(); ++it) { - std::string s = it->second->AsString(); - DU_putStringDOElement(result.get(), Convert(it->first), s.c_str()); + if (!it->second->IsNull()) + { + std::string s = it->second->GetContent(); + DU_putStringDOElement(result.get(), Convert(it->first), s.c_str()); + } } return result.release(); diff -r 1b82bb0446d2 -r c131566b8252 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/OrthancServer/main.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -307,13 +307,95 @@ }; -static void PrintHelp(char* path) + +class MyHttpExceptionFormatter : public IHttpExceptionFormatter +{ +private: + bool describeErrors_; + OrthancPlugins* plugins_; + +public: + MyHttpExceptionFormatter(bool describeErrors, + OrthancPlugins* plugins) : + describeErrors_(describeErrors), + plugins_(plugins) + { + } + + virtual void Format(HttpOutput& output, + const OrthancException& exception, + HttpMethod method, + const char* uri) + { + { + bool isPlugin = false; + +#if ORTHANC_PLUGINS_ENABLED == 1 + if (plugins_ != NULL) + { + plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true); + isPlugin = true; + } +#endif + + if (!isPlugin) + { + LOG(ERROR) << "Exception in the HTTP handler: " << exception.What(); + } + } + + Json::Value message = Json::objectValue; + ErrorCode errorCode = exception.GetErrorCode(); + HttpStatus httpStatus = exception.GetHttpStatus(); + + { + bool isPlugin = false; + +#if ORTHANC_PLUGINS_ENABLED == 1 + if (plugins_ != NULL && + plugins_->GetErrorDictionary().Format(message, httpStatus, exception)) + { + errorCode = ErrorCode_Plugin; + isPlugin = true; + } +#endif + + if (!isPlugin) + { + message["Message"] = exception.What(); + } + } + + if (!describeErrors_) + { + output.SendStatus(httpStatus); + } + else + { + message["Method"] = EnumerationToString(method); + message["Uri"] = uri; + message["HttpError"] = EnumerationToString(httpStatus); + message["HttpStatus"] = httpStatus; + message["OrthancError"] = EnumerationToString(errorCode); + message["OrthancStatus"] = errorCode; + + std::string info = message.toStyledString(); + output.SendStatus(httpStatus, info); + } + } +}; + + + +static void PrintHelp(const char* path) { std::cout << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl << std::endl - << "If no configuration file is given on the command line, a set of default " << std::endl + << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl + << "case of a directory, all the JSON files it contains will be merged. " << std::endl + << "If no configuration path is given on the command line, a set of default " << std::endl << "parameters is used. Please refer to the Orthanc homepage for the full " << std::endl << "instructions about how to use Orthanc ." << std::endl << std::endl @@ -322,6 +404,7 @@ << " --logdir=[dir]\tdirectory where to store the log files" << std::endl << "\t\t\t(if not used, the logs are dumped to stderr)" << std::endl << " --config=[file]\tcreate a sample configuration file and exit" << std::endl + << " --errors\t\tprint the supported error codes and exit" << std::endl << " --verbose\t\tbe verbose in logs" << std::endl << " --trace\t\thighest verbosity in logs (for debug)" << std::endl << " --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl @@ -331,12 +414,16 @@ << std::endl << "Exit status:" << std::endl << " 0 if success," << std::endl +#if defined(_WIN32) + << "!= 0 if error (use the --errors option to get the list of possible errors)." << std::endl +#else << " -1 if error (have a look at the logs)." << std::endl +#endif << std::endl; } -static void PrintVersion(char* path) +static void PrintVersion(const char* path) { std::cout << path << " " << ORTHANC_VERSION << std::endl @@ -349,6 +436,126 @@ } +static void PrintErrorCode(ErrorCode code, const char* description) +{ + std::cout + << std::right << std::setw(16) + << static_cast(code) + << " " << description << std::endl; +} + + +static void PrintErrors(const char* path) +{ + std::cout + << path << " " << ORTHANC_VERSION << std::endl + << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." + << std::endl << std::endl + << "List of error codes that could be returned by Orthanc:" + << std::endl << std::endl; + + // The content of the following brackets is automatically generated + // by the "GenerateErrorCodes.py" script + { + PrintErrorCode(ErrorCode_InternalError, "Internal error"); + PrintErrorCode(ErrorCode_Success, "Success"); + PrintErrorCode(ErrorCode_Plugin, "Error encountered within the plugin engine"); + PrintErrorCode(ErrorCode_NotImplemented, "Not implemented yet"); + PrintErrorCode(ErrorCode_ParameterOutOfRange, "Parameter out of range"); + PrintErrorCode(ErrorCode_NotEnoughMemory, "Not enough memory"); + PrintErrorCode(ErrorCode_BadParameterType, "Bad type for a parameter"); + PrintErrorCode(ErrorCode_BadSequenceOfCalls, "Bad sequence of calls"); + PrintErrorCode(ErrorCode_InexistentItem, "Accessing an inexistent item"); + PrintErrorCode(ErrorCode_BadRequest, "Bad request"); + PrintErrorCode(ErrorCode_NetworkProtocol, "Error in the network protocol"); + PrintErrorCode(ErrorCode_SystemCommand, "Error while calling a system command"); + PrintErrorCode(ErrorCode_Database, "Error with the database engine"); + PrintErrorCode(ErrorCode_UriSyntax, "Badly formatted URI"); + PrintErrorCode(ErrorCode_InexistentFile, "Inexistent file"); + PrintErrorCode(ErrorCode_CannotWriteFile, "Cannot write to file"); + PrintErrorCode(ErrorCode_BadFileFormat, "Bad file format"); + PrintErrorCode(ErrorCode_Timeout, "Timeout"); + PrintErrorCode(ErrorCode_UnknownResource, "Unknown resource"); + PrintErrorCode(ErrorCode_IncompatibleDatabaseVersion, "Incompatible version of the database"); + PrintErrorCode(ErrorCode_FullStorage, "The file storage is full"); + PrintErrorCode(ErrorCode_CorruptedFile, "Corrupted file (e.g. inconsistent MD5 hash)"); + PrintErrorCode(ErrorCode_InexistentTag, "Inexistent tag"); + PrintErrorCode(ErrorCode_ReadOnly, "Cannot modify a read-only data structure"); + PrintErrorCode(ErrorCode_IncompatibleImageFormat, "Incompatible format of the images"); + PrintErrorCode(ErrorCode_IncompatibleImageSize, "Incompatible size of the images"); + PrintErrorCode(ErrorCode_SharedLibrary, "Error while using a shared library (plugin)"); + PrintErrorCode(ErrorCode_UnknownPluginService, "Plugin invoking an unknown service"); + PrintErrorCode(ErrorCode_UnknownDicomTag, "Unknown DICOM tag"); + PrintErrorCode(ErrorCode_BadJson, "Cannot parse a JSON document"); + PrintErrorCode(ErrorCode_Unauthorized, "Bad credentials were provided to an HTTP request"); + PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file"); + PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface"); + PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area"); + PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty"); + PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header"); + PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); + PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); + PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); + PrintErrorCode(ErrorCode_SQLiteStatementAlreadyUsed, "SQLite: This cached statement is already being referred to"); + PrintErrorCode(ErrorCode_SQLiteExecute, "SQLite: Cannot execute a command"); + PrintErrorCode(ErrorCode_SQLiteRollbackWithoutTransaction, "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)"); + PrintErrorCode(ErrorCode_SQLiteCommitWithoutTransaction, "SQLite: Committing a nonexistent transaction"); + PrintErrorCode(ErrorCode_SQLiteRegisterFunction, "SQLite: Unable to register a function"); + PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database"); + PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement"); + PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement"); + PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)"); + PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement"); + PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice"); + PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction"); + PrintErrorCode(ErrorCode_SQLiteTransactionBegin, "SQLite: Cannot start a transaction"); + PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file"); + PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage"); + PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory"); + PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is already in use"); + PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is already in use"); + PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API"); + PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file"); + PrintErrorCode(ErrorCode_PathToExecutable, "Unable to get the path to the executable"); + PrintErrorCode(ErrorCode_MakeDirectory, "Cannot create a directory"); + PrintErrorCode(ErrorCode_BadApplicationEntityTitle, "An application entity title (AET) cannot be empty or be longer than 16 characters"); + PrintErrorCode(ErrorCode_NoCFindHandler, "No request handler factory for DICOM C-FIND SCP"); + PrintErrorCode(ErrorCode_NoCMoveHandler, "No request handler factory for DICOM C-MOVE SCP"); + PrintErrorCode(ErrorCode_NoCStoreHandler, "No request handler factory for DICOM C-STORE SCP"); + PrintErrorCode(ErrorCode_NoApplicationEntityFilter, "No application entity filter"); + PrintErrorCode(ErrorCode_NoSopClassOrInstance, "DicomUserConnection: Unable to find the SOP class and instance"); + PrintErrorCode(ErrorCode_NoPresentationContext, "DicomUserConnection: No acceptable presentation context for modality"); + PrintErrorCode(ErrorCode_DicomFindUnavailable, "DicomUserConnection: The C-FIND command is not supported by the remote SCP"); + PrintErrorCode(ErrorCode_DicomMoveUnavailable, "DicomUserConnection: The C-MOVE command is not supported by the remote SCP"); + PrintErrorCode(ErrorCode_CannotStoreInstance, "Cannot store an instance"); + PrintErrorCode(ErrorCode_CreateDicomNotString, "Only string values are supported when creating DICOM instances"); + PrintErrorCode(ErrorCode_CreateDicomOverrideTag, "Trying to override a value inherited from a parent module"); + PrintErrorCode(ErrorCode_CreateDicomUseContent, "Use \"Content\" to inject an image into a new DICOM instance"); + PrintErrorCode(ErrorCode_CreateDicomNoPayload, "No payload is present for one instance in the series"); + PrintErrorCode(ErrorCode_CreateDicomUseDataUriScheme, "The payload of the DICOM instance must be specified according to Data URI scheme"); + PrintErrorCode(ErrorCode_CreateDicomBadParent, "Trying to attach a new DICOM instance to an inexistent resource"); + PrintErrorCode(ErrorCode_CreateDicomParentIsInstance, "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)"); + PrintErrorCode(ErrorCode_CreateDicomParentEncoding, "Unable to get the encoding of the parent resource"); + PrintErrorCode(ErrorCode_UnknownModality, "Unknown modality"); + PrintErrorCode(ErrorCode_BadJobOrdering, "Bad ordering of filters in a job"); + PrintErrorCode(ErrorCode_JsonToLuaTable, "Cannot convert the given JSON object to a Lua table"); + PrintErrorCode(ErrorCode_CannotCreateLua, "Cannot create the Lua context"); + PrintErrorCode(ErrorCode_CannotExecuteLua, "Cannot execute a Lua command"); + PrintErrorCode(ErrorCode_LuaAlreadyExecuted, "Arguments cannot be pushed after the Lua function is executed"); + PrintErrorCode(ErrorCode_LuaBadOutput, "The Lua function does not give the expected number of outputs"); + PrintErrorCode(ErrorCode_NotLuaPredicate, "The Lua function is not a predicate (only true/false outputs allowed)"); + PrintErrorCode(ErrorCode_LuaReturnsNoString, "The Lua function does not return a string"); + PrintErrorCode(ErrorCode_StorageAreaAlreadyRegistered, "Another plugin has already registered a custom storage area"); + PrintErrorCode(ErrorCode_DatabaseBackendAlreadyRegistered, "Another plugin has already registered a custom database back-end"); + PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization"); + PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support"); + PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); + } + + std::cout << std::endl; +} + + static void LoadLuaScripts(ServerContext& context) { @@ -392,13 +599,27 @@ { LOG(WARNING) << "Orthanc has started"; +#if ORTHANC_PLUGINS_ENABLED == 1 + if (context.HasPlugins()) + { + context.GetPlugins().SignalOrthancStarted(); + } +#endif + context.GetLua().Execute("Initialize"); - Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag()); - bool restart = restApi.ResetRequestReceivedFlag(); + Toolbox::ServerBarrier(restApi.LeaveBarrierFlag()); + bool restart = restApi.IsResetRequestReceived(); context.GetLua().Execute("Finalize"); +#if ORTHANC_PLUGINS_ENABLED == 1 + if (context.HasPlugins()) + { + context.GetPlugins().SignalOrthancStopped(); + } +#endif + if (restart) { LOG(WARNING) << "Reset request received, restarting Orthanc"; @@ -413,7 +634,8 @@ static bool StartHttpServer(ServerContext& context, - OrthancRestApi& restApi) + OrthancRestApi& restApi, + OrthancPlugins* plugins) { if (!Configuration::GetGlobalBoolParameter("HttpServerEnabled", true)) { @@ -421,6 +643,9 @@ return WaitForExit(context, restApi); } + MyHttpExceptionFormatter exceptionFormatter(Configuration::GetGlobalBoolParameter("HttpDescribeErrors", true), plugins); + + // HTTP server MyIncomingHttpRequestFilter httpFilter(context); MongooseServer httpServer; @@ -428,8 +653,8 @@ httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false)); httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false)); httpServer.SetHttpCompressionEnabled(Configuration::GetGlobalBoolParameter("HttpCompressionEnabled", true)); - httpServer.SetDescribeErrorsEnabled(Configuration::GetGlobalBoolParameter("HttpDescribeErrors", true)); httpServer.SetIncomingHttpRequestFilter(httpFilter); + httpServer.SetHttpExceptionFormatter(exceptionFormatter); httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false)); Configuration::SetupRegisteredUsers(httpServer); @@ -461,12 +686,13 @@ static bool StartDicomServer(ServerContext& context, - OrthancRestApi& restApi) + OrthancRestApi& restApi, + OrthancPlugins* plugins) { if (!Configuration::GetGlobalBoolParameter("DicomServerEnabled", true)) { LOG(WARNING) << "The DICOM server is disabled"; - return StartHttpServer(context, restApi); + return StartHttpServer(context, restApi, plugins); } MyDicomServerFactory serverFactory(context); @@ -485,13 +711,28 @@ dicomServer.Start(); LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber(); - bool restart = StartHttpServer(context, restApi); + bool restart; + ErrorCode error = ErrorCode_Success; + + try + { + restart = StartHttpServer(context, restApi, plugins); + } + catch (OrthancException& e) + { + error = e.GetErrorCode(); + } dicomServer.Stop(); LOG(WARNING) << " DICOM server has stopped"; serverFactory.Done(); + if (error != ErrorCode_Success) + { + throw OrthancException(error); + } + return restart; } @@ -522,7 +763,7 @@ OrthancRestApi restApi(context); context.GetHttpHandler().Register(restApi, true); - return StartDicomServer(context, restApi); + return StartDicomServer(context, restApi, plugins); } @@ -530,22 +771,29 @@ IStorageArea& storageArea, bool allowDatabaseUpgrade) { - // Upgrade the database, if needed + // Upgrade the schema of the database, if needed unsigned int currentVersion = database.GetDatabaseVersion(); if (currentVersion == ORTHANC_DATABASE_VERSION) { return true; } + 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."; + return false; + } + if (!allowDatabaseUpgrade) { - LOG(ERROR) << "The database must be upgraded from version " + LOG(ERROR) << "The database schema must be upgraded from version " << currentVersion << " to " << ORTHANC_DATABASE_VERSION << ": Please run Orthanc with the \"--upgrade\" command-line option"; return false; } - LOG(WARNING) << "Upgrading the database from version " + LOG(WARNING) << "Upgrading the database from schema version " << currentVersion << " to " << ORTHANC_DATABASE_VERSION; database.Upgrade(ORTHANC_DATABASE_VERSION, storageArea); @@ -553,7 +801,7 @@ currentVersion = database.GetDatabaseVersion(); if (ORTHANC_DATABASE_VERSION != currentVersion) { - LOG(ERROR) << "The database was not properly updated, it is still at version " << currentVersion; + LOG(ERROR) << "The database schema was not properly upgraded, it is still at version " << currentVersion; throw OrthancException(ErrorCode_InternalError); } @@ -563,14 +811,8 @@ static bool ConfigureServerContext(IDatabaseWrapper& database, IStorageArea& storageArea, - OrthancPlugins *plugins, - bool allowDatabaseUpgrade) + OrthancPlugins *plugins) { - if (!UpgradeDatabase(database, storageArea, allowDatabaseUpgrade)) - { - return false; - } - ServerContext context(database, storageArea); HttpClient::SetDefaultTimeout(Configuration::GetGlobalIntegerParameter("HttpTimeout", 0)); @@ -606,7 +848,18 @@ } #endif - bool restart = ConfigureHttpHandler(context, plugins); + bool restart; + ErrorCode error = ErrorCode_Success; + + try + { + restart = ConfigureHttpHandler(context, plugins); + } + catch (OrthancException& e) + { + error = e.GetErrorCode(); + } + context.Stop(); #if ORTHANC_PLUGINS_ENABLED == 1 @@ -616,10 +869,35 @@ } #endif + if (error != ErrorCode_Success) + { + throw OrthancException(error); + } + return restart; } +static bool ConfigureDatabase(IDatabaseWrapper& database, + IStorageArea& storageArea, + OrthancPlugins *plugins, + bool allowDatabaseUpgrade) +{ + database.Open(); + + if (!UpgradeDatabase(database, storageArea, allowDatabaseUpgrade)) + { + return false; + } + + bool success = ConfigureServerContext(database, storageArea, plugins); + + database.Close(); + + return success; +} + + static bool ConfigurePlugins(int argc, char* argv[], bool allowDatabaseUpgrade) @@ -657,14 +935,14 @@ assert(database != NULL); assert(storage.get() != NULL); - return ConfigureServerContext(*database, *storage, &plugins, allowDatabaseUpgrade); + return ConfigureDatabase(*database, *storage, &plugins, allowDatabaseUpgrade); #elif ORTHANC_PLUGINS_ENABLED == 0 // The plugins are disabled databasePtr.reset(Configuration::CreateDatabaseWrapper()); storage.reset(Configuration::CreateStorageArea()); - return ConfigureServerContext(*databasePtr, *storage, NULL, allowDatabaseUpgrade); + return ConfigureDatabase(*databasePtr, *storage, NULL, allowDatabaseUpgrade); #else # error The macro ORTHANC_PLUGINS_ENABLED must be set to 0 or 1 @@ -685,34 +963,61 @@ Logging::Initialize(); bool allowDatabaseUpgrade = false; + const char* configurationFile = NULL; + + + /** + * Parse the command-line options. + **/ for (int i = 1; i < argc; i++) { - if (std::string(argv[i]) == "--help") + std::string argument(argv[i]); + + if (argument.empty()) + { + // Ignore empty arguments + } + else if (argument[0] != '-') + { + if (configurationFile != NULL) + { + LOG(ERROR) << "More than one configuration path were provided on the command line, aborting"; + return -1; + } + else + { + // Use the first argument that does not start with a "-" as + // the configuration file + configurationFile = argv[i]; + } + } + else if (argument == "--errors") + { + PrintErrors(argv[0]); + return 0; + } + else if (argument == "--help") { PrintHelp(argv[0]); return 0; } - - if (std::string(argv[i]) == "--version") + else if (argument == "--version") { PrintVersion(argv[0]); return 0; } - - if (std::string(argv[i]) == "--verbose") + else if (argument == "--verbose") { Logging::EnableInfoLevel(true); } - - if (std::string(argv[i]) == "--trace") + else if (argument == "--trace") { Logging::EnableTraceLevel(true); } - - if (boost::starts_with(argv[i], "--logdir=")) + else if (boost::starts_with(argument, "--logdir=")) { - std::string directory = std::string(argv[i]).substr(9); + std::string directory = argument.substr(9); try { @@ -720,17 +1025,16 @@ } catch (OrthancException&) { - fprintf(stderr, "The directory where to store the log files (%s) is inexistent, aborting.\n", directory.c_str()); + LOG(ERROR) << "The directory where to store the log files (" + << directory << ") is inexistent, aborting."; return -1; } } - - if (std::string(argv[i]) == "--upgrade") + else if (argument == "--upgrade") { allowDatabaseUpgrade = true; } - - if (boost::starts_with(argv[i], "--config=")) + else if (boost::starts_with(argument, "--config=")) { std::string configurationSample; GetFileResource(configurationSample, EmbeddedResources::CONFIGURATION_SAMPLE); @@ -740,24 +1044,20 @@ boost::replace_all(configurationSample, "\n", "\r\n"); #endif - std::string target = std::string(argv[i]).substr(9); - std::ofstream f(target.c_str()); - f << configurationSample; - f.close(); + std::string target = argument.substr(9); + Toolbox::WriteFile(configurationSample, target); return 0; } + else + { + LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument; + } } - const char* configurationFile = NULL; - for (int i = 1; i < argc; i++) - { - // Use the first argument that does not start with a "-" as - // the configuration file - if (argv[i][0] != '-') - { - configurationFile = argv[i]; - } - } + + /** + * Launch Orthanc. + **/ LOG(WARNING) << "Orthanc version: " << ORTHANC_VERSION; @@ -781,8 +1081,20 @@ } catch (const OrthancException& e) { - LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "]"; + LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")"; +#if defined(_WIN32) + if (e.GetErrorCode() >= ErrorCode_START_PLUGINS) + { + status = static_cast(ErrorCode_Plugin); + } + else + { + status = static_cast(e.GetErrorCode()); + } + +#else status = -1; +#endif } catch (const std::exception& e) { diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -46,50 +46,6 @@ namespace Orthanc { - static OrthancPluginResourceType Convert(ResourceType type) - { - switch (type) - { - case ResourceType_Patient: - return OrthancPluginResourceType_Patient; - - case ResourceType_Study: - return OrthancPluginResourceType_Study; - - case ResourceType_Series: - return OrthancPluginResourceType_Series; - - case ResourceType_Instance: - return OrthancPluginResourceType_Instance; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - static ResourceType Convert(OrthancPluginResourceType type) - { - switch (type) - { - case OrthancPluginResourceType_Patient: - return ResourceType_Patient; - - case OrthancPluginResourceType_Study: - return ResourceType_Study; - - case OrthancPluginResourceType_Series: - return ResourceType_Series; - - case OrthancPluginResourceType_Instance: - return ResourceType_Instance; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - static FileInfo Convert(const OrthancPluginAttachment& attachment) { return FileInfo(attachment.uuid, @@ -102,6 +58,16 @@ } + void OrthancPluginDatabase::CheckSuccess(OrthancPluginErrorCode code) + { + if (code != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + void OrthancPluginDatabase::ResetAnswers() { type_ = _OrthancPluginDatabaseAnswerType_None; @@ -194,11 +160,13 @@ OrthancPluginDatabase::OrthancPluginDatabase(SharedLibrary& library, + PluginsErrorDictionary& errorDictionary, const OrthancPluginDatabaseBackend& backend, const OrthancPluginDatabaseExtensions* extensions, size_t extensionsSize, void *payload) : library_(library), + errorDictionary_(errorDictionary), type_(_OrthancPluginDatabaseAnswerType_None), backend_(backend), payload_(payload), @@ -232,46 +200,26 @@ tmp.compressedSize = attachment.GetCompressedSize(); tmp.compressedHash = attachment.GetCompressedMD5().c_str(); - OrthancPluginErrorCode error = backend_.addAttachment(payload_, id, &tmp); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.addAttachment(payload_, id, &tmp)); } void OrthancPluginDatabase::AttachChild(int64_t parent, int64_t child) { - OrthancPluginErrorCode error = backend_.attachChild(payload_, parent, child); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.attachChild(payload_, parent, child)); } void OrthancPluginDatabase::ClearChanges() { - OrthancPluginErrorCode error = backend_.clearChanges(payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.clearChanges(payload_)); } void OrthancPluginDatabase::ClearExportedResources() { - OrthancPluginErrorCode error = backend_.clearExportedResources(payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.clearExportedResources(payload_)); } @@ -279,14 +227,7 @@ ResourceType type) { int64_t id; - - OrthancPluginErrorCode error = backend_.createResource(&id, payload_, publicId.c_str(), Convert(type)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.createResource(&id, payload_, publicId.c_str(), Plugins::Convert(type))); return id; } @@ -294,35 +235,20 @@ void OrthancPluginDatabase::DeleteAttachment(int64_t id, FileContentType attachment) { - OrthancPluginErrorCode error = backend_.deleteAttachment(payload_, id, static_cast(attachment)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.deleteAttachment(payload_, id, static_cast(attachment))); } void OrthancPluginDatabase::DeleteMetadata(int64_t id, MetadataType type) { - OrthancPluginErrorCode error = backend_.deleteMetadata(payload_, id, static_cast(type)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.deleteMetadata(payload_, id, static_cast(type))); } void OrthancPluginDatabase::DeleteResource(int64_t id) { - OrthancPluginErrorCode error = backend_.deleteResource(payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.deleteResource(payload_, id)); } @@ -348,18 +274,26 @@ } + void OrthancPluginDatabase::GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + if (extensions_.getAllInternalIds == NULL) + { + LOG(ERROR) << "The database plugin does not implement the GetAllInternalIds primitive"; + throw OrthancException(ErrorCode_DatabasePlugin); + } + + ResetAnswers(); + CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType))); + ForwardAnswers(target); + } + + void OrthancPluginDatabase::GetAllPublicIds(std::list& target, ResourceType resourceType) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.getAllPublicIds(GetContext(), payload_, Convert(resourceType)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getAllPublicIds(GetContext(), payload_, Plugins::Convert(resourceType))); ForwardAnswers(target); } @@ -373,15 +307,8 @@ { // This extension is available since Orthanc 0.9.4 ResetAnswers(); - - OrthancPluginErrorCode error = extensions_.getAllPublicIdsWithLimit - (GetContext(), payload_, Convert(resourceType), since, limit); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(extensions_.getAllPublicIdsWithLimit + (GetContext(), payload_, Plugins::Convert(resourceType), since, limit)); ForwardAnswers(target); } else @@ -428,12 +355,7 @@ answerDone_ = &done; done = false; - OrthancPluginErrorCode error = backend_.getChanges(GetContext(), payload_, since, maxResults); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.getChanges(GetContext(), payload_, since, maxResults)); } @@ -441,14 +363,7 @@ int64_t id) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.getChildrenInternalId(GetContext(), payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getChildrenInternalId(GetContext(), payload_, id)); ForwardAnswers(target); } @@ -457,14 +372,7 @@ int64_t id) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.getChildrenPublicId(GetContext(), payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getChildrenPublicId(GetContext(), payload_, id)); ForwardAnswers(target); } @@ -479,12 +387,7 @@ answerDone_ = &done; done = false; - OrthancPluginErrorCode error = backend_.getExportedResources(GetContext(), payload_, since, maxResults); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.getExportedResources(GetContext(), payload_, since, maxResults)); } @@ -496,12 +399,7 @@ answerChanges_ = ⌖ answerDone_ = &ignored; - OrthancPluginErrorCode error = backend_.getLastChange(GetContext(), payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.getLastChange(GetContext(), payload_)); } @@ -513,12 +411,7 @@ answerExportedResources_ = ⌖ answerDone_ = &ignored; - OrthancPluginErrorCode error = backend_.getLastExportedResource(GetContext(), payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.getLastExportedResource(GetContext(), payload_)); } @@ -528,12 +421,7 @@ ResetAnswers(); answerDicomMap_ = ↦ - OrthancPluginErrorCode error = backend_.getMainDicomTags(GetContext(), payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.getMainDicomTags(GetContext(), payload_, id)); } @@ -542,13 +430,8 @@ ResetAnswers(); std::string s; - OrthancPluginErrorCode error = backend_.getPublicId(GetContext(), payload_, resourceId); + CheckSuccess(backend_.getPublicId(GetContext(), payload_, resourceId)); - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - if (!ForwardSingleAnswer(s)) { throw OrthancException(ErrorCode_DatabasePlugin); @@ -561,14 +444,7 @@ uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType) { uint64_t count; - - OrthancPluginErrorCode error = backend_.getResourceCount(&count, payload_, Convert(resourceType)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getResourceCount(&count, payload_, Plugins::Convert(resourceType))); return count; } @@ -576,29 +452,15 @@ ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId) { OrthancPluginResourceType type; - - OrthancPluginErrorCode error = backend_.getResourceType(&type, payload_, resourceId); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - - return Convert(type); + CheckSuccess(backend_.getResourceType(&type, payload_, resourceId)); + return Plugins::Convert(type); } uint64_t OrthancPluginDatabase::GetTotalCompressedSize() { uint64_t size; - - OrthancPluginErrorCode error = backend_.getTotalCompressedSize(&size, payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getTotalCompressedSize(&size, payload_)); return size; } @@ -606,14 +468,7 @@ uint64_t OrthancPluginDatabase::GetTotalUncompressedSize() { uint64_t size; - - OrthancPluginErrorCode error = backend_.getTotalUncompressedSize(&size, payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.getTotalUncompressedSize(&size, payload_)); return size; } @@ -621,14 +476,7 @@ bool OrthancPluginDatabase::IsExistingResource(int64_t internalId) { int32_t existing; - - OrthancPluginErrorCode error = backend_.isExistingResource(&existing, payload_, internalId); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.isExistingResource(&existing, payload_, internalId)); return (existing != 0); } @@ -636,14 +484,7 @@ bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId) { int32_t isProtected; - - OrthancPluginErrorCode error = backend_.isProtectedPatient(&isProtected, payload_, internalId); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.isProtectedPatient(&isProtected, payload_, internalId)); return (isProtected != 0); } @@ -652,13 +493,7 @@ int64_t id) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.listAvailableMetadata(GetContext(), payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id)); if (type_ != _OrthancPluginDatabaseAnswerType_None && type_ != _OrthancPluginDatabaseAnswerType_Int32) @@ -684,12 +519,7 @@ { ResetAnswers(); - OrthancPluginErrorCode error = backend_.listAvailableAttachments(GetContext(), payload_, id); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.listAvailableAttachments(GetContext(), payload_, id)); if (type_ != _OrthancPluginDatabaseAnswerType_None && type_ != _OrthancPluginDatabaseAnswerType_Int32) @@ -716,16 +546,11 @@ OrthancPluginChange tmp; tmp.seq = change.GetSeq(); tmp.changeType = static_cast(change.GetChangeType()); - tmp.resourceType = Convert(change.GetResourceType()); + tmp.resourceType = Plugins::Convert(change.GetResourceType()); tmp.publicId = change.GetPublicId().c_str(); tmp.date = change.GetDate().c_str(); - OrthancPluginErrorCode error = backend_.logChange(payload_, &tmp); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.logChange(payload_, &tmp)); } @@ -733,7 +558,7 @@ { OrthancPluginExportedResource tmp; tmp.seq = resource.GetSeq(); - tmp.resourceType = Convert(resource.GetResourceType()); + tmp.resourceType = Plugins::Convert(resource.GetResourceType()); tmp.publicId = resource.GetPublicId().c_str(); tmp.modality = resource.GetModality().c_str(); tmp.date = resource.GetDate().c_str(); @@ -742,12 +567,7 @@ tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str(); tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str(); - OrthancPluginErrorCode error = backend_.logExportedResource(payload_, &tmp); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.logExportedResource(payload_, &tmp)); } @@ -757,13 +577,8 @@ { ResetAnswers(); - OrthancPluginErrorCode error = backend_.lookupAttachment - (GetContext(), payload_, id, static_cast(contentType)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.lookupAttachment + (GetContext(), payload_, id, static_cast(contentType))); if (type_ == _OrthancPluginDatabaseAnswerType_None) { @@ -787,53 +602,34 @@ { ResetAnswers(); - OrthancPluginErrorCode error = backend_.lookupGlobalProperty - (GetContext(), payload_, static_cast(property)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.lookupGlobalProperty + (GetContext(), payload_, static_cast(property))); return ForwardSingleAnswer(target); } - void OrthancPluginDatabase::LookupIdentifier(std::list& target, + void OrthancPluginDatabase::LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value) { - ResetAnswers(); + if (extensions_.lookupIdentifier3 == NULL) + { + LOG(ERROR) << "The database plugin does not implement the LookupIdentifier3 primitive"; + throw OrthancException(ErrorCode_DatabasePlugin); + } OrthancPluginDicomTag tmp; tmp.group = tag.GetGroup(); tmp.element = tag.GetElement(); tmp.value = value.c_str(); - OrthancPluginErrorCode error = backend_.lookupIdentifier(GetContext(), payload_, &tmp); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - - ForwardAnswers(target); - } - - - void OrthancPluginDatabase::LookupIdentifier(std::list& target, - const std::string& value) - { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.lookupIdentifier2(GetContext(), payload_, value.c_str()); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - - ForwardAnswers(target); + CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level), + &tmp, Plugins::Convert(type))); + ForwardAnswers(result); } @@ -842,14 +638,7 @@ MetadataType type) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.lookupMetadata(GetContext(), payload_, id, static_cast(type)); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.lookupMetadata(GetContext(), payload_, id, static_cast(type))); return ForwardSingleAnswer(target); } @@ -858,14 +647,7 @@ int64_t resourceId) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.lookupParent(GetContext(), payload_, resourceId); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.lookupParent(GetContext(), payload_, resourceId)); return ForwardSingleAnswer(parentId); } @@ -876,12 +658,7 @@ { ResetAnswers(); - OrthancPluginErrorCode error = backend_.lookupResource(GetContext(), payload_, publicId.c_str()); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.lookupResource(GetContext(), payload_, publicId.c_str())); if (type_ == _OrthancPluginDatabaseAnswerType_None) { @@ -904,14 +681,7 @@ bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.selectPatientToRecycle(GetContext(), payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.selectPatientToRecycle(GetContext(), payload_)); return ForwardSingleAnswer(internalId); } @@ -920,14 +690,7 @@ int64_t patientIdToAvoid) { ResetAnswers(); - - OrthancPluginErrorCode error = backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid)); return ForwardSingleAnswer(internalId); } @@ -935,13 +698,20 @@ void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property, const std::string& value) { - OrthancPluginErrorCode error = backend_.setGlobalProperty - (payload_, static_cast(property), value.c_str()); + CheckSuccess(backend_.setGlobalProperty + (payload_, static_cast(property), value.c_str())); + } + - if (error != OrthancPluginErrorCode_Success) + void OrthancPluginDatabase::ClearMainDicomTags(int64_t id) + { + if (extensions_.clearMainDicomTags == NULL) { - throw OrthancException(Plugins::Convert(error)); + LOG(ERROR) << "Your custom index plugin does not implement the ClearMainDicomTags() extension"; + throw OrthancException(ErrorCode_DatabasePlugin); } + + CheckSuccess(extensions_.clearMainDicomTags(payload_, id)); } @@ -954,21 +724,20 @@ tmp.element = tag.GetElement(); tmp.value = value.c_str(); - OrthancPluginErrorCode error; + CheckSuccess(backend_.setMainDicomTag(payload_, id, &tmp)); + } + - if (tag.IsIdentifier()) - { - error = backend_.setIdentifierTag(payload_, id, &tmp); - } - else - { - error = backend_.setMainDicomTag(payload_, id, &tmp); - } + void OrthancPluginDatabase::SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.setIdentifierTag(payload_, id, &tmp)); } @@ -976,25 +745,15 @@ MetadataType type, const std::string& value) { - OrthancPluginErrorCode error = backend_.setMetadata - (payload_, id, static_cast(type), value.c_str()); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.setMetadata + (payload_, id, static_cast(type), value.c_str())); } void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, bool isProtected) { - OrthancPluginErrorCode error = backend_.setProtectedPatient(payload_, internalId, isProtected); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.setProtectedPatient(payload_, internalId, isProtected)); } @@ -1003,50 +762,47 @@ private: const OrthancPluginDatabaseBackend& backend_; void* payload_; + PluginsErrorDictionary& errorDictionary_; + + void CheckSuccess(OrthancPluginErrorCode code) + { + if (code != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } public: Transaction(const OrthancPluginDatabaseBackend& backend, - void* payload) : + void* payload, + PluginsErrorDictionary& errorDictionary) : backend_(backend), - payload_(payload) + payload_(payload), + errorDictionary_(errorDictionary) { } virtual void Begin() { - OrthancPluginErrorCode error = backend_.startTransaction(payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.startTransaction(payload_)); } virtual void Rollback() { - OrthancPluginErrorCode error = backend_.rollbackTransaction(payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.rollbackTransaction(payload_)); } virtual void Commit() { - OrthancPluginErrorCode error = backend_.commitTransaction(payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } + CheckSuccess(backend_.commitTransaction(payload_)); } }; SQLite::ITransaction* OrthancPluginDatabase::StartTransaction() { - return new Transaction(backend_, payload_); + return new Transaction(backend_, payload_, errorDictionary_); } @@ -1065,14 +821,14 @@ case _OrthancPluginDatabaseAnswerType_RemainingAncestor: { - ResourceType type = Convert(static_cast(answer.valueInt32)); + ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); listener.SignalRemainingAncestor(type, answer.valueString); break; } case _OrthancPluginDatabaseAnswerType_DeletedResource: { - ResourceType type = Convert(static_cast(answer.valueInt32)); + ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); ServerIndexChange change(ChangeType_Deleted, type, answer.valueString); listener.SignalChange(change); break; @@ -1089,13 +845,7 @@ if (extensions_.getDatabaseVersion != NULL) { uint32_t version; - OrthancPluginErrorCode error = extensions_.getDatabaseVersion(&version, payload_); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - + CheckSuccess(extensions_.getDatabaseVersion(&version, payload_)); return version; } else @@ -1113,13 +863,22 @@ { if (extensions_.upgradeDatabase != NULL) { - OrthancPluginErrorCode error = extensions_.upgradeDatabase( + Transaction transaction(backend_, payload_, errorDictionary_); + transaction.Begin(); + + OrthancPluginErrorCode code = extensions_.upgradeDatabase( payload_, targetVersion, reinterpret_cast(&storageArea)); - if (error != OrthancPluginErrorCode_Success) + if (code == OrthancPluginErrorCode_Success) { - throw OrthancException(Plugins::Convert(error)); + transaction.Commit(); + } + else + { + transaction.Rollback(); + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); } } } @@ -1210,7 +969,7 @@ case _OrthancPluginDatabaseAnswerType_Resource: { OrthancPluginResourceType type = static_cast(answer.valueInt32); - answerResources_.push_back(std::make_pair(answer.valueInt64, Convert(type))); + answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type))); break; } @@ -1270,7 +1029,7 @@ answerChanges_->push_back (ServerIndexChange(change.seq, static_cast(change.changeType), - Convert(change.resourceType), + Plugins::Convert(change.resourceType), change.publicId, change.date)); } @@ -1296,7 +1055,7 @@ assert(answerExportedResources_ != NULL); answerExportedResources_->push_back (ExportedResource(exported.seq, - Convert(exported.resourceType), + Plugins::Convert(exported.resourceType), exported.publicId, exported.modality, exported.date, diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/OrthancPluginDatabase.h --- a/Plugins/Engine/OrthancPluginDatabase.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/OrthancPluginDatabase.h Wed Nov 18 10:16:21 2015 +0100 @@ -36,6 +36,7 @@ #include "../../OrthancServer/IDatabaseWrapper.h" #include "../Include/orthanc/OrthancCDatabasePlugin.h" +#include "PluginsErrorDictionary.h" #include "SharedLibrary.h" namespace Orthanc @@ -48,6 +49,7 @@ typedef std::pair AnswerResource; SharedLibrary& library_; + PluginsErrorDictionary& errorDictionary_; _OrthancPluginDatabaseAnswerType type_; OrthancPluginDatabaseBackend backend_; OrthancPluginDatabaseExtensions extensions_; @@ -70,6 +72,8 @@ return reinterpret_cast(this); } + void CheckSuccess(OrthancPluginErrorCode code); + void ResetAnswers(); void ForwardAnswers(std::list& target); @@ -82,11 +86,22 @@ public: OrthancPluginDatabase(SharedLibrary& library, + PluginsErrorDictionary& errorDictionary, const OrthancPluginDatabaseBackend& backend, const OrthancPluginDatabaseExtensions* extensions, size_t extensionsSize, void *payload); + virtual void Open() + { + CheckSuccess(backend_.open(payload_)); + } + + virtual void Close() + { + CheckSuccess(backend_.close(payload_)); + } + const SharedLibrary& GetSharedLibrary() const { return library_; @@ -125,6 +140,9 @@ virtual void GetAllMetadata(std::map& target, int64_t id); + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType); + virtual void GetAllPublicIds(std::list& target, ResourceType resourceType); @@ -188,11 +206,10 @@ virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property); - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, - const std::string& value); - - virtual void LookupIdentifier(std::list& target, + IdentifierConstraintType type, const std::string& value); virtual bool LookupMetadata(std::string& target, @@ -214,10 +231,16 @@ virtual void SetGlobalProperty(GlobalProperty property, const std::string& value); + virtual void ClearMainDicomTags(int64_t id); + virtual void SetMainDicomTag(int64_t id, const DicomTag& tag, const std::string& value); + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value); + virtual void SetMetadata(int64_t id, MetadataType type, const std::string& value); diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -43,6 +43,7 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../../Core/Toolbox.h" +#include "../../OrthancServer/FromDcmtkBridge.h" #include "../../OrthancServer/OrthancInitialization.h" #include "../../OrthancServer/ServerContext.h" #include "../../OrthancServer/ServerToolbox.h" @@ -66,6 +67,7 @@ { private: _OrthancPluginRegisterStorageArea callbacks_; + PluginsErrorDictionary& errorDictionary_; void Free(void* buffer) const { @@ -76,7 +78,10 @@ } public: - PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks) : callbacks_(callbacks) + PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, + PluginsErrorDictionary& errorDictionary) : + callbacks_(callbacks), + errorDictionary_(errorDictionary) { } @@ -91,7 +96,8 @@ if (error != OrthancPluginErrorCode_Success) { - throw OrthancException(Plugins::Convert(error)); + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast(error)); } } @@ -108,7 +114,8 @@ if (error != OrthancPluginErrorCode_Success) { - throw OrthancException(Plugins::Convert(error)); + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast(error)); } try @@ -118,7 +125,7 @@ catch (...) { Free(buffer); - throw; + throw OrthancException(ErrorCode_NotEnoughMemory); } if (size > 0) @@ -138,7 +145,8 @@ if (error != OrthancPluginErrorCode_Success) { - throw OrthancException(Plugins::Convert(error)); + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast(error)); } } }; @@ -149,12 +157,15 @@ private: SharedLibrary& sharedLibrary_; _OrthancPluginRegisterStorageArea callbacks_; + PluginsErrorDictionary& errorDictionary_; public: StorageAreaFactory(SharedLibrary& sharedLibrary, - const _OrthancPluginRegisterStorageArea& callbacks) : + const _OrthancPluginRegisterStorageArea& callbacks, + PluginsErrorDictionary& errorDictionary) : sharedLibrary_(sharedLibrary), - callbacks_(callbacks) + callbacks_(callbacks), + errorDictionary_(errorDictionary) { } @@ -165,7 +176,7 @@ IStorageArea* Create() const { - return new PluginStorageArea(callbacks_); + return new PluginStorageArea(callbacks_, errorDictionary_); } }; } @@ -242,6 +253,7 @@ int argc_; char** argv_; std::auto_ptr database_; + PluginsErrorDictionary dictionary_; PImpl() : context_(NULL), @@ -286,7 +298,17 @@ sizeof(int32_t) != sizeof(OrthancPluginChangeType) || sizeof(int32_t) != sizeof(OrthancPluginImageFormat) || sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || - sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || + sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || + sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || + sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || + static_cast(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast(DicomToJsonFlags_IncludeBinary) || + static_cast(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast(DicomToJsonFlags_IncludePrivateTags) || + static_cast(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast(DicomToJsonFlags_IncludeUnknownTags) || + static_cast(OrthancPluginDicomToJsonFlags_IncludePixelData) != static_cast(DicomToJsonFlags_IncludePixelData) || + static_cast(OrthancPluginDicomToJsonFlags_ConvertBinaryToNull) != static_cast(DicomToJsonFlags_ConvertBinaryToNull) || + static_cast(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast(DicomToJsonFlags_ConvertBinaryToAscii)) { /* Sanity check of the compiler */ throw OrthancException(ErrorCode_Plugin); @@ -463,7 +485,8 @@ } else { - throw OrthancException(Plugins::Convert(error)); + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast(error)); } } @@ -484,7 +507,30 @@ if (error != OrthancPluginErrorCode_Success) { - throw OrthancException(Plugins::Convert(error)); + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast(error)); + } + } + } + + + + void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resource) + { + boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_); + + for (std::list::const_iterator + callback = pimpl_->onChangeCallbacks_.begin(); + callback != pimpl_->onChangeCallbacks_.end(); ++callback) + { + OrthancPluginErrorCode error = (*callback) (changeType, resourceType, resource); + + if (error != OrthancPluginErrorCode_Success) + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast(error)); } } } @@ -493,22 +539,9 @@ void OrthancPlugins::SignalChange(const ServerIndexChange& change) { - boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_); - - for (std::list::const_iterator - callback = pimpl_->onChangeCallbacks_.begin(); - callback != pimpl_->onChangeCallbacks_.end(); ++callback) - { - OrthancPluginErrorCode error = (*callback) - (Plugins::Convert(change.GetChangeType()), - Plugins::Convert(change.GetResourceType()), - change.GetPublicId().c_str()); - - if (error != OrthancPluginErrorCode_Success) - { - throw OrthancException(Plugins::Convert(error)); - } - } + SignalChangeInternal(Plugins::Convert(change.GetChangeType()), + Plugins::Convert(change.GetResourceType()), + change.GetPublicId().c_str()); } @@ -743,8 +776,7 @@ { if (!pimpl_->context_) { - LOG(ERROR) << "Plugin trying to call the database during its initialization"; - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_DatabaseNotInitialized); } } @@ -787,6 +819,36 @@ } + void OrthancPlugins::RestApiGet2(const void* parameters) + { + const _OrthancPluginRestApiGet2& p = + *reinterpret_cast(parameters); + + LOG(INFO) << "Plugin making REST GET call on URI " << p.uri + << (p.afterPlugins ? " (after plugins)" : " (built-in API)"); + + IHttpHandler::Arguments headers; + + for (uint32_t i = 0; i < p.headersCount; i++) + { + headers[p.headersKeys[i]] = p.headersValues[i]; + } + + CheckContextAvailable(); + IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins); + + std::string result; + if (HttpToolbox::SimpleGet(result, handler, RequestOrigin_Plugins, p.uri, headers)) + { + CopyToMemoryBuffer(*p.target, result); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + void OrthancPlugins::RestApiPostPut(bool isPost, const void* parameters, bool afterPlugins) @@ -880,7 +942,7 @@ CheckContextAvailable(); std::list result; - pimpl_->context_->GetIndex().LookupIdentifier(result, tag, p.argument, level); + pimpl_->context_->GetIndex().LookupIdentifierExact(result, level, tag, p.argument); if (result.size() == 1) { @@ -991,7 +1053,7 @@ else { Json::Value simplified; - SimplifyTags(simplified, instance.GetJson()); + Toolbox::SimplifyTags(simplified, instance.GetJson()); s = writer.write(simplified); } @@ -1214,15 +1276,78 @@ font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b); } + + + void OrthancPlugins::ApplyDicomToJson(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginDicomToJson& p = + *reinterpret_cast(parameters); + + std::auto_ptr dicom; + + if (service == _OrthancPluginService_DicomBufferToJson) + { + dicom.reset(new ParsedDicomFile(p.buffer, p.size)); + } + else + { + if (p.instanceId == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + std::string content; + pimpl_->context_->ReadFile(content, p.instanceId, FileContentType_Dicom); + dicom.reset(new ParsedDicomFile(content)); + } + + Json::Value json; + dicom->ToJson(json, Plugins::Convert(p.format), + static_cast(p.flags), p.maxStringLength); + + Json::FastWriter writer; + *p.result = CopyString(writer.write(json)); + } + void OrthancPlugins::DatabaseAnswer(const void* parameters) + { + const _OrthancPluginDatabaseAnswer& p = + *reinterpret_cast(parameters); + + if (pimpl_->database_.get() != NULL) + { + pimpl_->database_->AnswerReceived(p); + } + else + { + LOG(ERROR) << "Cannot invoke this service without a custom database back-end"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + bool OrthancPlugins::InvokeService(SharedLibrary& plugin, _OrthancPluginService service, const void* parameters) { VLOG(1) << "Calling service " << service << " from plugin " << plugin.GetPath(); - boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + if (service == _OrthancPluginService_DatabaseAnswer) + { + // This case solves a deadlock at (*) reported by James Webster + // on 2015-10-27 that was present in versions of Orthanc <= + // 0.9.4 and related to database plugins implementing a custom + // index. The problem was that locking the database is already + // ensured by the "ServerIndex" class if the invoked service is + // "DatabaseAnswer". + DatabaseAnswer(parameters); + return true; + } + + + std::auto_ptr lock; // (*) switch (service) { @@ -1300,6 +1425,10 @@ RestApiGet(parameters, true); return true; + case _OrthancPluginService_RestApiGet2: + RestApiGet2(parameters); + return true; + case _OrthancPluginService_RestApiPost: RestApiPostPut(true, parameters, false); return true; @@ -1378,7 +1507,7 @@ if (pimpl_->storageArea_.get() == NULL) { - pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p)); + pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); } else { @@ -1457,7 +1586,8 @@ if (pimpl_->database_.get() == NULL) { - pimpl_->database_.reset(new OrthancPluginDatabase(plugin, *p.backend, NULL, 0, p.payload)); + pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), + *p.backend, NULL, 0, p.payload)); } else { @@ -1478,7 +1608,8 @@ if (pimpl_->database_.get() == NULL) { - pimpl_->database_.reset(new OrthancPluginDatabase(plugin, *p.backend, p.extensions, + pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), + *p.backend, p.extensions, p.extensionsSize, p.payload)); } else @@ -1492,21 +1623,7 @@ } case _OrthancPluginService_DatabaseAnswer: - { - const _OrthancPluginDatabaseAnswer& p = - *reinterpret_cast(parameters); - - if (pimpl_->database_.get() != NULL) - { - pimpl_->database_->AnswerReceived(p); - return true; - } - else - { - LOG(ERROR) << "Cannot invoke this service without a custom database back-end"; - throw OrthancException(ErrorCode_BadRequest); - } - } + throw OrthancException(ErrorCode_InternalError); // Implemented before locking (*) case _OrthancPluginService_GetExpectedDatabaseVersion: { @@ -1674,6 +1791,46 @@ return true; } + case _OrthancPluginService_RegisterErrorCode: + { + const _OrthancPluginRegisterErrorCode& p = + *reinterpret_cast(parameters); + *(p.target) = pimpl_->dictionary_.Register(plugin, p.code, p.httpStatus, p.message); + return true; + } + + case _OrthancPluginService_RegisterDictionaryTag: + { + const _OrthancPluginRegisterDictionaryTag& p = + *reinterpret_cast(parameters); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(p.group, p.element), + Plugins::Convert(p.vr), p.name, + p.minMultiplicity, p.maxMultiplicity); + return true; + } + + case _OrthancPluginService_ReconstructMainDicomTags: + { + const _OrthancPluginReconstructMainDicomTags& p = + *reinterpret_cast(parameters); + + if (pimpl_->database_.get() == NULL) + { + LOG(ERROR) << "The service ReconstructMainDicomTags can only be invoked by custom database plugins"; + throw OrthancException(ErrorCode_DatabasePlugin); + } + + IStorageArea& storage = *reinterpret_cast(p.storageArea); + Toolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level)); + + return true; + } + + case _OrthancPluginService_DicomBufferToJson: + case _OrthancPluginService_DicomInstanceToJson: + ApplyDicomToJson(service, parameters); + return true; + default: { // This service is unknown to the Orthanc plugin engine @@ -1785,4 +1942,10 @@ { return pimpl_->manager_; } + + + PluginsErrorDictionary& OrthancPlugins::GetErrorDictionary() + { + return pimpl_->dictionary_; + } } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Wed Nov 18 10:16:21 2015 +0100 @@ -32,6 +32,8 @@ #pragma once +#include "PluginsErrorDictionary.h" + #if ORTHANC_PLUGINS_ENABLED != 1 #include @@ -89,6 +91,8 @@ void RestApiGet(const void* parameters, bool afterPlugins); + void RestApiGet2(const void* parameters); + void RestApiPostPut(bool isPost, const void* parameters, bool afterPlugins); @@ -125,6 +129,15 @@ void DrawText(const void* parameters); + void DatabaseAnswer(const void* parameters); + + void ApplyDicomToJson(_OrthancPluginService service, + const void* parameters); + + void SignalChangeInternal(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resource); + public: OrthancPlugins(); @@ -179,6 +192,18 @@ PluginsManager& GetManager(); const PluginsManager& GetManager() const; + + PluginsErrorDictionary& GetErrorDictionary(); + + void SignalOrthancStarted() + { + SignalChangeInternal(OrthancPluginChangeType_OrthancStarted, OrthancPluginResourceType_None, NULL); + } + + void SignalOrthancStopped() + { + SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL); + } }; } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsEnumerations.cpp --- a/Plugins/Engine/PluginsEnumerations.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/PluginsEnumerations.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -44,277 +44,6 @@ { namespace Plugins { - ErrorCode Convert(OrthancPluginErrorCode error) - { - switch (error) - { - case OrthancPluginErrorCode_InternalError: - return ErrorCode_InternalError; - - case OrthancPluginErrorCode_Success: - return ErrorCode_Success; - - case OrthancPluginErrorCode_Plugin: - return ErrorCode_Plugin; - - case OrthancPluginErrorCode_NotImplemented: - return ErrorCode_NotImplemented; - - case OrthancPluginErrorCode_ParameterOutOfRange: - return ErrorCode_ParameterOutOfRange; - - case OrthancPluginErrorCode_NotEnoughMemory: - return ErrorCode_NotEnoughMemory; - - case OrthancPluginErrorCode_BadParameterType: - return ErrorCode_BadParameterType; - - case OrthancPluginErrorCode_BadSequenceOfCalls: - return ErrorCode_BadSequenceOfCalls; - - case OrthancPluginErrorCode_InexistentItem: - return ErrorCode_InexistentItem; - - case OrthancPluginErrorCode_BadRequest: - return ErrorCode_BadRequest; - - case OrthancPluginErrorCode_NetworkProtocol: - return ErrorCode_NetworkProtocol; - - case OrthancPluginErrorCode_SystemCommand: - return ErrorCode_SystemCommand; - - case OrthancPluginErrorCode_Database: - return ErrorCode_Database; - - case OrthancPluginErrorCode_UriSyntax: - return ErrorCode_UriSyntax; - - case OrthancPluginErrorCode_InexistentFile: - return ErrorCode_InexistentFile; - - case OrthancPluginErrorCode_CannotWriteFile: - return ErrorCode_CannotWriteFile; - - case OrthancPluginErrorCode_BadFileFormat: - return ErrorCode_BadFileFormat; - - case OrthancPluginErrorCode_Timeout: - return ErrorCode_Timeout; - - case OrthancPluginErrorCode_UnknownResource: - return ErrorCode_UnknownResource; - - case OrthancPluginErrorCode_IncompatibleDatabaseVersion: - return ErrorCode_IncompatibleDatabaseVersion; - - case OrthancPluginErrorCode_FullStorage: - return ErrorCode_FullStorage; - - case OrthancPluginErrorCode_CorruptedFile: - return ErrorCode_CorruptedFile; - - case OrthancPluginErrorCode_InexistentTag: - return ErrorCode_InexistentTag; - - case OrthancPluginErrorCode_ReadOnly: - return ErrorCode_ReadOnly; - - case OrthancPluginErrorCode_IncompatibleImageFormat: - return ErrorCode_IncompatibleImageFormat; - - case OrthancPluginErrorCode_IncompatibleImageSize: - return ErrorCode_IncompatibleImageSize; - - case OrthancPluginErrorCode_SharedLibrary: - return ErrorCode_SharedLibrary; - - case OrthancPluginErrorCode_UnknownPluginService: - return ErrorCode_UnknownPluginService; - - case OrthancPluginErrorCode_UnknownDicomTag: - return ErrorCode_UnknownDicomTag; - - case OrthancPluginErrorCode_BadJson: - return ErrorCode_BadJson; - - case OrthancPluginErrorCode_Unauthorized: - return ErrorCode_Unauthorized; - - case OrthancPluginErrorCode_BadFont: - return ErrorCode_BadFont; - - case OrthancPluginErrorCode_SQLiteNotOpened: - return ErrorCode_SQLiteNotOpened; - - case OrthancPluginErrorCode_SQLiteAlreadyOpened: - return ErrorCode_SQLiteAlreadyOpened; - - case OrthancPluginErrorCode_SQLiteCannotOpen: - return ErrorCode_SQLiteCannotOpen; - - case OrthancPluginErrorCode_SQLiteStatementAlreadyUsed: - return ErrorCode_SQLiteStatementAlreadyUsed; - - case OrthancPluginErrorCode_SQLiteExecute: - return ErrorCode_SQLiteExecute; - - case OrthancPluginErrorCode_SQLiteRollbackWithoutTransaction: - return ErrorCode_SQLiteRollbackWithoutTransaction; - - case OrthancPluginErrorCode_SQLiteCommitWithoutTransaction: - return ErrorCode_SQLiteCommitWithoutTransaction; - - case OrthancPluginErrorCode_SQLiteRegisterFunction: - return ErrorCode_SQLiteRegisterFunction; - - case OrthancPluginErrorCode_SQLiteFlush: - return ErrorCode_SQLiteFlush; - - case OrthancPluginErrorCode_SQLiteCannotRun: - return ErrorCode_SQLiteCannotRun; - - case OrthancPluginErrorCode_SQLiteCannotStep: - return ErrorCode_SQLiteCannotStep; - - case OrthancPluginErrorCode_SQLiteBindOutOfRange: - return ErrorCode_SQLiteBindOutOfRange; - - case OrthancPluginErrorCode_SQLitePrepareStatement: - return ErrorCode_SQLitePrepareStatement; - - case OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted: - return ErrorCode_SQLiteTransactionAlreadyStarted; - - case OrthancPluginErrorCode_SQLiteTransactionCommit: - return ErrorCode_SQLiteTransactionCommit; - - case OrthancPluginErrorCode_SQLiteTransactionBegin: - return ErrorCode_SQLiteTransactionBegin; - - case OrthancPluginErrorCode_DirectoryOverFile: - return ErrorCode_DirectoryOverFile; - - case OrthancPluginErrorCode_FileStorageCannotWrite: - return ErrorCode_FileStorageCannotWrite; - - case OrthancPluginErrorCode_DirectoryExpected: - return ErrorCode_DirectoryExpected; - - case OrthancPluginErrorCode_HttpPortInUse: - return ErrorCode_HttpPortInUse; - - case OrthancPluginErrorCode_DicomPortInUse: - return ErrorCode_DicomPortInUse; - - case OrthancPluginErrorCode_BadHttpStatusInRest: - return ErrorCode_BadHttpStatusInRest; - - case OrthancPluginErrorCode_RegularFileExpected: - return ErrorCode_RegularFileExpected; - - case OrthancPluginErrorCode_PathToExecutable: - return ErrorCode_PathToExecutable; - - case OrthancPluginErrorCode_MakeDirectory: - return ErrorCode_MakeDirectory; - - case OrthancPluginErrorCode_BadApplicationEntityTitle: - return ErrorCode_BadApplicationEntityTitle; - - case OrthancPluginErrorCode_NoCFindHandler: - return ErrorCode_NoCFindHandler; - - case OrthancPluginErrorCode_NoCMoveHandler: - return ErrorCode_NoCMoveHandler; - - case OrthancPluginErrorCode_NoCStoreHandler: - return ErrorCode_NoCStoreHandler; - - case OrthancPluginErrorCode_NoApplicationEntityFilter: - return ErrorCode_NoApplicationEntityFilter; - - case OrthancPluginErrorCode_NoSopClassOrInstance: - return ErrorCode_NoSopClassOrInstance; - - case OrthancPluginErrorCode_NoPresentationContext: - return ErrorCode_NoPresentationContext; - - case OrthancPluginErrorCode_DicomFindUnavailable: - return ErrorCode_DicomFindUnavailable; - - case OrthancPluginErrorCode_DicomMoveUnavailable: - return ErrorCode_DicomMoveUnavailable; - - case OrthancPluginErrorCode_CannotStoreInstance: - return ErrorCode_CannotStoreInstance; - - case OrthancPluginErrorCode_CreateDicomNotString: - return ErrorCode_CreateDicomNotString; - - case OrthancPluginErrorCode_CreateDicomOverrideTag: - return ErrorCode_CreateDicomOverrideTag; - - case OrthancPluginErrorCode_CreateDicomUseContent: - return ErrorCode_CreateDicomUseContent; - - case OrthancPluginErrorCode_CreateDicomNoPayload: - return ErrorCode_CreateDicomNoPayload; - - case OrthancPluginErrorCode_CreateDicomUseDataUriScheme: - return ErrorCode_CreateDicomUseDataUriScheme; - - case OrthancPluginErrorCode_CreateDicomBadParent: - return ErrorCode_CreateDicomBadParent; - - case OrthancPluginErrorCode_CreateDicomParentIsInstance: - return ErrorCode_CreateDicomParentIsInstance; - - case OrthancPluginErrorCode_CreateDicomParentEncoding: - return ErrorCode_CreateDicomParentEncoding; - - case OrthancPluginErrorCode_UnknownModality: - return ErrorCode_UnknownModality; - - case OrthancPluginErrorCode_BadJobOrdering: - return ErrorCode_BadJobOrdering; - - case OrthancPluginErrorCode_JsonToLuaTable: - return ErrorCode_JsonToLuaTable; - - case OrthancPluginErrorCode_CannotCreateLua: - return ErrorCode_CannotCreateLua; - - case OrthancPluginErrorCode_CannotExecuteLua: - return ErrorCode_CannotExecuteLua; - - case OrthancPluginErrorCode_LuaAlreadyExecuted: - return ErrorCode_LuaAlreadyExecuted; - - case OrthancPluginErrorCode_LuaBadOutput: - return ErrorCode_LuaBadOutput; - - case OrthancPluginErrorCode_NotLuaPredicate: - return ErrorCode_NotLuaPredicate; - - case OrthancPluginErrorCode_LuaReturnsNoString: - return ErrorCode_LuaReturnsNoString; - - case OrthancPluginErrorCode_StorageAreaAlreadyRegistered: - return ErrorCode_StorageAreaAlreadyRegistered; - - case OrthancPluginErrorCode_DatabaseBackendAlreadyRegistered: - return ErrorCode_DatabaseBackendAlreadyRegistered; - - case OrthancPluginErrorCode_DatabasePlugin: - return ErrorCode_DatabasePlugin; - - default: - return ErrorCode_Plugin; - } - } - - OrthancPluginResourceType Convert(ResourceType type) { switch (type) @@ -337,6 +66,28 @@ } + ResourceType Convert(OrthancPluginResourceType type) + { + switch (type) + { + case OrthancPluginResourceType_Patient: + return ResourceType_Patient; + + case OrthancPluginResourceType_Study: + return ResourceType_Study; + + case OrthancPluginResourceType_Series: + return ResourceType_Series; + + case OrthancPluginResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginChangeType Convert(ChangeType type) { switch (type) @@ -371,6 +122,12 @@ case ChangeType_StableStudy: return OrthancPluginChangeType_StableStudy; + case ChangeType_UpdatedAttachment: + return OrthancPluginChangeType_UpdatedAttachment; + + case ChangeType_UpdatedMetadata: + return OrthancPluginChangeType_UpdatedMetadata; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -457,5 +214,161 @@ return FileContentType_Unknown; } } + + + DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format) + { + switch (format) + { + case OrthancPluginDicomToJsonFormat_Full: + return DicomToJsonFormat_Full; + + case OrthancPluginDicomToJsonFormat_Short: + return DicomToJsonFormat_Short; + + case OrthancPluginDicomToJsonFormat_Simple: + return DicomToJsonFormat_Simple; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint) + { + switch (constraint) + { + case IdentifierConstraintType_Equal: + return OrthancPluginIdentifierConstraint_Equal; + + case IdentifierConstraintType_GreaterOrEqual: + return OrthancPluginIdentifierConstraint_GreaterOrEqual; + + case IdentifierConstraintType_SmallerOrEqual: + return OrthancPluginIdentifierConstraint_SmallerOrEqual; + + case IdentifierConstraintType_Wildcard: + return OrthancPluginIdentifierConstraint_Wildcard; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint) + { + switch (constraint) + { + case OrthancPluginIdentifierConstraint_Equal: + return IdentifierConstraintType_Equal; + + case OrthancPluginIdentifierConstraint_GreaterOrEqual: + return IdentifierConstraintType_GreaterOrEqual; + + case OrthancPluginIdentifierConstraint_SmallerOrEqual: + return IdentifierConstraintType_SmallerOrEqual; + + case OrthancPluginIdentifierConstraint_Wildcard: + return IdentifierConstraintType_Wildcard; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + +#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 + DcmEVR Convert(OrthancPluginValueRepresentation vr) + { + switch (vr) + { + case OrthancPluginValueRepresentation_AE: + return EVR_AE; + + case OrthancPluginValueRepresentation_AS: + return EVR_AS; + + case OrthancPluginValueRepresentation_AT: + return EVR_AT; + + case OrthancPluginValueRepresentation_CS: + return EVR_CS; + + case OrthancPluginValueRepresentation_DA: + return EVR_DA; + + case OrthancPluginValueRepresentation_DS: + return EVR_DS; + + case OrthancPluginValueRepresentation_DT: + return EVR_DT; + + case OrthancPluginValueRepresentation_FD: + return EVR_FD; + + case OrthancPluginValueRepresentation_FL: + return EVR_FL; + + case OrthancPluginValueRepresentation_IS: + return EVR_IS; + + case OrthancPluginValueRepresentation_LO: + return EVR_LO; + + case OrthancPluginValueRepresentation_LT: + return EVR_LT; + + case OrthancPluginValueRepresentation_OB: + return EVR_OB; + + case OrthancPluginValueRepresentation_OF: + return EVR_OF; + + case OrthancPluginValueRepresentation_OW: + return EVR_OW; + + case OrthancPluginValueRepresentation_PN: + return EVR_PN; + + case OrthancPluginValueRepresentation_SH: + return EVR_SH; + + case OrthancPluginValueRepresentation_SL: + return EVR_SL; + + case OrthancPluginValueRepresentation_SQ: + return EVR_SQ; + + case OrthancPluginValueRepresentation_SS: + return EVR_SS; + + case OrthancPluginValueRepresentation_ST: + return EVR_ST; + + case OrthancPluginValueRepresentation_TM: + return EVR_TM; + + case OrthancPluginValueRepresentation_UI: + return EVR_UI; + + case OrthancPluginValueRepresentation_UL: + return EVR_UL; + + case OrthancPluginValueRepresentation_UN: + return EVR_UN; + + case OrthancPluginValueRepresentation_US: + return EVR_US; + + case OrthancPluginValueRepresentation_UT: + return EVR_UT; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif } } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsEnumerations.h --- a/Plugins/Engine/PluginsEnumerations.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/PluginsEnumerations.h Wed Nov 18 10:16:21 2015 +0100 @@ -37,13 +37,17 @@ #include "../Include/orthanc/OrthancCPlugin.h" #include "../../OrthancServer/ServerEnumerations.h" +#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 +#include +#endif + namespace Orthanc { namespace Plugins { - ErrorCode Convert(OrthancPluginErrorCode error); + OrthancPluginResourceType Convert(ResourceType type); - OrthancPluginResourceType Convert(ResourceType type); + ResourceType Convert(OrthancPluginResourceType type); OrthancPluginChangeType Convert(ChangeType type); @@ -54,6 +58,16 @@ OrthancPluginContentType Convert(FileContentType type); FileContentType Convert(OrthancPluginContentType type); + + DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format); + + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint); + + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint); + +#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 + DcmEVR Convert(OrthancPluginValueRepresentation vr); +#endif } } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsErrorDictionary.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/PluginsErrorDictionary.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,138 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../OrthancServer/PrecompiledHeadersServer.h" +#include "PluginsErrorDictionary.h" + +#if ORTHANC_PLUGINS_ENABLED != 1 +#error The plugin support is disabled +#endif + + + +#include "PluginsEnumerations.h" +#include "PluginsManager.h" +#include "../../Core/Logging.h" + +#include + + +namespace Orthanc +{ + PluginsErrorDictionary::PluginsErrorDictionary() : + pos_(ErrorCode_START_PLUGINS) + { + } + + + PluginsErrorDictionary::~PluginsErrorDictionary() + { + for (Errors::iterator it = errors_.begin(); it != errors_.end(); ++it) + { + delete it->second; + } + } + + + OrthancPluginErrorCode PluginsErrorDictionary::Register(SharedLibrary& library, + int32_t pluginCode, + uint16_t httpStatus, + const char* message) + { + std::auto_ptr error(new Error); + + error->pluginName_ = PluginsManager::GetPluginName(library); + error->pluginCode_ = pluginCode; + error->message_ = message; + error->httpStatus_ = static_cast(httpStatus); + + OrthancPluginErrorCode code; + + { + boost::mutex::scoped_lock lock(mutex_); + errors_[pos_] = error.release(); + code = static_cast(pos_); + pos_ += 1; + } + + return code; + } + + + void PluginsErrorDictionary::LogError(ErrorCode code, + bool ignoreBuiltinErrors) + { + if (code >= ErrorCode_START_PLUGINS) + { + boost::mutex::scoped_lock lock(mutex_); + Errors::const_iterator error = errors_.find(static_cast(code)); + + if (error != errors_.end()) + { + LOG(ERROR) << "Error code " << error->second->pluginCode_ + << " inside plugin \"" << error->second->pluginName_ + << "\": " << error->second->message_; + return; + } + } + + if (!ignoreBuiltinErrors) + { + LOG(ERROR) << "Exception inside the plugin engine: " + << EnumerationToString(code); + } + } + + + bool PluginsErrorDictionary::Format(Json::Value& message, /* out */ + HttpStatus& httpStatus, /* out */ + const OrthancException& exception) + { + if (exception.GetErrorCode() >= ErrorCode_START_PLUGINS) + { + boost::mutex::scoped_lock lock(mutex_); + Errors::const_iterator error = errors_.find(static_cast(exception.GetErrorCode())); + + if (error != errors_.end()) + { + httpStatus = error->second->httpStatus_; + message["PluginName"] = error->second->pluginName_; + message["PluginCode"] = error->second->pluginCode_; + message["Message"] = error->second->message_; + + return true; + } + } + + return false; + } +} diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsErrorDictionary.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/PluginsErrorDictionary.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,92 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if ORTHANC_PLUGINS_ENABLED == 1 + +#include "../Include/orthanc/OrthancCPlugin.h" +#include "../../Core/OrthancException.h" +#include "SharedLibrary.h" + +#include +#include +#include +#include +#include + + +namespace Orthanc +{ + class PluginsErrorDictionary : public boost::noncopyable + { + private: + struct Error + { + std::string pluginName_; + int32_t pluginCode_; + HttpStatus httpStatus_; + std::string message_; + }; + + typedef std::map Errors; + + boost::mutex mutex_; + int32_t pos_; + Errors errors_; + + public: + PluginsErrorDictionary(); + + ~PluginsErrorDictionary(); + + OrthancPluginErrorCode Register(SharedLibrary& library, + int32_t pluginCode, + uint16_t httpStatus, + const char* message); + + void LogError(ErrorCode code, + bool ignoreBuiltinErrors); + + void LogError(OrthancPluginErrorCode code, + bool ignoreBuiltinErrors) + { + LogError(static_cast(code), ignoreBuiltinErrors); + } + + bool Format(Json::Value& message, /* out */ + HttpStatus& httpStatus, /* out */ + const OrthancException& exception); + }; +} + +#endif diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsManager.cpp --- a/Plugins/Engine/PluginsManager.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/PluginsManager.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -191,7 +191,7 @@ catch (OrthancException& e) { // This service provider has failed - LOG(ERROR) << "Exception while invoking a plugin service: " << e.What(); + LOG(ERROR) << "Exception while invoking plugin service " << service << ": " << e.What(); return static_cast(e.GetErrorCode()); } } @@ -346,4 +346,10 @@ return it->second->GetVersion(); } } + + + std::string PluginsManager::GetPluginName(SharedLibrary& library) + { + return CallGetName(library); + } } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Engine/PluginsManager.h --- a/Plugins/Engine/PluginsManager.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Engine/PluginsManager.h Wed Nov 18 10:16:21 2015 +0100 @@ -112,6 +112,8 @@ bool HasPlugin(const std::string& name) const; const std::string& GetPluginVersion(const std::string& name) const; + + static std::string GetPluginName(SharedLibrary& library); }; } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Include/orthanc/OrthancCDatabasePlugin.h --- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Wed Nov 18 10:16:21 2015 +0100 @@ -522,7 +522,9 @@ void* payload, int32_t property); - /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + /* Use "OrthancPluginDatabaseExtensions::lookupIdentifier3" + instead of this function as of Orthanc 0.9.5 (db v6), can be set to NULL. + Output: Use OrthancPluginDatabaseAnswerInt64() */ OrthancPluginErrorCode (*lookupIdentifier) ( /* outputs */ OrthancPluginDatabaseContext* context, @@ -530,7 +532,8 @@ void* payload, const OrthancPluginDicomTag* tag); - /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + /* Unused starting with Orthanc 0.9.5 (db v6), can be set to NULL. + Output: Use OrthancPluginDatabaseAnswerInt64() */ OrthancPluginErrorCode (*lookupIdentifier2) ( /* outputs */ OrthancPluginDatabaseContext* context, @@ -655,7 +658,30 @@ void* payload, uint32_t targetVersion, OrthancPluginStorageArea* storageArea); - } OrthancPluginDatabaseExtensions; + + OrthancPluginErrorCode (*clearMainDicomTags) ( + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + OrthancPluginErrorCode (*getAllInternalIds) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + OrthancPluginErrorCode (*lookupIdentifier3) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType, + const OrthancPluginDicomTag* tag, + OrthancPluginIdentifierConstraint constraint); + } OrthancPluginDatabaseExtensions; /*InvokeService(context, _OrthancPluginService_RegisterErrorCode, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + /* There was an error while assigned the error. Use a generic code. */ + return OrthancPluginErrorCode_Plugin; + } + } + + + + typedef struct + { + uint16_t group; + uint16_t element; + OrthancPluginValueRepresentation vr; + const char* name; + uint32_t minMultiplicity; + uint32_t maxMultiplicity; + } _OrthancPluginRegisterDictionaryTag; + + /** + * @brief Register a new tag into the DICOM dictionary. + * + * This function declares a new tag in the dictionary of DICOM tags + * that are known to Orthanc. This function should be used in the + * OrthancPluginInitialize() callback. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag. + * @param element The element of the tag. + * @param vr The value representation of the tag. + * @param name The nickname of the tag. + * @param minMultiplicity The minimum multiplicity of the tag (must be above 0). + * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means + * an arbitrary multiplicity ("n"). + * @return 0 if success, other value if error. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDictionaryTag( + OrthancPluginContext* context, + uint16_t group, + uint16_t element, + OrthancPluginValueRepresentation vr, + const char* name, + uint32_t minMultiplicity, + uint32_t maxMultiplicity) + { + _OrthancPluginRegisterDictionaryTag params; + params.group = group; + params.element = element; + params.vr = vr; + params.name = name; + params.minMultiplicity = minMultiplicity; + params.maxMultiplicity = maxMultiplicity; + + return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, ¶ms); + } + + + + + typedef struct + { + OrthancPluginStorageArea* storageArea; + OrthancPluginResourceType level; + } _OrthancPluginReconstructMainDicomTags; + + /** + * @brief Reconstruct the main DICOM tags. + * + * This function requests the Orthanc core to reconstruct the main + * DICOM tags of all the resources of the given type. This function + * can only be used as a part of the upgrade of a custom database + * back-end + * (cf. OrthancPlugins::IDatabaseBackend::UpgradeDatabase). A + * database transaction will be automatically setup. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storageArea The storage area. + * @param level The type of the resources of interest. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginReconstructMainDicomTags( + OrthancPluginContext* context, + OrthancPluginStorageArea* storageArea, + OrthancPluginResourceType level) + { + _OrthancPluginReconstructMainDicomTags params; + params.level = level; + params.storageArea = storageArea; + + return context->InvokeService(context, _OrthancPluginService_ReconstructMainDicomTags, ¶ms); + } + + + typedef struct + { + char** result; + const char* instanceId; + const char* buffer; + uint32_t size; + OrthancPluginDicomToJsonFormat format; + OrthancPluginDicomToJsonFlags flags; + uint32_t maxStringLength; + } _OrthancPluginDicomToJson; + + + /** + * @brief Format a DICOM memory buffer as a JSON string. + * + * This function takes as input a memory buffer containing a DICOM + * file, and outputs a JSON string representing the tags of this + * DICOM file. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM file. + * @param size The size of the memory buffer. + * @param format The output format. + * @param flags The output flags. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + * @see OrthancPluginDicomInstanceToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson( + OrthancPluginContext* context, + const char* buffer, + uint32_t size, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result; + + _OrthancPluginDicomToJson params; + memset(¶ms, 0, sizeof(params)); + params.result = &result; + params.buffer = buffer; + params.size = size; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_DicomBufferToJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Format a DICOM instance as a JSON string. + * + * This function formats a DICOM instance that is stored in Orthanc, + * and outputs a JSON string representing the tags of this DICOM + * instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The Orthanc identifier of the instance. + * @param format The output format. + * @param flags The output flags. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + * @see OrthancPluginDicomInstanceToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson( + OrthancPluginContext* context, + const char* instanceId, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result; + + _OrthancPluginDicomToJson params; + memset(¶ms, 0, sizeof(params)); + params.result = &result; + params.instanceId = instanceId; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_DicomInstanceToJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + int32_t afterPlugins; + } _OrthancPluginRestApiGet2; + + /** + * @brief Make a GET call to the Orthanc REST API, with custom HTTP headers. + * + * Make a GET call to the Orthanc REST API with extended + * parameters. The result to the query is stored into a newly + * allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers. + * @param headersValues Array containing the values of the HTTP headers. + * @param afterPlugins If 0, the built-in API of Orthanc is used. + * If 1, the API is tainted by the plugins. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginRestApiGet, OrthancPluginRestApiGetAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet2( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + int32_t afterPlugins) + { + _OrthancPluginRestApiGet2 params; + params.target = target; + params.uri = uri; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.afterPlugins = afterPlugins; + + return context->InvokeService(context, _OrthancPluginService_RestApiGet2, ¶ms); + } + #ifdef __cplusplus } #endif diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Include/orthanc/OrthancCppDatabasePlugin.h --- a/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h Wed Nov 18 10:16:21 2015 +0100 @@ -71,7 +71,7 @@ OrthancPluginErrorCode code_; public: - DatabaseException() : code_(OrthancPluginErrorCode_Plugin) + DatabaseException() : code_(OrthancPluginErrorCode_DatabasePlugin) { } @@ -122,6 +122,11 @@ { } + OrthancPluginContext* GetContext() + { + return context_; + } + void LogError(const std::string& message) { OrthancPluginLogError(context_, message.c_str()); @@ -334,6 +339,9 @@ virtual void DeleteResource(int64_t id) = 0; + virtual void GetAllInternalIds(std::list& target, + OrthancPluginResourceType resourceType) = 0; + virtual void GetAllPublicIds(std::list& target, OrthancPluginResourceType resourceType) = 0; @@ -398,18 +406,11 @@ virtual bool LookupGlobalProperty(std::string& target /*out*/, int32_t property) = 0; - /** - * "Identifiers" are necessarily one of the following tags: - * PatientID (0x0010, 0x0020), StudyInstanceUID (0x0020, 0x000d), - * SeriesInstanceUID (0x0020, 0x000e), SOPInstanceUID (0x0008, - * 0x0018) or AccessionNumber (0x0008, 0x0050). - **/ virtual void LookupIdentifier(std::list& target /*out*/, + OrthancPluginResourceType resourceType, uint16_t group, uint16_t element, - const char* value) = 0; - - virtual void LookupIdentifier(std::list& target /*out*/, + OrthancPluginIdentifierConstraint constraint, const char* value) = 0; virtual bool LookupMetadata(std::string& target /*out*/, @@ -456,8 +457,15 @@ virtual uint32_t GetDatabaseVersion() = 0; + /** + * Upgrade the database to the specified version of the database + * schema. The upgrade script is allowed to make calls to + * OrthancPluginReconstructMainDicomTags(). + **/ virtual void UpgradeDatabase(uint32_t targetVersion, OrthancPluginStorageArea* storageArea) = 0; + + virtual void ClearMainDicomTags(int64_t internalId) = 0; }; @@ -501,7 +509,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -525,7 +533,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -547,7 +555,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -569,7 +577,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -594,7 +602,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -618,7 +626,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -642,7 +650,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -665,7 +673,40 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; + } + catch (DatabaseException& e) + { + return e.GetErrorCode(); + } + } + + + static OrthancPluginErrorCode GetAllInternalIds(OrthancPluginDatabaseContext* context, + void* payload, + OrthancPluginResourceType resourceType) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list target; + backend->GetAllInternalIds(target, resourceType); + + for (std::list::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, *it); + } + + return OrthancPluginErrorCode_Success; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -699,7 +740,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -735,7 +776,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -768,7 +809,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -801,7 +842,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -835,7 +876,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -867,7 +908,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -890,7 +931,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -913,7 +954,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -937,7 +978,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -965,7 +1006,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -989,7 +1030,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1013,7 +1054,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1036,7 +1077,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1059,7 +1100,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1083,7 +1124,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1107,7 +1148,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1141,7 +1182,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1175,7 +1216,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1198,7 +1239,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1221,7 +1262,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1246,7 +1287,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1277,7 +1318,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1286,9 +1327,11 @@ } - static OrthancPluginErrorCode LookupIdentifier(OrthancPluginDatabaseContext* context, - void* payload, - const OrthancPluginDicomTag* tag) + static OrthancPluginErrorCode LookupIdentifier3(OrthancPluginDatabaseContext* context, + void* payload, + OrthancPluginResourceType resourceType, + const OrthancPluginDicomTag* tag, + OrthancPluginIdentifierConstraint constraint) { IDatabaseBackend* backend = reinterpret_cast(payload); backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); @@ -1296,7 +1339,7 @@ try { std::list target; - backend->LookupIdentifier(target, tag->group, tag->element, tag->value); + backend->LookupIdentifier(target, resourceType, tag->group, tag->element, constraint, tag->value); for (std::list::const_iterator it = target.begin(); it != target.end(); ++it) @@ -1310,40 +1353,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; - } - catch (DatabaseException& e) - { - return e.GetErrorCode(); - } - } - - - static OrthancPluginErrorCode LookupIdentifier2(OrthancPluginDatabaseContext* context, - void* payload, - const char* value) - { - IDatabaseBackend* backend = reinterpret_cast(payload); - backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); - - try - { - std::list target; - backend->LookupIdentifier(target, value); - - for (std::list::const_iterator - it = target.begin(); it != target.end(); ++it) - { - OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, - backend->GetOutput().database_, *it); - } - - return OrthancPluginErrorCode_Success; - } - catch (std::runtime_error& e) - { - LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1374,7 +1384,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1404,7 +1414,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1436,7 +1446,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1465,7 +1475,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1495,7 +1505,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1519,7 +1529,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1543,7 +1553,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1567,7 +1577,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1592,7 +1602,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1616,7 +1626,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1638,7 +1648,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1660,7 +1670,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1682,7 +1692,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1704,7 +1714,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1726,7 +1736,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1748,7 +1758,7 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1771,7 +1781,29 @@ catch (std::runtime_error& e) { LogError(backend, e); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_DatabasePlugin; + } + catch (DatabaseException& e) + { + return e.GetErrorCode(); + } + } + + + static OrthancPluginErrorCode ClearMainDicomTags(void* payload, + int64_t internalId) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + + try + { + backend->ClearMainDicomTags(internalId); + return OrthancPluginErrorCode_Success; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return OrthancPluginErrorCode_DatabasePlugin; } catch (DatabaseException& e) { @@ -1826,8 +1858,8 @@ params.logExportedResource = LogExportedResource; params.lookupAttachment = LookupAttachment; params.lookupGlobalProperty = LookupGlobalProperty; - params.lookupIdentifier = LookupIdentifier; - params.lookupIdentifier2 = LookupIdentifier2; + params.lookupIdentifier = NULL; // Unused starting with Orthanc 0.9.5 (db v6) + params.lookupIdentifier2 = NULL; // Unused starting with Orthanc 0.9.5 (db v6) params.lookupMetadata = LookupMetadata; params.lookupParent = LookupParent; params.lookupResource = LookupResource; @@ -1847,6 +1879,9 @@ extensions.getAllPublicIdsWithLimit = GetAllPublicIdsWithLimit; extensions.getDatabaseVersion = GetDatabaseVersion; extensions.upgradeDatabase = UpgradeDatabase; + extensions.clearMainDicomTags = ClearMainDicomTags; + extensions.getAllInternalIds = GetAllInternalIds; // New in Orthanc 0.9.5 (db v6) + extensions.lookupIdentifier3 = LookupIdentifier3; // New in Orthanc 0.9.5 (db v6) OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackendV2(context, ¶ms, &extensions, &backend); if (!context) diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/Basic/Plugin.c --- a/Plugins/Samples/Basic/Plugin.c Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/Basic/Plugin.c Wed Nov 18 10:16:21 2015 +0100 @@ -25,6 +25,8 @@ static OrthancPluginContext* context = NULL; +static OrthancPluginErrorCode customError; + ORTHANC_PLUGINS_API int32_t Callback1(OrthancPluginRestOutput* output, const char* url, @@ -192,9 +194,7 @@ } else { - printf("ICI1\n"); error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]); - printf("ICI2\n"); } if (error) @@ -401,6 +401,11 @@ 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); + return 0; } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/Common/ExportedSymbols.list --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/Common/ExportedSymbols.list Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,7 @@ +# This is the list of the symbols that must be exported by Orthanc +# plugins, if targeting OS X + +_OrthancPluginInitialize +_OrthancPluginFinalize +_OrthancPluginGetName +_OrthancPluginGetVersion diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/Common/OrthancPlugins.cmake --- a/Plugins/Samples/Common/OrthancPlugins.cmake Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/Common/OrthancPlugins.cmake Wed Nov 18 10:16:21 2015 +0100 @@ -1,28 +1,14 @@ +set(ORTHANC_ROOT ${SAMPLES_ROOT}/../..) include(CheckIncludeFiles) +include(CheckIncludeFileCXX) include(CheckLibraryExists) - - -if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - link_libraries(uuid) - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pthread") -elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - link_libraries(rpcrt4 ws2_32 secur32) - if (CMAKE_COMPILER_IS_GNUCXX) - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libgcc -static-libstdc++") - endif() - - CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD) - if (HAVE_WIN_PTHREAD) - # This line is necessary to compile with recent versions of MinGW, - # otherwise "libwinpthread-1.dll" is not statically linked. - SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic") - endif() -endif () +include(FindPythonInterp) +include(${ORTHANC_ROOT}/Resources/CMake/AutoGeneratedCode.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake) if (CMAKE_COMPILER_IS_GNUCXX) - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${SAMPLES_ROOT}/Common/VersionScript.map -Wl,--no-undefined") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic") endif() @@ -37,6 +23,7 @@ include_directories(${SAMPLES_ROOT}/../Include/) + if (MSVC) include_directories(${SAMPLES_ROOT}/../../Resources/ThirdParty/VisualStudio/) endif() diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/DatabasePlugin/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/DatabasePlugin/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 2.8) + +project(SampleDatabasePlugin) + +# Parameters of the build +SET(SAMPLE_DATABASE_VERSION "0.0" CACHE STRING "Version of the plugin") +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") +SET(STANDALONE_BUILD ON) + +# Advanced parameters to fine-tune linking against system libraries +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite") + +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) + +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/SQLiteConfiguration.cmake) + +EmbedResources( + --system-exception # Use "std::runtime_error" instead of "OrthancException" for embedded resources + PREPARE_DATABASE ${ORTHANC_ROOT}/OrthancServer/PrepareDatabase.sql + ) + +message("Setting the version of the plugin to ${SAMPLE_DATABASE_VERSION}") + +add_definitions( + -DORTHANC_SQLITE_STANDALONE=1 + -DORTHANC_ENABLE_LOGGING=0 + -DORTHANC_ENABLE_BASE64=0 + -DORTHANC_ENABLE_MD5=0 + -DORTHANC_ENABLE_DCMTK=0 + -DORTHANC_PLUGINS_ENABLED=1 + -DSAMPLE_DATABASE_VERSION="${SAMPLE_DATABASE_VERSION}" + ) + +add_library(SampleDatabase SHARED + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + ${SQLITE_SOURCES} + ${AUTOGENERATED_SOURCES} + + ${ORTHANC_ROOT}/Core/DicomFormat/DicomArray.cpp + ${ORTHANC_ROOT}/Core/DicomFormat/DicomMap.cpp + ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp + ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp + ${ORTHANC_ROOT}/Core/Enumerations.cpp + ${ORTHANC_ROOT}/Core/SQLite/Connection.cpp + ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp + ${ORTHANC_ROOT}/Core/SQLite/Statement.cpp + ${ORTHANC_ROOT}/Core/SQLite/StatementId.cpp + ${ORTHANC_ROOT}/Core/SQLite/StatementReference.cpp + ${ORTHANC_ROOT}/Core/SQLite/Transaction.cpp + ${ORTHANC_ROOT}/Core/Toolbox.cpp + ${ORTHANC_ROOT}/OrthancServer/DatabaseWrapperBase.cpp + ${ORTHANC_ROOT}/Plugins/Engine/PluginsEnumerations.cpp + + Database.cpp + Plugin.cpp + ) + +set_target_properties(SampleDatabase PROPERTIES + VERSION ${SAMPLE_DATABASE_VERSION} + SOVERSION ${SAMPLE_DATABASE_VERSION}) + +install( + TARGETS SampleDatabase + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/DatabasePlugin/Database.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/DatabasePlugin/Database.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,562 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "Database.h" + +#include "../../../Core/DicomFormat/DicomArray.h" + +#include +#include + + +namespace Internals +{ + class SignalFileDeleted : public Orthanc::SQLite::IScalarFunction + { + private: + OrthancPlugins::DatabaseBackendOutput& output_; + + public: + SignalFileDeleted(OrthancPlugins::DatabaseBackendOutput& output) : output_(output) + { + } + + virtual const char* GetName() const + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 7; + } + + virtual void Compute(Orthanc::SQLite::FunctionContext& context) + { + std::string uncompressedMD5, compressedMD5; + + if (!context.IsNullValue(5)) + { + uncompressedMD5 = context.GetStringValue(5); + } + + if (!context.IsNullValue(6)) + { + compressedMD5 = context.GetStringValue(6); + } + + output_.SignalDeletedAttachment(context.GetStringValue(0), + context.GetIntValue(1), + context.GetInt64Value(2), + uncompressedMD5, + context.GetIntValue(3), + context.GetInt64Value(4), + compressedMD5); + } + }; + + + class SignalResourceDeleted : public Orthanc::SQLite::IScalarFunction + { + private: + OrthancPlugins::DatabaseBackendOutput& output_; + + public: + SignalResourceDeleted(OrthancPlugins::DatabaseBackendOutput& output) : output_(output) + { + } + + virtual const char* GetName() const + { + return "SignalResourceDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(Orthanc::SQLite::FunctionContext& context) + { + output_.SignalDeletedResource(context.GetStringValue(0), + Orthanc::Plugins::Convert(static_cast(context.GetIntValue(1)))); + } + }; +} + + +class Database::SignalRemainingAncestor : public Orthanc::SQLite::IScalarFunction +{ +private: + bool hasRemainingAncestor_; + std::string remainingPublicId_; + OrthancPluginResourceType remainingType_; + +public: + SignalRemainingAncestor() : + hasRemainingAncestor_(false) + { + } + + void Reset() + { + hasRemainingAncestor_ = false; + } + + virtual const char* GetName() const + { + return "SignalRemainingAncestor"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(Orthanc::SQLite::FunctionContext& context) + { + if (!hasRemainingAncestor_ || + remainingType_ >= context.GetIntValue(1)) + { + hasRemainingAncestor_ = true; + remainingPublicId_ = context.GetStringValue(0); + remainingType_ = Orthanc::Plugins::Convert(static_cast(context.GetIntValue(1))); + } + } + + bool HasRemainingAncestor() const + { + return hasRemainingAncestor_; + } + + const std::string& GetRemainingAncestorId() const + { + assert(hasRemainingAncestor_); + return remainingPublicId_; + } + + OrthancPluginResourceType GetRemainingAncestorType() const + { + assert(hasRemainingAncestor_); + return remainingType_; + } +}; + + + +Database::Database(const std::string& path) : + path_(path), + base_(db_) +{ +} + + +void Database::Open() +{ + db_.Open(path_); + + db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); + + // http://www.sqlite.org/pragma.html + db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); + db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); + db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); + db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); + //db_.Execute("PRAGMA TEMP_STORE=memory"); + + if (!db_.DoesTableExist("GlobalProperties")) + { + std::string query; + Orthanc::EmbeddedResources::GetFileResource(query, Orthanc::EmbeddedResources::PREPARE_DATABASE); + db_.Execute(query); + } + + signalRemainingAncestor_ = new SignalRemainingAncestor; + db_.Register(signalRemainingAncestor_); + db_.Register(new Internals::SignalFileDeleted(GetOutput())); + db_.Register(new Internals::SignalResourceDeleted(GetOutput())); +} + + +void Database::Close() +{ + db_.Close(); +} + + +void Database::AddAttachment(int64_t id, + const OrthancPluginAttachment& attachment) +{ + Orthanc::FileInfo info(attachment.uuid, + static_cast(attachment.contentType), + attachment.uncompressedSize, + attachment.uncompressedHash, + static_cast(attachment.compressionType), + attachment.compressedSize, + attachment.compressedHash); + base_.AddAttachment(id, info); +} + + +void Database::DeleteResource(int64_t id) +{ + signalRemainingAncestor_->Reset(); + + Orthanc::SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); + s.BindInt64(0, id); + s.Run(); + + if (signalRemainingAncestor_->HasRemainingAncestor()) + { + GetOutput().SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorId(), + signalRemainingAncestor_->GetRemainingAncestorType()); + } +} + + +static void Answer(OrthancPlugins::DatabaseBackendOutput& output, + const Orthanc::ServerIndexChange& change) +{ + output.AnswerChange(change.GetSeq(), + change.GetChangeType(), + Orthanc::Plugins::Convert(change.GetResourceType()), + change.GetPublicId(), + change.GetDate()); +} + + +static void Answer(OrthancPlugins::DatabaseBackendOutput& output, + const Orthanc::ExportedResource& resource) +{ + output.AnswerExportedResource(resource.GetSeq(), + Orthanc::Plugins::Convert(resource.GetResourceType()), + resource.GetPublicId(), + resource.GetModality(), + resource.GetDate(), + resource.GetPatientId(), + resource.GetStudyInstanceUid(), + resource.GetSeriesInstanceUid(), + resource.GetSopInstanceUid()); +} + + +void Database::GetChanges(bool& done /*out*/, + int64_t since, + uint32_t maxResults) +{ + typedef std::list Changes; + + Changes changes; + base_.GetChanges(changes, done, since, maxResults); + + for (Changes::const_iterator it = changes.begin(); it != changes.end(); ++it) + { + Answer(GetOutput(), *it); + } +} + + +void Database::GetExportedResources(bool& done /*out*/, + int64_t since, + uint32_t maxResults) +{ + typedef std::list Resources; + + Resources resources; + base_.GetExportedResources(resources, done, since, maxResults); + + for (Resources::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + Answer(GetOutput(), *it); + } +} + + +void Database::GetLastChange() +{ + std::list change; + Orthanc::ErrorCode code = base_.GetLastChange(change); + + if (code != Orthanc::ErrorCode_Success) + { + throw OrthancPlugins::DatabaseException(static_cast(code)); + } + + if (!change.empty()) + { + Answer(GetOutput(), change.front()); + } +} + + +void Database::GetLastExportedResource() +{ + std::list resource; + base_.GetLastExportedResource(resource); + + if (!resource.empty()) + { + Answer(GetOutput(), resource.front()); + } +} + + +void Database::GetMainDicomTags(int64_t id) +{ + Orthanc::DicomMap tags; + base_.GetMainDicomTags(tags, id); + + Orthanc::DicomArray arr(tags); + for (size_t i = 0; i < arr.GetSize(); i++) + { + GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(), + arr.GetElement(i).GetTag().GetElement(), + arr.GetElement(i).GetValue().GetContent()); + } +} + + +std::string Database::GetPublicId(int64_t resourceId) +{ + std::string id; + if (base_.GetPublicId(id, resourceId)) + { + return id; + } + else + { + throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_UnknownResource); + } +} + + +OrthancPluginResourceType Database::GetResourceType(int64_t resourceId) +{ + Orthanc::ResourceType result; + Orthanc::ErrorCode code = base_.GetResourceType(result, resourceId); + + if (code == Orthanc::ErrorCode_Success) + { + return Orthanc::Plugins::Convert(result); + } + else + { + throw OrthancPlugins::DatabaseException(static_cast(code)); + } +} + + + +template +static void ConvertList(std::list& target, + const std::list& source) +{ + for (typename std::list::const_iterator + it = source.begin(); it != source.end(); it++) + { + target.push_back(*it); + } +} + + +void Database::ListAvailableMetadata(std::list& target /*out*/, + int64_t id) +{ + std::list tmp; + base_.ListAvailableMetadata(tmp, id); + ConvertList(target, tmp); +} + + +void Database::ListAvailableAttachments(std::list& target /*out*/, + int64_t id) +{ + std::list tmp; + base_.ListAvailableAttachments(tmp, id); + ConvertList(target, tmp); +} + + +void Database::LogChange(const OrthancPluginChange& change) +{ + int64_t id; + OrthancPluginResourceType type; + if (!LookupResource(id, type, change.publicId) || + type != change.resourceType) + { + throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_DatabasePlugin); + } + + Orthanc::ServerIndexChange tmp(change.seq, + static_cast(change.changeType), + Orthanc::Plugins::Convert(change.resourceType), + change.publicId, + change.date); + + base_.LogChange(id, tmp); +} + + +void Database::LogExportedResource(const OrthancPluginExportedResource& resource) +{ + Orthanc::ExportedResource tmp(resource.seq, + Orthanc::Plugins::Convert(resource.resourceType), + resource.publicId, + resource.modality, + resource.date, + resource.patientId, + resource.studyInstanceUid, + resource.seriesInstanceUid, + resource.sopInstanceUid); + + base_.LogExportedResource(tmp); +} + + +bool Database::LookupAttachment(int64_t id, + int32_t contentType) +{ + Orthanc::FileInfo attachment; + if (base_.LookupAttachment(attachment, id, static_cast(contentType))) + { + GetOutput().AnswerAttachment(attachment.GetUuid(), + attachment.GetContentType(), + attachment.GetUncompressedSize(), + attachment.GetUncompressedMD5(), + attachment.GetCompressionType(), + attachment.GetCompressedSize(), + attachment.GetCompressedMD5()); + return true; + } + else + { + return false; + } +} + + +bool Database::LookupParent(int64_t& parentId /*out*/, + int64_t resourceId) +{ + bool found; + Orthanc::ErrorCode code = base_.LookupParent(found, parentId, resourceId); + + if (code == Orthanc::ErrorCode_Success) + { + return found; + } + else + { + throw OrthancPlugins::DatabaseException(static_cast(code)); + } +} + + +bool Database::LookupResource(int64_t& id /*out*/, + OrthancPluginResourceType& type /*out*/, + const char* publicId) +{ + Orthanc::ResourceType tmp; + if (base_.LookupResource(id, tmp, publicId)) + { + type = Orthanc::Plugins::Convert(tmp); + return true; + } + else + { + return false; + } +} + + +void Database::StartTransaction() +{ + transaction_.reset(new Orthanc::SQLite::Transaction(db_)); + transaction_->Begin(); +} + + +void Database::RollbackTransaction() +{ + transaction_->Rollback(); + transaction_.reset(NULL); +} + + +void Database::CommitTransaction() +{ + transaction_->Commit(); + transaction_.reset(NULL); +} + + +uint32_t Database::GetDatabaseVersion() +{ + std::string version; + + if (!LookupGlobalProperty(version, Orthanc::GlobalProperty_DatabaseSchemaVersion)) + { + throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError); + } + + try + { + return boost::lexical_cast(version); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError); + } +} + + +void Database::UpgradeDatabase(uint32_t targetVersion, + OrthancPluginStorageArea* storageArea) +{ + if (targetVersion == 6) + { + OrthancPluginErrorCode code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, + OrthancPluginResourceType_Study); + if (code == OrthancPluginErrorCode_Success) + { + code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, + OrthancPluginResourceType_Series); + } + + if (code != OrthancPluginErrorCode_Success) + { + throw OrthancPlugins::DatabaseException(code); + } + + base_.SetGlobalProperty(Orthanc::GlobalProperty_DatabaseSchemaVersion, "6"); + } +} diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/DatabasePlugin/Database.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/DatabasePlugin/Database.h Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,284 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + +#include "../../../Core/SQLite/Connection.h" +#include "../../../Core/SQLite/Transaction.h" +#include "../../../OrthancServer/DatabaseWrapperBase.h" +#include "../../Engine/PluginsEnumerations.h" + +#include + +class Database : public OrthancPlugins::IDatabaseBackend +{ +private: + class SignalRemainingAncestor; + + std::string path_; + Orthanc::SQLite::Connection db_; + Orthanc::DatabaseWrapperBase base_; + SignalRemainingAncestor* signalRemainingAncestor_; + + std::auto_ptr transaction_; + +public: + Database(const std::string& path); + + virtual void Open(); + + virtual void Close(); + + virtual void AddAttachment(int64_t id, + const OrthancPluginAttachment& attachment); + + virtual void AttachChild(int64_t parent, + int64_t child) + { + base_.AttachChild(parent, child); + } + + virtual void ClearChanges() + { + db_.Execute("DELETE FROM Changes"); + } + + virtual void ClearExportedResources() + { + db_.Execute("DELETE FROM ExportedResources"); + } + + virtual int64_t CreateResource(const char* publicId, + OrthancPluginResourceType type) + { + return base_.CreateResource(publicId, Orthanc::Plugins::Convert(type)); + } + + virtual void DeleteAttachment(int64_t id, + int32_t attachment) + { + base_.DeleteAttachment(id, static_cast(attachment)); + } + + virtual void DeleteMetadata(int64_t id, + int32_t metadataType) + { + base_.DeleteMetadata(id, static_cast(metadataType)); + } + + virtual void DeleteResource(int64_t id); + + virtual void GetAllInternalIds(std::list& target, + OrthancPluginResourceType resourceType) + { + base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType)); + } + + virtual void GetAllPublicIds(std::list& target, + OrthancPluginResourceType resourceType) + { + base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType)); + } + + virtual void GetAllPublicIds(std::list& target, + OrthancPluginResourceType resourceType, + uint64_t since, + uint64_t limit) + { + base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType), since, limit); + } + + virtual void GetChanges(bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetChildrenInternalId(std::list& target /*out*/, + int64_t id) + { + base_.GetChildrenInternalId(target, id); + } + + virtual void GetChildrenPublicId(std::list& target /*out*/, + int64_t id) + { + base_.GetChildrenPublicId(target, id); + } + + virtual void GetExportedResources(bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetLastChange(); + + virtual void GetLastExportedResource(); + + virtual void GetMainDicomTags(int64_t id); + + virtual std::string GetPublicId(int64_t resourceId); + + virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) + { + return base_.GetResourceCount(Orthanc::Plugins::Convert(resourceType)); + } + + virtual OrthancPluginResourceType GetResourceType(int64_t resourceId); + + virtual uint64_t GetTotalCompressedSize() + { + return base_.GetTotalCompressedSize(); + } + + virtual uint64_t GetTotalUncompressedSize() + { + return base_.GetTotalUncompressedSize(); + } + + virtual bool IsExistingResource(int64_t internalId) + { + return base_.IsExistingResource(internalId); + } + + virtual bool IsProtectedPatient(int64_t internalId) + { + return base_.IsProtectedPatient(internalId); + } + + virtual void ListAvailableMetadata(std::list& target /*out*/, + int64_t id); + + virtual void ListAvailableAttachments(std::list& target /*out*/, + int64_t id); + + virtual void LogChange(const OrthancPluginChange& change); + + virtual void LogExportedResource(const OrthancPluginExportedResource& resource); + + virtual bool LookupAttachment(int64_t id, + int32_t contentType); + + virtual bool LookupGlobalProperty(std::string& target /*out*/, + int32_t property) + { + return base_.LookupGlobalProperty(target, static_cast(property)); + } + + virtual void LookupIdentifier(std::list& target /*out*/, + OrthancPluginResourceType level, + uint16_t group, + uint16_t element, + OrthancPluginIdentifierConstraint constraint, + const char* value) + { + base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level), + Orthanc::DicomTag(group, element), + Orthanc::Plugins::Convert(constraint), value); + } + + virtual bool LookupMetadata(std::string& target /*out*/, + int64_t id, + int32_t metadataType) + { + return base_.LookupMetadata(target, id, static_cast(metadataType)); + } + + virtual bool LookupParent(int64_t& parentId /*out*/, + int64_t resourceId); + + virtual bool LookupResource(int64_t& id /*out*/, + OrthancPluginResourceType& type /*out*/, + const char* publicId); + + virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/) + { + return base_.SelectPatientToRecycle(internalId); + } + + virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/, + int64_t patientIdToAvoid) + { + return base_.SelectPatientToRecycle(internalId, patientIdToAvoid); + } + + + virtual void SetGlobalProperty(int32_t property, + const char* value) + { + base_.SetGlobalProperty(static_cast(property), value); + } + + virtual void SetMainDicomTag(int64_t id, + uint16_t group, + uint16_t element, + const char* value) + { + base_.SetMainDicomTag(id, Orthanc::DicomTag(group, element), value); + } + + virtual void SetIdentifierTag(int64_t id, + uint16_t group, + uint16_t element, + const char* value) + { + base_.SetIdentifierTag(id, Orthanc::DicomTag(group, element), value); + } + + virtual void SetMetadata(int64_t id, + int32_t metadataType, + const char* value) + { + base_.SetMetadata(id, static_cast(metadataType), value); + } + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) + { + base_.SetProtectedPatient(internalId, isProtected); + } + + virtual void StartTransaction(); + + virtual void RollbackTransaction(); + + virtual void CommitTransaction(); + + virtual uint32_t GetDatabaseVersion(); + + virtual void UpgradeDatabase(uint32_t targetVersion, + OrthancPluginStorageArea* storageArea); + + virtual void ClearMainDicomTags(int64_t internalId) + { + base_.ClearMainDicomTags(internalId); + } +}; diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/DatabasePlugin/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/DatabasePlugin/Plugin.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,101 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "Database.h" + +#include +#include +#include + +static OrthancPluginContext* context_ = NULL; +static std::auto_ptr backend_; + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context_ = c; + OrthancPluginLogWarning(context_, "Sample plugin is initializing"); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[256]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context_, info); + return -1; + } + + std::string path = "SampleDatabase.sqlite"; + uint32_t argCount = OrthancPluginGetCommandLineArgumentsCount(context_); + for (uint32_t i = 0; i < argCount; i++) + { + char* tmp = OrthancPluginGetCommandLineArgument(context_, i); + std::string argument(tmp); + OrthancPluginFreeString(context_, tmp); + + if (boost::starts_with(argument, "--database=")) + { + path = argument.substr(11); + } + } + + std::string s = "Using the following SQLite database: " + path; + OrthancPluginLogWarning(context_, s.c_str()); + + backend_.reset(new Database(path)); + OrthancPlugins::DatabaseBackendAdapter::Register(context_, *backend_); + + return 0; + } + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + backend_.reset(NULL); + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "sample-database"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "1.0"; + } +} diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/GdcmDecoding/CMakeLists.txt --- a/Plugins/Samples/GdcmDecoding/CMakeLists.txt Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/GdcmDecoding/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 @@ -3,18 +3,15 @@ project(GdcmDecoding) SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") -SET(USE_SYSTEM_GOOGLE_LOG ON CACHE BOOL "Use the system version of Google Log") +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..) -set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -include(CheckIncludeFiles) -include(CheckIncludeFileCXX) +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) + include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake) include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) find_package(GDCM REQUIRED) diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/ServeFolders/CMakeLists.txt --- a/Plugins/Samples/ServeFolders/CMakeLists.txt Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/ServeFolders/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 @@ -5,16 +5,12 @@ SET(SERVE_FOLDERS_VERSION "0.0" CACHE STRING "Version of the plugin") SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") + SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") -set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../../) set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) - -include(CheckIncludeFiles) -include(CheckIncludeFileCXX) include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) @@ -24,7 +20,6 @@ ${BOOST_SOURCES} ) - message("Setting the version of the plugin to ${SERVE_FOLDERS_VERSION}") add_definitions(-DSERVE_FOLDERS_VERSION="${SERVE_FOLDERS_VERSION}") diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/StorageArea/Plugin.cpp --- a/Plugins/Samples/StorageArea/Plugin.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/StorageArea/Plugin.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -43,13 +43,13 @@ FILE* fp = fopen(path.c_str(), "wb"); if (!fp) { - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_StorageAreaPlugin; } bool ok = fwrite(content, size, 1, fp) == 1; fclose(fp); - return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_Plugin; + return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin; } @@ -63,13 +63,13 @@ FILE* fp = fopen(path.c_str(), "rb"); if (!fp) { - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_StorageAreaPlugin; } if (fseek(fp, 0, SEEK_END) < 0) { fclose(fp); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_StorageAreaPlugin; } *size = ftell(fp); @@ -77,7 +77,7 @@ if (fseek(fp, 0, SEEK_SET) < 0) { fclose(fp); - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_StorageAreaPlugin; } bool ok = true; @@ -98,7 +98,7 @@ fclose(fp); - return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_Plugin; + return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin; } @@ -113,7 +113,7 @@ } else { - return OrthancPluginErrorCode_Plugin; + return OrthancPluginErrorCode_StorageAreaPlugin; } } diff -r 1b82bb0446d2 -r c131566b8252 Plugins/Samples/WebSkeleton/CMakeLists.txt --- a/Plugins/Samples/WebSkeleton/CMakeLists.txt Wed Sep 23 10:29:06 2015 +0200 +++ b/Plugins/Samples/WebSkeleton/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 @@ -5,11 +5,11 @@ SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources) -include(Framework/Framework.cmake) - set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) +include(Framework/Framework.cmake) + add_library(WebSkeleton SHARED ${AUTOGENERATED_SOURCES} ) diff -r 1b82bb0446d2 -r c131566b8252 Resources/CMake/Compiler.cmake --- a/Resources/CMake/Compiler.cmake Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/CMake/Compiler.cmake Wed Nov 18 10:16:21 2015 +0100 @@ -49,7 +49,7 @@ ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined -Wl,--version-script=${ORTHANC_ROOT}/Plugins/Samples/Common/VersionScript.map") # Remove the "-rdynamic" option # http://www.mail-archive.com/cmake@cmake.org/msg08837.html @@ -109,6 +109,8 @@ endif() elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -exported_symbols_list ${ORTHANC_ROOT}/Plugins/Samples/Common/ExportedSymbols.list") + add_definitions( -D_XOPEN_SOURCE=1 ) diff -r 1b82bb0446d2 -r c131566b8252 Resources/CMake/DcmtkConfiguration.cmake --- a/Resources/CMake/DcmtkConfiguration.cmake Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Wed Nov 18 10:16:21 2015 +0100 @@ -1,18 +1,3 @@ -# Lookup for DICOM dictionaries, if none is specified by the user -if (DCMTK_DICTIONARY_DIR STREQUAL "") - find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic - /usr/share/dcmtk - /usr/share/libdcmtk2 - /usr/local/share/dcmtk - ) - - message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}") - add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}") -else() - add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR}") -endif() - - if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK) SET(DCMTK_VERSION_NUMBER 361) set(DCMTK_PACKAGE_VERSION "3.6.1") @@ -166,17 +151,16 @@ set(DCMTK_BUNDLES_LOG4CPLUS 1) if (STANDALONE_BUILD) - add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=1) + set(DCMTK_USE_EMBEDDED_DICTIONARIES 1) + set(DCMTK_DICTIONARIES + DICTIONARY_DICOM ${DCMTK_SOURCES_DIR}/dcmdata/data/dicom.dic + DICTIONARY_PRIVATE ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic + DICTIONARY_DICONDE ${DCMTK_SOURCES_DIR}/dcmdata/data/diconde.dic + ) else() - add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=0) + set(DCMTK_USE_EMBEDDED_DICTIONARIES 0) endif() - set(DCMTK_DICTIONARIES - DICTIONARY_DICOM ${DCMTK_SOURCES_DIR}/dcmdata/data/dicom.dic - DICTIONARY_PRIVATE ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic - DICTIONARY_DICONDE ${DCMTK_SOURCES_DIR}/dcmdata/data/diconde.dic - ) - else() # The following line allows to manually add libraries at the # command-line, which is necessary for Ubuntu/Debian packages @@ -210,9 +194,32 @@ DCMTK_VERSION_NUMBER ${DCMTK_VERSION_NUMBER1}) - add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=0) + set(DCMTK_USE_EMBEDDED_DICTIONARIES 0) +endif() -endif() add_definitions(-DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}) message("DCMTK version: ${DCMTK_VERSION_NUMBER}") + + +add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=${DCMTK_USE_EMBEDDED_DICTIONARIES}) +if (NOT DCMTK_USE_EMBEDDED_DICTIONARIES) + # Lookup for DICOM dictionaries, if none is specified by the user + if (DCMTK_DICTIONARY_DIR STREQUAL "") + find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic + /usr/share/dcmtk + /usr/share/libdcmtk2 + /usr/share/libdcmtk4 + /usr/local/share/dcmtk + ) + + if (${DCMTK_DICTIONARY_DIR_AUTO} MATCHES "DCMTK_DICTIONARY_DIR_AUTO-NOTFOUND") + message(FATAL_ERROR "Cannot locate the DICOM dictionary on this system") + endif() + + message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}") + add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}") + else() + add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR}") + endif() +endif() diff -r 1b82bb0446d2 -r c131566b8252 Resources/Configuration.json --- a/Resources/Configuration.json Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/Configuration.json Wed Nov 18 10:16:21 2015 +0100 @@ -190,17 +190,19 @@ **/ // Dictionary of symbolic names for the user-defined metadata. Each - // entry must map a number between 1024 and 65535 to an unique - // string. + // entry must map an unique string to an unique number between 1024 + // and 65535. "UserMetadata" : { // "Sample" : 1024 }, // Dictionary of symbolic names for the user-defined types of - // attached files. Each entry must map a number between 1024 and - // 65535 to an unique string. + // attached files. Each entry must map an unique string to an unique + // number between 1024 and 65535. Optionally, a second argument can + // provided to specify a MIME content type for the attachment. "UserContentType" : { // "sample" : 1024 + // "sample2" : [ 1025, "application/pdf" ] }, // Number of seconds without receiving any instance before a @@ -243,7 +245,8 @@ "KeepAlive" : false, // If this option is set to "false", Orthanc will run in index-only - // mode. The DICOM files will not be stored on the drive. + // mode. The DICOM files will not be stored on the drive. Note that + // this option might prevent the upgrade to newer versions of Orthanc. "StoreDicom" : true, // DICOM associations are kept open as long as new DICOM commands @@ -257,8 +260,19 @@ // deleted as new requests are issued. "QueryRetrieveSize" : 10, - // When handling a C-Find SCP request, setting this flag to "false" - // will enable case-insensitive match for PN value representation - // (such as PatientName). By default, the search is case-insensitive. - "CaseSensitivePN" : false + // When handling a C-Find SCP request, setting this flag to "true" + // will enable case-sensitive match for PN value representation + // (such as PatientName). By default, the search is + // case-insensitive, which does not follow the DICOM standard. + "CaseSensitivePN" : false, + + // Register a new tag in the dictionary of DICOM tags that are known + // to Orthanc. Each line must contain the tag (formatted as 2 + // hexadecimal numbers), the value representation (2 upcase + // characters), a nickname for the tag, possibly the minimum + // multiplicity (> 0 with defaults to 1), and possibly the maximum + // multiplicity (0 means arbitrary multiplicity, defaults to 1). + "Dictionary" : { + // "0014,1020" : [ "DA", "ValidationExpiryDate", 1, 1 ] + } } diff -r 1b82bb0446d2 -r c131566b8252 Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/ErrorCodes.json Wed Nov 18 10:16:21 2015 +0100 @@ -175,7 +175,27 @@ "Name": "BadFont", "Description": "Badly formatted font file" }, - + { + "Code": 31, + "Name": "DatabasePlugin", + "Description": "The plugin implementing a custom database back-end does not fulfill the proper interface" + }, + { + "Code": 32, + "Name": "StorageAreaPlugin", + "Description": "Error in the plugin implementing a custom storage area" + }, + { + "Code": 33, + "Name": "EmptyRequest", + "Description": "The request is empty" + }, + { + "Code": 34, + "HttpStatus": 406, + "Name": "NotAcceptable", + "Description": "Cannot send a response which is acceptable according to the Accept HTTP header" + }, @@ -478,10 +498,20 @@ "Code": 2037, "Name": "DatabaseBackendAlreadyRegistered", "Description": "Another plugin has already registered a custom database back-end" - }, + }, { - "Code": 2038, - "Name": "DatabasePlugin", - "Description": "The plugin implementing a custom database back-end does not fulfill the proper interface" + "Code": 2038, + "Name": "DatabaseNotInitialized", + "Description": "Plugin trying to call the database during its initialization" + }, + { + "Code": 2039, + "Name": "SslDisabled", + "Description": "Orthanc has been built without SSL support" + }, + { + "Code": 2040, + "Name": "CannotOrderSlices", + "Description": "Unable to order the slices of the series" } ] diff -r 1b82bb0446d2 -r c131566b8252 Resources/GenerateErrorCodes.py --- a/Resources/GenerateErrorCodes.py Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/GenerateErrorCodes.py Wed Nov 18 10:16:21 2015 +0100 @@ -33,7 +33,9 @@ import json import os import re +import sys +START_PLUGINS = 1000000 BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) @@ -45,6 +47,11 @@ with open(os.path.join(BASE, 'Resources', 'ErrorCodes.json'), 'r') as f: ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read())) +for error in ERRORS: + if error['Code'] >= START_PLUGINS: + print('ERROR: Error code must be below %d, but "%s" is set to %d' % (START_PLUGINS, error['Name'], error['Code'])) + sys.exit(-1) + with open(os.path.join(BASE, 'Core', 'Enumerations.h'), 'r') as f: a = f.read() @@ -63,6 +70,8 @@ a = f.read() s = ',\n'.join(map(lambda x: ' ErrorCode_%s = %d /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS)) + +s += ',\n ErrorCode_START_PLUGINS = %d' % START_PLUGINS a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL) with open(path, 'w') as f: @@ -136,17 +145,15 @@ ## -## Generate the "Plugins::Convert(OrthancPluginErrorCode)" in -## "PluginsEnumerations.cpp" +## Generate the "PrintErrors" function in "main.cpp" ## -path = os.path.join(BASE, 'Plugins', 'Engine', 'PluginsEnumerations.cpp') +path = os.path.join(BASE, 'OrthancServer', 'main.cpp') with open(path, 'r') as f: a = f.read() -s = '\n\n'.join(map(lambda x: ' case OrthancPluginErrorCode_%s:\n return ErrorCode_%s;' % (x['Name'], x['Name']), ERRORS)) -a = re.sub('(Convert\(OrthancPluginErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', - r'\1\n%s\2' % s, a, re.DOTALL) +s = '\n'.join(map(lambda x: ' PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS)) +a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n }' % s, a, re.DOTALL) with open(path, 'w') as f: f.write(a) diff -r 1b82bb0446d2 -r c131566b8252 Resources/Samples/ImportDicomFiles/ImportDicomFiles.py --- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Wed Sep 23 10:29:06 2015 +0200 +++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Wed Nov 18 10:16:21 2015 +0100 @@ -81,7 +81,7 @@ sys.stdout.write(" => success\n") success_count += 1 else: - sys.stdout.write(" => failure (Is it a DICOM file?)\n") + sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n") except: sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n") diff -r 1b82bb0446d2 -r c131566b8252 Resources/Samples/Lua/ModifyInstanceWithSequence.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/ModifyInstanceWithSequence.lua Wed Nov 18 10:16:21 2015 +0100 @@ -0,0 +1,28 @@ +-- Answer to: +-- https://groups.google.com/d/msg/orthanc-users/0ymHe1cDBCQ/YfD0NoOTn0wJ +-- Applicable starting with Orthanc 0.9.5 + +function OnStoredInstance(instanceId, tags, metadata, origin) + -- Do not modify twice the same file + if origin['RequestOrigin'] ~= 'Lua' then + local replace = {} + replace['0010,1002'] = {} + replace['0010,1002'][1] = {} + replace['0010,1002'][1]['PatientID'] = 'Hello' + replace['0010,1002'][2] = {} + replace['0010,1002'][2]['PatientID'] = 'World' + + local request = {} + request['Replace'] = replace + + -- Create the modified instance + local modified = RestApiPost('/instances/' .. instanceId .. '/modify', + DumpJson(request)) + + -- Upload the modified instance to the Orthanc store + RestApiPost('/instances/', modified) + + -- Delete the original instance + RestApiDelete('/instances/' .. instanceId) + end +end diff -r 1b82bb0446d2 -r c131566b8252 THANKS --- a/THANKS Wed Sep 23 10:29:06 2015 +0200 +++ b/THANKS Wed Nov 18 10:16:21 2015 +0100 @@ -28,7 +28,6 @@ * Vincent Kersten , for DICOMDIR in the GUI. * Emsy Chan , for various contributions and sample DICOM files. -* Mikhail , for FreeBSD support. Thanks also to all the contributors active in our Google Group: @@ -49,6 +48,12 @@ * Mario Ceresa , for help about packaging. +FreeBSD +------- + +* Mikhail , for FreeBSD packaging. + + Artwork ------- diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/DicomMapTests.cpp --- a/UnitTestsSources/DicomMapTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/DicomMapTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -36,7 +36,6 @@ #include "../Core/Uuid.h" #include "../Core/OrthancException.h" #include "../Core/DicomFormat/DicomMap.h" -#include "../Core/DicomFormat/DicomNullValue.h" #include "../OrthancServer/FromDcmtkBridge.h" #include @@ -103,7 +102,7 @@ m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID"); ASSERT_TRUE(m.HasTag(0x0010, 0x0020)); m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2"); - ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString()); + ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).GetContent()); m.GetTags(s); ASSERT_EQ(2u, s.size()); @@ -116,14 +115,14 @@ ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin()); std::auto_ptr mm(m.Clone()); - ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString()); + ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); m.SetValue(DICOM_TAG_PATIENT_ID, "Hello"); ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException); mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID); - ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).AsString()); + ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).GetContent()); - DicomNullValue v; + DicomValue v; ASSERT_TRUE(v.IsNull()); } @@ -170,18 +169,12 @@ }*/ // Exceptions for the Instance level - if ((/* Accession number, from Image module */ - *it == DicomTag(0x0020, 0x0012) && - level == ResourceType_Instance) || - (/* Image Index, from PET Image module */ - *it == DicomTag(0x0054, 0x1330) && - level == ResourceType_Instance) || - (/* Temporal Position Identifier, from MR Image module */ - *it == DicomTag(0x0020, 0x0100) && - level == ResourceType_Instance) || - (/* Number of Frames, from Multi-frame module attributes, related to Image IOD */ - *it == DicomTag(0x0028, 0x0008) && - level == ResourceType_Instance )) + if (level == ResourceType_Instance && + (*it == DicomTag(0x0020, 0x0012) || /* Accession number, from Image module */ + *it == DicomTag(0x0054, 0x1330) || /* Image Index, from PET Image module */ + *it == DicomTag(0x0020, 0x0100) || /* Temporal Position Identifier, from MR Image module */ + *it == DicomTag(0x0028, 0x0008) || /* Number of Frames, from Multi-frame module attributes, related to Image IOD */ + *it == DICOM_TAG_IMAGE_POSITION_PATIENT)) { ok = true; } diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -36,6 +36,7 @@ #include "../OrthancServer/FromDcmtkBridge.h" #include "../OrthancServer/OrthancInitialization.h" #include "../OrthancServer/DicomModification.h" +#include "../OrthancServer/ServerToolbox.h" #include "../Core/OrthancException.h" #include "../Core/Images/ImageBuffer.h" #include "../Core/Images/PngReader.h" @@ -43,6 +44,8 @@ #include "../Core/Uuid.h" #include "../Resources/EncodingTests.h" +#include + using namespace Orthanc; TEST(DicomFormat, Tag) @@ -102,7 +105,7 @@ ParsedDicomFile o; o.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); ASSERT_FALSE(o.GetTagValue(s, privateTag)); - o.Insert(privateTag, "private tag"); + o.Insert(privateTag, "private tag", false); ASSERT_TRUE(o.GetTagValue(s, privateTag)); ASSERT_STREQ("private tag", s.c_str()); @@ -145,14 +148,11 @@ // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image) std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; - std::string m, c; - Toolbox::DecodeDataUriScheme(m, c, s); + std::string m, cc; + Toolbox::DecodeDataUriScheme(m, cc, s); ASSERT_EQ("image/png", m); - ASSERT_EQ(116u, c.size()); - std::string cc; - Toolbox::DecodeBase64(cc, c); PngReader reader; reader.ReadFromMemory(cc); @@ -184,7 +184,7 @@ img.SetHeight(256); img.SetFormat(PixelFormat_Grayscale16); - int v = 0; + uint16_t v = 0; for (unsigned int y = 0; y < img.GetHeight(); y++) { uint16_t *p = reinterpret_cast(img.GetAccessor().GetRow(y)); @@ -207,7 +207,7 @@ std::string source(testEncodingsEncoded[i]); std::string expected(testEncodingsExpected[i]); std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i]); - std::cout << EnumerationToString(testEncodings[i]) << std::endl; + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; EXPECT_EQ(expected, s); } } @@ -262,13 +262,15 @@ { for (unsigned int i = 0; i < testEncodingsCount; i++) { - std::cout << EnumerationToString(testEncodings[i]) << std::endl; + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; std::string dicom; { ParsedDicomFile f; f.SetEncoding(testEncodings[i]); - f.Insert(DICOM_TAG_PATIENT_NAME, testEncodingsEncoded[i]); + + std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]); + f.Insert(DICOM_TAG_PATIENT_NAME, s, false); f.SaveToMemoryBuffer(dicom); } @@ -302,3 +304,305 @@ ASSERT_EQ(ValueRepresentation_Other, FromDcmtkBridge::GetValueRepresentation(DICOM_TAG_PATIENT_ID)); } + + + +static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110); +static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120); + +static void CreateSampleJson(Json::Value& a) +{ + { + Json::Value b = Json::objectValue; + b["PatientName"] = "Hello"; + b["PatientID"] = "World"; + b["StudyDescription"] = "Toto"; + a.append(b); + } + + { + Json::Value b = Json::objectValue; + b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y"; // echo -n "Hello2" | base64 + b["PatientID"] = "World2"; + a.append(b); + } +} + + +TEST(FromDcmtkBridge, FromJson) +{ + std::auto_ptr element; + + { + Json::Value a; + a = "Hello"; + element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)); + + Json::Value b; + FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii); + ASSERT_EQ("Hello", b["0010,0010"].asString()); + } + + { + Json::Value a; + a = "Hello"; + // Cannot assign a string to a sequence + ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8)), OrthancException); + } + + { + Json::Value a = Json::arrayValue; + a.append("Hello"); + // Cannot assign an array to a string + ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)), OrthancException); + } + + { + Json::Value a; + a = "data:application/octet-stream;base64,SGVsbG8="; // echo -n "Hello" | base64 + element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8)); + + Json::Value b; + FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii); + ASSERT_EQ("Hello", b["0010,0010"].asString()); + } + + { + Json::Value a = Json::arrayValue; + CreateSampleJson(a); + element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8)); + + { + Json::Value b; + FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii); + ASSERT_EQ(Json::arrayValue, b["0008,1110"].type()); + ASSERT_EQ(2, b["0008,1110"].size()); + + Json::Value::ArrayIndex i = (b["0008,1110"][0]["0010,0010"].asString() == "Hello") ? 0 : 1; + + ASSERT_EQ(3, b["0008,1110"][i].size()); + ASSERT_EQ(2, b["0008,1110"][1 - i].size()); + ASSERT_EQ(b["0008,1110"][i]["0010,0010"].asString(), "Hello"); + ASSERT_EQ(b["0008,1110"][i]["0010,0020"].asString(), "World"); + ASSERT_EQ(b["0008,1110"][i]["0008,1030"].asString(), "Toto"); + ASSERT_EQ(b["0008,1110"][1 - i]["0010,0010"].asString(), "Hello2"); + ASSERT_EQ(b["0008,1110"][1 - i]["0010,0020"].asString(), "World2"); + } + + { + Json::Value b; + FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0, Encoding_Ascii); + + Json::Value c; + Toolbox::SimplifyTags(c, b); + + a[1]["PatientName"] = "Hello2"; // To remove the Data URI Scheme encoding + ASSERT_EQ(0, c["ReferencedStudySequence"].compare(a)); + } + } +} + + + +TEST(ParsedDicomFile, InsertReplaceStrings) +{ + ParsedDicomFile f; + + f.Insert(DICOM_TAG_PATIENT_NAME, "World", false); + ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException); // Already existing tag + f.Replace(DICOM_TAG_SOP_INSTANCE_UID, "Toto"); // (*) + f.Replace(DICOM_TAG_SOP_CLASS_UID, "Tata"); // (**) + + std::string s; + + ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_ThrowIfAbsent), OrthancException); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_IgnoreIfAbsent); + ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_InsertIfAbsent); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession"); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession2", DicomReplaceMode_IgnoreIfAbsent); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession2"); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession3", DicomReplaceMode_ThrowIfAbsent); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession3"); + + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(s, "World"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_EQ(s, "Toto"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); // Implicitly modified by (*) + ASSERT_EQ(s, "Toto"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID)); + ASSERT_EQ(s, "Tata"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID)); // Implicitly modified by (**) + ASSERT_EQ(s, "Tata"); +} + + + + +TEST(ParsedDicomFile, InsertReplaceJson) +{ + ParsedDicomFile f; + + Json::Value a; + CreateSampleJson(a); + + ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + f.Remove(REFERENCED_STUDY_SEQUENCE); // No effect + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true); + ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true), OrthancException); + f.Remove(REFERENCED_STUDY_SEQUENCE); + ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true); + ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent), OrthancException); + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent); + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent); + ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + + { + Json::Value b; + f.ToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0); + + Json::Value c; + Toolbox::SimplifyTags(c, b); + + ASSERT_EQ(0, c["ReferencedPatientSequence"].compare(a)); + ASSERT_NE(0, c["ReferencedStudySequence"].compare(a)); // Because Data URI Scheme decoding was enabled + } + + a = "data:application/octet-stream;base64,VGF0YQ=="; // echo -n "Tata" | base64 + f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false); // (*) + f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true); // (**) + + std::string s; + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_EQ(s, a.asString()); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); // Implicitly modified by (*) + ASSERT_EQ(s, a.asString()); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID)); + ASSERT_EQ(s, "Tata"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID)); // Implicitly modified by (**) + ASSERT_EQ(s, "Tata"); +} + + +TEST(ParsedDicomFile, JsonEncoding) +{ + ParsedDicomFile f; + + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + if (testEncodings[i] != Encoding_Windows1251) + { + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; + f.SetEncoding(testEncodings[i]); + + if (testEncodings[i] != Encoding_Ascii) + { + ASSERT_EQ(testEncodings[i], f.GetEncoding()); + } + + Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]); + f.Replace(DICOM_TAG_PATIENT_NAME, s, false); + + Json::Value v; + f.ToJson(v, DicomToJsonFormat_Simple, DicomToJsonFlags_Default, 0); + ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i])); + } + } +} + + +TEST(ParsedDicomFile, ToJsonFlags1) +{ + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), EVR_PN, "MyPrivateTag", 1, 1); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), EVR_PN, "Declared public tag", 1, 1); + + ParsedDicomFile f; + f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false); // Even group => public tag + f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false); // Even group => public, unknown tag + f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false); // Odd group => private tag + + Json::Value v; + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_EQ(Json::stringValue, v["7050,1000"].type()); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); // TODO SHOULD BE STRING + + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludeUnknownTags, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); // TODO SHOULD BE STRING + + f.ToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(8, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); // TODO SHOULD BE STRING + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); // TODO SHOULD BE STRING +} + + +TEST(ParsedDicomFile, ToJsonFlags2) +{ + ParsedDicomFile f; + f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false); + + Json::Value v; + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(5, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7fe0,0010")); + + f.ToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type()); + + f.ToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type()); + ASSERT_EQ("Pixels", v["7fe0,0010"].asString()); + + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type()); + std::string mime, content; + Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("Pixels", content); +} diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/ImageTests.cpp --- a/UnitTestsSources/ImageTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/ImageTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -50,15 +50,15 @@ TEST(PngWriter, ColorPattern) { Orthanc::PngWriter w; - int width = 17; - int height = 61; - int pitch = width * 3; + unsigned int width = 17; + unsigned int height = 61; + unsigned int pitch = width * 3; std::vector image(height * pitch); - for (int y = 0; y < height; y++) + for (unsigned int y = 0; y < height; y++) { uint8_t *p = &image[0] + y * pitch; - for (int x = 0; x < width; x++, p += 3) + for (unsigned int x = 0; x < width; x++, p += 3) { p[0] = (y % 3 == 0) ? 255 : 0; p[1] = (y % 3 == 1) ? 255 : 0; diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/LuaTests.cpp --- a/UnitTestsSources/LuaTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/LuaTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -171,7 +171,7 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson("hello"); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ("hello", v.asString()); } @@ -179,7 +179,7 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson(42.25); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_FLOAT_EQ(42.25f, v.asFloat()); } @@ -187,7 +187,7 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson(-42); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ(-42, v.asInt()); } @@ -196,7 +196,7 @@ Json::Value vv = Json::arrayValue; f.PushJson(vv); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ(Json::arrayValue, v.type()); } @@ -205,7 +205,7 @@ Json::Value vv = Json::objectValue; f.PushJson(vv); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); // Lua does not make the distinction between empty lists and empty objects ASSERT_EQ(Json::arrayValue, v.type()); } @@ -214,7 +214,7 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson(b); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat()); ASSERT_FLOAT_EQ(44.37f, v["b"].asFloat()); @@ -225,7 +225,7 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson(c); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ(Json::arrayValue, v.type()); ASSERT_EQ("test3", v[0].asString()); ASSERT_EQ("test1", v[1].asString()); @@ -236,9 +236,13 @@ Orthanc::LuaFunctionCall f(lua, "identity"); f.PushJson(a); Json::Value v; - f.ExecuteToJson(v); + f.ExecuteToJson(v, false); ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(Json::intValue, v["List"][0]["a"].type()); + ASSERT_EQ(Json::realValue, v["List"][0]["b"].type()); + ASSERT_EQ(Json::intValue, v["List"][0]["c"].type()); ASSERT_EQ(42, v["List"][0]["a"].asInt()); + ASSERT_FLOAT_EQ(44.37f, v["List"][0]["b"].asFloat()); ASSERT_EQ(44, v["List"][0]["b"].asInt()); ASSERT_EQ(-43, v["List"][0]["c"].asInt()); ASSERT_EQ("test3", v["List"][1][0].asString()); @@ -247,6 +251,23 @@ } { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(a); + Json::Value v; + f.ExecuteToJson(v, true); + ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(Json::stringValue, v["List"][0]["a"].type()); + ASSERT_EQ(Json::stringValue, v["List"][0]["b"].type()); + ASSERT_EQ(Json::stringValue, v["List"][0]["c"].type()); + ASSERT_EQ("42", v["List"][0]["a"].asString()); + ASSERT_EQ("44.37", v["List"][0]["b"].asString()); + ASSERT_EQ("-43", v["List"][0]["c"].asString()); + ASSERT_EQ("test3", v["List"][1][0].asString()); + ASSERT_EQ("test1", v["List"][1][1].asString()); + ASSERT_EQ("test2", v["List"][1][2].asString()); + } + + { Orthanc::LuaFunctionCall f(lua, "DumpJson"); f.PushJson(a); std::string s; diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/MemoryCacheTests.cpp --- a/UnitTestsSources/MemoryCacheTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/MemoryCacheTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -190,11 +190,6 @@ LOG(INFO) << "Removing cache entry for " << value_; log_ += boost::lexical_cast(value_) + " "; } - - int GetValue() const - { - return value_; - } }; class IntegerProvider : public Orthanc::ICachePageProvider @@ -235,8 +230,6 @@ - - namespace { class S : public Orthanc::IDynamicObject @@ -253,11 +246,6 @@ { return value_; } - - static const std::string& Access(const Orthanc::IDynamicObject& obj) - { - return dynamic_cast(obj).GetValue(); - } }; } diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -183,7 +183,7 @@ outputs.push_back(boost::lexical_cast(b)); } - Toolbox::USleep(100000); + Toolbox::USleep(30000); return true; } @@ -202,7 +202,7 @@ { printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it)); } - Toolbox::USleep(10000); + Toolbox::USleep(3000); } } diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/RestApiTests.cpp --- a/UnitTestsSources/RestApiTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/RestApiTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -34,6 +34,8 @@ #include "gtest/gtest.h" #include +#include +#include #include "../Core/ChunkedBuffer.h" #include "../Core/HttpClient.h" @@ -43,6 +45,7 @@ #include "../Core/OrthancException.h" #include "../Core/Compression/ZlibCompressor.h" #include "../Core/RestApi/RestApiHierarchy.h" +#include "../Core/HttpServer/HttpContentNegociation.h" using namespace Orthanc; @@ -335,3 +338,126 @@ ASSERT_TRUE(HandleGet(root, "/hello2/a/b")); ASSERT_EQ(testValue, 4); } + + + + + +namespace +{ + class AcceptHandler : public Orthanc::HttpContentNegociation::IHandler + { + private: + std::string type_; + std::string subtype_; + + public: + AcceptHandler() + { + Reset(); + } + + void Reset() + { + Handle("nope", "nope"); + } + + const std::string& GetType() const + { + return type_; + } + + const std::string& GetSubType() const + { + return subtype_; + } + + virtual void Handle(const std::string& type, + const std::string& subtype) + { + type_ = type; + subtype_ = subtype; + } + }; +} + + +TEST(RestApi, HttpContentNegociation) +{ + // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + + AcceptHandler h; + + { + Orthanc::HttpContentNegociation d; + d.Register("audio/mp3", h); + d.Register("audio/basic", h); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("basic", h.GetSubType()); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("mp3", h.GetSubType()); + + ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf")); + + ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf")); + ASSERT_EQ("audio", h.GetType()); + } + + // "This would be interpreted as "text/html and text/x-c are the + // preferred media types, but if they do not exist, then send the + // text/x-dvi entity, and if that does not exist, send the + // text/plain entity."" + const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; + + { + Orthanc::HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/html", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("html", h.GetSubType()); + } + + { + Orthanc::HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-c", h.GetSubType()); + } + + { + Orthanc::HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + d.Register("text/html", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html"); + } + + { + Orthanc::HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-dvi", h.GetSubType()); + } + + { + Orthanc::HttpContentNegociation d; + d.Register("text/plain", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("plain", h.GetSubType()); + } +} diff -r 1b82bb0446d2 -r c131566b8252 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Wed Sep 23 10:29:06 2015 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Wed Nov 18 10:16:21 2015 +0100 @@ -33,13 +33,13 @@ #include "PrecompiledHeadersUnitTests.h" #include "gtest/gtest.h" -#include "../Core/DicomFormat/DicomNullValue.h" #include "../Core/FileStorage/FilesystemStorage.h" #include "../Core/Logging.h" #include "../Core/Uuid.h" #include "../OrthancServer/DatabaseWrapper.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerIndex.h" +#include "../OrthancServer/Search/LookupIdentifierQuery.h" #include #include @@ -122,10 +122,12 @@ } index_->SetListener(*listener_); + index_->Open(); } virtual void TearDown() { + index_->Close(); index_.reset(NULL); listener_.reset(NULL); } @@ -244,6 +246,18 @@ throw OrthancException(ErrorCode_InternalError); } } + + + void DoLookup(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value) + { + LookupIdentifierQuery query(level); + query.AddConstraint(tag, IdentifierConstraintType_Equal, value); + query.Apply(result, *index_); + } + }; } @@ -660,6 +674,7 @@ Toolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory + db.Open(); ServerContext context(db, storage); ServerIndex& index = context.GetIndex(); @@ -669,6 +684,7 @@ ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); context.Stop(); + db.Close(); } @@ -682,41 +698,69 @@ index_->CreateResource("d", ResourceType_Series) // 3 }; - index_->SetMainDicomTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0"); - index_->SetMainDicomTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1"); - index_->SetMainDicomTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); - index_->SetMainDicomTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); + index_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1"); + index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); - std::list s; + std::list s; - index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "0"); ASSERT_EQ(2u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end()); + + DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "0"); + ASSERT_EQ(1u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end()); - index_->LookupIdentifier(s, "0"); - ASSERT_EQ(3u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end()); + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + ASSERT_EQ(1u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); + + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + ASSERT_EQ(1u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); + + DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "1"); + ASSERT_EQ(0u, s.size()); - index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); - ASSERT_EQ(1u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); + query.Apply(s, *index_); + ASSERT_EQ(3u, s.size()); + } - index_->LookupIdentifier(s, "1"); - ASSERT_EQ(1u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); - + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "0"); + query.Apply(s, *index_); + ASSERT_EQ(2u, s.size()); + } - /*{ - std::list s; - context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); - for (std::list::iterator i = s.begin(); i != s.end(); i++) - { - std::cout << "*** " << *i << std::endl;; - } - }*/ + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "1"); + query.Apply(s, *index_); + ASSERT_EQ(1u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); + query.Apply(s, *index_); + ASSERT_EQ(1u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "2"); + query.Apply(s, *index_); + ASSERT_EQ(0u, s.size()); + } } @@ -728,6 +772,7 @@ Toolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory + db.Open(); ServerContext context(db, storage); ServerIndex& index = context.GetIndex(); @@ -781,4 +826,12 @@ ASSERT_THROW(Toolbox::GetFileSize(path + "/index"), OrthancException); context.Stop(); + db.Close(); } + + +TEST(LookupIdentifierQuery, NormalizeIdentifier) +{ + ASSERT_EQ("H^L.LO", LookupIdentifierQuery::NormalizeIdentifier(" Hé^l.LO %_ ")); + ASSERT_EQ("1.2.840.113619.2.176.2025", LookupIdentifierQuery::NormalizeIdentifier(" 1.2.840.113619.2.176.2025 ")); +}