view Plugin/QidoRs.cpp @ 1:174f4b0ec527

agpl
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 13 Mar 2015 16:14:12 +0100
parents d5f459244111
children 8e7bc8bf874d
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "QidoRs.h"

#include "Plugin.h"
#include "StowRs.h"  // For IsXmlExpected()
#include "../Core/Dicom.h"
#include "../Core/Toolbox.h"
#include "../Core/Configuration.h"
#include "../Core/MultipartWriter.h"

#include <gdcmTag.h>
#include <list>
#include <stdexcept>
#include <boost/lexical_cast.hpp>
#include <gdcmDict.h>
#include <gdcmDicts.h>
#include <gdcmGlobal.h>
#include <gdcmDictEntry.h>
#include <boost/regex.hpp>
#include <boost/algorithm/string/replace.hpp>



enum QueryLevel
{
  QueryLevel_Study,
  QueryLevel_Series,
  QueryLevel_Instance
};


class ModuleMatcher
{
private:
  typedef std::map<gdcm::Tag, std::string>  Filters;

  const gdcm::Dict&     dictionary_;
  bool                  fuzzy_;
  unsigned int          offset_;
  unsigned int          limit_;
  std::list<gdcm::Tag>  includeFields_;
  bool                  includeAllFields_;
  Filters               filters_;



  static inline uint16_t GetCharValue(char c)
  {
    if (c >= '0' && c <= '9')
      return c - '0';
    else if (c >= 'a' && c <= 'f')
      return c - 'a' + 10;
    else if (c >= 'A' && c <= 'F')
      return c - 'A' + 10;
    else
      return 0;
  }

  static inline uint16_t GetTagValue(const char* c)
  {
    return ((GetCharValue(c[0]) << 12) + 
            (GetCharValue(c[1]) << 8) + 
            (GetCharValue(c[2]) << 4) + 
            GetCharValue(c[3]));
  }


  gdcm::Tag  ParseTag(const std::string& key) const
  {
    if (key.size() == 8 &&
        isxdigit(key[0]) &&
        isxdigit(key[1]) &&
        isxdigit(key[2]) &&
        isxdigit(key[3]) &&
        isxdigit(key[4]) &&
        isxdigit(key[5]) &&
        isxdigit(key[6]) &&
        isxdigit(key[7]))        
    {
      return gdcm::Tag(GetTagValue(key.c_str()),
                       GetTagValue(key.c_str() + 4));
    }
    else
    {
      gdcm::Tag tag;
      dictionary_.GetDictEntryByKeyword(key.c_str(), tag);

      if (tag.IsIllegal() || tag.IsPrivate())
      {
        if (key.find('.') != std::string::npos)
        {
          throw std::runtime_error("This QIDO-RS implementation does not support search over sequences: " + key);
        }
        else
        {
          throw std::runtime_error("Illegal tag name in QIDO-RS: " + key);
        }
      }

      return tag;
    }
  }


  static bool IsWildcard(const std::string& constraint)
  {
    return (constraint.find('-') != std::string::npos ||
            constraint.find('*') != std::string::npos ||
            constraint.find('\\') != std::string::npos ||
            constraint.find('?') != std::string::npos);
  }

  static bool ApplyRangeConstraint(const std::string& value,
                                   const std::string& constraint)
  {
    size_t separator = constraint.find('-');
    std::string lower(constraint.substr(0, separator));
    std::string upper(constraint.substr(separator + 1));
    std::string v(value);

    OrthancPlugins::ToLowerCase(lower);
    OrthancPlugins::ToLowerCase(upper);
    OrthancPlugins::ToLowerCase(v);

    if (lower.size() == 0 && upper.size() == 0)
    {
      return false;
    }

    if (lower.size() == 0)
    {
      return v <= upper;
    }

    if (upper.size() == 0)
    {
      return v >= lower;
    }
    
    return (v >= lower && v <= upper);
  }


  static bool ApplyListConstraint(const std::string& value,
                                  const std::string& constraint)
  {
    std::string v1(value);
    OrthancPlugins::ToLowerCase(v1);

    std::vector<std::string> items;
    OrthancPlugins::TokenizeString(items, constraint, '\\');

    for (size_t i = 0; i < items.size(); i++)
    {
      std::string lower(items[i]);
      OrthancPlugins::ToLowerCase(lower);
      if (lower == v1)
      {
        return true;
      }
    }

    return false;
  }


