changeset 4740:c1d6ce00be3f openssl-3.x

integration mainline->openssl-3.x
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 06 Jul 2021 08:40:43 +0200
parents 8cc9137b5c2e (current diff) 9ce946d28e41 (diff)
children a6b7c29f5118
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancServer/OrthancExplorer/explorer.js OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h OrthancServer/UnitTestsSources/ServerJobsTests.cpp TODO
diffstat 23 files changed, 1025 insertions(+), 292 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Jul 06 08:36:54 2021 +0200
+++ b/NEWS	Tue Jul 06 08:40:43 2021 +0200
@@ -1,6 +1,40 @@
 Pending changes in the mainline
 ===============================
 
+OpenSSL 3.x branch
+------------------
+
+* General information:
+  https://www.openssl.org/blog/blog/2021/06/17/OpenSSL3.0ReleaseCandidate/
+* Dropped support for static compilation of OpenSSL 1.0.2 and 1.1.1
+* Removed the OpenSSL license exception, as binary versions of Orthanc are now
+  designed to use OpenSSL 3.x, that was re-licensed under Apache 2.0, making
+  it compatible with the GPL/AGPL licenses used by the Orthanc project:
+  https://en.wikipedia.org/wiki/OpenSSL#Licensing
+  https://people.gnome.org/~markmc/openssl-and-the-gpl.html
+* Upgraded dependencies for static builds (notably on Windows and LSB):
+  - openssl 3.0.0-beta1
+
+General
+-------
+
+* Anonymization is now also applied recursively to nested tags
+
+REST API
+--------
+
+* API version upgraded to 14
+* Added "Short", "Simplify" and/or "Full" options to control the format of DICOM tags in:
+  - POST /modalities/{id}/find-worklist
+  - POST /queries/{id}/answers/{index}/retrieve
+  - POST /queries/{id}/retrieve
+
+Maintenance
+-----------
+
+* Fix broken "Do lookup" button in Orthanc Explorer
+* Error code and description of jobs are now saved into the Orthanc database
+
 
 Version 1.9.4 (2021-06-24)
 ==========================
@@ -44,6 +78,7 @@
   - GET /series/{id}/instances-tags, GET /studies/{id}/shared-tags
   - GET /patients/{id}/module, GET /patients/{id}/patient-module
   - GET /series/{id}/module, GET /studies/{id}/module, GET /instances/{id}/module
+  - GET /queries/{id}/answers&expand, GET /queries/{id}/answers/{index}/content
   - POST /tools/find
 * "/studies/{id}/split" accepts "Instances" parameter to split instances instead of series
 * "/studies/{id}/merge" accepts instances inside its "Resources" parameter
@@ -57,20 +92,6 @@
 * Upgraded dependencies for static builds (notably on Windows):
   - curl 7.77.0
 
-OpenSSL 3.x branch
-------------------
-
-* General information:
-  https://www.openssl.org/blog/blog/2021/06/17/OpenSSL3.0ReleaseCandidate/
-* Dropped support for static compilation of OpenSSL 1.0.2 and 1.1.1
-* Removed the OpenSSL license exception, as binary versions of Orthanc are now
-  designed to use OpenSSL 3.x, that was re-licensed under Apache 2.0, making
-  it compatible with the GPL/AGPL licenses used by the Orthanc project:
-  https://en.wikipedia.org/wiki/OpenSSL#Licensing
-  https://people.gnome.org/~markmc/openssl-and-the-gpl.html
-* Upgraded dependencies for static builds (notably on Windows and LSB):
-  - openssl 3.0.0-beta1
-
 
 Version 1.9.3 (2021-05-07)
 ==========================
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Jul 06 08:40:43 2021 +0200
@@ -37,7 +37,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "13")
+set(ORTHANC_API_VERSION "14")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -372,4 +372,42 @@
       }
     }
   }
+
+
+  bool DicomPath::IsMatch(const DicomPath& pattern,
+                          const std::vector<Orthanc::DicomTag>& prefixTags,
+                          const std::vector<size_t>& prefixIndexes,
+                          const DicomTag& finalTag)
+  {
+    if (prefixTags.size() != prefixIndexes.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    if (prefixTags.size() < pattern.GetPrefixLength())
+    {
+      return false;
+    }
+    else
+    {
+      for (size_t i = 0; i < pattern.GetPrefixLength(); i++)
+      {
+        if (prefixTags[i] != pattern.GetPrefixTag(i) ||
+            (!pattern.IsPrefixUniversal(i) &&
+             prefixIndexes[i] != pattern.GetPrefixIndex(i)))
+        {
+          return false;
+        }
+      }
+
+      if (prefixTags.size() == pattern.GetPrefixLength())
+      {
+        return (finalTag == pattern.GetFinalTag());
+      }
+      else
+      {
+        return (prefixTags[pattern.GetPrefixLength()] == pattern.GetFinalTag());
+      }
+    }
+  }    
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h	Tue Jul 06 08:40:43 2021 +0200
@@ -130,5 +130,10 @@
 
     static bool IsMatch(const DicomPath& pattern,
                         const DicomPath& path);
+
+    static bool IsMatch(const DicomPath& pattern,
+                        const std::vector<Orthanc::DicomTag>& prefixTags,
+                        const std::vector<size_t>& prefixIndexes,
+                        const DicomTag& finalTag);
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -197,24 +197,22 @@
 
   void DicomFindAnswers::ToJson(Json::Value& target,
                                 size_t index,
-                                bool simplify) const
+                                DicomToJsonFormat format) const
   {
-    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
-    
     const ParsedDicomFile& answer = GetAnswer(index);
     answer.DatasetToJson(target, format, DicomToJsonFlags_None, 0);
   }
 
 
   void DicomFindAnswers::ToJson(Json::Value& target,
-                                bool simplify) const
+                                DicomToJsonFormat format) const
   {
     target = Json::arrayValue;
 
     for (size_t i = 0; i < GetSize(); i++)
     {
       Json::Value answer;
-      ToJson(answer, i, simplify);
+      ToJson(answer, i, format);
       target.append(answer);
     }
   }
@@ -236,5 +234,21 @@
   {
     return Add(const_cast<const ParsedDicomFile&>(dicom));
   }
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                size_t index,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
+    ToJson(target, index, format);
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
+    ToJson(target, format);
+  }
 #endif
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Tue Jul 06 08:40:43 2021 +0200
@@ -39,6 +39,13 @@
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
     // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
     void Add(ParsedDicomFile& dicom);
+
+    void ToJson(Json::Value& target,
+                bool simplify) const;
+
+    void ToJson(Json::Value& target,
+                size_t index,
+                bool simplify) const;
 #endif
 
   public:
