changeset 1811:67466dcd23b1

integration worklists->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 23 Nov 2015 17:10:11 +0100
parents cd213ebcaefd (current diff) 796d0b087fb8 (diff)
children 5fc11b79b406
files
diffstat 15 files changed, 313 insertions(+), 119 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Nov 23 15:24:39 2015 +0100
+++ b/CMakeLists.txt	Mon Nov 23 17:10:11 2015 +0100
@@ -31,6 +31,7 @@
 SET(ENABLE_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Build the ServeFolders plugin")
+SET(BUILD_MODALITY_WORKLISTS ON CACHE BOOL "Build the sample plugin to serve modality worklists")
 
 # Advanced parameters to fine-tune linking against system libraries
 SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
@@ -511,6 +512,49 @@
 
 
 #####################################################################
+## Build the "ModalityWorklists" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_MODALITY_WORKLISTS)
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+    ${ORTHANC_VERSION} ModalityWorklists ModalityWorklists.dll "Sample Orthanc plugin to serve modality worklists"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/ModalityWorklists.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  add_definitions(-DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}")
+
+  include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include)
+
+  add_library(ModalityWorklists SHARED 
+    ${BOOST_SOURCES}
+    ${JSONCPP_SOURCES}
+    Plugins/Samples/ModalityWorklists/Plugin.cpp
+    ${AUTOGENERATED_DIR}/ModalityWorklists.rc
+    )
+
+  set_target_properties(
+    ModalityWorklists PROPERTIES 
+    VERSION ${ORTHANC_VERSION} 
+    SOVERSION ${ORTHANC_VERSION}
+    )
+
+  install(
+    TARGETS ModalityWorklists
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+
+
+#####################################################################
 ## Generate the documentation if Doxygen is present
 #####################################################################
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -353,7 +353,8 @@
     struct FindPayload
     {
       DicomFindAnswers* answers;
-      std::string       level;
+      const char*       level;
+      bool              isWorklist;
     };
   }
 
@@ -371,15 +372,23 @@
 
     if (responseIdentifiers != NULL)
     {
-      DicomMap m;
-      FromDcmtkBridge::Convert(m, *responseIdentifiers);
-
-      if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+      if (payload.isWorklist)
+      {
+        ParsedDicomFile answer(*responseIdentifiers);
+        payload.answers->Add(answer);
+      }
+      else
       {
-        m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level);
+        DicomMap m;
+        FromDcmtkBridge::Convert(m, *responseIdentifiers);
+
+        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+        {
+          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level);
+        }
+
+        payload.answers->Add(m);
       }
-
-      payload.answers->Add(m);
     }
   }
 
@@ -474,6 +483,52 @@
   }
 
 
+  static void ExecuteFind(DicomFindAnswers& answers,
+                          T_ASC_Association* association,
+                          DcmDataset* dataset,
+                          const char* sopClass,
+                          bool isWorklist,
+                          const char* level,
+                          uint32_t dimseTimeout)
+  {
+    assert(isWorklist ^ (level != NULL));
+
+    FindPayload payload;
+    payload.answers = &answers;
+    payload.level = level;
+    payload.isWorklist = isWorklist;
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(association, sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomFindUnavailable);
+    }
+
+    T_DIMSE_C_FindRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association->nextMsgID++;
+    strcpy(request.AffectedSOPClassUID, sopClass);
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+
+    T_DIMSE_C_FindRSP response;
+    DcmDataset* statusDetail = NULL;
+    OFCondition cond = DIMSE_findUser(association, presID, &request, dataset,
+                                      FindCallback, &payload,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ dimseTimeout,
+                                      &response, &statusDetail);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    Check(cond);
+  }
+
+
   void DicomUserConnection::Find(DicomFindAnswers& result,
                                  ResourceType level,
                                  const DicomMap& originalFields)
@@ -483,34 +538,32 @@
 
     CheckIsOpen();
 
-    FindPayload payload;
-    payload.answers = &result;
+    std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_));
+    const char* clevel = NULL;
+    const char* sopClass = NULL;
 