  static std::string WildcardToRegularExpression(const std::string& source)
  {
    std::string result = source;

    // Escape all special characters
    boost::replace_all(result, "\\", "\\\\");
    boost::replace_all(result, "^", "\\^");
    boost::replace_all(result, ".", "\\.");
    boost::replace_all(result, "$", "\\$");
    boost::replace_all(result, "|", "\\|");
    boost::replace_all(result, "(", "\\(");
    boost::replace_all(result, ")", "\\)");
    boost::replace_all(result, "[", "\\[");
    boost::replace_all(result, "]", "\\]");
    boost::replace_all(result, "+", "\\+");
    boost::replace_all(result, "/", "\\/");
    boost::replace_all(result, "{", "\\{");
    boost::replace_all(result, "}", "\\}");

    // Convert wildcards '*' and '?' to their regex equivalents
    boost::replace_all(result, "?", ".");
    boost::replace_all(result, "*", ".*");

    return result;
  }


  static bool Matches(const std::string& value,
                      const std::string& constraint)
  {
    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  

    if (constraint.find('-') != std::string::npos)
    {
      return ApplyRangeConstraint(value, constraint);
    }
    
    if (constraint.find('\\') != std::string::npos)
    {
      return ApplyListConstraint(value, constraint);
    }

    if (constraint.find('*') != std::string::npos ||
        constraint.find('?') != std::string::npos)
    {
      boost::regex pattern(WildcardToRegularExpression(constraint),
                           boost::regex::icase /* case insensitive search */);
      return boost::regex_match(value, pattern);
    }
    else
    {
      std::string v(value), c(constraint);
      OrthancPlugins::ToLowerCase(v);
      OrthancPlugins::ToLowerCase(c);
      return v == c;
    }
  }



  static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result,
                                          QueryLevel level)
  {
    switch (level)
    {
      case QueryLevel_Study:
        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2
        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
        result.push_back(gdcm::Tag(0x0008, 0x0020));  // Study Date
        result.push_back(gdcm::Tag(0x0008, 0x0030));  // Study Time
        result.push_back(gdcm::Tag(0x0008, 0x0050));  // Accession Number
        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
        result.push_back(gdcm::Tag(0x0008, 0x0061));  // Modalities in Study
        result.push_back(gdcm::Tag(0x0008, 0x0090));  // Referring Physician's Name
        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
        //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
        result.push_back(gdcm::Tag(0x0010, 0x0010));  // Patient's Name
        result.push_back(gdcm::Tag(0x0010, 0x0020));  // Patient ID
        result.push_back(gdcm::Tag(0x0010, 0x0030));  // Patient's Birth Date
        result.push_back(gdcm::Tag(0x0010, 0x0040));  // Patient's Sex
        result.push_back(gdcm::Tag(0x0020, 0x000D));  // Study Instance UID
        result.push_back(gdcm::Tag(0x0020, 0x0010));  // Study ID
        result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series
        result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances
        break;

      case QueryLevel_Series:
        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a
        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Modality
        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
        result.push_back(gdcm::Tag(0x0008, 0x103E));  // Series Description
        //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
        result.push_back(gdcm::Tag(0x0020, 0x000E));  // Series Instance UID
        result.push_back(gdcm::Tag(0x0020, 0x0011));  // Series Number
        result.push_back(gdcm::Tag(0x0020, 0x1209));  // Number of Series Related Instances
        result.push_back(gdcm::Tag(0x0040, 0x0244));  // Performed Procedure Step Start Date
        result.push_back(gdcm::Tag(0x0040, 0x0245));  // Performed Procedure Step Start Time
        result.push_back(gdcm::Tag(0x0040, 0x0275));  // Request Attribute Sequence
        break;

      case QueryLevel_Instance:
        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2b
        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
        result.push_back(gdcm::Tag(0x0008, 0x0016));  // SOP Class UID
        result.push_back(gdcm::Tag(0x0008, 0x0018));  // SOP Instance UID
        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
        result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL
        result.push_back(gdcm::Tag(0x0020, 0x0013));  // Instance Number
        result.push_back(gdcm::Tag(0x0028, 0x0010));  // Rows
        result.push_back(gdcm::Tag(0x0028, 0x0011));  // Columns
        result.push_back(gdcm::Tag(0x0028, 0x0100));  // Bits Allocated
        result.push_back(gdcm::Tag(0x0028, 0x0008));  // Number of Frames
        break;

      default:
        throw std::runtime_error("Internal error");
    }
  }