@@ -72,11 +79,11 @@
     DcmDataset* ExtractDcmDataset(size_t index) const;
 
     void ToJson(Json::Value& target,
-                bool simplify) const;
+                DicomToJsonFormat format) const;
 
     void ToJson(Json::Value& target,
                 size_t index,
-                bool simplify) const;
+                DicomToJsonFormat format) const;
 
     bool IsComplete() const;
 
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -44,6 +44,16 @@
 
 namespace Orthanc
 {
+  namespace
+  {
+    enum TagOperation
+    {
+      TagOperation_Keep,
+      TagOperation_Remove
+    };
+  }
+
+  
   DicomModification::DicomTagRange::DicomTagRange(uint16_t groupFrom,
                                                   uint16_t groupTo,
                                                   uint16_t elementFrom,
@@ -76,7 +86,57 @@
       return (that_.IsCleared(tag) ||
               that_.IsRemoved(tag) ||
               that_.IsReplaced(tag));
-    }                         
+    }
+
+    bool IsKeptSequence(const std::vector<DicomTag>& parentTags,
+                        const std::vector<size_t>& parentIndexes,
+                        const DicomTag& tag)
+    {
+      for (DicomModification::ListOfPaths::const_iterator
+             it = that_.keepSequences_.begin(); it != that_.keepSequences_.end(); ++it)
+      {
+        if (DicomPath::IsMatch(*it, parentTags, parentIndexes, tag))
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    Action GetDefaultAction(const std::vector<DicomTag>& parentTags,
+                            const std::vector<size_t>& parentIndexes,
+                            const DicomTag& tag)
+    {
+      if (parentTags.empty() ||
+          !that_.isAnonymization_)
+      {
+        // Don't interfere with first-level tags or with modification
+        return Action_None;
+      }
+      else if (IsKeptSequence(parentTags, parentIndexes, tag))
+      {
+        return Action_None;
+      }
+      else if (that_.ArePrivateTagsRemoved() &&
+               tag.IsPrivate())
+      {
+        // New in Orthanc 1.9.5
+        // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+        return Action_Remove;
+      }
+      else if (that_.IsCleared(tag) ||
+               that_.IsRemoved(tag))
+      {
+        // New in Orthanc 1.9.5
+        // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+        return Action_Remove;
+      }
+      else
+      {
+        return Action_None;
+      }
+    }
 
   public:
     explicit RelationshipsVisitor(DicomModification& that) :
@@ -84,49 +144,56 @@
     {
     }
 
-    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
-                                   const std::vector<size_t>& parentIndexes,
-                                   const DicomTag& tag,
-                                   ValueRepresentation vr)
+    virtual Action VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr) ORTHANC_OVERRIDE
     {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
     }
 
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag)
+    virtual Action VisitSequence(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems) ORTHANC_OVERRIDE
     {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
     }
 
-    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
-                             const std::vector<size_t>& parentIndexes,
-                             const DicomTag& tag,
-                             ValueRepresentation vr,
-                             const void* data,
-                             size_t size)
-    {
-    }
-
-    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+    virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
                                const DicomTag& tag,
                                ValueRepresentation vr,
-                               const std::vector<int64_t>& values)
+                               const void* data,
+                               size_t size) ORTHANC_OVERRIDE
     {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector<int64_t>& values) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
     }
 
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& value)
+    virtual Action VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector<double>& value) ORTHANC_OVERRIDE
     {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
     }
 
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<DicomTag>& value)
+    virtual Action VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector<DicomTag>& value) ORTHANC_OVERRIDE
     {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
     }
 
     virtual Action VisitString(std::string& newValue,
@@ -134,7 +201,7 @@
                                const std::vector<size_t>& parentIndexes,
                                const DicomTag& tag,
                                ValueRepresentation vr,
-                               const std::string& value)
+                               const std::string& value) ORTHANC_OVERRIDE
     {
       /**
        * Note that all the tags in "uids_" have the VR UI (unique
@@ -181,18 +248,30 @@
       {
         // We are within a sequence
 
-        if (!that_.keepSequences_.empty())
+        if (IsKeptSequence(parentTags, parentIndexes, tag))
         {
           // New in Orthanc 1.9.4 - Solves issue LSD-629
-          DicomPath path(parentTags, parentIndexes, tag);
-          
-          for (ListOfPaths::const_iterator it = that_.keepSequences_.begin();
-               it != that_.keepSequences_.end(); ++it)
+          return Action_None;
+        }
+
+        if (that_.isAnonymization_)
+        {
+          // New in Orthanc 1.9.5, similar to "GetDefaultAction()"
+          // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+          if (that_.ArePrivateTagsRemoved() &&
+              tag.IsPrivate())
           {
-            if (DicomPath::IsMatch(*it, path))
-            {
-              return Action_None;
-            }
+            return Action_Remove;
+          }
+          else if (that_.IsRemoved(tag))
+          {
+            return Action_Remove;
+          }
+          else if (that_.IsCleared(tag))
+          {
+            // This is different from "GetDefaultAction()", because we know how to clear string tags
+            newValue.clear();
+            return Action_Replace;
           }
         }
 
@@ -1100,7 +1179,7 @@
 
   static void ParseListOfTags(DicomModification& target,
                               const Json::Value& query,
-                              DicomModification::TagOperation operation,
+                              TagOperation operation,
                               bool force)
   {
     if (!query.isArray())
@@ -1125,18 +1204,18 @@
       {
         throw OrthancException(ErrorCode_BadRequest,
                                "Marking tag \"" + name + "\" as to be " +
-                               (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") +
+                               (operation == TagOperation_Keep ? "kept" : "removed") +
                                " requires the \"Force\" option to be set to true");
       }
 
       switch (operation)
       {
-        case DicomModification::TagOperation_Keep:
+        case TagOperation_Keep:
           target.Keep(path);
           LOG(TRACE) << "Keep: " << name << " = " << path.Format();
           break;
 
-        case DicomModification::TagOperation_Remove:
+        case TagOperation_Remove:
           target.Remove(path);
           LOG(TRACE) << "Remove: " << name << " = " << path.Format();
           break;
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Jul 06 08:40:43 2021 +0200
@@ -39,12 +39,6 @@
      **/
 
   public:
-    enum TagOperation
-    {
-      TagOperation_Keep,
-      TagOperation_Remove
-    };
-
     class IDicomIdentifierGenerator : public boost::noncopyable
     {
     public:
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -371,32 +371,40 @@
 #endif
 
 
-  void DicomWebJsonVisitor::VisitNotSupported(const std::vector<DicomTag> &parentTags,
-                                              const std::vector<size_t> &parentIndexes,
-                                              const DicomTag &tag,
-                                              ValueRepresentation vr)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitNotSupported(const std::vector<DicomTag> &parentTags,
+                                         const std::vector<size_t> &parentIndexes,
+                                         const DicomTag &tag,
+                                         ValueRepresentation vr)
   {
+    return Action_None;
   }
 
 
-  void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                               const std::vector<size_t>& parentIndexes,
-                                               const DicomTag& tag)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitSequence(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     size_t countItems)
   {
-    if (tag.GetElement() != 0x0000)
+    if (countItems == 0 &&
+        tag.GetElement() != 0x0000)
     {
       Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
       node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
     }
+
+    return Action_None;
   }
   
 
-  void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
-                                        const std::vector<size_t>& parentIndexes,
-                                        const DicomTag& tag,
-                                        ValueRepresentation vr,
-                                        const void* data,
-                                        size_t size)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr,
+                                   const void* data,
+                                   size_t size)
   {
     assert(vr == ValueRepresentation_OtherByte ||
            vr == ValueRepresentation_OtherDouble ||
@@ -456,14 +464,17 @@
         }
       }
     }