-    std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_));
-
-    const char* sopClass;
     switch (level)
     {
       case ResourceType_Patient:
-        payload.level = "PATIENT";
+        clevel = "PATIENT";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
         sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
         break;
 
       case ResourceType_Study:
-        payload.level = "STUDY";
+        clevel = "STUDY";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
         sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
         break;
 
       case ResourceType_Series:
-        payload.level = "SERIES";
+        clevel = "SERIES";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
         sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
         break;
 
       case ResourceType_Instance:
-        payload.level = "INSTANCE";
+        clevel = "INSTANCE";
         if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
             manufacturer_ == ModalityManufacturer_Dcm4Chee)
         {
@@ -565,34 +618,8 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    // Figure out which of the accepted presentation contexts should be used
-    int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
-    if (presID == 0)
-    {
-      throw OrthancException(ErrorCode_DicomFindUnavailable);
-    }
-
-    T_DIMSE_C_FindRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = pimpl_->assoc_->nextMsgID++;
-    strcpy(request.AffectedSOPClassUID, sopClass);
-    request.DataSetType = DIMSE_DATASET_PRESENT;
-    request.Priority = DIMSE_PRIORITY_MEDIUM;
-
-    T_DIMSE_C_FindRSP response;
-    DcmDataset* statusDetail = NULL;
-    OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(),
-                                      FindCallback, &payload,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
-                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
-                                      &response, &statusDetail);
-
-    if (statusDetail)
-    {
-      delete statusDetail;
-    }
-
-    Check(cond);
+    assert(clevel != NULL && sopClass != NULL);
+    ExecuteFind(result, pimpl_->assoc_, dataset.get(), sopClass, false, clevel, pimpl_->dimseTimeout_);
   }
 
 
@@ -685,13 +712,14 @@
     defaultStorageSOPClasses_.clear();
 
     // Copy the short list of storage SOP classes from DCMTK, making
-    // room for the 4 SOP classes reserved for C-ECHO, C-FIND, C-MOVE.
+    // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**).
 
     std::set<std::string> uncommon;
     uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage);
     uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage);
     uncommon.insert(UID_ColorSoftcopyPresentationStateStorage);
     uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage);
+    uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage);
 
     // Add the storage syntaxes for C-STORE
     for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++)
@@ -721,11 +749,12 @@
     pimpl_->params_ = NULL;
     pimpl_->assoc_ = NULL;
 
-    // SOP classes for C-ECHO, C-FIND and C-MOVE
+    // SOP classes for C-ECHO, C-FIND and C-MOVE (**)
     reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass);
     reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
     reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
     reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel);
 
     ResetStorageSOPClasses();
   }
@@ -1101,4 +1130,15 @@
     CheckStorageSOPClassesInvariant();
   }
 
+
+  void DicomUserConnection::FindWorklist(DicomFindAnswers& result,
+                                         ParsedDicomFile& query)
+  {
+    CheckIsOpen();
+
+    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
+    const char* sopClass = UID_FINDModalityWorklistInformationModel;
+
+    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_);
+  }
 }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Mon Nov 23 17:10:11 2015 +0100
@@ -164,5 +164,8 @@
     void SetTimeout(uint32_t seconds);
 
     void DisableTimeout();
+
+    void FindWorklist(DicomFindAnswers& result,
+                      ParsedDicomFile& query);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -620,7 +620,7 @@
       throw OrthancException(ErrorCode_BadRequest);
     }
 
-    ParsedDicomFile dicom;
+    ParsedDicomFile dicom(true);
 
     {
       Encoding encoding;
@@ -834,7 +834,7 @@
     else
     {
       // Compatibility with Orthanc <= 0.9.3
-      ParsedDicomFile dicom;
+      ParsedDicomFile dicom(true);
       CreateDicomV1(dicom, call, request);
 
       std::string id;
--- a/OrthancServer/ParsedDicomFile.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -814,13 +814,17 @@
   }
 
 
-  ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl)
+  ParsedDicomFile::ParsedDicomFile(bool createIdentifiers) : pimpl_(new PImpl)
   {
     pimpl_->file_.reset(new DcmFileFormat);
-    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));
-    Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+
+    if (createIdentifiers)
+    {
+      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));
+      Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+    }
   }
 
 
--- a/OrthancServer/ParsedDicomFile.h	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.h	Mon Nov 23 17:10:11 2015 +0100
@@ -62,7 +62,7 @@
                           bool decodeBinaryTags);
 
   public:
-    ParsedDicomFile();  // Create a minimal DICOM instance
+    ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
 
     ParsedDicomFile(const DicomMap& map);
 
--- a/OrthancServer/main.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/OrthancServer/main.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -736,7 +736,8 @@
   dicomServer.SetFindRequestHandlerFactory(serverFactory);
 
 #if ORTHANC_PLUGINS_ENABLED == 1
