# HG changeset patch # User Sebastien Jodogne # Date 1397467715 -7200 # Node ID 5197fd35333ce5d828c3c0dbe31616354b57e34b # Parent 4afad8cb94fdb4955243c5b4dcc1a9ba5cdb40ab refactoring of OrthancRestApi diff -r 4afad8cb94fd -r 5197fd35333c CMakeLists.txt --- a/CMakeLists.txt Mon Apr 14 10:27:53 2014 +0200 +++ b/CMakeLists.txt Mon Apr 14 11:28:35 2014 +0200 @@ -207,7 +207,13 @@ OrthancServer/Internals/MoveScp.cpp OrthancServer/Internals/StoreScp.cpp OrthancServer/OrthancInitialization.cpp + OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp + OrthancServer/OrthancRestApi/OrthancRestArchive.cpp + OrthancServer/OrthancRestApi/OrthancRestChanges.cpp + OrthancServer/OrthancRestApi/OrthancRestModalities.cpp + OrthancServer/OrthancRestApi/OrthancRestResources.cpp + OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ServerIndex.cpp OrthancServer/ToDcmtkBridge.cpp OrthancServer/DatabaseWrapper.cpp diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,692 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include + +namespace Orthanc +{ + // TODO IMPROVE MULTITHREADING + // Every call to "ParsedDicomFile" must lock this mutex!!! + static boost::mutex cacheMutex_; + + + // Raw access to the DICOM tags of an instance ------------------------------ + + static void GetRawContent(RestApi::GetCall& call) + { + boost::mutex::scoped_lock lock(cacheMutex_); + + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri()); + } + + + + // Modification of DICOM instances ------------------------------------------ + + namespace + { + typedef std::set Removals; + typedef std::map Replacements; + typedef std::map< std::pair, std::string> UidMap; + } + + static void ReplaceInstanceInternal(ParsedDicomFile& toModify, + const Removals& removals, + const Replacements& replacements, + DicomReplaceMode mode, + bool removePrivateTags) + { + if (removePrivateTags) + { + toModify.RemovePrivateTags(); + } + + for (Removals::const_iterator it = removals.begin(); + it != removals.end(); ++it) + { + toModify.Remove(*it); + } + + for (Replacements::const_iterator it = replacements.begin(); + it != replacements.end(); ++it) + { + toModify.Replace(it->first, it->second, mode); + } + + // A new SOP instance UID is automatically generated + std::string instanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance); + toModify.Replace(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, DicomReplaceMode_InsertIfAbsent); + } + + + static void ParseRemovals(Removals& target, + const Json::Value& removals) + { + if (!removals.isArray()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + for (Json::Value::ArrayIndex i = 0; i < removals.size(); i++) + { + std::string name = removals[i].asString(); + DicomTag tag = FromDcmtkBridge::ParseTag(name); + target.insert(tag); + + VLOG(1) << "Removal: " << name << " " << tag << std::endl; + } + } + + + static void ParseReplacements(Replacements& target, + const Json::Value& replacements) + { + if (!replacements.isObject()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + Json::Value::Members members = replacements.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + std::string value = replacements[name].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + target[tag] = value; + + VLOG(1) << "Replacement: " << name << " " << tag << " == " << value << std::endl; + } + } + + + static std::string GeneratePatientName(ServerContext& context) + { + uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence); + return "Anonymized" + boost::lexical_cast(seq); + } + + + static void SetupAnonymization(Removals& removals, + Replacements& replacements) + { + // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles + removals.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID + //removals.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set by ReplaceInstanceInternal() + removals.insert(DicomTag(0x0008, 0x0050)); // Accession Number + removals.insert(DicomTag(0x0008, 0x0080)); // Institution Name + removals.insert(DicomTag(0x0008, 0x0081)); // Institution Address + removals.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name + removals.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address + removals.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers + removals.insert(DicomTag(0x0008, 0x1010)); // Station Name + removals.insert(DicomTag(0x0008, 0x1030)); // Study Description + removals.insert(DicomTag(0x0008, 0x103e)); // Series Description + removals.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name + removals.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record + removals.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name + removals.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study + removals.insert(DicomTag(0x0008, 0x1070)); // Operators' Name + removals.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description + removals.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID + removals.insert(DicomTag(0x0008, 0x2111)); // Derivation Description + removals.insert(DicomTag(0x0010, 0x0010)); // Patient's Name + //removals.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*) + removals.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date + removals.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time + removals.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex + removals.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids + removals.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names + removals.insert(DicomTag(0x0010, 0x1010)); // Patient's Age + removals.insert(DicomTag(0x0010, 0x1020)); // Patient's Size + removals.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight + removals.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator + removals.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group + removals.insert(DicomTag(0x0010, 0x2180)); // Occupation + removals.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History + removals.insert(DicomTag(0x0010, 0x4000)); // Patient Comments + removals.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number + removals.insert(DicomTag(0x0018, 0x1030)); // Protocol Name + //removals.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => cf. below (*) + //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => cf. below (*) + removals.insert(DicomTag(0x0020, 0x0010)); // Study ID + removals.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID + removals.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID + removals.insert(DicomTag(0x0020, 0x4000)); // Image Comments + removals.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence + removals.insert(DicomTag(0x0040, 0xa124)); // UID + removals.insert(DicomTag(0x0040, 0xa730)); // Content Sequence + removals.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID + removals.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID + removals.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID + + /** + * (*) Patient ID, Study Instance UID and Series Instance UID + * are modified by "AnonymizeInstance()" if anonymizing a single + * instance, or by "RetrieveMappedUid()" if anonymizing a + * patient/study/series. + **/ + + + // Some more removals (from the experience of DICOM files at the CHU of Liege) + removals.insert(DicomTag(0x0010, 0x1040)); // Patient's Address + removals.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician + removals.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers + removals.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts + + // Set the DeidentificationMethod tag + replacements.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1")); + + // Set the PatientIdentityRemoved tag + replacements.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); + } + + + static bool ParseModifyRequest(Removals& removals, + Replacements& replacements, + bool& removePrivateTags, + const RestApi::PostCall& call) + { + removePrivateTags = false; + Json::Value request; + if (call.ParseJsonRequest(request) && + request.isObject()) + { + Json::Value removalsPart = Json::arrayValue; + Json::Value replacementsPart = Json::objectValue; + + if (request.isMember("Remove")) + { + removalsPart = request["Remove"]; + } + + if (request.isMember("Replace")) + { + replacementsPart = request["Replace"]; + } + + if (request.isMember("RemovePrivateTags")) + { + removePrivateTags = true; + } + + ParseRemovals(removals, removalsPart); + ParseReplacements(replacements, replacementsPart); + + return true; + } + else + { + return false; + } + } + + + static bool ParseAnonymizationRequest(Removals& removals, + Replacements& replacements, + bool& removePrivateTags, + bool& keepPatientId, + RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + removePrivateTags = true; + keepPatientId = false; + + Json::Value request; + if (call.ParseJsonRequest(request) && + request.isObject()) + { + Json::Value keepPart = Json::arrayValue; + Json::Value removalsPart = Json::arrayValue; + Json::Value replacementsPart = Json::objectValue; + + if (request.isMember("Keep")) + { + keepPart = request["Keep"]; + } + + if (request.isMember("KeepPrivateTags")) + { + removePrivateTags = false; + } + + if (request.isMember("Replace")) + { + replacementsPart = request["Replace"]; + } + + Removals toKeep; + ParseRemovals(toKeep, keepPart); + + SetupAnonymization(removals, replacements); + + for (Removals::iterator it = toKeep.begin(); it != toKeep.end(); ++it) + { + if (*it == DICOM_TAG_PATIENT_ID) + { + keepPatientId = true; + } + + removals.erase(*it); + } + + Removals additionalRemovals; + ParseRemovals(additionalRemovals, removalsPart); + + for (Removals::iterator it = additionalRemovals.begin(); + it != additionalRemovals.end(); ++it) + { + removals.insert(*it); + } + + ParseReplacements(replacements, replacementsPart); + + // Generate random Patient's Name if none is specified + if (toKeep.find(DICOM_TAG_PATIENT_NAME) == toKeep.end() && + replacements.find(DICOM_TAG_PATIENT_NAME) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_PATIENT_NAME, GeneratePatientName(context))); + } + + return true; + } + else + { + return false; + } + } + + + static void AnonymizeOrModifyInstance(Removals& removals, + Replacements& replacements, + bool removePrivateTags, + RestApi::PostCall& call) + { + boost::mutex::scoped_lock lock(cacheMutex_); + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + + std::auto_ptr modified(dicom.Clone()); + ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); + modified->Answer(call.GetOutput()); + } + + + static bool RetrieveMappedUid(ParsedDicomFile& dicom, + DicomRootLevel level, + Replacements& replacements, + UidMap& uidMap) + { + std::auto_ptr tag; + + switch (level) + { + case DicomRootLevel_Series: + tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + break; + + case DicomRootLevel_Study: + tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + break; + + case DicomRootLevel_Patient: + tag.reset(new DicomTag(DICOM_TAG_PATIENT_ID)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string original; + if (!dicom.GetTagValue(original, *tag)) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string mapped; + bool isNew; + + UidMap::const_iterator previous = uidMap.find(std::make_pair(level, original)); + if (previous == uidMap.end()) + { + mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level); + uidMap.insert(std::make_pair(std::make_pair(level, original), mapped)); + isNew = true; + } + else + { + mapped = previous->second; + isNew = false; + } + + replacements[*tag] = mapped; + return isNew; + } + + + static void AnonymizeOrModifyResource(Removals& removals, + Replacements& replacements, + bool removePrivateTags, + bool keepPatientId, + MetadataType metadataType, + ChangeType changeType, + ResourceType resourceType, + RestApi::PostCall& call) + { + typedef std::list Instances; + + bool isFirst = true; + Json::Value result(Json::objectValue); + + boost::mutex::scoped_lock lock(cacheMutex_); + ServerContext& context = OrthancRestApi::GetContext(call); + + Instances instances; + std::string id = call.GetUriComponent("id", ""); + context.GetIndex().GetChildInstances(instances, id); + + if (instances.empty()) + { + return; + } + + /** + * Loop over all the instances of the resource. + **/ + + UidMap uidMap; + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + LOG(INFO) << "Modifying instance " << *it; + ParsedDicomFile& original = context.GetDicomFile(*it); + + DicomInstanceHasher originalHasher = original.GetHasher(); + + if (isFirst && keepPatientId) + { + std::string patientId = originalHasher.GetPatientId(); + uidMap[std::make_pair(DicomRootLevel_Patient, patientId)] = patientId; + } + + bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap); + bool isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap); + bool isNewPatient = RetrieveMappedUid(original, DicomRootLevel_Patient, replacements, uidMap); + + + /** + * Compute the resulting DICOM instance and store it into the Orthanc store. + **/ + + std::auto_ptr modified(original.Clone()); + ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); + + std::string modifiedInstance; + if (context.Store(modifiedInstance, modified->GetDicom()) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << *it; + return; + } + + + /** + * Record metadata information (AnonymizedFrom/ModifiedFrom). + **/ + + DicomInstanceHasher modifiedHasher = modified->GetHasher(); + + if (isNewSeries) + { + context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), + metadataType, originalHasher.HashSeries()); + } + + if (isNewStudy) + { + context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), + metadataType, originalHasher.HashStudy()); + } + + if (isNewPatient) + { + context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), + metadataType, originalHasher.HashPatient()); + } + + assert(*it == originalHasher.HashInstance()); + assert(modifiedInstance == modifiedHasher.HashInstance()); + context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); + + + /** + * Compute the JSON object that is returned by the REST call. + **/ + + if (isFirst) + { + std::string newId; + + switch (resourceType) + { + case ResourceType_Series: + newId = modifiedHasher.HashSeries(); + break; + + case ResourceType_Study: + newId = modifiedHasher.HashStudy(); + break; + + case ResourceType_Patient: + newId = modifiedHasher.HashPatient(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + result["Type"] = EnumerationToString(resourceType); + result["ID"] = newId; + result["Path"] = GetBasePath(resourceType, newId); + result["PatientID"] = modifiedHasher.HashPatient(); + isFirst = false; + } + } + + call.GetOutput().AnswerJson(result); + } + + + + static void ModifyInstance(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); + } + } + + + static void AnonymizeInstance(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + // TODO Handle "keepPatientId" + + // Generate random patient ID if not specified + if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient))); + } + + // Generate random study UID if not specified + if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study))); + } + + // Generate random series UID if not specified + if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series))); + } + + AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); + } + } + + + static void ModifySeriesInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, + MetadataType_ModifiedFrom, ChangeType_ModifiedSeries, + ResourceType_Series, call); + } + } + + + static void AnonymizeSeriesInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedSeries, + ResourceType_Series, call); + } + } + + + static void ModifyStudyInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, + MetadataType_ModifiedFrom, ChangeType_ModifiedStudy, + ResourceType_Study, call); + } + } + + + static void AnonymizeStudyInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedStudy, + ResourceType_Study, call); + } + } + + + /*static void ModifyPatientInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, + MetadataType_ModifiedFrom, ChangeType_ModifiedPatient, + ResourceType_Patient, call); + } + }*/ + + + static void AnonymizePatientInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedPatient, + ResourceType_Patient, call); + } + } + + + + void OrthancRestApi::RegisterAnonymizeModify() + { + Register("/instances/{id}/content/*", GetRawContent); + + Register("/instances/{id}/modify", ModifyInstance); + Register("/series/{id}/modify", ModifySeriesInplace); + Register("/studies/{id}/modify", ModifyStudyInplace); + //Register("/patients/{id}/modify", ModifyPatientInplace); + + Register("/instances/{id}/anonymize", AnonymizeInstance); + Register("/series/{id}/anonymize", AnonymizeSeriesInplace); + Register("/studies/{id}/anonymize", AnonymizeStudyInplace); + Register("/patients/{id}/anonymize", AnonymizePatientInplace); + } +} diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Mon Apr 14 10:27:53 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -32,897 +32,10 @@ #include "OrthancRestApi.h" -#include "../../Core/Compression/HierarchicalZipWriter.h" -#include "../../Core/HttpClient.h" -#include "../../Core/HttpServer/FilesystemHttpSender.h" -#include "../../Core/Uuid.h" -#include "../DicomProtocol/DicomUserConnection.h" -#include "../FromDcmtkBridge.h" -#include "../OrthancInitialization.h" -#include "../ServerToolbox.h" - -#include -#include -#include #include -#if defined(_MSC_VER) -#define snprintf _snprintf -#endif - -static const uint64_t MEGA_BYTES = 1024 * 1024; -static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024; - - namespace Orthanc { - // TODO IMPROVE MULTITHREADING - // Every call to "ParsedDicomFile" must lock this mutex!!! - static boost::mutex cacheMutex_; - - - // DICOM SCU ---------------------------------------------------------------- - - static bool MergeQueryAndTemplate(DicomMap& result, - const std::string& postData) - { - Json::Value query; - Json::Reader reader; - - if (!reader.parse(postData, query) || - query.type() != Json::objectValue) - { - return false; - } - - Json::Value::Members members = query.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - DicomTag t = FromDcmtkBridge::ParseTag(members[i]); - result.SetValue(t, query[members[i]].asString()); - } - - return true; - } - - static void DicomFindPatient(RestApi::PostCall& call) - { - DicomMap m; - DicomMap::SetupFindPatientTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); - - DicomFindAnswers answers; - connection.FindPatient(answers, m); - - Json::Value result; - answers.ToJson(result); - call.GetOutput().AnswerJson(result); - } - - static void DicomFindStudy(RestApi::PostCall& call) - { - DicomMap m; - DicomMap::SetupFindStudyTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - - if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); - - DicomFindAnswers answers; - connection.FindStudy(answers, m); - - Json::Value result; - answers.ToJson(result); - call.GetOutput().AnswerJson(result); - } - - static void DicomFindSeries(RestApi::PostCall& call) - { - DicomMap m; - DicomMap::SetupFindSeriesTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - - if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); - - DicomFindAnswers answers; - connection.FindSeries(answers, m); - - Json::Value result; - answers.ToJson(result); - call.GetOutput().AnswerJson(result); - } - - static void DicomFindInstance(RestApi::PostCall& call) - { - DicomMap m; - DicomMap::SetupFindInstanceTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - - if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || - m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); - - DicomFindAnswers answers; - connection.FindInstance(answers, m); - - Json::Value result; - answers.ToJson(result); - call.GetOutput().AnswerJson(result); - } - - static void DicomFind(RestApi::PostCall& call) - { - DicomMap m; - DicomMap::SetupFindPatientTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); - - DicomFindAnswers patients; - connection.FindPatient(patients, m); - - // Loop over the found patients - Json::Value result = Json::arrayValue; - for (size_t i = 0; i < patients.GetSize(); i++) - { - Json::Value patient(Json::objectValue); - FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); - - DicomMap::SetupFindStudyTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); - - DicomFindAnswers studies; - connection.FindStudy(studies, m); - - patient["Studies"] = Json::arrayValue; - - // Loop over the found studies - for (size_t j = 0; j < studies.GetSize(); j++) - { - Json::Value study(Json::objectValue); - FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); - - DicomMap::SetupFindSeriesTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) - { - return; - } - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); - - DicomFindAnswers series; - connection.FindSeries(series, m); - - // Loop over the found series - study["Series"] = Json::arrayValue; - for (size_t k = 0; k < series.GetSize(); k++) - { - Json::Value series2(Json::objectValue); - FromDcmtkBridge::ToJson(series2, series.GetAnswer(k)); - study["Series"].append(series2); - } - - patient["Studies"].append(study); - } - - result.append(patient); - } - - call.GetOutput().AnswerJson(result); - } - - - static bool GetInstancesToExport(std::list& instances, - const std::string& remote, - RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string stripped = Toolbox::StripSpaces(call.GetPostBody()); - - Json::Value request; - if (Toolbox::IsSHA1(stripped)) - { - // This is for compatibility with Orthanc <= 0.5.1. - request = stripped; - } - else if (!call.ParseJsonRequest(request)) - { - // Bad JSON request - return false; - } - - if (request.isString()) - { - context.GetIndex().LogExportedResource(request.asString(), remote); - context.GetIndex().GetChildInstances(instances, request.asString()); - } - else if (request.isArray()) - { - for (Json::Value::ArrayIndex i = 0; i < request.size(); i++) - { - if (!request[i].isString()) - { - return false; - } - - std::string stripped = Toolbox::StripSpaces(request[i].asString()); - if (!Toolbox::IsSHA1(stripped)) - { - return false; - } - - context.GetIndex().LogExportedResource(stripped, remote); - - std::list tmp; - context.GetIndex().GetChildInstances(tmp, stripped); - - for (std::list::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - instances.push_back(*it); - } - } - } - else - { - // Neither a string, nor a list of strings. Bad request. - return false; - } - - return true; - } - - - static void DicomStore(RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string remote = call.GetUriComponent("id", ""); - - std::list instances; - if (!GetInstancesToExport(instances, remote, call)) - { - return; - } - - DicomUserConnection connection; - ConnectToModalityUsingSymbolicName(connection, remote); - - for (std::list::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\""; - - std::string dicom; - context.ReadFile(dicom, *it, FileContentType_Dicom); - connection.Store(dicom); - } - - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - - - - // System information ------------------------------------------------------- - - static void ServeRoot(RestApi::GetCall& call) - { - call.GetOutput().Redirect("app/explorer.html"); - } - - static void GetSystemInformation(RestApi::GetCall& call) - { - Json::Value result = Json::objectValue; - - result["Version"] = ORTHANC_VERSION; - result["Name"] = GetGlobalStringParameter("Name", ""); - - call.GetOutput().AnswerJson(result); - } - - static void GetStatistics(RestApi::GetCall& call) - { - Json::Value result = Json::objectValue; - OrthancRestApi::GetIndex(call).ComputeStatistics(result); - call.GetOutput().AnswerJson(result); - } - - static void GenerateUid(RestApi::GetCall& call) - { - std::string level = call.GetArgument("level", ""); - if (level == "patient") - { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient), "text/plain"); - } - else if (level == "study") - { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study), "text/plain"); - } - else if (level == "series") - { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series), "text/plain"); - } - else if (level == "instance") - { - call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance), "text/plain"); - } - } - - static void ExecuteScript(RestApi::PostCall& call) - { - std::string result; - ServerContext& context = OrthancRestApi::GetContext(call); - context.GetLuaContext().Execute(result, call.GetPostBody()); - call.GetOutput().AnswerBuffer(result, "text/plain"); - } - - static void GetNowIsoString(RestApi::GetCall& call) - { - call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain"); - } - - - - - - - // List all the patients, studies, series or instances ---------------------- - - template - static void ListResources(RestApi::GetCall& call) - { - Json::Value result; - OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType); - call.GetOutput().AnswerJson(result); - } - - template - static void GetSingleResource(RestApi::GetCall& call) - { - Json::Value result; - if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) - { - call.GetOutput().AnswerJson(result); - } - } - - template - static void DeleteSingleResource(RestApi::DeleteCall& call) - { - Json::Value result; - if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) - { - call.GetOutput().AnswerJson(result); - } - } - - - // Download of ZIP files ---------------------------------------------------- - - - static std::string GetDirectoryNameInArchive(const Json::Value& resource, - ResourceType resourceType) - { - switch (resourceType) - { - case ResourceType_Patient: - { - std::string p = resource["MainDicomTags"]["PatientID"].asString(); - std::string n = resource["MainDicomTags"]["PatientName"].asString(); - return p + " " + n; - } - - case ResourceType_Study: - { - return resource["MainDicomTags"]["StudyDescription"].asString(); - } - - case ResourceType_Series: - { - std::string d = resource["MainDicomTags"]["SeriesDescription"].asString(); - std::string m = resource["MainDicomTags"]["Modality"].asString(); - return m + " " + d; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, - ServerContext& context, - const Json::Value& resource, - ResourceType resourceType) - { - if (resourceType == ResourceType_Patient) - { - return true; - } - - ResourceType parentType = GetParentResourceType(resourceType); - Json::Value parent; - - switch (resourceType) - { - case ResourceType_Study: - { - if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType)) - { - return false; - } - - break; - } - - case ResourceType_Series: - if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) || - !CreateRootDirectoryInArchive(writer, context, parent, parentType)) - { - return false; - } - break; - - default: - throw OrthancException(ErrorCode_NotImplemented); - } - - writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str()); - return true; - } - - static bool ArchiveInstance(HierarchicalZipWriter& writer, - ServerContext& context, - const std::string& instancePublicId, - const char* filename) - { - Json::Value instance; - - if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance)) - { - return false; - } - - writer.OpenFile(filename); - - std::string dicom; - context.ReadFile(dicom, instancePublicId, FileContentType_Dicom); - writer.Write(dicom); - - return true; - } - - static bool ArchiveInternal(HierarchicalZipWriter& writer, - ServerContext& context, - const std::string& publicId, - ResourceType resourceType, - bool isFirstLevel) - { - Json::Value resource; - if (!context.GetIndex().LookupResource(resource, publicId, resourceType)) - { - return false; - } - - if (isFirstLevel && - !CreateRootDirectoryInArchive(writer, context, resource, resourceType)) - { - return false; - } - - writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str()); - - switch (resourceType) - { - case ResourceType_Patient: - for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++) - { - std::string studyId = resource["Studies"][i].asString(); - if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false)) - { - return false; - } - } - break; - - case ResourceType_Study: - for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++) - { - std::string seriesId = resource["Series"][i].asString(); - if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false)) - { - return false; - } - } - break; - - case ResourceType_Series: - { - // Create a filename prefix, depending on the modality - char format[16] = "%08d"; - - if (resource["MainDicomTags"].isMember("Modality")) - { - std::string modality = resource["MainDicomTags"]["Modality"].asString(); - - if (modality.size() == 1) - { - snprintf(format, sizeof(format) - 1, "%c%%07d", toupper(modality[0])); - } - else if (modality.size() >= 2) - { - snprintf(format, sizeof(format) - 1, "%c%c%%06d", toupper(modality[0]), toupper(modality[1])); - } - } - - char filename[16]; - - for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++) - { - snprintf(filename, sizeof(filename) - 1, format, i); - - std::string publicId = resource["Instances"][i].asString(); - - // This was the implementation up to Orthanc 0.7.0: - // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm"; - - if (!ArchiveInstance(writer, context, publicId, filename)) - { - return false; - } - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - writer.CloseDirectory(); - return true; - } - - template - static void GetArchive(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string id = call.GetUriComponent("id", ""); - - /** - * Determine whether ZIP64 is required. Original ZIP format can - * store up to 2GB of data (some implementation supporting up to - * 4GB of data), and up to 65535 files. - * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64 - **/ - - uint64_t uncompressedSize; - uint64_t compressedSize; - unsigned int countStudies; - unsigned int countSeries; - unsigned int countInstances; - context.GetIndex().GetStatistics(compressedSize, uncompressedSize, - countStudies, countSeries, countInstances, id); - const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES || - countInstances >= 65535); - - LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size " - << (uncompressedSize / MEGA_BYTES) << "MB using the " - << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; - - // Create a RAII for the temporary file to manage the ZIP file - Toolbox::TemporaryFile tmp; - - { - // Create a ZIP writer - HierarchicalZipWriter writer(tmp.GetPath().c_str()); - writer.SetZip64(isZip64); - - // Store the requested resource into the ZIP - if (!ArchiveInternal(writer, context, id, resourceType, true)) - { - return; - } - } - - // Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp.GetPath().c_str()); - sender.SetContentType("application/zip"); - sender.SetDownloadFilename(id + ".zip"); - - // Send the ZIP - call.GetOutput().AnswerFile(sender); - - // The temporary file is automatically removed thanks to the RAII - } - - - // Changes API -------------------------------------------------------------- - - static void GetSinceAndLimit(int64_t& since, - unsigned int& limit, - bool& last, - const RestApi::GetCall& call) - { - static const unsigned int MAX_RESULTS = 100; - - if (call.HasArgument("last")) - { - last = true; - return; - } - - last = false; - - try - { - since = boost::lexical_cast(call.GetArgument("since", "0")); - limit = boost::lexical_cast(call.GetArgument("limit", "0")); - } - catch (boost::bad_lexical_cast) - { - return; - } - - if (limit == 0 || limit > MAX_RESULTS) - { - limit = MAX_RESULTS; - } - } - - static void GetChanges(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - //std::string filter = GetArgument(getArguments, "filter", ""); - int64_t since; - unsigned int limit; - bool last; - GetSinceAndLimit(since, limit, last, call); - - Json::Value result; - if ((!last && context.GetIndex().GetChanges(result, since, limit)) || - ( last && context.GetIndex().GetLastChange(result))) - { - call.GetOutput().AnswerJson(result); - } - } - - - static void DeleteChanges(RestApi::DeleteCall& call) - { - OrthancRestApi::GetIndex(call).DeleteChanges(); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - - - static void GetExports(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - int64_t since; - unsigned int limit; - bool last; - GetSinceAndLimit(since, limit, last, call); - - Json::Value result; - if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) || - ( last && context.GetIndex().GetLastExportedResource(result))) - { - call.GetOutput().AnswerJson(result); - } - } - - - static void DeleteExports(RestApi::DeleteCall& call) - { - OrthancRestApi::GetIndex(call).DeleteExportedResources(); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - - - // Get information about a single patient ----------------------------------- - - static void IsProtectedPatient(RestApi::GetCall& call) - { - std::string publicId = call.GetUriComponent("id", ""); - bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); - call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); - } - - - static void SetPatientProtection(RestApi::PutCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string s = Toolbox::StripSpaces(call.GetPutBody()); - - if (s == "0") - { - context.GetIndex().SetProtectedPatient(publicId, false); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - else if (s == "1") - { - context.GetIndex().SetProtectedPatient(publicId, true); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - else - { - // Bad request - } - } - - - // Get information about a single instance ---------------------------------- - - static void GetInstanceFile(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string publicId = call.GetUriComponent("id", ""); - context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom); - } - - - static void ExportInstanceFile(RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string publicId = call.GetUriComponent("id", ""); - - std::string dicom; - context.ReadFile(dicom, publicId, FileContentType_Dicom); - - Toolbox::WriteFile(dicom, call.GetPostBody()); - - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - - - template - static void GetInstanceTags(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string publicId = call.GetUriComponent("id", ""); - - Json::Value full; - context.ReadJson(full, publicId); - - if (simplify) - { - Json::Value simplified; - SimplifyTags(simplified, full); - call.GetOutput().AnswerJson(simplified); - } - else - { - call.GetOutput().AnswerJson(full); - } - } - - - static void ListFrames(RestApi::GetCall& call) - { - Json::Value instance; - if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) - { - unsigned int numberOfFrames = 1; - - try - { - Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; - numberOfFrames = boost::lexical_cast(tmp.asString()); - } - catch (...) - { - } - - Json::Value result = Json::arrayValue; - for (unsigned int i = 0; i < numberOfFrames; i++) - { - result.append(i); - } - - call.GetOutput().AnswerJson(result); - } - } - - - template - static void GetImage(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string frameId = call.GetUriComponent("frame", "0"); - - unsigned int frame; - try - { - frame = boost::lexical_cast(frameId); - } - catch (boost::bad_lexical_cast) - { - return; - } - - std::string publicId = call.GetUriComponent("id", ""); - std::string dicomContent, png; - context.ReadFile(dicomContent, publicId, FileContentType_Dicom); - - try - { - FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); - call.GetOutput().AnswerBuffer(png, "image/png"); - } - catch (OrthancException& e) - { - if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) - { - // The frame number is out of the range for this DICOM - // instance, the resource is not existent - } - else - { - std::string root = ""; - for (size_t i = 1; i < call.GetFullUri().size(); i++) - { - root += "../"; - } - - call.GetOutput().Redirect(root + "app/images/unsupported.png"); - } - } - } - - // Upload of DICOM files through HTTP --------------------------------------- static void UploadDicomFile(RestApi::PostCall& call) @@ -953,1181 +66,19 @@ - // DICOM bridge ------------------------------------------------------------- - - static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities, - const std::string& id) - { - return modalities.find(id) != modalities.end(); - } - - static void ListModalities(RestApi::GetCall& call) - { - OrthancRestApi::SetOfStrings modalities; - GetListOfDicomModalities(modalities); - - Json::Value result = Json::arrayValue; - for (OrthancRestApi::SetOfStrings::const_iterator - it = modalities.begin(); it != modalities.end(); ++it) - { - result.append(*it); - } - - call.GetOutput().AnswerJson(result); - } - - - static void ListModalityOperations(RestApi::GetCall& call) - { - OrthancRestApi::SetOfStrings modalities; - GetListOfDicomModalities(modalities); - - std::string id = call.GetUriComponent("id", ""); - if (IsExistingModality(modalities, id)) - { - Json::Value result = Json::arrayValue; - result.append("find-patient"); - result.append("find-study"); - result.append("find-series"); - result.append("find-instance"); - result.append("find"); - result.append("store"); - call.GetOutput().AnswerJson(result); - } - } - - - - // Raw access to the DICOM tags of an instance ------------------------------ - - static void GetRawContent(RestApi::GetCall& call) - { - boost::mutex::scoped_lock lock(cacheMutex_); - - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string id = call.GetUriComponent("id", ""); - ParsedDicomFile& dicom = context.GetDicomFile(id); - dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri()); - } - - - - // Modification of DICOM instances ------------------------------------------ - - namespace - { - typedef std::set Removals; - typedef std::map Replacements; - typedef std::map< std::pair, std::string> UidMap; - } - - static void ReplaceInstanceInternal(ParsedDicomFile& toModify, - const Removals& removals, - const Replacements& replacements, - DicomReplaceMode mode, - bool removePrivateTags) - { - if (removePrivateTags) - { - toModify.RemovePrivateTags(); - } - - for (Removals::const_iterator it = removals.begin(); - it != removals.end(); ++it) - { - toModify.Remove(*it); - } - - for (Replacements::const_iterator it = replacements.begin(); - it != replacements.end(); ++it) - { - toModify.Replace(it->first, it->second, mode); - } - - // A new SOP instance UID is automatically generated - std::string instanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance); - toModify.Replace(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, DicomReplaceMode_InsertIfAbsent); - } - - - static void ParseRemovals(Removals& target, - const Json::Value& removals) - { - if (!removals.isArray()) - { - throw OrthancException(ErrorCode_BadRequest); - } - - for (Json::Value::ArrayIndex i = 0; i < removals.size(); i++) - { - std::string name = removals[i].asString(); - DicomTag tag = FromDcmtkBridge::ParseTag(name); - target.insert(tag); - - VLOG(1) << "Removal: " << name << " " << tag << std::endl; - } - } - - - static void ParseReplacements(Replacements& target, - const Json::Value& replacements) - { - if (!replacements.isObject()) - { - throw OrthancException(ErrorCode_BadRequest); - } - - Json::Value::Members members = replacements.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - const std::string& name = members[i]; - std::string value = replacements[name].asString(); - - DicomTag tag = FromDcmtkBridge::ParseTag(name); - target[tag] = value; - - VLOG(1) << "Replacement: " << name << " " << tag << " == " << value << std::endl; - } - } - - - static std::string GeneratePatientName(ServerContext& context) - { - uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence); - return "Anonymized" + boost::lexical_cast(seq); - } - - - static void SetupAnonymization(Removals& removals, - Replacements& replacements) - { - // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles - removals.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID - //removals.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set by ReplaceInstanceInternal() - removals.insert(DicomTag(0x0008, 0x0050)); // Accession Number - removals.insert(DicomTag(0x0008, 0x0080)); // Institution Name - removals.insert(DicomTag(0x0008, 0x0081)); // Institution Address - removals.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name - removals.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address - removals.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers - removals.insert(DicomTag(0x0008, 0x1010)); // Station Name - removals.insert(DicomTag(0x0008, 0x1030)); // Study Description - removals.insert(DicomTag(0x0008, 0x103e)); // Series Description - removals.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name - removals.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record - removals.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name - removals.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study - removals.insert(DicomTag(0x0008, 0x1070)); // Operators' Name - removals.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description - removals.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID - removals.insert(DicomTag(0x0008, 0x2111)); // Derivation Description - removals.insert(DicomTag(0x0010, 0x0010)); // Patient's Name - //removals.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*) - removals.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date - removals.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time - removals.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex - removals.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids - removals.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names - removals.insert(DicomTag(0x0010, 0x1010)); // Patient's Age - removals.insert(DicomTag(0x0010, 0x1020)); // Patient's Size - removals.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight - removals.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator - removals.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group - removals.insert(DicomTag(0x0010, 0x2180)); // Occupation - removals.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History - removals.insert(DicomTag(0x0010, 0x4000)); // Patient Comments - removals.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number - removals.insert(DicomTag(0x0018, 0x1030)); // Protocol Name - //removals.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => cf. below (*) - //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => cf. below (*) - removals.insert(DicomTag(0x0020, 0x0010)); // Study ID - removals.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID - removals.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID - removals.insert(DicomTag(0x0020, 0x4000)); // Image Comments - removals.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence - removals.insert(DicomTag(0x0040, 0xa124)); // UID - removals.insert(DicomTag(0x0040, 0xa730)); // Content Sequence - removals.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID - removals.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID - removals.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID - - /** - * (*) Patient ID, Study Instance UID and Series Instance UID - * are modified by "AnonymizeInstance()" if anonymizing a single - * instance, or by "RetrieveMappedUid()" if anonymizing a - * patient/study/series. - **/ - - - // Some more removals (from the experience of DICOM files at the CHU of Liege) - removals.insert(DicomTag(0x0010, 0x1040)); // Patient's Address - removals.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician - removals.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers - removals.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts - - // Set the DeidentificationMethod tag - replacements.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1")); - - // Set the PatientIdentityRemoved tag - replacements.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); - } - - - static bool ParseModifyRequest(Removals& removals, - Replacements& replacements, - bool& removePrivateTags, - const RestApi::PostCall& call) - { - removePrivateTags = false; - Json::Value request; - if (call.ParseJsonRequest(request) && - request.isObject()) - { - Json::Value removalsPart = Json::arrayValue; - Json::Value replacementsPart = Json::objectValue; - - if (request.isMember("Remove")) - { - removalsPart = request["Remove"]; - } - - if (request.isMember("Replace")) - { - replacementsPart = request["Replace"]; - } - - if (request.isMember("RemovePrivateTags")) - { - removePrivateTags = true; - } - - ParseRemovals(removals, removalsPart); - ParseReplacements(replacements, replacementsPart); - - return true; - } - else - { - return false; - } - } - - - static bool ParseAnonymizationRequest(Removals& removals, - Replacements& replacements, - bool& removePrivateTags, - bool& keepPatientId, - RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - removePrivateTags = true; - keepPatientId = false; - - Json::Value request; - if (call.ParseJsonRequest(request) && - request.isObject()) - { - Json::Value keepPart = Json::arrayValue; - Json::Value removalsPart = Json::arrayValue; - Json::Value replacementsPart = Json::objectValue; - - if (request.isMember("Keep")) - { - keepPart = request["Keep"]; - } - - if (request.isMember("KeepPrivateTags")) - { - removePrivateTags = false; - } - - if (request.isMember("Replace")) - { - replacementsPart = request["Replace"]; - } - - Removals toKeep; - ParseRemovals(toKeep, keepPart); - - SetupAnonymization(removals, replacements); - - for (Removals::iterator it = toKeep.begin(); it != toKeep.end(); ++it) - { - if (*it == DICOM_TAG_PATIENT_ID) - { - keepPatientId = true; - } - - removals.erase(*it); - } - - Removals additionalRemovals; - ParseRemovals(additionalRemovals, removalsPart); - - for (Removals::iterator it = additionalRemovals.begin(); - it != additionalRemovals.end(); ++it) - { - removals.insert(*it); - } - - ParseReplacements(replacements, replacementsPart); - - // Generate random Patient's Name if none is specified - if (toKeep.find(DICOM_TAG_PATIENT_NAME) == toKeep.end() && - replacements.find(DICOM_TAG_PATIENT_NAME) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_PATIENT_NAME, GeneratePatientName(context))); - } - - return true; - } - else - { - return false; - } - } - - - static void AnonymizeOrModifyInstance(Removals& removals, - Replacements& replacements, - bool removePrivateTags, - RestApi::PostCall& call) - { - boost::mutex::scoped_lock lock(cacheMutex_); - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string id = call.GetUriComponent("id", ""); - ParsedDicomFile& dicom = context.GetDicomFile(id); - - std::auto_ptr modified(dicom.Clone()); - ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); - modified->Answer(call.GetOutput()); - } - - - static bool RetrieveMappedUid(ParsedDicomFile& dicom, - DicomRootLevel level, - Replacements& replacements, - UidMap& uidMap) - { - std::auto_ptr tag; - - switch (level) - { - case DicomRootLevel_Series: - tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); - break; - - case DicomRootLevel_Study: - tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); - break; - - case DicomRootLevel_Patient: - tag.reset(new DicomTag(DICOM_TAG_PATIENT_ID)); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - std::string original; - if (!dicom.GetTagValue(original, *tag)) - { - throw OrthancException(ErrorCode_InternalError); - } - - std::string mapped; - bool isNew; - - UidMap::const_iterator previous = uidMap.find(std::make_pair(level, original)); - if (previous == uidMap.end()) - { - mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level); - uidMap.insert(std::make_pair(std::make_pair(level, original), mapped)); - isNew = true; - } - else - { - mapped = previous->second; - isNew = false; - } - - replacements[*tag] = mapped; - return isNew; - } - - - static void AnonymizeOrModifyResource(Removals& removals, - Replacements& replacements, - bool removePrivateTags, - bool keepPatientId, - MetadataType metadataType, - ChangeType changeType, - ResourceType resourceType, - RestApi::PostCall& call) - { - typedef std::list Instances; - - bool isFirst = true; - Json::Value result(Json::objectValue); - - boost::mutex::scoped_lock lock(cacheMutex_); - ServerContext& context = OrthancRestApi::GetContext(call); - - Instances instances; - std::string id = call.GetUriComponent("id", ""); - context.GetIndex().GetChildInstances(instances, id); - - if (instances.empty()) - { - return; - } - - /** - * Loop over all the instances of the resource. - **/ - - UidMap uidMap; - for (Instances::const_iterator it = instances.begin(); - it != instances.end(); ++it) - { - LOG(INFO) << "Modifying instance " << *it; - ParsedDicomFile& original = context.GetDicomFile(*it); - - DicomInstanceHasher originalHasher = original.GetHasher(); - - if (isFirst && keepPatientId) - { - std::string patientId = originalHasher.GetPatientId(); - uidMap[std::make_pair(DicomRootLevel_Patient, patientId)] = patientId; - } - - bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap); - bool isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap); - bool isNewPatient = RetrieveMappedUid(original, DicomRootLevel_Patient, replacements, uidMap); - - - /** - * Compute the resulting DICOM instance and store it into the Orthanc store. - **/ - - std::auto_ptr modified(original.Clone()); - ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); - - std::string modifiedInstance; - if (context.Store(modifiedInstance, modified->GetDicom()) != StoreStatus_Success) - { - LOG(ERROR) << "Error while storing a modified instance " << *it; - return; - } - - - /** - * Record metadata information (AnonymizedFrom/ModifiedFrom). - **/ - - DicomInstanceHasher modifiedHasher = modified->GetHasher(); - - if (isNewSeries) - { - context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), - metadataType, originalHasher.HashSeries()); - } - - if (isNewStudy) - { - context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), - metadataType, originalHasher.HashStudy()); - } - - if (isNewPatient) - { - context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), - metadataType, originalHasher.HashPatient()); - } - - assert(*it == originalHasher.HashInstance()); - assert(modifiedInstance == modifiedHasher.HashInstance()); - context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); - - - /** - * Compute the JSON object that is returned by the REST call. - **/ - - if (isFirst) - { - std::string newId; - - switch (resourceType) - { - case ResourceType_Series: - newId = modifiedHasher.HashSeries(); - break; - - case ResourceType_Study: - newId = modifiedHasher.HashStudy(); - break; - - case ResourceType_Patient: - newId = modifiedHasher.HashPatient(); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - result["Type"] = EnumerationToString(resourceType); - result["ID"] = newId; - result["Path"] = GetBasePath(resourceType, newId); - result["PatientID"] = modifiedHasher.HashPatient(); - isFirst = false; - } - } - - call.GetOutput().AnswerJson(result); - } - - - - static void ModifyInstance(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags; - - if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) - { - AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); - } - } - - - static void AnonymizeInstance(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags, keepPatientId; - - if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) - { - // TODO Handle "keepPatientId" - - // Generate random patient ID if not specified - if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient))); - } - - // Generate random study UID if not specified - if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study))); - } - - // Generate random series UID if not specified - if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series))); - } - - AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); - } - } - - - static void ModifySeriesInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags; - - if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) - { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, - MetadataType_ModifiedFrom, ChangeType_ModifiedSeries, - ResourceType_Series, call); - } - } - - - static void AnonymizeSeriesInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags, keepPatientId; - - if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) - { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, - MetadataType_AnonymizedFrom, ChangeType_AnonymizedSeries, - ResourceType_Series, call); - } - } - - - static void ModifyStudyInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags; - - if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) - { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, - MetadataType_ModifiedFrom, ChangeType_ModifiedStudy, - ResourceType_Study, call); - } - } - - - static void AnonymizeStudyInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags, keepPatientId; - - if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) - { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, - MetadataType_AnonymizedFrom, ChangeType_AnonymizedStudy, - ResourceType_Study, call); - } - } - - - /*static void ModifyPatientInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags; - - if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) - { - AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, - MetadataType_ModifiedFrom, ChangeType_ModifiedPatient, - ResourceType_Patient, call); - } - }*/ - - - static void AnonymizePatientInplace(RestApi::PostCall& call) - { - Removals removals; - Replacements replacements; - bool removePrivateTags, keepPatientId; - - if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) - { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, - MetadataType_AnonymizedFrom, ChangeType_AnonymizedPatient, - ResourceType_Patient, call); - } - } - - - // Handling of metadata ----------------------------------------------------- - - static void CheckValidResourceType(RestApi::Call& call) - { - std::string resourceType = call.GetUriComponent("resourceType", ""); - StringToResourceType(resourceType.c_str()); - } - - - static void ListMetadata(RestApi::GetCall& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::list metadata; - - OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId); - Json::Value result = Json::arrayValue; - - for (std::list::const_iterator - it = metadata.begin(); it != metadata.end(); ++it) - { - result.append(EnumerationToString(*it)); - } - - call.GetOutput().AnswerJson(result); - } - - - static void GetMetadata(RestApi::GetCall& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - MetadataType metadata = StringToMetadata(name); - - std::string value; - if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata)) - { - call.GetOutput().AnswerBuffer(value, "text/plain"); - } - } - - - static void DeleteMetadata(RestApi::DeleteCall& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - MetadataType metadata = StringToMetadata(name); - - if (metadata >= MetadataType_StartUser && - metadata <= MetadataType_EndUser) - { - // It is forbidden to modify internal metadata - OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - } - - - static void SetMetadata(RestApi::PutCall& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - MetadataType metadata = StringToMetadata(name); - std::string value = call.GetPutBody(); - - if (metadata >= MetadataType_StartUser && - metadata <= MetadataType_EndUser) - { - // It is forbidden to modify internal metadata - OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); - call.GetOutput().AnswerBuffer("", "text/plain"); - } - } - - - static void GetResourceStatistics(RestApi::GetCall& call) - { - std::string publicId = call.GetUriComponent("id", ""); - Json::Value result; - OrthancRestApi::GetIndex(call).GetStatistics(result, publicId); - call.GetOutput().AnswerJson(result); - } - - - - // Orthanc Peers ------------------------------------------------------------ - - static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers, - const std::string& id) - { - return peers.find(id) != peers.end(); - } - - static void ListPeers(RestApi::GetCall& call) - { - OrthancRestApi::SetOfStrings peers; - GetListOfOrthancPeers(peers); - - Json::Value result = Json::arrayValue; - for (OrthancRestApi::SetOfStrings::const_iterator - it = peers.begin(); it != peers.end(); ++it) - { - result.append(*it); - } - - call.GetOutput().AnswerJson(result); - } - - static void ListPeerOperations(RestApi::GetCall& call) - { - OrthancRestApi::SetOfStrings peers; - GetListOfOrthancPeers(peers); - - std::string id = call.GetUriComponent("id", ""); - if (IsExistingPeer(peers, id)) - { - Json::Value result = Json::arrayValue; - result.append("store"); - call.GetOutput().AnswerJson(result); - } - } - - static void PeerStore(RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string remote = call.GetUriComponent("id", ""); - - std::list instances; - if (!GetInstancesToExport(instances, remote, call)) - { - return; - } - - std::string url, username, password; - GetOrthancPeer(remote, url, username, password); - - // Configure the HTTP client - HttpClient client; - if (username.size() != 0 && password.size() != 0) - { - client.SetCredentials(username.c_str(), password.c_str()); - } - - client.SetUrl(url + "instances"); - client.SetMethod(HttpMethod_Post); - - // Loop over the instances that are to be sent - for (std::list::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\""; - - context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); - - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\""; - return; - } - } - - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - - - - - - - // Handling of attached files ----------------------------------------------- - - static void ListAttachments(RestApi::GetCall& call) - { - std::string resourceType = call.GetUriComponent("resourceType", ""); - std::string publicId = call.GetUriComponent("id", ""); - std::list attachments; - OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); - - Json::Value result = Json::arrayValue; - - for (std::list::const_iterator - it = attachments.begin(); it != attachments.end(); ++it) - { - result.append(EnumerationToString(*it)); - } - - call.GetOutput().AnswerJson(result); - } - - - static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - FileContentType contentType = StringToContentType(name); - - return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); - } - - - static void GetAttachmentOperations(RestApi::GetCall& call) - { - FileInfo info; - if (GetAttachmentInfo(info, call)) - { - Json::Value operations = Json::arrayValue; - - operations.append("compressed-data"); - - if (info.GetCompressedMD5() != "") - { - operations.append("compressed-md5"); - } - - operations.append("compressed-size"); - operations.append("data"); - - if (info.GetUncompressedMD5() != "") - { - operations.append("md5"); - } - - operations.append("size"); - - if (info.GetCompressedMD5() != "" && - info.GetUncompressedMD5() != "") - { - operations.append("verify-md5"); - } - - call.GetOutput().AnswerJson(operations); - } - } - - - template - static void GetAttachmentData(RestApi::GetCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - - std::string content; - context.ReadFile(content, publicId, StringToContentType(name), - (uncompress == 1)); - - call.GetOutput().AnswerBuffer(content, "application/octet-stream"); - } - - - static void GetAttachmentSize(RestApi::GetCall& call) - { - FileInfo info; - if (GetAttachmentInfo(info, call)) - { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedSize()), "text/plain"); - } - } - - - static void GetAttachmentCompressedSize(RestApi::GetCall& call) - { - FileInfo info; - if (GetAttachmentInfo(info, call)) - { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedSize()), "text/plain"); - } - } - - - static void GetAttachmentMD5(RestApi::GetCall& call) - { - FileInfo info; - if (GetAttachmentInfo(info, call) && - info.GetUncompressedMD5() != "") - { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedMD5()), "text/plain"); - } - } - - - static void GetAttachmentCompressedMD5(RestApi::GetCall& call) - { - FileInfo info; - if (GetAttachmentInfo(info, call) && - info.GetCompressedMD5() != "") - { - call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedMD5()), "text/plain"); - } - } - - - static void VerifyAttachment(RestApi::PostCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - - FileInfo info; - if (!GetAttachmentInfo(info, call) || - info.GetCompressedMD5() == "" || - info.GetUncompressedMD5() == "") - { - // Inexistent resource, or no MD5 available - return; - } - - bool ok = false; - - // First check whether the compressed data is correctly stored in the disk - std::string data; - context.ReadFile(data, publicId, StringToContentType(name), false); - - std::string actualMD5; - Toolbox::ComputeMD5(actualMD5, data); - - if (actualMD5 == info.GetCompressedMD5()) - { - // The compressed data is OK. If a compression algorithm was - // applied to it, now check the MD5 of the uncompressed data. - if (info.GetCompressionType() == CompressionType_None) - { - ok = true; - } - else - { - context.ReadFile(data, publicId, StringToContentType(name), true); - Toolbox::ComputeMD5(actualMD5, data); - ok = (actualMD5 == info.GetUncompressedMD5()); - } - } - - if (ok) - { - LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5"; - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - else - { - LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!"; - } - } - - - static void UploadAttachment(RestApi::PutCall& call) - { - ServerContext& context = OrthancRestApi::GetContext(call); - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - - const void* data = call.GetPutBody().size() ? &call.GetPutBody()[0] : NULL; - - FileContentType contentType = StringToContentType(name); - if (contentType >= FileContentType_StartUser && // It is forbidden to modify internal attachments - contentType <= FileContentType_EndUser && - context.AddAttachment(publicId, StringToContentType(name), data, call.GetPutBody().size())) - { - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - } - - - static void DeleteAttachment(RestApi::DeleteCall& call) - { - CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); - FileContentType contentType = StringToContentType(name); - - if (contentType >= FileContentType_StartUser && - contentType <= FileContentType_EndUser) - { - // It is forbidden to delete internal attachments - OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - } - - - - // Registration of the various REST handlers -------------------------------- OrthancRestApi::OrthancRestApi(ServerContext& context) : context_(context) { - Register("/", ServeRoot); - Register("/system", GetSystemInformation); - Register("/statistics", GetStatistics); - Register("/changes", GetChanges); - Register("/changes", DeleteChanges); - Register("/exports", GetExports); - Register("/exports", DeleteExports); + RegisterSystem(); + + RegisterChanges(); + RegisterResources(); + RegisterModalities(); + RegisterAnonymizeModify(); + RegisterArchive(); Register("/instances", UploadDicomFile); - Register("/instances", ListResources); - Register("/patients", ListResources); - Register("/series", ListResources); - Register("/studies", ListResources); - - Register("/instances/{id}", DeleteSingleResource); - Register("/instances/{id}", GetSingleResource); - Register("/patients/{id}", DeleteSingleResource); - Register("/patients/{id}", GetSingleResource); - Register("/series/{id}", DeleteSingleResource); - Register("/series/{id}", GetSingleResource); - Register("/studies/{id}", DeleteSingleResource); - Register("/studies/{id}", GetSingleResource); - - Register("/patients/{id}/archive", GetArchive); - Register("/studies/{id}/archive", GetArchive); - Register("/series/{id}/archive", GetArchive); - - Register("/instances/{id}/statistics", GetResourceStatistics); - Register("/patients/{id}/statistics", GetResourceStatistics); - Register("/studies/{id}/statistics", GetResourceStatistics); - Register("/series/{id}/statistics", GetResourceStatistics); - - Register("/patients/{id}/protected", IsProtectedPatient); - Register("/patients/{id}/protected", SetPatientProtection); - Register("/instances/{id}/file", GetInstanceFile); - Register("/instances/{id}/export", ExportInstanceFile); - Register("/instances/{id}/tags", GetInstanceTags); - Register("/instances/{id}/simplified-tags", GetInstanceTags); - Register("/instances/{id}/frames", ListFrames); - Register("/instances/{id}/content/*", GetRawContent); - - Register("/instances/{id}/frames/{frame}/preview", GetImage); - Register("/instances/{id}/frames/{frame}/image-uint8", GetImage); - Register("/instances/{id}/frames/{frame}/image-uint16", GetImage); - Register("/instances/{id}/frames/{frame}/image-int16", GetImage); - Register("/instances/{id}/preview", GetImage); - Register("/instances/{id}/image-uint8", GetImage); - Register("/instances/{id}/image-uint16", GetImage); - Register("/instances/{id}/image-int16", GetImage); - - Register("/modalities", ListModalities); - Register("/modalities/{id}", ListModalityOperations); - Register("/modalities/{id}/find-patient", DicomFindPatient); - Register("/modalities/{id}/find-study", DicomFindStudy); - Register("/modalities/{id}/find-series", DicomFindSeries); - Register("/modalities/{id}/find-instance", DicomFindInstance); - Register("/modalities/{id}/find", DicomFind); - Register("/modalities/{id}/store", DicomStore); - - Register("/peers", ListPeers); - Register("/peers/{id}", ListPeerOperations); - Register("/peers/{id}/store", PeerStore); - - Register("/instances/{id}/modify", ModifyInstance); - Register("/series/{id}/modify", ModifySeriesInplace); - Register("/studies/{id}/modify", ModifyStudyInplace); - //Register("/patients/{id}/modify", ModifyPatientInplace); - - Register("/instances/{id}/anonymize", AnonymizeInstance); - Register("/series/{id}/anonymize", AnonymizeSeriesInplace); - Register("/studies/{id}/anonymize", AnonymizeStudyInplace); - Register("/patients/{id}/anonymize", AnonymizePatientInplace); - - Register("/tools/generate-uid", GenerateUid); - Register("/tools/execute-script", ExecuteScript); - Register("/tools/now", GetNowIsoString); - - Register("/{resourceType}/{id}/metadata", ListMetadata); - Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata); - Register("/{resourceType}/{id}/metadata/{name}", GetMetadata); - Register("/{resourceType}/{id}/metadata/{name}", SetMetadata); - - Register("/{resourceType}/{id}/attachments", ListAttachments); - Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment); - Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations); - Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); - Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); - Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); - Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>); - Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5); - Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize); - Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); - Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); } } diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Mon Apr 14 10:27:53 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Mon Apr 14 11:28:35 2014 +0200 @@ -47,6 +47,18 @@ private: ServerContext& context_; + void RegisterSystem(); + + void RegisterChanges(); + + void RegisterResources(); + + void RegisterModalities(); + + void RegisterAnonymizeModify(); + + void RegisterArchive(); + public: OrthancRestApi(ServerContext& context); diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,295 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include "../../Core/Compression/HierarchicalZipWriter.h" +#include "../../Core/HttpServer/FilesystemHttpSender.h" +#include "../../Core/Uuid.h" + +#include + +#if defined(_MSC_VER) +#define snprintf _snprintf +#endif + +static const uint64_t MEGA_BYTES = 1024 * 1024; +static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024; + +namespace Orthanc +{ + // Download of ZIP files ---------------------------------------------------- + + static std::string GetDirectoryNameInArchive(const Json::Value& resource, + ResourceType resourceType) + { + switch (resourceType) + { + case ResourceType_Patient: + { + std::string p = resource["MainDicomTags"]["PatientID"].asString(); + std::string n = resource["MainDicomTags"]["PatientName"].asString(); + return p + " " + n; + } + + case ResourceType_Study: + { + return resource["MainDicomTags"]["StudyDescription"].asString(); + } + + case ResourceType_Series: + { + std::string d = resource["MainDicomTags"]["SeriesDescription"].asString(); + std::string m = resource["MainDicomTags"]["Modality"].asString(); + return m + " " + d; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, + ServerContext& context, + const Json::Value& resource, + ResourceType resourceType) + { + if (resourceType == ResourceType_Patient) + { + return true; + } + + ResourceType parentType = GetParentResourceType(resourceType); + Json::Value parent; + + switch (resourceType) + { + case ResourceType_Study: + { + if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType)) + { + return false; + } + + break; + } + + case ResourceType_Series: + if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) || + !CreateRootDirectoryInArchive(writer, context, parent, parentType)) + { + return false; + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str()); + return true; + } + + static bool ArchiveInstance(HierarchicalZipWriter& writer, + ServerContext& context, + const std::string& instancePublicId, + const char* filename) + { + Json::Value instance; + + if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance)) + { + return false; + } + + writer.OpenFile(filename); + + std::string dicom; + context.ReadFile(dicom, instancePublicId, FileContentType_Dicom); + writer.Write(dicom); + + return true; + } + + static bool ArchiveInternal(HierarchicalZipWriter& writer, + ServerContext& context, + const std::string& publicId, + ResourceType resourceType, + bool isFirstLevel) + { + Json::Value resource; + if (!context.GetIndex().LookupResource(resource, publicId, resourceType)) + { + return false; + } + + if (isFirstLevel && + !CreateRootDirectoryInArchive(writer, context, resource, resourceType)) + { + return false; + } + + writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str()); + + switch (resourceType) + { + case ResourceType_Patient: + for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++) + { + std::string studyId = resource["Studies"][i].asString(); + if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false)) + { + return false; + } + } + break; + + case ResourceType_Study: + for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++) + { + std::string seriesId = resource["Series"][i].asString(); + if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false)) + { + return false; + } + } + break; + + case ResourceType_Series: + { + // Create a filename prefix, depending on the modality + char format[16] = "%08d"; + + if (resource["MainDicomTags"].isMember("Modality")) + { + std::string modality = resource["MainDicomTags"]["Modality"].asString(); + + if (modality.size() == 1) + { + snprintf(format, sizeof(format) - 1, "%c%%07d", toupper(modality[0])); + } + else if (modality.size() >= 2) + { + snprintf(format, sizeof(format) - 1, "%c%c%%06d", toupper(modality[0]), toupper(modality[1])); + } + } + + char filename[16]; + + for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++) + { + snprintf(filename, sizeof(filename) - 1, format, i); + + std::string publicId = resource["Instances"][i].asString(); + + // This was the implementation up to Orthanc 0.7.0: + // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm"; + + if (!ArchiveInstance(writer, context, publicId, filename)) + { + return false; + } + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + writer.CloseDirectory(); + return true; + } + + template + static void GetArchive(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + + /** + * Determine whether ZIP64 is required. Original ZIP format can + * store up to 2GB of data (some implementation supporting up to + * 4GB of data), and up to 65535 files. + * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64 + **/ + + uint64_t uncompressedSize; + uint64_t compressedSize; + unsigned int countStudies; + unsigned int countSeries; + unsigned int countInstances; + context.GetIndex().GetStatistics(compressedSize, uncompressedSize, + countStudies, countSeries, countInstances, id); + const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES || + countInstances >= 65535); + + LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size " + << (uncompressedSize / MEGA_BYTES) << "MB using the " + << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + + { + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + writer.SetZip64(isZip64); + + // Store the requested resource into the ZIP + if (!ArchiveInternal(writer, context, id, resourceType, true)) + { + return; + } + } + + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath().c_str()); + sender.SetContentType("application/zip"); + sender.SetDownloadFilename(id + ".zip"); + + // Send the ZIP + call.GetOutput().AnswerFile(sender); + + // The temporary file is automatically removed thanks to the RAII + } + + + void OrthancRestApi::RegisterArchive() + { + Register("/patients/{id}/archive", GetArchive); + Register("/studies/{id}/archive", GetArchive); + Register("/series/{id}/archive", GetArchive); + } +} diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestChanges.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,132 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include + +namespace Orthanc +{ + // Changes API -------------------------------------------------------------- + + static void GetSinceAndLimit(int64_t& since, + unsigned int& limit, + bool& last, + const RestApi::GetCall& call) + { + static const unsigned int MAX_RESULTS = 100; + + if (call.HasArgument("last")) + { + last = true; + return; + } + + last = false; + + try + { + since = boost::lexical_cast(call.GetArgument("since", "0")); + limit = boost::lexical_cast(call.GetArgument("limit", "0")); + } + catch (boost::bad_lexical_cast) + { + return; + } + + if (limit == 0 || limit > MAX_RESULTS) + { + limit = MAX_RESULTS; + } + } + + static void GetChanges(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + //std::string filter = GetArgument(getArguments, "filter", ""); + int64_t since; + unsigned int limit; + bool last; + GetSinceAndLimit(since, limit, last, call); + + Json::Value result; + if ((!last && context.GetIndex().GetChanges(result, since, limit)) || + ( last && context.GetIndex().GetLastChange(result))) + { + call.GetOutput().AnswerJson(result); + } + } + + + static void DeleteChanges(RestApi::DeleteCall& call) + { + OrthancRestApi::GetIndex(call).DeleteChanges(); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + + + // Exports API -------------------------------------------------------------- + + static void GetExports(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + int64_t since; + unsigned int limit; + bool last; + GetSinceAndLimit(since, limit, last, call); + + Json::Value result; + if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) || + ( last && context.GetIndex().GetLastExportedResource(result))) + { + call.GetOutput().AnswerJson(result); + } + } + + + static void DeleteExports(RestApi::DeleteCall& call) + { + OrthancRestApi::GetIndex(call).DeleteExportedResources(); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + + + void OrthancRestApi::RegisterChanges() + { + Register("/changes", GetChanges); + Register("/changes", DeleteChanges); + Register("/exports", GetExports); + Register("/exports", DeleteExports); + } +} diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,470 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include "../DicomProtocol/DicomUserConnection.h" +#include "../OrthancInitialization.h" +#include "../../Core/HttpClient.h" + +#include + +namespace Orthanc +{ + // DICOM SCU ---------------------------------------------------------------- + + static bool MergeQueryAndTemplate(DicomMap& result, + const std::string& postData) + { + Json::Value query; + Json::Reader reader; + + if (!reader.parse(postData, query) || + query.type() != Json::objectValue) + { + return false; + } + + Json::Value::Members members = query.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + DicomTag t = FromDcmtkBridge::ParseTag(members[i]); + result.SetValue(t, query[members[i]].asString()); + } + + return true; + } + + static void DicomFindPatient(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindPatientTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindPatient(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindStudy(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindStudyTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindStudy(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindSeries(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindSeriesTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindSeries(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFindInstance(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindInstanceTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || + m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindInstance(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + + static void DicomFind(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindPatientTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers patients; + connection.FindPatient(patients, m); + + // Loop over the found patients + Json::Value result = Json::arrayValue; + for (size_t i = 0; i < patients.GetSize(); i++) + { + Json::Value patient(Json::objectValue); + FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); + + DicomMap::SetupFindStudyTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); + + DicomFindAnswers studies; + connection.FindStudy(studies, m); + + patient["Studies"] = Json::arrayValue; + + // Loop over the found studies + for (size_t j = 0; j < studies.GetSize(); j++) + { + Json::Value study(Json::objectValue); + FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); + + DicomMap::SetupFindSeriesTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); + m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); + + DicomFindAnswers series; + connection.FindSeries(series, m); + + // Loop over the found series + study["Series"] = Json::arrayValue; + for (size_t k = 0; k < series.GetSize(); k++) + { + Json::Value series2(Json::objectValue); + FromDcmtkBridge::ToJson(series2, series.GetAnswer(k)); + study["Series"].append(series2); + } + + patient["Studies"].append(study); + } + + result.append(patient); + } + + call.GetOutput().AnswerJson(result); + } + + + static bool GetInstancesToExport(std::list& instances, + const std::string& remote, + RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string stripped = Toolbox::StripSpaces(call.GetPostBody()); + + Json::Value request; + if (Toolbox::IsSHA1(stripped)) + { + // This is for compatibility with Orthanc <= 0.5.1. + request = stripped; + } + else if (!call.ParseJsonRequest(request)) + { + // Bad JSON request + return false; + } + + if (request.isString()) + { + context.GetIndex().LogExportedResource(request.asString(), remote); + context.GetIndex().GetChildInstances(instances, request.asString()); + } + else if (request.isArray()) + { + for (Json::Value::ArrayIndex i = 0; i < request.size(); i++) + { + if (!request[i].isString()) + { + return false; + } + + std::string stripped = Toolbox::StripSpaces(request[i].asString()); + if (!Toolbox::IsSHA1(stripped)) + { + return false; + } + + context.GetIndex().LogExportedResource(stripped, remote); + + std::list tmp; + context.GetIndex().GetChildInstances(tmp, stripped); + + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + instances.push_back(*it); + } + } + } + else + { + // Neither a string, nor a list of strings. Bad request. + return false; + } + + return true; + } + + + static void DicomStore(RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string remote = call.GetUriComponent("id", ""); + + std::list instances; + if (!GetInstancesToExport(instances, remote, call)) + { + return; + } + + DicomUserConnection connection; + ConnectToModalityUsingSymbolicName(connection, remote); + + for (std::list::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\""; + + std::string dicom; + context.ReadFile(dicom, *it, FileContentType_Dicom); + connection.Store(dicom); + } + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + // Orthanc Peers ------------------------------------------------------------ + + static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers, + const std::string& id) + { + return peers.find(id) != peers.end(); + } + + static void ListPeers(RestApi::GetCall& call) + { + OrthancRestApi::SetOfStrings peers; + GetListOfOrthancPeers(peers); + + Json::Value result = Json::arrayValue; + for (OrthancRestApi::SetOfStrings::const_iterator + it = peers.begin(); it != peers.end(); ++it) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + static void ListPeerOperations(RestApi::GetCall& call) + { + OrthancRestApi::SetOfStrings peers; + GetListOfOrthancPeers(peers); + + std::string id = call.GetUriComponent("id", ""); + if (IsExistingPeer(peers, id)) + { + Json::Value result = Json::arrayValue; + result.append("store"); + call.GetOutput().AnswerJson(result); + } + } + + static void PeerStore(RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string remote = call.GetUriComponent("id", ""); + + std::list instances; + if (!GetInstancesToExport(instances, remote, call)) + { + return; + } + + std::string url, username, password; + GetOrthancPeer(remote, url, username, password); + + // Configure the HTTP client + HttpClient client; + if (username.size() != 0 && password.size() != 0) + { + client.SetCredentials(username.c_str(), password.c_str()); + } + + client.SetUrl(url + "instances"); + client.SetMethod(HttpMethod_Post); + + // Loop over the instances that are to be sent + for (std::list::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\""; + + context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); + + std::string answer; + if (!client.Apply(answer)) + { + LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\""; + return; + } + } + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + // DICOM bridge ------------------------------------------------------------- + + static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities, + const std::string& id) + { + return modalities.find(id) != modalities.end(); + } + + static void ListModalities(RestApi::GetCall& call) + { + OrthancRestApi::SetOfStrings modalities; + GetListOfDicomModalities(modalities); + + Json::Value result = Json::arrayValue; + for (OrthancRestApi::SetOfStrings::const_iterator + it = modalities.begin(); it != modalities.end(); ++it) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + + static void ListModalityOperations(RestApi::GetCall& call) + { + OrthancRestApi::SetOfStrings modalities; + GetListOfDicomModalities(modalities); + + std::string id = call.GetUriComponent("id", ""); + if (IsExistingModality(modalities, id)) + { + Json::Value result = Json::arrayValue; + result.append("find-patient"); + result.append("find-study"); + result.append("find-series"); + result.append("find-instance"); + result.append("find"); + result.append("store"); + call.GetOutput().AnswerJson(result); + } + } + + + void OrthancRestApi::RegisterModalities() + { + Register("/modalities", ListModalities); + Register("/modalities/{id}", ListModalityOperations); + Register("/modalities/{id}/find-patient", DicomFindPatient); + Register("/modalities/{id}/find-study", DicomFindStudy); + Register("/modalities/{id}/find-series", DicomFindSeries); + Register("/modalities/{id}/find-instance", DicomFindInstance); + Register("/modalities/{id}/find", DicomFind); + Register("/modalities/{id}/store", DicomStore); + + Register("/peers", ListPeers); + Register("/peers/{id}", ListPeerOperations); + Register("/peers/{id}/store", PeerStore); + } +} diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,602 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include "../ServerToolbox.h" + +#include + +namespace Orthanc +{ + // List all the patients, studies, series or instances ---------------------- + + template + static void ListResources(RestApi::GetCall& call) + { + Json::Value result; + OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType); + call.GetOutput().AnswerJson(result); + } + + template + static void GetSingleResource(RestApi::GetCall& call) + { + Json::Value result; + if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) + { + call.GetOutput().AnswerJson(result); + } + } + + template + static void DeleteSingleResource(RestApi::DeleteCall& call) + { + Json::Value result; + if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) + { + call.GetOutput().AnswerJson(result); + } + } + + + // Get information about a single patient ----------------------------------- + + static void IsProtectedPatient(RestApi::GetCall& call) + { + std::string publicId = call.GetUriComponent("id", ""); + bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); + call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); + } + + + static void SetPatientProtection(RestApi::PutCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string s = Toolbox::StripSpaces(call.GetPutBody()); + + if (s == "0") + { + context.GetIndex().SetProtectedPatient(publicId, false); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else if (s == "1") + { + context.GetIndex().SetProtectedPatient(publicId, true); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else + { + // Bad request + } + } + + + // Get information about a single instance ---------------------------------- + + static void GetInstanceFile(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom); + } + + + static void ExportInstanceFile(RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + std::string dicom; + context.ReadFile(dicom, publicId, FileContentType_Dicom); + + Toolbox::WriteFile(dicom, call.GetPostBody()); + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + template + static void GetInstanceTags(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + Json::Value full; + context.ReadJson(full, publicId); + + if (simplify) + { + Json::Value simplified; + SimplifyTags(simplified, full); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(full); + } + } + + + static void ListFrames(RestApi::GetCall& call) + { + Json::Value instance; + if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) + { + unsigned int numberOfFrames = 1; + + try + { + Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; + numberOfFrames = boost::lexical_cast(tmp.asString()); + } + catch (...) + { + } + + Json::Value result = Json::arrayValue; + for (unsigned int i = 0; i < numberOfFrames; i++) + { + result.append(i); + } + + call.GetOutput().AnswerJson(result); + } + } + + + template + static void GetImage(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast(frameId); + } + catch (boost::bad_lexical_cast) + { + return; + } + + std::string publicId = call.GetUriComponent("id", ""); + std::string dicomContent, png; + context.ReadFile(dicomContent, publicId, FileContentType_Dicom); + + try + { + FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); + call.GetOutput().AnswerBuffer(png, "image/png"); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) + { + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) + { + root += "../"; + } + + call.GetOutput().Redirect(root + "app/images/unsupported.png"); + } + } + } + + + + static void GetResourceStatistics(RestApi::GetCall& call) + { + std::string publicId = call.GetUriComponent("id", ""); + Json::Value result; + OrthancRestApi::GetIndex(call).GetStatistics(result, publicId); + call.GetOutput().AnswerJson(result); + } + + + + // Handling of metadata ----------------------------------------------------- + + static void CheckValidResourceType(RestApi::Call& call) + { + std::string resourceType = call.GetUriComponent("resourceType", ""); + StringToResourceType(resourceType.c_str()); + } + + + static void ListMetadata(RestApi::GetCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::list metadata; + + OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId); + Json::Value result = Json::arrayValue; + + for (std::list::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + result.append(EnumerationToString(*it)); + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetMetadata(RestApi::GetCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + + std::string value; + if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata)) + { + call.GetOutput().AnswerBuffer(value, "text/plain"); + } + } + + + static void DeleteMetadata(RestApi::DeleteCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + + if (metadata >= MetadataType_StartUser && + metadata <= MetadataType_EndUser) + { + // It is forbidden to modify internal metadata + OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + } + + + static void SetMetadata(RestApi::PutCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + std::string value = call.GetPutBody(); + + if (metadata >= MetadataType_StartUser && + metadata <= MetadataType_EndUser) + { + // It is forbidden to modify internal metadata + OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + } + + + + + // Handling of attached files ----------------------------------------------- + + static void ListAttachments(RestApi::GetCall& call) + { + std::string resourceType = call.GetUriComponent("resourceType", ""); + std::string publicId = call.GetUriComponent("id", ""); + std::list attachments; + OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); + + Json::Value result = Json::arrayValue; + + for (std::list::const_iterator + it = attachments.begin(); it != attachments.end(); ++it) + { + result.append(EnumerationToString(*it)); + } + + call.GetOutput().AnswerJson(result); + } + + + static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); + } + + + static void GetAttachmentOperations(RestApi::GetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + Json::Value operations = Json::arrayValue; + + operations.append("compressed-data"); + + if (info.GetCompressedMD5() != "") + { + operations.append("compressed-md5"); + } + + operations.append("compressed-size"); + operations.append("data"); + + if (info.GetUncompressedMD5() != "") + { + operations.append("md5"); + } + + operations.append("size"); + + if (info.GetCompressedMD5() != "" && + info.GetUncompressedMD5() != "") + { + operations.append("verify-md5"); + } + + call.GetOutput().AnswerJson(operations); + } + } + + + template + static void GetAttachmentData(RestApi::GetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + + std::string content; + context.ReadFile(content, publicId, StringToContentType(name), + (uncompress == 1)); + + call.GetOutput().AnswerBuffer(content, "application/octet-stream"); + } + + + static void GetAttachmentSize(RestApi::GetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedSize()), "text/plain"); + } + } + + + static void GetAttachmentCompressedSize(RestApi::GetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedSize()), "text/plain"); + } + } + + + static void GetAttachmentMD5(RestApi::GetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call) && + info.GetUncompressedMD5() != "") + { + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedMD5()), "text/plain"); + } + } + + + static void GetAttachmentCompressedMD5(RestApi::GetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call) && + info.GetCompressedMD5() != "") + { + call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedMD5()), "text/plain"); + } + } + + + static void VerifyAttachment(RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + + FileInfo info; + if (!GetAttachmentInfo(info, call) || + info.GetCompressedMD5() == "" || + info.GetUncompressedMD5() == "") + { + // Inexistent resource, or no MD5 available + return; + } + + bool ok = false; + + // First check whether the compressed data is correctly stored in the disk + std::string data; + context.ReadFile(data, publicId, StringToContentType(name), false); + + std::string actualMD5; + Toolbox::ComputeMD5(actualMD5, data); + + if (actualMD5 == info.GetCompressedMD5()) + { + // The compressed data is OK. If a compression algorithm was + // applied to it, now check the MD5 of the uncompressed data. + if (info.GetCompressionType() == CompressionType_None) + { + ok = true; + } + else + { + context.ReadFile(data, publicId, StringToContentType(name), true); + Toolbox::ComputeMD5(actualMD5, data); + ok = (actualMD5 == info.GetUncompressedMD5()); + } + } + + if (ok) + { + LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5"; + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!"; + } + } + + + static void UploadAttachment(RestApi::PutCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + + const void* data = call.GetPutBody().size() ? &call.GetPutBody()[0] : NULL; + + FileContentType contentType = StringToContentType(name); + if (contentType >= FileContentType_StartUser && // It is forbidden to modify internal attachments + contentType <= FileContentType_EndUser && + context.AddAttachment(publicId, StringToContentType(name), data, call.GetPutBody().size())) + { + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + } + + + static void DeleteAttachment(RestApi::DeleteCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + if (contentType >= FileContentType_StartUser && + contentType <= FileContentType_EndUser) + { + // It is forbidden to delete internal attachments + OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + } + + + + + void OrthancRestApi::RegisterResources() + { + Register("/instances", ListResources); + Register("/patients", ListResources); + Register("/series", ListResources); + Register("/studies", ListResources); + + Register("/instances/{id}", DeleteSingleResource); + Register("/instances/{id}", GetSingleResource); + Register("/patients/{id}", DeleteSingleResource); + Register("/patients/{id}", GetSingleResource); + Register("/series/{id}", DeleteSingleResource); + Register("/series/{id}", GetSingleResource); + Register("/studies/{id}", DeleteSingleResource); + Register("/studies/{id}", GetSingleResource); + + Register("/instances/{id}/statistics", GetResourceStatistics); + Register("/patients/{id}/statistics", GetResourceStatistics); + Register("/studies/{id}/statistics", GetResourceStatistics); + Register("/series/{id}/statistics", GetResourceStatistics); + + Register("/instances/{id}/file", GetInstanceFile); + Register("/instances/{id}/export", ExportInstanceFile); + Register("/instances/{id}/tags", GetInstanceTags); + Register("/instances/{id}/simplified-tags", GetInstanceTags); + Register("/instances/{id}/frames", ListFrames); + + Register("/instances/{id}/frames/{frame}/preview", GetImage); + Register("/instances/{id}/frames/{frame}/image-uint8", GetImage); + Register("/instances/{id}/frames/{frame}/image-uint16", GetImage); + Register("/instances/{id}/frames/{frame}/image-int16", GetImage); + Register("/instances/{id}/preview", GetImage); + Register("/instances/{id}/image-uint8", GetImage); + Register("/instances/{id}/image-uint16", GetImage); + Register("/instances/{id}/image-int16", GetImage); + + Register("/patients/{id}/protected", IsProtectedPatient); + Register("/patients/{id}/protected", SetPatientProtection); + + Register("/{resourceType}/{id}/metadata", ListMetadata); + Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata); + Register("/{resourceType}/{id}/metadata/{name}", GetMetadata); + Register("/{resourceType}/{id}/metadata/{name}", SetMetadata); + + Register("/{resourceType}/{id}/attachments", ListAttachments); + Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment); + Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations); + Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); + Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); + Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); + Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>); + Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5); + Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize); + Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); + Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); + } +} diff -r 4afad8cb94fd -r 5197fd35333c OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancRestApi.h" + +#include "../OrthancInitialization.h" + +#include + + +namespace Orthanc +{ + // System information ------------------------------------------------------- + + static void ServeRoot(RestApi::GetCall& call) + { + call.GetOutput().Redirect("app/explorer.html"); + } + + static void GetSystemInformation(RestApi::GetCall& call) + { + Json::Value result = Json::objectValue; + + result["Version"] = ORTHANC_VERSION; + result["Name"] = GetGlobalStringParameter("Name", ""); + + call.GetOutput().AnswerJson(result); + } + + static void GetStatistics(RestApi::GetCall& call) + { + Json::Value result = Json::objectValue; + OrthancRestApi::GetIndex(call).ComputeStatistics(result); + call.GetOutput().AnswerJson(result); + } + + static void GenerateUid(RestApi::GetCall& call) + { + std::string level = call.GetArgument("level", ""); + if (level == "patient") + { + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient), "text/plain"); + } + else if (level == "study") + { + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study), "text/plain"); + } + else if (level == "series") + { + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series), "text/plain"); + } + else if (level == "instance") + { + call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance), "text/plain"); + } + } + + static void ExecuteScript(RestApi::PostCall& call) + { + std::string result; + ServerContext& context = OrthancRestApi::GetContext(call); + context.GetLuaContext().Execute(result, call.GetPostBody()); + call.GetOutput().AnswerBuffer(result, "text/plain"); + } + + static void GetNowIsoString(RestApi::GetCall& call) + { + call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain"); + } + + void OrthancRestApi::RegisterSystem() + { + Register("/", ServeRoot); + Register("/system", GetSystemInformation); + Register("/statistics", GetStatistics); + Register("/tools/generate-uid", GenerateUid); + Register("/tools/execute-script", ExecuteScript); + Register("/tools/now", GetNowIsoString); + } +}