+
+    return Action_None;
   }
 
 
-  void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
-                                          const std::vector<size_t>& parentIndexes,
-                                          const DicomTag& tag,
-                                          ValueRepresentation vr,
-                                          const std::vector<int64_t>& values)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr,
+                                     const std::vector<int64_t>& values)
   {
     if (tag.GetElement() != 0x0000 &&
         vr != ValueRepresentation_NotSupported)
@@ -482,13 +493,16 @@
         node[KEY_VALUE] = content;
       }
     }
+
+    return Action_None;
   }
 
-  void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
-                                         const std::vector<size_t>& parentIndexes,
-                                         const DicomTag& tag,
-                                         ValueRepresentation vr,
-                                         const std::vector<double>& values)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag,
+                                    ValueRepresentation vr,
+                                    const std::vector<double>& values)
   {
     if (tag.GetElement() != 0x0000 &&
         vr != ValueRepresentation_NotSupported)
@@ -507,13 +521,16 @@
         node[KEY_VALUE] = content;
       }
     }
+
+    return Action_None;
   }
 
   
-  void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                            const std::vector<size_t>& parentIndexes,
-                                            const DicomTag& tag,
-                                            const std::vector<DicomTag>& values)
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                       const std::vector<size_t>& parentIndexes,
+                                       const DicomTag& tag,
+                                       const std::vector<DicomTag>& values)
   {
     if (tag.GetElement() != 0x0000)
     {
@@ -531,6 +548,8 @@
         node[KEY_VALUE] = content;
       }
     }
+
+    return Action_None;
   }
 
   
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Tue Jul 06 08:40:43 2021 +0200
@@ -85,43 +85,44 @@
     void FormatXml(std::string& target) const;
 #endif
 
-    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
-                                   const std::vector<size_t>& parentIndexes,
-                                   const DicomTag& tag,
-                                   ValueRepresentation vr)
+    virtual Action VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr)
       ORTHANC_OVERRIDE;
 
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag)
+    virtual Action VisitSequence(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems)
       ORTHANC_OVERRIDE;
 
-    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
-                             const std::vector<size_t>& parentIndexes,
-                             const DicomTag& tag,
-                             ValueRepresentation vr,
-                             const void* data,
-                             size_t size)
-      ORTHANC_OVERRIDE;
-
-    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+    virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
                                const DicomTag& tag,
                                ValueRepresentation vr,
-                               const std::vector<int64_t>& values)
+                               const void* data,
+                               size_t size)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector<int64_t>& values)
       ORTHANC_OVERRIDE;
 
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& values)
+    virtual Action VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector<double>& values)
       ORTHANC_OVERRIDE;
 
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<DicomTag>& values)
+    virtual Action VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector<DicomTag>& values)
       ORTHANC_OVERRIDE;
 
     virtual Action VisitString(std::string& newValue,
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -2340,7 +2340,7 @@
 
 
   // Forward declaration
-  static void ApplyVisitorToElement(DcmElement& element,
+  static bool ApplyVisitorToElement(DcmElement& element,
                                     ITagVisitor& visitor,
                                     const std::vector<DicomTag>& parentTags,
                                     const std::vector<size_t>& parentIndexes,
@@ -2356,6 +2356,8 @@
   {
     assert(parentTags.size() == parentIndexes.size());
 
+    std::set<DcmTagKey> toRemove;
+    
     for (unsigned long i = 0; i < dataset.card(); i++)
     {
       DcmElement* element = dataset.getElement(i);
@@ -2365,13 +2367,25 @@
       }
       else
       {
-        ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
+        if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions))
+        {
+          toRemove.insert(element->getTag());
+        }
       }      
     }
+
+    // Remove all the tags that were planned for removal (cf. ITagVisitor::Action_Remove)
+    for (std::set<DcmTagKey>::const_iterator
+           it = toRemove.begin(); it != toRemove.end(); ++it)
+    {
+      std::unique_ptr<DcmElement> tmp(dataset.remove(*it));
+    }
   }
 
 
-  static void ApplyVisitorToLeaf(DcmElement& element,
+  // Returns "true" iff the element must be kept. If "false" is
+  // returned, the element will be removed.
+  static bool ApplyVisitorToLeaf(DcmElement& element,
                                  ITagVisitor& visitor,
                                  const std::vector<DicomTag>& parentTags,
                                  const std::vector<size_t>& parentIndexes,
@@ -2401,6 +2415,20 @@
       evr = EVR_UN;
     }
 
+    if (evr == EVR_UN)
+    {
+      // New in Orthanc 1.9.5
+      DictionaryLocker locker;
+      
+      const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(),
+                                                    element.getTag().getPrivateCreator());
+
+      if (entry != NULL)
+      {
+        evr = entry->getEVR();
+      }
+    }
+
     const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
 
     
@@ -2415,11 +2443,13 @@
       Uint16* data16 = NULL;
       Uint8* data = NULL;
 
+      ITagVisitor::Action action;
+      
       if ((element.getTag() == DCM_PixelData ||  // (*) New in Orthanc 1.9.1
            evr == EVR_OW) &&
           element.getUint16Array(data16) == EC_Normal)
       {
-        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
+        action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
       }
       else if (evr != EVR_OW &&
                element.getUint8Array(data) == EC_Normal)
@@ -2432,14 +2462,27 @@
          * reimplemented in derived class "DcmPixelData"). However,
          * "getUint16Array()" works correctly, hence (*).
          **/
-        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+        action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
       }
       else
       {
-        visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+        action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
       }
 
-      return;  // We're done
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          return true;  // We're done
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace binary data");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
     }
 
 