-  if (plugins)
+  if (plugins &&
+      plugins->HasWorklistHandler())
   {
     dicomServer.SetWorklistRequestHandlerFactory(*plugins);
   }
@@ -1106,6 +1107,23 @@
     {
       OrthancInitialize(configurationFile);
 
+      if (0)
+      {
+        // TODO REMOVE THIS TEST
+        DicomUserConnection c;
+        c.SetRemoteHost("localhost");
+        c.SetRemotePort(4243);
+        c.SetRemoteApplicationEntityTitle("ORTHANCTEST");
+        c.Open();
+        ParsedDicomFile f(false);
+        f.Replace(DICOM_TAG_PATIENT_NAME, "M*");
+        DicomFindAnswers a;
+        c.FindWorklist(a, f);
+        Json::Value j;
+        a.ToJson(j, true);
+        std::cout << j;
+      }
+
       bool restart = StartOrthanc(argc, argv, allowDatabaseUpgrade);
       if (restart)
       {
--- a/Plugins/Engine/OrthancPlugins.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -1932,7 +1932,7 @@
         ApplyDicomToJson(service, parameters);
         return true;
 
-      case _OrthancPluginService_AddWorklistAnswer:
+      case _OrthancPluginService_WorklistAddAnswer:
       {
         const _OrthancPluginWorklistAnswersOperation& p =
           *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
@@ -1940,7 +1940,7 @@
         return true;
       }
 
-      case _OrthancPluginService_MarkWorklistAnswersIncomplete:
+      case _OrthancPluginService_WorklistMarkIncomplete:
       {
         const _OrthancPluginWorklistAnswersOperation& p =
           *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
@@ -1948,7 +1948,7 @@
         return true;
       }
 
-      case _OrthancPluginService_IsWorklistMatch:
+      case _OrthancPluginService_WorklistIsMatch:
       {
         const _OrthancPluginWorklistQueryOperation& p =
           *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
@@ -1956,7 +1956,7 @@
         return true;
       }
 
-      case _OrthancPluginService_GetWorklistQueryDicom:
+      case _OrthancPluginService_WorklistGetDicomQuery:
       {
         const _OrthancPluginWorklistQueryOperation& p =
           *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
@@ -2085,14 +2085,7 @@
 
   IWorklistRequestHandler* OrthancPlugins::ConstructWorklistRequestHandler()
   {
-    bool hasHandler;
-
-    {
-      boost::recursive_mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_);
-      hasHandler = !pimpl_->worklistCallbacks_.empty();
-    }
-
-    if (hasHandler)
+    if (HasWorklistHandler())
     {
       return new WorklistHandler(*this);
     }
@@ -2101,4 +2094,12 @@
       return NULL;
     }
   }
+
+
+  bool OrthancPlugins::HasWorklistHandler()
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_);
+    return !pimpl_->worklistCallbacks_.empty();
+  }
+
 }
--- a/Plugins/Engine/OrthancPlugins.h	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.h	Mon Nov 23 17:10:11 2015 +0100
@@ -211,6 +211,7 @@
       SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
     }
 
+    bool HasWorklistHandler();
 
     virtual IWorklistRequestHandler* ConstructWorklistRequestHandler();
   };
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Nov 23 17:10:11 2015 +0100
@@ -469,10 +469,10 @@
     _OrthancPluginService_DrawText = 6011,
 
     /* Primitives for handling worklists */
