view OrthancServer/OrthancFindRequestHandler.cpp @ 4095:642b0947af84 zeiss-cirrus-investigations

temporarily modified the FindRequestHandler such that ZeissCirrus accepts the response from Orthanc. Work to be cleaned
author Alain Mazy <alain@mazy.be>
date Tue, 30 Jun 2020 17:20:22 +0200
parents c6e82885f570
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2020 Osimis S.A., Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * In addition, as a special exception, the copyright holders of this
 * program give permission to link the code of its release with the
 * OpenSSL project's "OpenSSL" library (or with modified versions of it
 * that use the same license as the "OpenSSL" library), and distribute
 * the linked executables. You must obey the GNU General Public License
 * in all respects for all of the code used other than "OpenSSL". If you
 * modify file(s) with this exception, you may extend this exception to
 * your version of the file(s), but you are not obligated to do so. If
 * you do not wish to do so, delete this exception statement from your
 * version. If you delete this exception statement from all source files
 * in the program, then also delete it here.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "PrecompiledHeadersServer.h"
#include "OrthancFindRequestHandler.h"

#include "../Core/DicomFormat/DicomArray.h"
#include "../Core/DicomParsing/FromDcmtkBridge.h"
#include "../Core/Logging.h"
#include "../Core/Lua/LuaFunctionCall.h"
#include "../Core/MetricsRegistry.h"
#include "OrthancConfiguration.h"
#include "Search/DatabaseLookup.h"
#include "ServerContext.h"
#include "ServerToolbox.h"

#include <boost/regex.hpp> 


namespace Orthanc
{
  static void GetChildren(std::list<std::string>& target,
                          ServerIndex& index,
                          const std::list<std::string>& source)
  {
    target.clear();

    for (std::list<std::string>::const_iterator
           it = source.begin(); it != source.end(); ++it)
    {
      std::list<std::string> tmp;
      index.GetChildren(tmp, *it);
      target.splice(target.end(), tmp);
    }
  }


  static void StoreSetOfStrings(DicomMap& result,
                                const DicomTag& tag,
                                const std::set<std::string>& values)
  {
    bool isFirst = true;

    std::string s;
    for (std::set<std::string>::const_iterator
           it = values.begin(); it != values.end(); ++it)
    {
      if (isFirst)
      {
        isFirst = false;
      }
      else
      {
        s += "\\";
      }

      s += *it;
    }

    result.SetValue(tag, s, false);
  }