@@ -2473,7 +2516,10 @@
       switch (action)
       {
         case ITagVisitor::Action_None:
-          break;
+          return true;
+
+        case ITagVisitor::Action_Remove:
+          return false;
 
         case ITagVisitor::Action_Replace:
         {
@@ -2481,20 +2527,20 @@
           if (element.putString(s.c_str()) != EC_Normal)
           {
             throw OrthancException(ErrorCode_InternalError,
-                                   "Cannot replace value of tag: " + tag.Format());
+                                   "Iterator cannot replace value of tag: " + tag.Format());
           }
 
-          break;
+          return true;
         }
 
         default:
           throw OrthancException(ErrorCode_InternalError);
       }
-
-      return;  // We're done
     }
 
 
+    ITagVisitor::Action action;
+    
     try
     {
       // http://support.dcmtk.org/docs/dcvr_8h-source.html
@@ -2522,7 +2568,7 @@
         case EVR_UI:  // unique identifier
         {
           Uint8* data = NULL;
-
+          
           if (element.getUint8Array(data) == EC_Normal)
           {
             const Uint32 length = element.getLength();
@@ -2536,30 +2582,30 @@
             if (l == length)
             {
               // Not a null-terminated plain string
-              visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+              action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
             }
             else
             {
               std::string ignored;
               std::string s(reinterpret_cast<const char*>(data), l);
-              ITagVisitor::Action action = visitor.VisitString
-                (ignored, parentTags, parentIndexes, tag, vr,
-                 Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
-
-              if (action != ITagVisitor::Action_None)
-              {
-                LOG(WARNING) << "Cannot replace this string tag: "
-                             << FromDcmtkBridge::GetTagName(element)
-                             << " (" << tag.Format() << ")";
-              }
+              action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr,
+                                           Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
             }
           }
           else
           {
-            visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+            action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
           }
 
-          return;
+          if (action == ITagVisitor::Action_Replace)
+          {
+            LOG(WARNING) << "Iterator cannot replace this string tag: "
+                         << FromDcmtkBridge::GetTagName(element)
+                         << " (" << tag.Format() << ")";
+            return true;
+          }
+
+          break;
         }
     
         /**
@@ -2582,7 +2628,7 @@
             }
           }
 
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2602,7 +2648,7 @@
             }
           }
 
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2625,7 +2671,7 @@
             }
           }
 
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2645,7 +2691,7 @@
             }
           }
 
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2666,7 +2712,7 @@
             }
           }
 
-          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2689,7 +2735,7 @@
             }
           }
 
-          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2716,7 +2762,7 @@
           }
 
           assert(vr == ValueRepresentation_AttributeTag);
-          visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
+          action = visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
           break;
         }
 
@@ -2728,7 +2774,7 @@
 
         case EVR_SQ:  // sequence of items
         {
-          return;
+          return true;
         }
         
         
@@ -2751,8 +2797,8 @@
         case EVR_PixelData:  // used internally for uncompressed pixeld data
         case EVR_OverlayData:  // used internally for overlay data
         {
-          visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
-          return;
+          action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+          break;
         }
         
 
@@ -2761,21 +2807,38 @@
          **/ 
 
         default:
-          return;
+          return true;
+      }
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          return true;  // We're done
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace non-string-like data");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
     catch (boost::bad_lexical_cast&)
     {
-      return;
+      return true;
     }
     catch (std::bad_cast&)
     {
-      return;
+      return true;
     }
   }
 
 
-  static void ApplyVisitorToElement(DcmElement& element,
+  // Returns "true" iff the element must be kept. If "false" is
+  // returned, the element will be removed.
+  static bool ApplyVisitorToElement(DcmElement& element,
                                     ITagVisitor& visitor,
                                     const std::vector<DicomTag>& parentTags,
                                     const std::vector<size_t>& parentIndexes,
@@ -2788,7 +2851,7 @@
 
     if (element.isLeaf())
     {
-      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
+      return ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
     }
     else
     {
@@ -2797,24 +2860,38 @@
       // etc. are not." The following dynamic_cast is thus OK.
       DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
 
-      if (sequence.card() == 0)
-      {
-        visitor.VisitEmptySequence(parentTags, parentIndexes, tag);
-      }
-      else
+      ITagVisitor::Action action = visitor.VisitSequence(parentTags, parentIndexes, tag, sequence.card());
+
+      switch (action)
       {
-        std::vector<DicomTag> tags = parentTags;
-        std::vector<size_t> indexes = parentIndexes;
-        tags.push_back(tag);
-        indexes.push_back(0);
-
-        for (unsigned long i = 0; i < sequence.card(); i++)
-        {
-          indexes.back() = static_cast<size_t>(i);
-          DcmItem* child = sequence.getItem(i);
-          ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions);
-        }
+        case ITagVisitor::Action_None:
+          if (sequence.card() != 0)  // Minor optimization to avoid creating "tags" and "indexes" if not needed
+          {
+            std::vector<DicomTag> tags = parentTags;
+            std::vector<size_t> indexes = parentIndexes;
+            tags.push_back(tag);
+            indexes.push_back(0);
+
+            for (unsigned long i = 0; i < sequence.card(); i++)
+            {
+              indexes.back() = static_cast<size_t>(i);
+              DcmItem* child = sequence.getItem(i);
+              ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions);
+            }
+          }
+
+          return true;  // Keep
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace sequences");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
+
     }
   }
 
--- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Tue Jul 06 08:40:43 2021 +0200
@@ -35,6 +35,7 @@
     enum Action
     {
       Action_Replace,
+      Action_Remove,  // New in Orthanc 1.9.5
       Action_None
     };
 
@@ -42,46 +43,48 @@
     {
     }
 
-    // Visiting a DICOM element that is internal to DCMTK
-    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+    // Visiting a DICOM element that is internal to DCMTK. Can return
+    // "Remove" or "None".
+    virtual Action VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr) = 0;
+
+    // SQ - can return "Remove" or "None"
+    virtual Action VisitSequence(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems) = 0;
+
+    // SL, SS, UL, US - can return "Remove" or "None"
+    virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector<int64_t>& values) = 0;
+
+    // FL, FD, OD, OF - can return "Remove" or "None"
+    virtual Action VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector<double>& values) = 0;
+
+    // AT - can return "Remove" or "None"
+    virtual Action VisitAttributes(const std::vector<DicomTag>& parentTags,
                                    const std::vector<size_t>& parentIndexes,
                                    const DicomTag& tag,
-                                   ValueRepresentation vr) = 0;
+                                   const std::vector<DicomTag>& values) = 0;
 
-    // SQ
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag) = 0;
-
-    // SL, SS, UL, US
-    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+    // OB, OL, OW, UN - can return "Remove" or "None"
+    virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
                                const DicomTag& tag,
                                ValueRepresentation vr,
-                               const std::vector<int64_t>& values) = 0;
-
-    // FL, FD, OD, OF
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& values) = 0;
+                               const void* data,
+                               size_t size) = 0;
 