public:
  ModuleMatcher(const OrthancPluginHttpRequest* request) :
  dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()),
  fuzzy_(false),
  offset_(0),
  limit_(0),
  includeAllFields_(false)
  {
    for (int32_t i = 0; i < request->getCount; i++)
    {
      std::string key(request->getKeys[i]);
      std::string value(request->getValues[i]);

      if (key == "limit")
      {
        limit_ = boost::lexical_cast<unsigned int>(value);
      }
      else if (key == "offset")
      {
        offset_ = boost::lexical_cast<unsigned int>(value);
      }
      else if (key == "fuzzymatching")
      {
        if (value == "true")
        {
          fuzzy_ = true;
        }
        else if (value == "false")
        {
          fuzzy_ = false;
        }
        else
        {
          throw std::runtime_error("Not a proper value for fuzzy matching (true or false): " + value);
        }
      }
      else if (key == "includefield")
      {
        if (key == "all")
        {
          includeAllFields_ = true;
        }
        else
        {
          includeFields_.push_back(ParseTag(key));
        }
      }
      else
      {
        filters_[ParseTag(key)] = value;
      }
    }
  }

  unsigned int GetLimit() const
  {
    return limit_;
  }

  unsigned int GetOffset() const
  {
    return offset_;
  }

  void AddFilter(const gdcm::Tag& tag,
                 const std::string& constraint)
  {
    filters_[tag] = constraint;
  }

  bool LookupExactFilter(std::string& constraint,
                         const gdcm::Tag& tag) const
  {
    Filters::const_iterator it = filters_.find(tag);
    if (it != filters_.end() &&
        !IsWildcard(it->second))
    {
      constraint = it->second;
      return true;
    }
    else
    {
      return false;
    }
  }

  bool Matches(const OrthancPlugins::ParsedDicomFile& dicom) const
  {
    for (Filters::const_iterator it = filters_.begin();
         it != filters_.end(); ++it)
    {
      std::string value;
      if (!dicom.GetTag(value, it->first, true))
      {
        return false;
      }

      if (!Matches(value, it->second))
      {
        return false;
      }
    }

    return true;
  }


  void ExtractFields(gdcm::DataSet& result,
                     const OrthancPlugins::ParsedDicomFile& dicom,
                     const std::string& wadoBase,
                     QueryLevel level) const
  {
    std::list<gdcm::Tag> fields = includeFields_;

    // The list of attributes for this query level
    AddResultAttributesForLevel(fields, level);

    // All other attributes passed as query keys
    for (Filters::const_iterator it = filters_.begin();
         it != filters_.end(); ++it)
    {
      fields.push_back(it->first);
    }

    // For instances and series, add all Study-level attributes if
    // {StudyInstanceUID} is not specified.
    if ((level == QueryLevel_Instance  || level == QueryLevel_Series) 
        && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end()
      )
    {
      AddResultAttributesForLevel(fields, QueryLevel_Study);
    }

    // For instances, add all Series-level attributes if
    // {SeriesInstanceUID} is not specified.
    if (level == QueryLevel_Instance
        && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end()
      )
    {
      AddResultAttributesForLevel(fields, QueryLevel_Series);
    }

    // Copy all the required fields to the target
    for (std::list<gdcm::Tag>::const_iterator
           it = fields.begin(); it != fields.end(); it++)
    {
      if (dicom.GetDataSet().FindDataElement(*it))
      {
        const gdcm::DataElement& element = dicom.GetDataSet().GetDataElement(*it);
        result.Replace(element);
      }
    }

    // Set the retrieve URL for WADO-RS
    std::string url = (wadoBase + "/studies/" + 
                       dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "", true));

    if (level == QueryLevel_Series || level == QueryLevel_Instance)
    {
      url += "/series/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "", true);
    }

    if (level == QueryLevel_Instance)
    {
      url += "/instances/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "", true);
    }
    
    gdcm::DataElement element(OrthancPlugins::DICOM_TAG_RETRIEVE_URL);
    element.SetByteValue(url.c_str(), url.size());
    result.Replace(element);
  }
};




class CandidateResources
{
private:
  typedef std::set<std::string>  Resources;

  bool        all_;
  QueryLevel  level_;
  Resources   resources_;

  static bool CallLookup(std::string& orthancId,
                         const std::string& dicomId,
                         char* (lookup) (OrthancPluginContext*, const char*))
  {
    bool result = false;

    char* tmp = lookup(context_, dicomId.c_str());
    if (tmp != NULL)
    {
      orthancId = tmp;
      result = true;
    }

    OrthancPluginFreeString(context_, tmp);

    return result;
  }