  static void ComputePatientCounters(DicomMap& result,
                                     ServerIndex& index,
                                     const std::string& patient,
                                     const DicomMap& query)
  {
    std::list<std::string> studies;
    index.GetChildren(studies, patient);

    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
                      boost::lexical_cast<std::string>(studies.size()), false);
    }

    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
        !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
    {
      return;
    }

    std::list<std::string> series;
    GetChildren(series, index, studies);
    studies.clear();  // This information is useless below
    
    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
                      boost::lexical_cast<std::string>(series.size()), false);
    }

    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
    {
      return;
    }

    std::list<std::string> instances;
    GetChildren(instances, index, series);

    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
                      boost::lexical_cast<std::string>(instances.size()), false);
    }
  }


  static void ComputeStudyCounters(DicomMap& result,
                                   ServerContext& context,
                                   const std::string& study,
                                   const DicomMap& query)
  {
    ServerIndex& index = context.GetIndex();

    std::list<std::string> series;
    index.GetChildren(series, study);
    
    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
                      boost::lexical_cast<std::string>(series.size()), false);
    }

    if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
    {
      std::set<std::string> values;

      for (std::list<std::string>::const_iterator
             it = series.begin(); it != series.end(); ++it)
      {
        DicomMap tags;
        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
        {
          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);

          if (value != NULL &&
              !value->IsNull() &&
              !value->IsBinary())
          {
            values.insert(value->GetContent());
          }
        }
      }

      StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
    }

    if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
        !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
    {
      return;
    }

    std::list<std::string> instances;
    GetChildren(instances, index, series);

    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
                      boost::lexical_cast<std::string>(instances.size()), false);
    }

    if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
    {
      std::set<std::string> values;

      for (std::list<std::string>::const_iterator
             it = instances.begin(); it != instances.end(); ++it)
      {
        std::string value;
        if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid))
        {
          values.insert(value);
        }
      }

      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
    }
  }


  static void ComputeSeriesCounters(DicomMap& result,
                                    ServerIndex& index,
                                    const std::string& series,
                                    const DicomMap& query)
  {
    std::list<std::string> instances;
    index.GetChildren(instances, series);

    if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
    {
      result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
                      boost::lexical_cast<std::string>(instances.size()), false);
    }
  }


  static DicomMap* ComputeCounters(ServerContext& context,
                                   const std::string& instanceId,
                                   ResourceType level,
                                   const DicomMap& query)
  {
    switch (level)
    {
      case ResourceType_Patient:
        if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) &&
            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
        {
          return NULL;
        }

        break;

      case ResourceType_Study:
        if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) &&
            !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
            !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) &&
            !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
        {
          return NULL;
        }

        break;

      case ResourceType_Series:
        if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
        {
          return NULL;
        }

        break;

      default:
        return NULL;
    }

    std::string parent;
    if (!context.GetIndex().LookupParent(parent, instanceId, level))
    {
      throw OrthancException(ErrorCode_UnknownResource);  // The resource was deleted in between
    }

    std::unique_ptr<DicomMap> result(new DicomMap);

    switch (level)
    {
      case ResourceType_Patient:
        ComputePatientCounters(*result, context.GetIndex(), parent, query);
        break;

      case ResourceType_Study:
        ComputeStudyCounters(*result, context, parent, query);
        break;

      case ResourceType_Series:
        ComputeSeriesCounters(*result, context.GetIndex(), parent, query);
        break;

      default:
        throw OrthancException(ErrorCode_InternalError);
    }

    return result.release();
  }


  static void AddAnswer(DicomFindAnswers& answers,
                        const DicomMap& mainDicomTags,
                        const Json::Value* dicomAsJson,
                        const DicomArray& query,
                        const std::list<DicomTag>& sequencesToReturn,
                        const DicomMap* counters,
                        const std::string& defaultPrivateCreator,
                        const std::map<uint16_t, std::string>& privateCreators)
  {
    DicomMap match;

    if (dicomAsJson != NULL)
    {
      match.FromDicomAsJson(*dicomAsJson);
    }
    else
    {
      match.Assign(mainDicomTags);
    }
    
    DicomMap result;

    for (size_t i = 0; i < query.GetSize(); i++)
    {
      if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
      {
        // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052))
        result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue());
      }
      else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET)
      {
        // Do not include the encoding, this is handled by class ParsedDicomFile
      }
      else
      {
        const DicomTag& tag = query.GetElement(i).GetTag();
        const DicomValue* value = match.TestAndGetValue(tag);

        if (value != NULL &&
            !value->IsNull() &&
            !value->IsBinary())
        {
          result.SetValue(tag, value->GetContent(), false);
        }
        else
        {
          result.SetValue(tag, "", false);
        }
      }
    }

    if (counters != NULL)
    {
      DicomArray tmp(*counters);
      for (size_t i = 0; i < tmp.GetSize(); i++)
      {
        result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false);
      }
    }

    if (result.GetSize() == 0 &&
        sequencesToReturn.empty())
    {
      LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
    }
    else if (sequencesToReturn.empty())
    {
      answers.Add(result);
    }
    else if (dicomAsJson == NULL)
    {
      LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
      answers.Add(result);
    }
    else
    {
      ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
                            true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators);

      for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
           tag != sequencesToReturn.end(); ++tag)
      {
        assert(dicomAsJson != NULL);
        const Json::Value& source = (*dicomAsJson) [tag->Format()];

        // the options below were introduced during an investigation with Zeiss Cirrus software
        // in order to reproduce exactly the same "weird" answers as a Zeiss server.
        // Of course, this code should not remain as is.  The Zeiss Cirrus Patch
        // shall be implemented in a "lua callback" that would modify the response before it is sent.
        // Note that, during these investigations, DefaultEncoding was set to "Utf8" and
        // this line was modified in dimse.cc (DCMTK):
        // E_EncodingType sequenceType_encoding = EET_UndefinedLength; //   g_dimse_send_sequenceType_encoding;
        // and the following dictionnary was declared:
        // "Dictionary" : { 
	      //   "0407,10a1" : ["SQ", "OCT Cube Sequence", 1,1,"99CZM_CapeCod_OctGeneral" ], 
        //   "0407,1005" : ["SQ", "OCT Cube Sequence2", 1, 1, "99CZM_CapeCod_OctGeneral" ], 
        //   "0407,0010" : ["LO", "OCT Cube Sequence3", 1, 1, "99CZM_CapeCod_OctGeneral" ],
        //   "0407,1001" : ["US", "Undefined Private Tag 1001", 1, 1, "99CZM_CapeCod_OctGeneral" ], 
        //   "0407,1002" : ["US", "Undefined Private Tag 1002", 1, 1, "99CZM_CapeCod_OctGeneral" ], 
        //   "0407,1003" : ["US", "Undefined Private Tag 1003", 1, 1, "99CZM_CapeCod_OctGeneral" ], 
        //   "0405,1001" : ["SH", "PatternType", 1, 1, "99CZM_CapeCod_OctSettings" ],
        //   "0405,101a" : ["FL", "SignalStrength", 1, 1, "99CZM_CapeCod_OctSettings" ]
        // }
        bool addEmptySequences = false;   // this could go into a "Manufacturer" option
        bool zeissCirrusMinimumPatch = true; // investigations we tried and that are necessary for Cirrus to accept the response
        if (zeissCirrusMinimumPatch)
        {
          addEmptySequences = true;
        }
        bool zeissCirrusMaximumPatch = false; // investigations we tried but that finally do not seem necessary for Cirrus to accept the response (but that are required to get the same response as the Zeiss Server)

        if ((source.type() == Json::objectValue &&
            source.isMember("Type") &&
            source.isMember("Value") &&
            source["Type"].asString() == "Sequence" &&
            source["Value"].type() == Json::arrayValue) ||
            (addEmptySequences && source.type() == Json::nullValue))
        {
          Json::Value content = Json::arrayValue;
          bool replace = true;

          if (source.type() == Json::objectValue)
          {
            for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
            {
              Json::Value item;
              ServerToolbox::SimplifyTags(item, source["Value"][i], DicomToJsonFormat_Short);
              content.append(item);
            }
          }
          else
          {
            LOG(INFO) << "inserting empty sequence for " << tag->Format();

            DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();

            {
              std::unique_ptr<DcmSequenceOfItems> sequence;

              if (zeissCirrusMinimumPatch && tag->GetGroup() == 1031 && tag->GetElement() == 4257)
              {
                sequence.reset(new DcmSequenceOfItems(DcmTag(tag->GetGroup(), tag->GetElement(), "99CZM_CapeCod_OctGeneral")));

                LOG(INFO) << "inserting first 4 empty elements into " << tag->Format();
                std::unique_ptr<DcmItem> item(new DcmItem(DcmTag(0xfffe, 0xe000)));
                bool ok = false;
                ok = item->putAndInsertString(DcmTag(0x0407, 0x0010), "99CZM_CapeCod_OctGeneral").good();
                ok = item->insertEmptyElement(DcmTag(0x0407, 0x1001, "99CZM_CapeCod_OctGeneral")).good();
                ok = item->insertEmptyElement(DcmTag(0x0407, 0x1002, "99CZM_CapeCod_OctGeneral")).good();
                ok = item->insertEmptyElement(DcmTag(0x0407, 0x1003, "99CZM_CapeCod_OctGeneral")).good();
                ok = sequence->insert(item.release(), false, false).good();
                // LOG(INFO) << ok;
              }
              else
              {
                sequence.reset(new DcmSequenceOfItems(DcmTagKey(tag->GetGroup(), tag->GetElement())));
                std::unique_ptr<DcmItem> item;
                if (zeissCirrusMaximumPatch)
                {
                  item.reset(new DcmItem(DcmTag(0xfffe, 0xe000)));
                }
                else
                {
                  item.reset(new DcmItem);
                }
                bool ok = sequence->insert(item.release(), false, false).good();
                // LOG(INFO) << ok;
              }

              bool ok3 = dataset.insert(sequence.release(), false, false).good();
              replace = false;
              // LOG(INFO) << ok3;
            }
          }

          if (zeissCirrusMaximumPatch && tag->GetGroup() == 1031 && tag->GetElement() == 4257 && content.size() > 0 && content[0].size() >= 4)
          {
            LOG(INFO) << "keeping only the first elements from " << tag->Format();
            Json::Value cloneContent = Json::Value();

            Json::Value::Members members = content[0].getMemberNames();
            for (size_t i = 0; i < members.size(); i++)
            {
              const std::string& member = members[i];
              cloneContent[0][member] = content[0][member];
              Json::Value& v = cloneContent[0][member];
              if (i == 3)
              {
                break;
              }
            }
            content = cloneContent;
          }

          if (replace)
          {
            if (tag->IsPrivate())
            {
              std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup());

              if (found != privateCreators.end())
              {
                dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
              }
              else
              {
                dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
              }
            }
            else
            {
              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
            }
          }
        }
      }

      answers.Add(dicom);
    }
  }



  bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */,
                                                 ResourceType level,
                                                 const DicomTag& tag,
                                                 ModalityManufacturer manufacturer)
  {
    // Whatever the manufacturer, remove the GenericGroupLength tags
    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.2.html
    // https://bitbucket.org/sjodogne/orthanc/issues/31/
    if (tag.GetElement() == 0x0000)
    {
      return false;
    }

    switch (manufacturer)
    {
      case ModalityManufacturer_Vitrea:
        // Following Denis Nesterov's mail on 2015-11-30
        if (tag == DicomTag(0x5653, 0x0010))  // "PrivateCreator = Vital Images SW 3.4"
        {
          return false;
        }

        break;

      default:
        break;
    }

    return true;
  }


  bool OrthancFindRequestHandler::ApplyLuaFilter(DicomMap& target,
                                                 const DicomMap& source,
                                                 const std::string& remoteIp,
                                                 const std::string& remoteAet,
                                                 const std::string& calledAet,
                                                 ModalityManufacturer manufacturer)
  {
    static const char* LUA_CALLBACK = "IncomingFindRequestFilter";
    
    LuaScripting::Lock lock(context_.GetLuaScripting());

    if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK))
    {
      return false;
    }
    else
    {
      Json::Value origin;
      FormatOrigin(origin, remoteIp, remoteAet, calledAet, manufacturer);

      LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK);
      call.PushDicom(source);
      call.PushJson(origin);
      FromDcmtkBridge::ExecuteToDicom(target, call);

      return true;
    }
  }


  OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) :
    context_(context),
    maxResults_(0),
    maxInstances_(0)
  {
  }


  class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor
  {
  private:
    DicomFindAnswers&           answers_;
    ServerContext&              context_;
    ResourceType                level_;
    const DicomMap&             query_;
    DicomArray                  queryAsArray_;
    const std::list<DicomTag>&  sequencesToReturn_;
    std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
    const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself

  public:
    LookupVisitor(DicomFindAnswers&  answers,
                  ServerContext& context,
                  ResourceType level,
                  const DicomMap& query,
                  const std::list<DicomTag>& sequencesToReturn,
                  const std::map<uint16_t, std::string>& privateCreators) :
      answers_(answers),
      context_(context),
      level_(level),
      query_(query),
      queryAsArray_(query),
      sequencesToReturn_(sequencesToReturn),
      privateCreators_(privateCreators)
    {
      answers_.SetComplete(false);

      {
        OrthancConfiguration::ReaderLock lock;
        defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
      }
    }

    virtual bool IsDicomAsJsonNeeded() const
    {
      // Ask the "DICOM-as-JSON" attachment only if sequences are to
      // be returned OR if "query_" contains non-main DICOM tags!

      DicomMap withoutSpecialTags;
      withoutSpecialTags.Assign(query_);

      // Check out "ComputeCounters()"
      withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
      withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);

      // Check out "AddAnswer()"
      withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
      withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
      
      return (!sequencesToReturn_.empty() ||
              !withoutSpecialTags.HasOnlyMainDicomTags());
    }
      
    virtual void MarkAsComplete()
    {
      answers_.SetComplete(true);
    }

    virtual void Visit(const std::string& publicId,
                       const std::string& instanceId,
                       const DicomMap& mainDicomTags,
                       const Json::Value* dicomAsJson) 
    {
      std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));

      AddAnswer(answers_, mainDicomTags, dicomAsJson,
                queryAsArray_, sequencesToReturn_, counters.get(), defaultPrivateCreator_, privateCreators_);
    }
  };


  void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                         const DicomMap& input,
                                         const std::list<DicomTag>& sequencesToReturn,
                                         const std::string& remoteIp,
                                         const std::string& remoteAet,
                                         const std::string& calledAet,
                                         ModalityManufacturer manufacturer)
  {
    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms");

    /**
     * Possibly apply the user-supplied Lua filter.
     **/

    DicomMap lua;
    const DicomMap* filteredInput = &input;

    if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer))
    {
      filteredInput = &lua;
    }


    /**
     * Retrieve the query level.
     **/

    assert(filteredInput != NULL);
    const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
    if (levelTmp == NULL ||
        levelTmp->IsNull() ||
        levelTmp->IsBinary())
    {
      throw OrthancException(ErrorCode_BadRequest,
                             "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)");
    }

    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());

    if (level != ResourceType_Patient &&
        level != ResourceType_Study &&
        level != ResourceType_Series &&
        level != ResourceType_Instance)
    {
      throw OrthancException(ErrorCode_NotImplemented);
    }


    DicomArray query(*filteredInput);
    LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);

    for (size_t i = 0; i < query.GetSize(); i++)
    {
      if (!query.GetElement(i).GetValue().IsNull())
      {
        LOG(INFO) << "  " << query.GetElement(i).GetTag()
                  << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
                  << " = " << query.GetElement(i).GetValue().GetContent();
      }
    }

    for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin();
         it != sequencesToReturn.end(); ++it)
    {
      LOG(INFO) << "  (" << it->Format()
                << ")  " << FromDcmtkBridge::GetTagName(*it, "")
                << " : sequence tag whose content will be copied";
    }

    // collect the private creators from the query itself
    std::map<uint16_t, std::string> privateCreators;
    for (size_t i = 0; i < query.GetSize(); i++)
    {
      const DicomElement& element = query.GetElement(i);
      if (element.GetTag().IsPrivate() && element.GetTag().GetElement() == 0x10)
      {
        privateCreators[element.GetTag().GetGroup()] = element.GetValue().GetContent();
      }
    }

    /**
     * Build up the query object.
     **/

    DatabaseLookup lookup;

    bool caseSensitivePN;

    {
      OrthancConfiguration::ReaderLock lock;
      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
    }

    for (size_t i = 0; i < query.GetSize(); i++)
    {
      const DicomElement& element = query.GetElement(i);
      const DicomTag tag = element.GetTag();

      if (element.GetValue().IsNull() ||
          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
          tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
      {
        continue;
      }

      std::string value = element.GetValue().GetContent();
      if (value.size() == 0)
      {
        // An empty string corresponds to an universal constraint, so we ignore it
        continue;
      }

      if (FilterQueryTag(value, level, tag, manufacturer))
      {
        ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);

        // DICOM specifies that searches must be case sensitive, except
        // for tags with a PN value representation
        bool sensitive = true;
        if (vr == ValueRepresentation_PersonName)
        {
          sensitive = caseSensitivePN;
        }

        lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */);
      }
      else
      {
        LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " 
                  << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetTagName(element);
      }
    }


    /**
     * Run the query.
     **/

    size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;


    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators);
    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
  }


  void OrthancFindRequestHandler::FormatOrigin(Json::Value& origin,
                                               const std::string& remoteIp,
                                               const std::string& remoteAet,
                                               const std::string& calledAet,
                                               ModalityManufacturer manufacturer)
  {
    origin = Json::objectValue;
    origin["RemoteIp"] = remoteIp;
    origin["RemoteAet"] = remoteAet;
    origin["CalledAet"] = calledAet;
    origin["Manufacturer"] = EnumerationToString(manufacturer);
  }
}