-    // AT
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<DicomTag>& values) = 0;
-
-    // OB, OL, OW, UN
-    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
-                             const std::vector<size_t>& parentIndexes,
-                             const DicomTag& tag,
-                             ValueRepresentation vr,
-                             const void* data,
-                             size_t size) = 0;
-
-    // Visiting an UTF-8 string
+    // Visiting an UTF-8 string - can return "Replace", "Remove" or "None"
     virtual Action VisitString(std::string& newValue,
                                const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
--- a/OrthancFramework/Sources/Enumerations.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -1158,6 +1158,25 @@
   }
 
 
+  const char* EnumerationToString(DicomToJsonFormat format)
+  {
+    switch (format)
+    {
+      case DicomToJsonFormat_Full:
+        return "Full";
+
+      case DicomToJsonFormat_Human:
+        return "Simplify";
+
+      case DicomToJsonFormat_Short:
+        return "Short";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   Encoding StringToEncoding(const char* encoding)
   {
     std::string s(encoding);
@@ -1809,6 +1828,27 @@
   }
   
 
+  DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format)
+  {
+    if (format == "Full")
+    {
+      return DicomToJsonFormat_Full;
+    }
+    else if (format == "Short")
+    {
+      return DicomToJsonFormat_Short;
+    }
+    else if (format == "Simplify")
+    {
+      return DicomToJsonFormat_Human;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   unsigned int GetBytesPerPixel(PixelFormat format)
   {
     switch (format)
--- a/OrthancFramework/Sources/Enumerations.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Jul 06 08:40:43 2021 +0200
@@ -801,6 +801,9 @@
   const char* EnumerationToString(StorageCommitmentFailureReason reason);
 
   ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomToJsonFormat format);
+
+  ORTHANC_PUBLIC
   Encoding StringToEncoding(const char* encoding);
 
   ORTHANC_PUBLIC
@@ -832,6 +835,9 @@
   MimeType StringToMimeType(const std::string& mime);
   
   ORTHANC_PUBLIC
+  DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format);
+  
+  ORTHANC_PUBLIC
   bool LookupMimeType(MimeType& target,
                       const std::string& source);
   
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -39,6 +39,8 @@
   static const char* CREATION_TIME = "CreationTime";
   static const char* LAST_CHANGE_TIME = "LastChangeTime";
   static const char* RUNTIME = "Runtime";
+  static const char* ERROR_CODE = "ErrorCode";
+  static const char* ERROR_DETAILS = "ErrorDetails";
 
 
   class JobsRegistry::JobHandler : public boost::noncopyable
@@ -276,6 +278,11 @@
         target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_);
         target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_);
         target[RUNTIME] = static_cast<unsigned int>(runtime_.total_milliseconds());
+
+        // New in Orthanc 1.9.5
+        target[ERROR_CODE] = static_cast<int>(lastStatus_.GetErrorCode());
+        target[ERROR_DETAILS] = lastStatus_.GetDetails();
+        
         return true;
       }
       else
@@ -307,7 +314,23 @@
       job_->GetJobType(jobType_);
       job_->Start();
 
-      lastStatus_ = JobStatus(ErrorCode_Success, "", *job_);
+      ErrorCode errorCode;
+      if (serialized.isMember(ERROR_CODE))
+      {
+        errorCode = static_cast<ErrorCode>(SerializationToolbox::ReadInteger(serialized, ERROR_CODE));
+      }
+      else
+      {
+        errorCode = ErrorCode_Success;  // Backward compatibility with Orthanc <= 1.9.4
+      }
+
+      std::string details;
+      if (serialized.isMember(ERROR_DETAILS))  // Backward compatibility with Orthanc <= 1.9.4
+      {
+        details = SerializationToolbox::ReadString(serialized, ERROR_DETAILS);
+      }
+
+      lastStatus_ = JobStatus(errorCode, details, *job_);
     }
   };
 
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -61,6 +61,9 @@
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcelem.h>
 #include <dcmtk/dcmdata/dcvrat.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+#include <dcmtk/dcmdata/dcvrss.h>
+#include <dcmtk/dcmdata/dcvrfl.h>
 
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/lexical_cast.hpp>
@@ -689,7 +692,7 @@
   }
 
   Json::Value j;
-  a.ToJson(j, true);
+  a.ToJson(j, DicomToJsonFormat_Human);
   ASSERT_EQ(3u, j.size());
 
   //std::cout << j;
@@ -2258,6 +2261,31 @@
 }
 
 
+static bool MyIsMatch(const DicomPath& a,
+                      const DicomPath& b)
+{
+  bool expected = DicomPath::IsMatch(a, b);
+
+  std::vector<DicomTag> prefixTags;
+  std::vector<size_t> prefixIndexes;
+
+  for (size_t i = 0; i < b.GetPrefixLength(); i++)
+  {
+    prefixTags.push_back(b.GetPrefixTag(i));
+    prefixIndexes.push_back(b.GetPrefixIndex(i));
+  }
+
+  if (expected == DicomPath::IsMatch(a, prefixTags, prefixIndexes, b.GetFinalTag()))
+  {
+    return expected;
+  }
+  else
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+}
+
+
 TEST(DicomModification, DicomPath)
 {
   // Those are samples inspired by those from "man dcmodify"
@@ -2363,30 +2391,30 @@
   ASSERT_THROW(DicomPath::Parse("(0010,0010)0].PatientID"), OrthancException);
   ASSERT_THROW(DicomPath::Parse("(0010,0010)[-1].PatientID"), OrthancException);
 
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"),
-                                 DicomPath::Parse("(0010,0010)")));
-  ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"),
-                                  DicomPath::Parse("(0010,0020)")));
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"),
-                                 DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
-  ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
-                                  DicomPath::Parse("(0010,0010)")));
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
-                                 DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)"),
-                                 DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
-  ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)"),
-                                  DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
-  ASSERT_THROW(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
-                                  DicomPath::Parse("(0010,0010)[*].(0010,0020)")), OrthancException);
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)[*].(0010,0030)"),
-                                 DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
-  ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)"),
-                                 DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
-  ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[3].(0010,0030)"),
-                                  DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
-  ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)[2].(0010,0030)"),
-                                  DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"),
+                        DicomPath::Parse("(0010,0010)")));
+  ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)"),
+                         DicomPath::Parse("(0010,0020)")));
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"),
+                        DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
+  ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
+                         DicomPath::Parse("(0010,0010)")));
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
+                        DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)"),
+                        DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
+  ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)"),
+                         DicomPath::Parse("(0010,0010)[1].(0010,0020)")));
+  ASSERT_THROW(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"),
+                         DicomPath::Parse("(0010,0010)[*].(0010,0020)")), OrthancException);
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)[*].(0010,0030)"),
+                        DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
+  ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)"),
+                        DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
+  ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[3].(0010,0030)"),
+                         DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
+  ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)[2].(0010,0030)"),
+                         DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)")));
 }
 
 