  void FilterByIdentifierInternal(const ModuleMatcher& matcher,
                                  const gdcm::Tag& tag,
                                  char* (lookup) (OrthancPluginContext*, const char*))
  {
    std::string orthancId, dicomId;

    if (!matcher.LookupExactFilter(dicomId, tag))
    {
      // There is no restriction at this level
      return;
    }

    if (CallLookup(orthancId, dicomId, lookup) &&
        (all_ || resources_.find(orthancId) != resources_.end()))
    {
      // There remains a single candidate resource
      resources_.clear();
      resources_.insert(orthancId);
    }
    else
    {
      // No matching resource remains
      resources_.clear();            
    }

    all_ = false;
  }


  bool PickOneInstance(std::string& instance,
                       const std::string& resource) const
  {
    if (level_ == QueryLevel_Instance)
    {
      instance = resource;
      return true;
    }

    std::string uri;
    if (level_ == QueryLevel_Study)
    {
      uri = "/studies/" + resource + "/instances";
    }
    else
    {
      assert(level_ == QueryLevel_Series);
      uri = "/series/" + resource + "/instances";
    }

    Json::Value instances;
    if (!OrthancPlugins::RestApiGetJson(instances, context_, uri) ||
        instances.type() != Json::arrayValue &&
        instances.size() != 0)
    {
      return false;
    }

    instance = instances[0]["ID"].asString();
    return true;
  }


public:
  CandidateResources() : all_(true), level_(QueryLevel_Study)
  {
  }

  void GoDown()
  {
    std::string baseUri;
    std::string nextLevel;
    switch (level_)
    {
      case QueryLevel_Study:
        baseUri = "/studies/";
        nextLevel = "Series";
        break;

      case QueryLevel_Series:
        baseUri = "/series/";
        nextLevel = "Instances";
        break;

      default:
        throw std::runtime_error("Internal error");
    }


    if (!all_)
    {
      Resources  children;
      
      for (Resources::const_iterator it = resources_.begin();
           it != resources_.end(); it++)
      {
        Json::Value tmp;
        if (OrthancPlugins::RestApiGetJson(tmp, context_, baseUri + *it) &&
            tmp.type() == Json::objectValue &&
            tmp.isMember(nextLevel) &&
            tmp[nextLevel].type() == Json::arrayValue)
        {
          for (Json::Value::ArrayIndex i = 0; i < tmp[nextLevel].size(); i++)
          {
            children.insert(tmp[nextLevel][i].asString());
          }
        }
      }

      resources_ = children;
    }


    switch (level_)
    {
      case QueryLevel_Study:
        level_ = QueryLevel_Series;
        break;

      case QueryLevel_Series:
        level_ = QueryLevel_Instance;
        break;

      default:
        throw std::runtime_error("Internal error");
    }
  }


  void FilterByIdentifier(const ModuleMatcher& matcher)
  {
    switch (level_)
    {
      case QueryLevel_Study:
        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID,
                                   OrthancPluginLookupStudy);
        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_ACCESSION_NUMBER,
                                   OrthancPluginLookupStudyWithAccessionNumber);
        break;

      case QueryLevel_Series:
        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID,
                                   OrthancPluginLookupSeries);
        break;

      case QueryLevel_Instance:
        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID,
                                   OrthancPluginLookupInstance);
        break;

      default:
        throw std::runtime_error("Internal error");
    }
  }


  void Flatten(std::list<std::string>& result) const
  {
    std::string instance;

    result.clear();

    if (all_)
    {
      std::string uri;
      switch (level_)
      {
        case QueryLevel_Study:
          uri = "/studies/";
          break;

        case QueryLevel_Series:
          uri = "/series/";
          break;

        case QueryLevel_Instance:
          uri = "/instances/";
          break;

        default:
          throw std::runtime_error("Internal error");
      }

      Json::Value tmp;
      if (OrthancPlugins::RestApiGetJson(tmp, context_, uri) &&
          tmp.type() == Json::arrayValue)
      {
        for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
        {
          if (PickOneInstance(instance, tmp[i].asString()))
          {
            result.push_back(instance);
          }
        }
      }
    }
    else
    {
      for (Resources::const_iterator 
             it = resources_.begin(); it != resources_.end(); it++)
      {
        if (PickOneInstance(instance, *it))
        {
          result.push_back(instance);
        }
      }
    }
  }
};



class SearchResults
{
private:
  typedef std::list<gdcm::DataSet*>   Results;

  Results results_;

public:
  ~SearchResults()
  {
    for (Results::iterator it = results_.begin();
         it != results_.end(); it++)
    {
      delete *it;
    }
  }