-    _OrthancPluginService_AddWorklistAnswer = 7000,
-    _OrthancPluginService_MarkWorklistAnswersIncomplete = 7001,
-    _OrthancPluginService_IsWorklistMatch = 7002,
-    _OrthancPluginService_GetWorklistQueryDicom = 7003,
+    _OrthancPluginService_WorklistAddAnswer = 7000,
+    _OrthancPluginService_WorklistMarkIncomplete = 7001,
+    _OrthancPluginService_WorklistIsMatch = 7002,
+    _OrthancPluginService_WorklistGetDicomQuery = 7003,
 
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
@@ -4126,7 +4126,7 @@
    * @return 0 if success, other value if error.
    * @ingroup Worklists
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddWorklistAnswer(
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddAnswer(
     OrthancPluginContext*             context,
     OrthancPluginWorklistAnswers*     answers,
     const OrthancPluginWorklistQuery* query,
@@ -4139,7 +4139,7 @@
     params.dicom = dicom;
     params.size = size;
 
-    return context->InvokeService(context, _OrthancPluginService_AddWorklistAnswer, &params);
+    return context->InvokeService(context, _OrthancPluginService_WorklistAddAnswer, &params);
   }
 
 
@@ -4156,7 +4156,7 @@
    * @return 0 if success, other value if error.
    * @ingroup Worklists
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginMarkWorklistAnswersIncomplete(
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistMarkIncomplete(
     OrthancPluginContext*          context,
     OrthancPluginWorklistAnswers*  answers)
   {
@@ -4166,7 +4166,7 @@
     params.dicom = NULL;
     params.size = 0;
 
-    return context->InvokeService(context, _OrthancPluginService_MarkWorklistAnswersIncomplete, &params);
+    return context->InvokeService(context, _OrthancPluginService_WorklistMarkIncomplete, &params);
   }
 
 
@@ -4185,8 +4185,7 @@
    * This function checks whether one worklist (encoded as a DICOM
    * file) matches the C-Find SCP query against modality
    * worklists. This function must be called before adding the
-   * worklist as an answer through
-   * OrthancPluginWorklistAddWorklistAnswer().
+   * worklist as an answer through OrthancPluginWorklistAddAnswer().
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param query The worklist query, as received by the callback.
@@ -4195,7 +4194,7 @@
    * @return 1 if the worklist matches the query, 0 otherwise.
    * @ingroup Worklists
    **/
-  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginIsWorklistMatch(
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginWorklistIsMatch(
     OrthancPluginContext*              context,
     const OrthancPluginWorklistQuery*  query,
     const void*                        dicom,
@@ -4210,7 +4209,7 @@
     params.isMatch = &isMatch;
     params.target = NULL;
 
-    if (context->InvokeService(context, _OrthancPluginService_IsWorklistMatch, &params) == OrthancPluginErrorCode_Success)
+    if (context->InvokeService(context, _OrthancPluginService_WorklistIsMatch, &params) == OrthancPluginErrorCode_Success)
     {
       return isMatch;
     }
@@ -4234,7 +4233,7 @@
    * @return 0 if success, other value if error.
    * @ingroup Worklists
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetWorklistQueryDicom(
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistGetDicomQuery(
     OrthancPluginContext*              context,
     OrthancPluginMemoryBuffer*         target,
     const OrthancPluginWorklistQuery*  query)
@@ -4246,7 +4245,7 @@
     params.isMatch = NULL;
     params.target = target;
 
-    return context->InvokeService(context, _OrthancPluginService_GetWorklistQueryDicom, &params);
+    return context->InvokeService(context, _OrthancPluginService_WorklistGetDicomQuery, &params);
   }
 
 
--- a/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Mon Nov 23 17:10:11 2015 +0100
@@ -1,8 +1,8 @@
 cmake_minimum_required(VERSION 2.8)
 
-project(SampleModalityWorklists)
+project(ModalityWorklists)
 
-SET(SAMPLE_MODALITY_WORKLISTS_VERSION "0.0" CACHE STRING "Version of the plugin")
+SET(MODALITY_WORKLISTS_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")
 
@@ -10,28 +10,27 @@
 SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
 
 set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
 include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
 include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
 
-add_library(SampleModalityWorklists SHARED 
+add_library(ModalityWorklists SHARED 
   Plugin.cpp
   ${JSONCPP_SOURCES}
   ${BOOST_SOURCES}
   )
 
-message("Setting the version of the plugin to ${SAMPLE_MODALITY_WORKLISTS_VERSION}")
+message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}")
 add_definitions(
-  -DSAMPLE_MODALITY_WORKLISTS_VERSION="${SAMPLE_MODALITY_WORKLISTS_VERSION}"
-  -DDEFAULT_WORKLISTS_FOLDER="${CMAKE_SOURCE_DIR}/WorklistsDatabase"
+  -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}"
   )
 
-set_target_properties(SampleModalityWorklists PROPERTIES 
-  VERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION} 
-  SOVERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION})
+set_target_properties(ModalityWorklists PROPERTIES 
+  VERSION ${MODALITY_WORKLISTS_VERSION} 
+  SOVERSION ${MODALITY_WORKLISTS_VERSION})
 
 install(
-  TARGETS SampleModalityWorklists
+  TARGETS ModalityWorklists
   RUNTIME DESTINATION lib    # Destination for Windows
   LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
   )
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -61,10 +61,10 @@
     return OrthancPluginErrorCode_Success;
   }
 
-  if (OrthancPluginIsWorklistMatch(context_, query, dicom.c_str(), dicom.size()))
+  if (OrthancPluginWorklistIsMatch(context_, query, dicom.c_str(), dicom.size()))
   {
     // This DICOM file matches the worklist query, add it to the answers
-    return OrthancPluginWorklistAddWorklistAnswer
+    return OrthancPluginWorklistAddAnswer
       (context_, answers, query, dicom.c_str(), dicom.size());
   }
   else
@@ -98,7 +98,7 @@
                           const OrthancPluginWorklistQuery* query)
 {
   OrthancPluginMemoryBuffer dicom;
-  if (OrthancPluginGetWorklistQueryDicom(context_, &dicom, query))
+  if (OrthancPluginWorklistGetDicomQuery(context_, &dicom, query))
   {
     return false;
   }
@@ -169,7 +169,8 @@
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
     context_ = c;
-    OrthancPluginLogWarning(context_, "Storage plugin is initializing");
+    OrthancPluginLogWarning(context_, "Sample worklist plugin is initializing");
+    OrthancPluginSetDescription(context_, "Serve DICOM modality worklists from a folder with Orthanc.");
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
@@ -187,29 +188,53 @@
     Json::Value configuration;
     if (!ConvertToJson(configuration, OrthancPluginGetConfiguration(context_)))
     {
-      OrthancPluginLogError(context_, "Cannot access the configuration");
+      OrthancPluginLogError(context_, "Cannot access the configuration of the worklist server");
       return -1;
     }
 
-    if (configuration.isMember("WorklistsFolder"))
+    bool enabled = false;
+
+    if (configuration.isMember("Worklists"))
     {
-      if (configuration["WorklistsFolder"].type() != Json::stringValue)
+      const Json::Value& config = configuration["Worklists"];
+      if (!config.isMember("Enable") ||
+          config["Enable"].type() != Json::booleanValue)
       {
-        OrthancPluginLogError(context_, "The configuration option \"WorklistsFolder\" must be a string");
+        OrthancPluginLogError(context_, "The configuration option \"Worklists.Enable\" must contain a Boolean");
         return -1;
       }
+      else
+      {
+        enabled = config["Enable"].asBool();
+        if (enabled)
+        {
+          if (!config.isMember("Database") ||
+              config["Database"].type() != Json::stringValue)
+          {
+            OrthancPluginLogError(context_, "The configuration option \"Worklists.Database\" must contain a path");
+            return -1;
+          }
 
-      folder_ = configuration["WorklistsFolder"].asString();
+          folder_ = config["Database"].asString();
+        }
+        else
+        {
+          OrthancPluginLogWarning(context_, "Worklists server is disabled by the configuration file");
+        }
+      }
     }
     else
     {
-      folder_ = DEFAULT_WORKLISTS_FOLDER;
+      OrthancPluginLogWarning(context_, "Worklists server is disabled, no suitable configuration section was provided");
     }
 
-    std::string message = "The database of worklists will be read from folder: " + folder_;
-    OrthancPluginLogWarning(context_, message.c_str());
+    if (enabled)
+    {
+      std::string message = "The database of worklists will be read from folder: " + folder_;
+      OrthancPluginLogWarning(context_, message.c_str());
 
-    OrthancPluginRegisterWorklistCallback(context_, Callback);
+      OrthancPluginRegisterWorklistCallback(context_, Callback);
+    }
 
     return 0;
   }