@@ -2686,7 +2714,10 @@
     ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv1[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString());
     ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv1[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString());
     ASSERT_NE("1.2.840.113704.1.111.7016.1342451220.40", vv1[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString());
-    ASSERT_EQ("WORLD", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][SERIES_DESCRIPTION].asString());
+
+    // Contrarily to Orthanc 1.9.4, the "SERIES_DESCRIPTION" is also removed from nested sequences
+    ASSERT_EQ(1u, vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0].size());
+    ASSERT_EQ("122403", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0]["0008,0100"].asString());
   }
 
   {
@@ -2710,6 +2741,258 @@
 }
 
 
+TEST(FromDcmtkBridge, VisitorRemoveTag)
+{
+  class V : public ITagVisitor
+  {
+  private:
+    uint32_t seen_;
+    
+  public:
+    V() : seen_(0)
+    {
+    }
+
+    unsigned int GetSeen() const
+    {
+      return seen_;
+    }
+    
+    virtual Action VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                     const std::vector<size_t>& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 0);
+      
+      if (parentTags.size() == 0u &&
+          parentIndexes.size() == 0u &&
+          DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_PixelData)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }        
+    }
+
+    virtual Action VisitSequence(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 1);
+      
+      if (parentTags.size() == 0u &&
+          parentIndexes.size() == 0u &&
+          tag == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+          countItems == 1)
+      {
+        return Action_None;
+      }
+      else if (parentTags.size() == 1u &&
+               parentIndexes.size() == 1u &&
+               parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+               parentIndexes[0] == 0u &&
+               countItems == 0 &&
+               DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedPatientSequence)
+      {
+        return Action_Remove;
+      }
+      else if (parentTags.size() == 1u &&
+               parentIndexes.size() == 1u &&
+               parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+               parentIndexes[0] == 0u &&
+               countItems == 1 &&
+               DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedStudySequence)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }        
+    }
+
+    virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector<int64_t>& values) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 2);
+      
+      if (parentTags.size() == 0u &&
+          parentIndexes.size() == 0u &&
+          DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_TagAngleSecondAxis &&
+          values.size() == 2 &&
+          values[0] == 12 &&
+          values[1] == 13)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual Action VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector<double>& values) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 3);
+      
+      if (parentTags.size() == 1u &&
+          parentIndexes.size() == 1u &&
+          parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+          parentIndexes[0] == 0u &&
+          DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ExaminedBodyThickness &&
+          values.size() == 3 &&
+          std::abs(values[0] - 42.0f) <= 0.001f &&
+          std::abs(values[1] - 43.0f) <= 0.001f &&
+          std::abs(values[2] - 47.0f) <= 0.001f)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual Action VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector<DicomTag>& values) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 4);
+      
+      if (parentTags.size() == 1u &&
+          parentIndexes.size() == 1u &&
+          parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+          parentIndexes[0] == 0u &&
+          DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_DimensionIndexPointer &&
+          values.size() == 2 &&
+          values[0] == DICOM_TAG_STUDY_DATE &&
+          values[1] == DICOM_TAG_STUDY_TIME)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const void* data,
+                               size_t size) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 5);
+      
+      if (parentTags.size() == 1u &&
+          parentIndexes.size() == 1u &&
+          parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE &&
+          parentIndexes[0] == 0u &&
+          tag.GetGroup() == 0x0011 &&
+          tag.GetElement() == 0x1311 &&
+          size == 4u &&
+          memcmp(data, "abcd", 4) == 0)
+      {
+        return Action_Remove;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      seen_ |= (1 << 6);
+      return Action_Remove;
+    }
+  };
+
+
+  std::unique_ptr<ParsedDicomFile> dicom;
+
+  {
+    Json::Value v = Json::objectValue;
+    v["PatientName"] = "Hello";
+    v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4";
+    v["ReferencedImageSequence"][0]["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4";
+    v["ReferencedImageSequence"][0]["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719";
+    v["ReferencedImageSequence"][0]["ReferencedPatientSequence"] = Json::arrayValue;  // Empty nested sequence
+    v["ReferencedImageSequence"][0]["ReferencedStudySequence"][0]["PatientID"] = "Hello";  // Non-empty nested sequence
+    v["ReferencedImageSequence"][0]["0011,1311"] = "abcd";  // Binary
+
+    dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "PrivateCreator"));
+
+    {
+      // Test value multiplicity (cannot be done using "ParsedDicomFile::CreateFromJson()")
+      const int16_t a[] = { 12, 13 };
+      std::unique_ptr<DcmSignedShort> s(new DcmSignedShort(DCM_TagAngleSecondAxis));  // VisitIntegers()
+      ASSERT_TRUE(s->putSint16Array(a, 2).good());
+      dicom->GetDcmtkObject().getDataset()->insert(s.release());
+    }
+  
+    DcmItem *parent = NULL;
+    ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->findAndGetSequenceItem(DCM_ReferencedImageSequence, parent, 0).good());
+
+    {
+      const float a[] = { 42, 43, 47 };
+      std::unique_ptr<DcmFloatingPointSingle> s(new DcmFloatingPointSingle(DCM_ExaminedBodyThickness));  // VisitDoubles()
+      ASSERT_TRUE(s->putFloat32Array(a, 3).good());
+      parent->insert(s.release());
+    }
+  
+    {
+      const uint16_t a[] = { 0x0008, 0x0020, 0x0008, 0x0030 };
+      std::unique_ptr<DcmAttributeTag> s(new DcmAttributeTag(DCM_DimensionIndexPointer));  // VisitAttributes()
+      ASSERT_TRUE(s->putUint16Array(a, 2).good());
+      parent->insert(s.release());
+    }
+
+    ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->insert(new DcmPixelItem(DCM_PixelData)).good());  // VisitNotSupported()
+  }
+
+  {
+    V visitor;
+    dicom->Apply(visitor);
+    ASSERT_EQ(127u, visitor.GetSeen());  // Make sure all the methods have been applied
+  }
+
+  {
+    Json::Value b;
+    dicom->DatasetToJson(b, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0);
+    ASSERT_EQ(Json::objectValue, b.type());
+
+    Json::Value::Members members = b.getMemberNames();
+    ASSERT_EQ(1u, members.size());
+    ASSERT_EQ("0008,1140", members[0]);
+
+    // Check that "b["0008,1140"]" is a sequence with one single empty object
+    ASSERT_EQ(Json::arrayValue, b["0008,1140"].type());
+    ASSERT_EQ(1u, b["0008,1140"].size());
+    ASSERT_EQ(Json::objectValue, b["0008,1140"][0].type());
+    ASSERT_EQ(0u, b["0008,1140"][0].size());
+  }
+}
+
+
 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
 