  void Add(const OrthancPlugins::ParsedDicomFile& dicom,
           const ModuleMatcher& matcher,
           const std::string& wadoBase,
           QueryLevel level)
  {
    std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet);
    matcher.ExtractFields(*result, dicom, wadoBase, level);
    results_.push_back(result.release());
  }

  void Answer(OrthancPluginContext* context,
              OrthancPluginRestOutput* output,
              bool isXml)
  {
    if (isXml)
    {
      OrthancPlugins::MultipartWriter writer("application/dicom+xml");

      for (Results::const_iterator it = results_.begin();
           it != results_.end(); it++)
      {
        std::string answer;
        OrthancPlugins::GenerateSingleDicomAnswer(answer, *dictionary_, **it, true);
        writer.AddPart(answer);
      }

      writer.Answer(context_, output);
    }
    else
    {
      OrthancPlugins::ChunkedBuffer chunks;
      chunks.AddChunk("[\n");

      std::string s = "[\n";

      bool isFirst = true;
      for (Results::const_iterator it = results_.begin();
           it != results_.end(); it++)
      {
        std::string item;
        OrthancPlugins::GenerateSingleDicomAnswer(item, *dictionary_, **it, false);
        chunks.AddChunk(item);
        
        if (isFirst)
        {
          isFirst = false;
        }
        else
        {
          chunks.AddChunk(",\n");
        }
      }

      chunks.AddChunk("]\n");

      std::string answer;
      chunks.Flatten(answer);
      OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
    }
  }
};



static void ApplyMatcher(OrthancPluginRestOutput* output,
                         const OrthancPluginHttpRequest* request,
                         const ModuleMatcher& matcher,
                         const CandidateResources& candidates,
                         QueryLevel level)
{
  std::list<std::string> resources;
  candidates.Flatten(resources);

  std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request) + "/wado-rs";
  SearchResults results;
  for (std::list<std::string>::const_iterator
         it = resources.begin(); it != resources.end(); it++)
  {
    std::string file;
    if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + *it + "/file"))
    {
      OrthancPlugins::ParsedDicomFile dicom(file);
      if (matcher.Matches(dicom))
      {
        results.Add(dicom, matcher, wadoBase, level);
      }
    }
  }

  results.Answer(context_, output, IsXmlExpected(request));
}



int32_t SearchForStudies(OrthancPluginRestOutput* output,
                         const char* url,
                         const OrthancPluginHttpRequest* request)
{
  try
  {
    if (request->method != OrthancPluginHttpMethod_Get)
    {
      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
      return 0;
    }

    ModuleMatcher matcher(request);

    CandidateResources candidates;
    candidates.FilterByIdentifier(matcher);

    ApplyMatcher(output, request, matcher, candidates, QueryLevel_Study);

    return 0;
  }
  catch (std::runtime_error& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
  catch (boost::bad_lexical_cast& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
}


int32_t SearchForSeries(OrthancPluginRestOutput* output,
                        const char* url,
                        const OrthancPluginHttpRequest* request)
{
  try
  {
    if (request->method != OrthancPluginHttpMethod_Get)
    {
      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
      return 0;
    }

    ModuleMatcher matcher(request);

    if (request->groupsCount == 1)
    {
      // The "StudyInstanceUID" is provided by the regular expression
      matcher.AddFilter(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, request->groups[0]);
    }

    CandidateResources candidates;
    candidates.FilterByIdentifier(matcher);
    candidates.GoDown();
    candidates.FilterByIdentifier(matcher);

    ApplyMatcher(output, request, matcher, candidates, QueryLevel_Series);

    return 0;
  }
  catch (std::runtime_error& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
  catch (boost::bad_lexical_cast& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
}


int32_t SearchForInstances(OrthancPluginRestOutput* output,
                           const char* url,
                           const OrthancPluginHttpRequest* request)
{
  try
  {
    if (request->method != OrthancPluginHttpMethod_Get)
    {
      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
      return 0;
    }

    ModuleMatcher matcher(request);

    if (request->groupsCount == 1 || request->groupsCount == 2)
    {
      // The "StudyInstanceUID" is provided by the regular expression
      matcher.AddFilter(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, request->groups[0]);
    }

    if (request->groupsCount == 2)
    {
      // The "SeriesInstanceUID" is provided by the regular expression
      matcher.AddFilter(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, request->groups[1]);
    }

    CandidateResources candidates;
    candidates.FilterByIdentifier(matcher);
    candidates.GoDown();
    candidates.FilterByIdentifier(matcher);
    candidates.GoDown();
    candidates.FilterByIdentifier(matcher);

    ApplyMatcher(output, request, matcher, candidates, QueryLevel_Instance);

    return 0;
  }
  catch (std::runtime_error& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
  catch (boost::bad_lexical_cast& e)
  {
    OrthancPluginLogError(context_, e.what());
    return -1;
  }
}