@@ -223,12 +248,12 @@
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "sample-worklists";
+    return "worklists";
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
   {
-    return SAMPLE_MODALITY_WORKLISTS_VERSION;
+    return MODALITY_WORKLISTS_VERSION;
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/README	Mon Nov 23 17:10:11 2015 +0100
@@ -0,0 +1,60 @@
+Introduction
+============
+
+This sample plugin enables Orthanc to serve DICOM modality worklists
+that are read from some folder.
+
+Whenever a C-Find SCP request is issued to Orthanc, it will read the
+content of this folder to locate the worklists that match the request.
+An external application can dynamically modify the content of this
+folder while Orthanc is running to add/remove worklists.
+
+This sample mimics the behavior of the "wlmscpfs" tool from the
+DCMTK package:
+http://support.dcmtk.org/docs/wlmscpfs.html
+
+
+Compilation for Linux
+=====================
+
+# mkdir Build
+# cd Build
+# cmake ..
+# make
+
+
+Cross-compilation for Windows using MinGW
+=========================================
+
+# mkdir Build
+# cd Build
+# cmake .. -DCMAKE_TOOLCHAIN_FILE=../../../Resources/MinGWToolchain.cmake
+# make
+
+
+Configuration
+=============
+
+First, generate the default configuration of Orthanc:
+https://orthanc.chu.ulg.ac.be/book/users/configuration.html
+
+Then, modify the "Plugins" option to point to the folder containing
+the built plugins. Finally, create a section "ModalityWorklists" in
+the configuration file to configure the worklist server. If using the
+build commands above, a sample configuration would read as follows:
+
+{
+  [...]
+  "Plugins" : [ 
+    "."
+  ],
+  "Worklists" : {
+    "Enable": true,
+    "Database": "../WorklistsDatabase"
+  }
+}
+
+The folder "WorklistsDatabase" contains a database of sample
+worklists, that come from the DCMTK source distribution, as described
+in the FAQ entry #37 of the DCMTK project:
+http://forum.dcmtk.org/viewtopic.php?t=84
--- a/Plugins/Samples/ServeFolders/README	Mon Nov 23 15:24:39 2015 +0100
+++ b/Plugins/Samples/ServeFolders/README	Mon Nov 23 17:10:11 2015 +0100
@@ -27,7 +27,7 @@
 =============
 
 First, generate the default configuration of Orthanc:
-https://code.google.com/p/orthanc/wiki/OrthancConfiguration
+https://orthanc.chu.ulg.ac.be/book/users/configuration.html
 
 Then, modify the "Plugins" option to point to the folder containing
 the built plugins.
--- a/UnitTestsSources/FromDcmtkTests.cpp	Mon Nov 23 15:24:39 2015 +0100
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Mon Nov 23 17:10:11 2015 +0100
@@ -75,7 +75,7 @@
   //m.Replace(DICOM_TAG_PATIENT_ID, "coucou");
   //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
 
-  ParsedDicomFile o;
+  ParsedDicomFile o(true);
   o.SaveToFile("UnitTestsResults/anon.dcm");
 
   for (int i = 0; i < 10; i++)
@@ -103,7 +103,7 @@
   ASSERT_EQ(0x1020, privateTag2.GetElement());
 
   std::string s;
-  ParsedDicomFile o;
+  ParsedDicomFile o(true);
   o.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
   ASSERT_FALSE(o.GetTagValue(s, privateTag));
   o.Insert(privateTag, "private tag", false);
@@ -161,7 +161,7 @@
   ASSERT_EQ(5u, reader.GetWidth());
   ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
 
-  ParsedDicomFile o;
+  ParsedDicomFile o(true);
   o.EmbedContent(s);
   o.SaveToFile("UnitTestsResults/png1.dcm");
 
@@ -267,7 +267,7 @@
     std::string dicom;
 
     {
-      ParsedDicomFile f;
+      ParsedDicomFile f(true);
       f.SetEncoding(testEncodings[i]);
 
       std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]);
@@ -407,7 +407,7 @@
 
 TEST(ParsedDicomFile, InsertReplaceStrings)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   f.Insert(DICOM_TAG_PATIENT_NAME, "World", false);
   ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException);  // Already existing tag
@@ -446,7 +446,7 @@
 
 TEST(ParsedDicomFile, InsertReplaceJson)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   Json::Value a;
   CreateSampleJson(a);
@@ -498,7 +498,7 @@
 
 TEST(ParsedDicomFile, JsonEncoding)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   for (unsigned int i = 0; i < testEncodingsCount; i++)
   {
@@ -528,7 +528,7 @@
   FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), EVR_PN, "MyPrivateTag", 1, 1);
   FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), EVR_PN, "Declared public tag", 1, 1);
 
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
   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
@@ -575,7 +575,7 @@
 
 TEST(ParsedDicomFile, ToJsonFlags2)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
   f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false);
 
   Json::Value v;
@@ -620,7 +620,7 @@
   }
 
   {
-    ParsedDicomFile d;
+    ParsedDicomFile d(true);
     d.Replace(DICOM_TAG_PATIENT_ID, "my");
     a.Add(d);
   }