--- a/OrthancServer/OrthancExplorer/explorer.js	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Tue Jul 06 08:40:43 2021 +0200
@@ -511,7 +511,8 @@
     'Limit' : LIMIT_RESOURCES + 1,
     'Query' : {
       'StudyDate' : $('#lookup-study-date').val()
-    }
+    },
+    'Full' : true
   };
 
   $('#lookup-form input').each(function(index, input) {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -379,7 +379,7 @@
     }
 
     Json::Value result;
-    answers.ToJson(result, true);
+    answers.ToJson(result, DicomToJsonFormat_Human);
     call.GetOutput().AnswerJson(result);
   }
 
@@ -422,7 +422,7 @@
     }
 
     Json::Value result;
-    answers.ToJson(result, true);
+    answers.ToJson(result, DicomToJsonFormat_Human);
     call.GetOutput().AnswerJson(result);
   }
 
@@ -466,7 +466,7 @@
     }
 
     Json::Value result;
-    answers.ToJson(result, true);
+    answers.ToJson(result, DicomToJsonFormat_Human);
     call.GetOutput().AnswerJson(result);
   }
 
@@ -511,7 +511,7 @@
     }
 
     Json::Value result;
-    answers.ToJson(result, true);
+    answers.ToJson(result, DicomToJsonFormat_Human);
     call.GetOutput().AnswerJson(result);
   }
 
@@ -565,7 +565,7 @@
     for (size_t i = 0; i < patients.GetSize(); i++)
     {
       Json::Value patient;
-      patients.ToJson(patient, i, true);
+      patients.ToJson(patient, i, DicomToJsonFormat_Human);
 
       DicomMap::SetupFindStudyTemplate(m);
       if (!MergeQueryAndTemplate(m, call))
@@ -584,7 +584,7 @@
       for (size_t j = 0; j < studies.GetSize(); j++)
       {
         Json::Value study;
-        studies.ToJson(study, j, true);
+        studies.ToJson(study, j, DicomToJsonFormat_Human);
 
         DicomMap::SetupFindSeriesTemplate(m);
         if (!MergeQueryAndTemplate(m, call))
@@ -603,7 +603,7 @@
         for (size_t k = 0; k < series.GetSize(); k++)
         {
           Json::Value series2;
-          series.ToJson(series2, k, true);
+          series.ToJson(series2, k, DicomToJsonFormat_Human);
           study["Series"].append(series2);
         }
 
@@ -924,6 +924,7 @@
     }
     
     std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
     
     {
       QueryAccessor query(call);
@@ -967,6 +968,8 @@
   static void DocumentRetrieveShared(RestApiPostCall& call)
   {
     OrthancRestApi::DocumentSubmitCommandsJob(call);
+    OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Short);
+
     call.GetDocumentation()
       .SetTag("Networking")
       .SetUriArgument("id", "Identifier of the query of interest")
@@ -1511,7 +1514,6 @@
   {
     if (call.IsDocumentation())
     {
-      OrthancRestApi::DocumentSubmitCommandsJob(call);
       call.GetDocumentation()
         .SetTag("Networking")
         .SetSummary("Trigger C-MOVE SCU")
@@ -1560,7 +1562,7 @@
       MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomAssociationParameters params(localAet, source);
-    InjectAssociationTimeout(params, request);
+    InjectAssociationTimeout(params, request);  // Handles KEY_TIMEOUT
 
     DicomControlUserConnection connection(params);
 
@@ -2128,13 +2130,16 @@
   {
     if (call.IsDocumentation())
     {
+      OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+
       call.GetDocumentation()
         .SetTag("Networking")
         .SetSummary("C-FIND SCU for worklist")
         .SetDescription("Trigger C-FIND SCU command against the remote worklists of the DICOM modality "
                         "whose identifier is provided in URL")
         .SetUriArgument("id", "Identifier of the modality of interest")
-        .AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags")
+        .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "Associative array containing the filter on the values of the DICOM tags", true)
         .AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching worklists");
       return;
     }
@@ -2142,9 +2147,23 @@
     Json::Value json;
     if (call.ParseJsonRequest(json))
     {
-      std::unique_ptr<ParsedDicomFile> query
-        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
-                                         "" /* no private creator */));
+      std::unique_ptr<ParsedDicomFile> query;
+      DicomToJsonFormat format;
+
+      if (json.isMember(KEY_QUERY))
+      {
+        // New in Orthanc 1.9.5
+        query.reset(ParsedDicomFile::CreateFromJson(json[KEY_QUERY], static_cast<DicomFromJsonFlags>(0),
+                                                    "" /* no private creator */));
+        format = OrthancRestApi::GetDicomFormat(json, DicomToJsonFormat_Human);
+      }
+      else
+      {
+        // Compatibility with Orthanc <= 1.9.4
+        query.reset(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
+                                                    "" /* no private creator */));
+        format = DicomToJsonFormat_Human;
+      }
 
       DicomFindAnswers answers(true);
 
@@ -2154,7 +2173,7 @@
       }
 
       Json::Value result;
-      answers.ToJson(result, true);
+      answers.ToJson(result, format);
       call.GetOutput().AnswerJson(result);
     }
     else
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -381,7 +381,6 @@
     Json::Value json;
     if (call.ParseJsonRequest(json))
     {
-      std::cout << json.toStyledString();
       OrthancConfiguration::ParseAcceptedTransferSyntaxes(syntaxes, json);
     }
     else
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -21,13 +21,15 @@
 
 #include "DicomMoveScuJob.h"
 
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../ServerContext.h"
 
 static const char* const LOCAL_AET = "LocalAet";
-static const char* const TARGET_AET = "TargetAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
 static const char* const REMOTE = "Remote";
-static const char* const QUERY = "Query";
+static const char* const TARGET_AET = "TargetAet";
 static const char* const TIMEOUT = "Timeout";
 
 namespace Orthanc
@@ -80,7 +82,6 @@
   };
 
 
-
   void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer)
   {
     if (connection_.get() == NULL)
@@ -92,33 +93,30 @@
   }
 
 
-  static void AddTagIfString(Json::Value& target,
-                             const DicomMap& answer,
-                             const DicomTag& tag)
+  static void AddToQuery(DicomFindAnswers& query,
+                         const DicomMap& item)
   {
-    const DicomValue* value = answer.TestAndGetValue(tag);
-    if (value != NULL &&
-        !value->IsNull() &&
-        !value->IsBinary())
-    {
-      target[tag.Format()] = value->GetContent();
-    }
+    query.Add(item);
+
+    /**
+     * Compatibility with Orthanc <= 1.9.4: Remove the
+     * "SpecificCharacterSet" (0008,0005) tag that is automatically
+     * added if creating a ParsedDicomFile object from a DicomMap.
+     **/
+    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
   }
-  
+
 
   void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
   {
-    assert(query_.type() == Json::arrayValue);
-
-    // Copy the identifiers tags, if they exist
-    Json::Value item = Json::objectValue;
-    AddTagIfString(item, answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    AddTagIfString(item, answer, DICOM_TAG_PATIENT_ID);
-    AddTagIfString(item, answer, DICOM_TAG_STUDY_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_SERIES_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_SOP_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_ACCESSION_NUMBER);
-    query_.append(item);
+    DicomMap item;
+    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
+    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
+    AddToQuery(query_, item);
     
     AddCommand(new Command(*this, answer));
   }
@@ -191,13 +189,28 @@
   }
   
 
+  void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      queryFormat_ = format;
+    }
+  }
+
+
   void DicomMoveScuJob::GetPublicContent(Json::Value& value)
   {
     SetOfCommandsJob::GetPublicContent(value);
 
-    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
+    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
     value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-    value["Query"] = query_;
+
+    value[QUERY] = Json::objectValue;
+    query_.ToJson(value[QUERY], queryFormat_);
   }
 
 
@@ -207,12 +220,26 @@
     context_(context),
     parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
     targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)),
-    query_(Json::arrayValue)
+    query_(true),
+    queryFormat_(DicomToJsonFormat_Short)
   {
-    if (serialized.isMember(QUERY) &&
-        serialized[QUERY].type() == Json::arrayValue)
+    if (serialized.isMember(QUERY))
     {
-      query_ = serialized[QUERY];
+      const Json::Value& query = serialized[QUERY];
+      if (query.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+        {
+          DicomMap item;
+          FromDcmtkBridge::FromJson(item, query[i]);
+          AddToQuery(query_, item);
+        }
+      }
+    }
+
+    if (serialized.isMember(QUERY_FORMAT))
+    {
+      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
     }
   }
 
@@ -227,7 +254,13 @@
     {
       parameters_.SerializeJob(target);
       target[TARGET_AET] = targetAet_;
-      target[QUERY] = query_;
+
+      // "Short" is for compatibility with Orthanc <= 1.9.4
+      target[QUERY] = Json::objectValue;
+      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
+
+      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
+      
       return true;
     }
   }
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Tue Jul 06 08:40:43 2021 +0200
@@ -40,7 +40,8 @@
     ServerContext&              context_;
     DicomAssociationParameters  parameters_;
     std::string                 targetAet_;
-    Json::Value                 query_;
+    DicomFindAnswers            query_;
+    DicomToJsonFormat           queryFormat_;  // New in 1.9.5
 
     std::unique_ptr<DicomControlUserConnection>  connection_;
     
@@ -49,7 +50,8 @@
   public:
     explicit DicomMoveScuJob(ServerContext& context) :
       context_(context),
-      query_(Json::arrayValue)
+      query_(true /* this is for worklists */),
+      queryFormat_(DicomToJsonFormat_Short)
     {
     }
 
@@ -79,6 +81,13 @@
     
     void SetTargetAet(const std::string& aet);
 
+    void SetQueryFormat(DicomToJsonFormat format);
+
+    DicomToJsonFormat GetQueryFormat() const
+    {
+      return queryFormat_;
+    }
+
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
     virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Tue Jul 06 08:36:54 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Tue Jul 06 08:40:43 2021 +0200
@@ -1184,6 +1184,7 @@
     ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
     ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
+    ASSERT_EQ(DicomToJsonFormat_Short, job->GetQueryFormat());
   }
   
   {
@@ -1196,6 +1197,8 @@
     job.SetLocalAet("WORLD");
     job.SetRemoteModality(r);
     job.SetTimeout(43);
+    job.SetQueryFormat(DicomToJsonFormat_Human);
+
     job.Serialize(v);
   }
   
@@ -1209,5 +1212,63 @@
     ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
     ASSERT_EQ(43u, job->GetParameters().GetTimeout());
+    ASSERT_EQ(DicomToJsonFormat_Human, job->GetQueryFormat());
   }
 }
+
+
+TEST_F(OrthancJobsSerialization, DicomMoveScuJob)
+{
+  Json::Value command = Json::objectValue;
+  command["0008,0005"]["Type"] = "String";
+  command["0008,0005"]["Content"] = "ISO_IR 100";
+  command["0010,0020"]["Type"] = "String";
+  command["0010,0020"]["Content"] = "1234";
+
+  Json::Value query = Json::objectValue;
+  query["0010,0020"] = "456";
+  query["0008,0052"] = "STUDY";
+  
+  Json::Value remote = Json::objectValue;
+  remote["AET"] = "REMOTE";
+  remote["Host"] = "192.168.1.1";
+  remote["Port"] = 4242;
+  
+  Json::Value s = Json::objectValue;
+  s["Permissive"] = true;
+  s["Position"] = 1;
+  s["Description"] = "test";
+  s["Remote"] = remote;
+  s["LocalAet"] = "LOCAL";
+  s["TargetAet"] = "TARGET";
+  s["QueryFormat"] = "Full";
+  s["Query"] = Json::arrayValue;
+  s["Query"].append(query);
+  s["Commands"] = Json::arrayValue;
+  s["Commands"].append(command);
+
+  Json::Value s2;
+
+  {
+    DicomMoveScuJob job(GetContext(), s);
+    job.Serialize(s2);
+  }
+
+  {
+    DicomMoveScuJob job(GetContext(), s2);
+    ASSERT_EQ("TARGET", job.GetTargetAet());
+    ASSERT_EQ("LOCAL", job.GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("REMOTE", job.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", job.GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(4242u, job.GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ("test", job.GetDescription());
+    ASSERT_TRUE(job.IsPermissive());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_EQ(1u, job.GetCommandsCount());
+    ASSERT_EQ(DicomToJsonFormat_Full, job.GetQueryFormat());
+    ASSERT_EQ(1u, s2["Commands"].size());
+    ASSERT_EQ(command.toStyledString(), s2["Commands"][0].toStyledString());
+    ASSERT_EQ(1u, s2["Query"].size());
+    ASSERT_EQ(query.toStyledString(), s2["Query"][0].toStyledString());
+  }
+}
--- a/TODO	Tue Jul 06 08:36:54 2021 +0200
+++ b/TODO	Tue Jul 06 08:40:43 2021 +0200
@@ -35,7 +35,8 @@
   https://book.orthanc-server.com/integrations.html
 * Discuss HL7 in a dedicated page:
   https://groups.google.com/d/msg/orthanc-users/4dt4992O0lQ/opTjTFU2BgAJ
-
+  https://groups.google.com/g/orthanc-users/c/Spjtcj9vSPo/m/ktUArWxUDQAJ
+  
 
 ========
 REST API