Skip to content
Commits on Source (5)
repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe
node: 6f5e38ec1f120b8c0ac9c7bfaeafd50ef819d863
branch: Orthanc-1.5.3
node: 30418468410719d5301689330d1d282ff61e9751
branch: Orthanc-1.5.4
latesttag: dcmtk-3.6.1
latesttagdistance: 677
changessincelatesttag: 786
latesttagdistance: 717
changessincelatesttag: 829
......@@ -56,6 +56,7 @@ set(ORTHANC_SERVER_SOURCES
OrthancServer/Database/Compatibility/DatabaseLookup.cpp
OrthancServer/Database/Compatibility/ICreateInstance.cpp
OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp
OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp
OrthancServer/Database/Compatibility/ILookupResources.cpp
OrthancServer/Database/Compatibility/SetOfResources.cpp
OrthancServer/Database/ResourcesContent.cpp
......
......@@ -148,7 +148,8 @@ namespace Orthanc
if (!pimpl_->file_)
{
throw OrthancException(ErrorCode_CannotWriteFile);
throw OrthancException(ErrorCode_CannotWriteFile,
"Cannot create new ZIP archive: " + path_);
}
}
......@@ -169,7 +170,8 @@ namespace Orthanc
if (level >= 10)
{
throw OrthancException(ErrorCode_ParameterOutOfRange,
"ZIP compression level must be between 0 (no compression) and 9 (highest compression)");
"ZIP compression level must be between 0 (no compression) "
"and 9 (highest compression)");
}
Close();
......@@ -208,7 +210,8 @@ namespace Orthanc
if (result != 0)
{
throw OrthancException(ErrorCode_CannotWriteFile);
throw OrthancException(ErrorCode_CannotWriteFile,
"Cannot add new file inside ZIP archive: " + std::string(path));
}
hasFileInZip_ = true;
......@@ -239,7 +242,8 @@ namespace Orthanc
if (zipWriteInFileInZip(pimpl_->file_, data, bytes))
{
throw OrthancException(ErrorCode_CannotWriteFile);
throw OrthancException(ErrorCode_CannotWriteFile,
"Cannot write data to ZIP archive: " + path_);
}
data += bytes;
......@@ -253,6 +257,4 @@ namespace Orthanc
Close();
append_ = append;
}
}
......@@ -174,6 +174,9 @@ namespace Orthanc
static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
static const DicomTag DICOM_TAG_CASSETTE_ORIENTATION(0x0018, 0x1402);
static const DicomTag DICOM_TAG_CASSETTE_SIZE(0x0018, 0x1403);
static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
......
......@@ -164,12 +164,50 @@ namespace Orthanc
};
static void Check(const OFCondition& cond)
static void Check(const OFCondition& cond,
const std::string& aet)
{
if (cond.bad())
{
// Reformat the error message from DCMTK by turning multiline
// errors into a single line
std::string s(cond.text());
std::string info;
info.reserve(s.size());
bool isMultiline = false;
for (size_t i = 0; i < s.size(); i++)
{
if (s[i] == '\r')
{
// Ignore
}
else if (s[i] == '\n')
{
if (isMultiline)
{
info += "; ";
}
else
{
info += " (";
isMultiline = true;
}
}
else
{
info.push_back(s[i]);
}
}
if (isMultiline)
{
info += ")";
}
throw OrthancException(ErrorCode_NetworkProtocol,
"DicomUserConnection: " + std::string(cond.text()));
"DicomUserConnection to AET \"" + aet + "\": " + info);
}
}
......@@ -193,16 +231,17 @@ namespace Orthanc
unsigned int& presentationContextId,
const std::string& sopClass,
const char* asPreferred[],
std::vector<const char*>& asFallback)
std::vector<const char*>& asFallback,
const std::string& aet)
{
Check(ASC_addPresentationContext(params, presentationContextId,
sopClass.c_str(), asPreferred, 1));
sopClass.c_str(), asPreferred, 1), aet);
presentationContextId += 2;
if (asFallback.size() > 0)
{
Check(ASC_addPresentationContext(params, presentationContextId,
sopClass.c_str(), &asFallback[0], asFallback.size()));
sopClass.c_str(), &asFallback[0], asFallback.size()), aet);
presentationContextId += 2;
}
}
......@@ -236,21 +275,21 @@ namespace Orthanc
it != reservedStorageSOPClasses_.end(); ++it)
{
RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
*it, asPreferred, asFallback);
*it, asPreferred, asFallback, remoteAet_);
}
for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
it != storageSOPClasses_.end(); ++it)
{
RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
*it, asPreferred, asFallback);
*it, asPreferred, asFallback, remoteAet_);
}
for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
it != defaultStorageSOPClasses_.end(); ++it)
{
RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
*it, asPreferred, asFallback);
*it, asPreferred, asFallback, remoteAet_);
}
}
......@@ -269,7 +308,7 @@ namespace Orthanc
uint16_t moveOriginatorID)
{
DcmFileFormat dcmff;
Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength), connection.remoteAet_);
// Determine the storage SOP class UID for this instance
static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016);
......@@ -379,7 +418,7 @@ namespace Orthanc
Check(DIMSE_storeUser(assoc_, presID, &request,
NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
/*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
&rsp, &statusDetail, NULL));
&rsp, &statusDetail, NULL), connection.remoteAet_);
if (statusDetail != NULL)
{
......@@ -556,7 +595,8 @@ namespace Orthanc
const char* sopClass,
bool isWorklist,
const char* level,
uint32_t dimseTimeout)
uint32_t dimseTimeout,
const std::string& remoteAet)
{
assert(isWorklist ^ (level != NULL));
......@@ -600,7 +640,7 @@ namespace Orthanc
delete statusDetail;
}
Check(cond);
Check(cond, remoteAet);
}
......@@ -720,7 +760,8 @@ namespace Orthanc
}
assert(clevel != NULL && sopClass != NULL);
ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_);
ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel,
pimpl_->dimseTimeout_, remoteAet_);
}
......@@ -803,7 +844,7 @@ namespace Orthanc
delete responseIdentifiers;
}
Check(cond);
Check(cond, remoteAet_);
}
......@@ -972,11 +1013,11 @@ namespace Orthanc
<< GetRemoteHost() << ":" << GetRemotePort()
<< " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_), remoteAet_);
Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU), remoteAet_);
// Set this application's title and the called application's title in the params
Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL), remoteAet_);
// Set the network addresses of the local and remote entities
char localHost[HOST_NAME_MAX];
......@@ -991,15 +1032,15 @@ namespace Orthanc
#endif
(remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort), remoteAet_);
// Set various options
Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), remoteAet_);
SetupPresentationContexts(preferredTransferSyntax_);
// Do the association
Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_));
Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), remoteAet_);
if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0)
{
......@@ -1077,7 +1118,7 @@ namespace Orthanc
Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++,
/*opt_blockMode*/ DIMSE_BLOCKING,
/*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
&status, NULL));
&status, NULL), remoteAet_);
return status == STATUS_Success;
}
......@@ -1276,7 +1317,8 @@ namespace Orthanc
DcmDataset* dataset = query.GetDcmtkObject().getDataset();
const char* sopClass = UID_FINDModalityWorklistInformationModel;
ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_);
ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true,
NULL, pimpl_->dimseTimeout_, remoteAet_);
}
......
......@@ -79,13 +79,19 @@ namespace Orthanc
{
}
virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr)
{
}
virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag)
{
}
virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
......@@ -95,27 +101,26 @@ namespace Orthanc
{
}
virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
int64_t value)
const std::vector<int64_t>& values)
{
}
virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
double value)
const std::vector<double>& value)
{
}
virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const DicomTag& value)
const std::vector<DicomTag>& value)
{
}
......
/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2019 Osimis S.A., Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* In addition, as a special exception, the copyright holders of this
* program give permission to link the code of its release with the
* OpenSSL project's "OpenSSL" library (or with modified versions of it
* that use the same license as the "OpenSSL" library), and distribute
* the linked executables. You must obey the GNU General Public License
* in all respects for all of the code used other than "OpenSSL". If you
* modify file(s) with this exception, you may extend this exception to
* your version of the file(s), but you are not obligated to do so. If
* you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files
* in the program, then also delete it here.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#include "../PrecompiledHeaders.h"
#include "DicomWebJsonVisitor.h"
#include "../OrthancException.h"
#include "../Toolbox.h"
#include "FromDcmtkBridge.h"
#include <boost/math/special_functions/round.hpp>
#include <boost/lexical_cast.hpp>
static const char* const KEY_ALPHABETIC = "Alphabetic";
static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
static const char* const KEY_INLINE_BINARY = "InlineBinary";
static const char* const KEY_SQ = "SQ";
static const char* const KEY_VALUE = "Value";
static const char* const KEY_VR = "vr";
namespace Orthanc
{
#if ORTHANC_ENABLE_PUGIXML == 1
static void ExploreXmlDataset(pugi::xml_node& target,
const Json::Value& source)
{
assert(source.type() == Json::objectValue);
Json::Value::Members members = source.getMemberNames();
for (size_t i = 0; i < members.size(); i++)
{
const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
const Json::Value& content = source[members[i]];
assert(content.type() == Json::objectValue &&
content.isMember("vr") &&
content["vr"].type() == Json::stringValue);
const std::string vr = content["vr"].asString();
const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
pugi::xml_node node = target.append_child("DicomAttribute");
node.append_attribute("tag").set_value(members[i].c_str());
node.append_attribute("vr").set_value(vr.c_str());
if (keyword != std::string(DcmTag_ERROR_TagName))
{
node.append_attribute("keyword").set_value(keyword.c_str());
}
if (content.isMember(KEY_VALUE))
{
assert(content[KEY_VALUE].type() == Json::arrayValue);
for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
{
std::string number = boost::lexical_cast<std::string>(j + 1);
if (vr == "SQ")
{
if (content[KEY_VALUE][j].type() == Json::objectValue)
{
pugi::xml_node child = node.append_child("Item");
child.append_attribute("number").set_value(number.c_str());
ExploreXmlDataset(child, content[KEY_VALUE][j]);
}
}
if (vr == "PN")
{
if (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue)
{
std::vector<std::string> tokens;
Toolbox::TokenizeString(tokens, content[KEY_VALUE][j][KEY_ALPHABETIC].asString(), '^');
pugi::xml_node child = node.append_child("PersonName");
child.append_attribute("number").set_value(number.c_str());
pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
if (tokens.size() >= 1)
{
name.append_child("FamilyName").text() = tokens[0].c_str();
}
if (tokens.size() >= 2)
{
name.append_child("GivenName").text() = tokens[1].c_str();
}
if (tokens.size() >= 3)
{
name.append_child("MiddleName").text() = tokens[2].c_str();
}
if (tokens.size() >= 4)
{
name.append_child("NamePrefix").text() = tokens[3].c_str();
}
if (tokens.size() >= 5)
{
name.append_child("NameSuffix").text() = tokens[4].c_str();
}
}
}
else
{
pugi::xml_node child = node.append_child("Value");
child.append_attribute("number").set_value(number.c_str());
switch (content[KEY_VALUE][j].type())
{
case Json::stringValue:
child.text() = content[KEY_VALUE][j].asCString();
break;
case Json::realValue:
child.text() = content[KEY_VALUE][j].asFloat();
break;
case Json::intValue:
child.text() = content[KEY_VALUE][j].asInt();
break;
case Json::uintValue:
child.text() = content[KEY_VALUE][j].asUInt();
break;
default:
break;
}
}
}
}
else if (content.isMember(KEY_BULK_DATA_URI) &&
content[KEY_BULK_DATA_URI].type() == Json::stringValue)
{
pugi::xml_node child = node.append_child("BulkData");
child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
}
else if (content.isMember(KEY_INLINE_BINARY) &&
content[KEY_INLINE_BINARY].type() == Json::stringValue)
{
pugi::xml_node child = node.append_child("InlineBinary");
child.text() = content[KEY_INLINE_BINARY].asCString();
}
}
}
#endif
#if ORTHANC_ENABLE_PUGIXML == 1
static void DicomWebJsonToXml(pugi::xml_document& target,
const Json::Value& source)
{
pugi::xml_node root = target.append_child("NativeDicomModel");
root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
ExploreXmlDataset(root, source);
pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
decl.append_attribute("version").set_value("1.0");
decl.append_attribute("encoding").set_value("utf-8");
}
#endif
std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
{
char buf[16];
sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
return std::string(buf);
}
Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag)
{
assert(parentTags.size() == parentIndexes.size());
Json::Value* node = &result_;
for (size_t i = 0; i < parentTags.size(); i++)
{
std::string t = FormatTag(parentTags[i]);
if (!node->isMember(t))
{
Json::Value item = Json::objectValue;
item[KEY_VR] = KEY_SQ;
item[KEY_VALUE] = Json::arrayValue;
item[KEY_VALUE].append(Json::objectValue);
(*node) [t] = item;
node = &(*node)[t][KEY_VALUE][0];
}
else if ((*node) [t].type() != Json::objectValue ||
!(*node) [t].isMember(KEY_VR) ||
(*node) [t][KEY_VR].type() != Json::stringValue ||
(*node) [t][KEY_VR].asString() != KEY_SQ ||
!(*node) [t].isMember(KEY_VALUE) ||
(*node) [t][KEY_VALUE].type() != Json::arrayValue)
{
throw OrthancException(ErrorCode_InternalError);
}
else
{
size_t currentSize = (*node) [t][KEY_VALUE].size();
if (parentIndexes[i] < currentSize)
{
// The node already exists
}
else if (parentIndexes[i] == currentSize)
{
(*node) [t][KEY_VALUE].append(Json::objectValue);
}
else
{
throw OrthancException(ErrorCode_InternalError);
}
node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
}
}
assert(node->type() == Json::objectValue);
std::string t = FormatTag(tag);
if (node->isMember(t))
{
throw OrthancException(ErrorCode_InternalError);
}
else
{
(*node) [t] = Json::objectValue;
return (*node) [t];
}
}
Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
{
if (value < 0)
{
return Json::Value(static_cast<int32_t>(value));
}
else
{
return Json::Value(static_cast<uint32_t>(value));
}
}
Json::Value DicomWebJsonVisitor::FormatDouble(double value)
{
long long a = boost::math::llround<double>(value);
double d = fabs(value - static_cast<double>(a));
if (d <= std::numeric_limits<double>::epsilon() * 100.0)
{
return FormatInteger(a);
}
else
{
return Json::Value(value);
}
}
#if ORTHANC_ENABLE_PUGIXML == 1
void DicomWebJsonVisitor::FormatXml(std::string& target) const
{
pugi::xml_document doc;
DicomWebJsonToXml(doc, result_);
Toolbox::XmlToString(target, doc);
}
#endif
void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag)
{
if (tag.GetElement() != 0x0000)
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
}
}
void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const void* data,
size_t size)
{
assert(vr == ValueRepresentation_OtherByte ||
vr == ValueRepresentation_OtherDouble ||
vr == ValueRepresentation_OtherFloat ||
vr == ValueRepresentation_OtherLong ||
vr == ValueRepresentation_OtherWord ||
vr == ValueRepresentation_Unknown);
if (tag.GetElement() != 0x0000)
{
BinaryMode mode;
std::string bulkDataUri;
if (formatter_ == NULL)
{
mode = BinaryMode_InlineBinary;
}
else
{
mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
}
if (mode != BinaryMode_Ignore)
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(vr);
switch (mode)
{
case BinaryMode_BulkDataUri:
node[KEY_BULK_DATA_URI] = bulkDataUri;
break;
case BinaryMode_InlineBinary:
{
std::string tmp(static_cast<const char*>(data), size);
std::string base64;
Toolbox::EncodeBase64(base64, tmp);
node[KEY_INLINE_BINARY] = base64;
break;
}
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
}
}
}
void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::vector<int64_t>& values)
{
if (tag.GetElement() != 0x0000 &&
vr != ValueRepresentation_NotSupported)
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(vr);
if (!values.empty())
{
Json::Value content = Json::arrayValue;
for (size_t i = 0; i < values.size(); i++)
{
content.append(FormatInteger(values[i]));
}
node[KEY_VALUE] = content;
}
}
}
void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::vector<double>& values)
{
if (tag.GetElement() != 0x0000 &&
vr != ValueRepresentation_NotSupported)
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(vr);
if (!values.empty())
{
Json::Value content = Json::arrayValue;
for (size_t i = 0; i < values.size(); i++)
{
content.append(FormatDouble(values[i]));
}
node[KEY_VALUE] = content;
}
}
}
void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
const std::vector<DicomTag>& values)
{
if (tag.GetElement() != 0x0000)
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
if (!values.empty())
{
Json::Value content = Json::arrayValue;
for (size_t i = 0; i < values.size(); i++)
{
content.append(FormatTag(values[i]));
}
node[KEY_VALUE] = content;
}
}
}
ITagVisitor::Action
DicomWebJsonVisitor::VisitString(std::string& newValue,
const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::string& value)
{
if (tag.GetElement() == 0x0000 ||
vr == ValueRepresentation_NotSupported)
{
return Action_None;
}
else
{
Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
node[KEY_VR] = EnumerationToString(vr);
if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
{
// TODO - The JSON file has an UTF-8 encoding, thus DCMTK
// replaces the specific character set with "ISO_IR 192"
// (UNICODE UTF-8). It is unclear whether the source
// character set should be kept: We thus mimic DCMTK.
node[KEY_VALUE].append("ISO_IR 192");
}
else
{
std::string truncated;
if (!value.empty() &&
value[value.size() - 1] == '\0')
{
truncated = value.substr(0, value.size() - 1);
}
else
{
truncated = value;
}
if (!truncated.empty())
{
std::vector<std::string> tokens;
Toolbox::TokenizeString(tokens, truncated, '\\');
node[KEY_VALUE] = Json::arrayValue;
for (size_t i = 0; i < tokens.size(); i++)
{
try
{
switch (vr)
{
case ValueRepresentation_PersonName:
{
Json::Value value = Json::objectValue;
if (!tokens[i].empty())
{
value[KEY_ALPHABETIC] = tokens[i];
}
node[KEY_VALUE].append(value);
break;
}
case ValueRepresentation_IntegerString:
if (tokens[i].empty())
{
node[KEY_VALUE].append(Json::nullValue);
}
else
{
int64_t value = boost::lexical_cast<int64_t>(tokens[i]);
node[KEY_VALUE].append(FormatInteger(value));
}
break;
case ValueRepresentation_DecimalString:
if (tokens[i].empty())
{
node[KEY_VALUE].append(Json::nullValue);
}
else
{
double value = boost::lexical_cast<double>(tokens[i]);
node[KEY_VALUE].append(FormatDouble(value));
}
break;
default:
if (tokens[i].empty())
{
node[KEY_VALUE].append(Json::nullValue);
}
else
{
node[KEY_VALUE].append(tokens[i]);
}
break;
}
}
catch (boost::bad_lexical_cast&)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
}
}
}
return Action_None;
}
}
/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2019 Osimis S.A., Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* In addition, as a special exception, the copyright holders of this
* program give permission to link the code of its release with the
* OpenSSL project's "OpenSSL" library (or with modified versions of it
* that use the same license as the "OpenSSL" library), and distribute
* the linked executables. You must obey the GNU General Public License
* in all respects for all of the code used other than "OpenSSL". If you
* modify file(s) with this exception, you may extend this exception to
* your version of the file(s), but you are not obligated to do so. If
* you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files
* in the program, then also delete it here.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#pragma once
#if !defined(ORTHANC_ENABLE_PUGIXML)
# error Macro ORTHANC_ENABLE_PUGIXML must be defined to use this file
#endif
#include "ITagVisitor.h"
#include <json/value.h>
namespace Orthanc
{
class DicomWebJsonVisitor : public ITagVisitor
{
public:
enum BinaryMode
{
BinaryMode_Ignore,
BinaryMode_BulkDataUri,
BinaryMode_InlineBinary
};
class IBinaryFormatter : public boost::noncopyable
{
public:
virtual ~IBinaryFormatter()
{
}
virtual BinaryMode Format(std::string& bulkDataUri,
const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr) = 0;
};
private:
Json::Value result_;
IBinaryFormatter *formatter_;
static std::string FormatTag(const DicomTag& tag);
Json::Value& CreateNode(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag);
static Json::Value FormatInteger(int64_t value);
static Json::Value FormatDouble(double value);
public:
DicomWebJsonVisitor() :
formatter_(NULL)
{
Clear();
}
void SetFormatter(IBinaryFormatter& formatter)
{
formatter_ = &formatter;
}
void Clear()
{
result_ = Json::objectValue;
}
const Json::Value& GetResult() const
{
return result_;
}
#if ORTHANC_ENABLE_PUGIXML == 1
void FormatXml(std::string& target) const;
#endif
virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr)
ORTHANC_OVERRIDE
{
}
virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag)
ORTHANC_OVERRIDE;
virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const void* data,
size_t size)
ORTHANC_OVERRIDE;
virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::vector<int64_t>& values)
ORTHANC_OVERRIDE;
virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::vector<double>& values)
ORTHANC_OVERRIDE;
virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
const std::vector<DicomTag>& values)
ORTHANC_OVERRIDE;
virtual Action VisitString(std::string& newValue,
const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const std::string& value)
ORTHANC_OVERRIDE;
};
}
......@@ -95,6 +95,11 @@
#include <dcmtk/dcmdata/dcvrus.h>
#include <dcmtk/dcmdata/dcvrut.h>
#if DCMTK_VERSION_NUMBER >= 361
# include <dcmtk/dcmdata/dcvruc.h>
# include <dcmtk/dcmdata/dcvrur.h>
#endif
#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
# include <EmbeddedResources.h>
#endif
......@@ -1289,16 +1294,18 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_OB:
return ValueRepresentation_OtherByte;
// Not supported as of DCMTK 3.6.0
/*case EVR_OD:
return ValueRepresentation_OtherDouble;*/
#if DCMTK_VERSION_NUMBER >= 361
case EVR_OD:
return ValueRepresentation_OtherDouble;
#endif
case EVR_OF:
return ValueRepresentation_OtherFloat;
// Not supported as of DCMTK 3.6.0
/*case EVR_OL:
return ValueRepresentation_OtherLong;*/
#if DCMTK_VERSION_NUMBER >= 362
case EVR_OL:
return ValueRepresentation_OtherLong;
#endif
case EVR_OW:
return ValueRepresentation_OtherWord;
......@@ -1324,9 +1331,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_TM:
return ValueRepresentation_Time;
// Not supported as of DCMTK 3.6.0
/*case EVR_UC:
return ValueRepresentation_UnlimitedCharacters;*/
#if DCMTK_VERSION_NUMBER >= 361
case EVR_UC:
return ValueRepresentation_UnlimitedCharacters;
#endif
case EVR_UI:
return ValueRepresentation_UniqueIdentifier;
......@@ -1337,9 +1345,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_UN:
return ValueRepresentation_Unknown;
// Not supported as of DCMTK 3.6.0
/*case EVR_UR:
return ValueRepresentation_UniversalResource;*/
#if DCMTK_VERSION_NUMBER >= 361
case EVR_UR:
return ValueRepresentation_UniversalResource;
#endif
case EVR_US:
return ValueRepresentation_UnsignedShort;
......@@ -1356,6 +1365,13 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
static bool IsBinaryTag(const DcmTag& key)
{
return (key.isUnknownVR() ||
#if DCMTK_VERSION_NUMBER >= 361
key.getEVR() == EVR_OD ||
#endif
#if DCMTK_VERSION_NUMBER >= 362
key.getEVR() == EVR_OL ||
#endif
key.getEVR() == EVR_OB ||
key.getEVR() == EVR_OF ||
key.getEVR() == EVR_OW ||
......@@ -1382,6 +1398,14 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
* Binary types, handled above
**/
#if DCMTK_VERSION_NUMBER >= 361
case EVR_OD:
#endif
#if DCMTK_VERSION_NUMBER >= 362
case EVR_OL:
#endif
case EVR_OB: // other byte
case EVR_OF: // other float
case EVR_OW: // other word
......@@ -1440,6 +1464,16 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_PN: // person name
return new DcmPersonName(key);
#if DCMTK_VERSION_NUMBER >= 361
case EVR_UC: // unlimited characters
return new DcmUnlimitedCharacters(key);
#endif
#if DCMTK_VERSION_NUMBER >= 361
case EVR_UR: // URI/URL
return new DcmUniversalResourceIdentifierOrLocator(key);
#endif
/**
* Numerical types
......@@ -1540,7 +1574,29 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
if (tag.IsPrivate() ||
IsBinaryTag(key))
{
if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good())
bool ok;
switch (key.getEVR())
{
case EVR_OW:
if (decoded->size() % sizeof(Uint16) != 0)
{
LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
ok = false;
}
else
{
ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
}
break;
default:
ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
break;
}
if (ok)
{
return;
}
......@@ -1591,6 +1647,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_UT: // unlimited text
case EVR_PN: // person name
case EVR_UI: // unique identifier
#if DCMTK_VERSION_NUMBER >= 361
case EVR_UC: // unlimited characters
case EVR_UR: // URI/URL
#endif
{
ok = element.putString(decoded->c_str()).good();
break;
......@@ -2164,12 +2224,71 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
const DicomTag& tag,
Encoding encoding)
{
// TODO - Merge this function with ConvertLeafElement()
// TODO - Merge this function, that is more recent, with ConvertLeafElement()
assert(element.isLeaf());
DcmEVR evr = element.getTag().getEVR();
ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
/**
* Fix the EVR for types internal to DCMTK
**/
if (evr == EVR_ox) // OB or OW depending on context
{
evr = EVR_OB;
}
if (evr == EVR_UNKNOWN || // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
evr == EVR_UNKNOWN2B) // used internally for elements with unknown VR with 2-byte length field in explicit VR
{
evr = EVR_UN;
}
const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
/**
* Deal with binary data (including PixelData).
**/
if (evr == EVR_OB || // other byte
evr == EVR_OF || // other float
#if DCMTK_VERSION_NUMBER >= 361
evr == EVR_OD || // other double
#endif
#if DCMTK_VERSION_NUMBER >= 362
evr == EVR_OL || // other long
#endif
evr == EVR_OW || // other word
evr == EVR_UN) // unknown value representation
{
Uint16* data16 = NULL;
Uint8* data = NULL;
if (evr == EVR_OW &&
element.getUint16Array(data16) == EC_Normal)
{
visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
}
else if (evr != EVR_OW &&
element.getUint8Array(data) == EC_Normal)
{
visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
}
else
{
visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
}
return; // We're done
}
/**
* Deal with plain strings (and convert them to UTF-8)
**/
char *c = NULL;
if (element.isaString() &&
......@@ -2215,18 +2334,13 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
try
{
// http://support.dcmtk.org/docs/dcvr_8h-source.html
switch (element.getVR())
switch (evr)
{
/**
* Deal with binary data (including PixelData).
* Plain string values.
**/
case EVR_OB: // other byte
case EVR_OF: // other float
case EVR_OW: // other word
case EVR_UN: // unknown value representation
case EVR_ox: // OB or OW depending on context
case EVR_DS: // decimal string
case EVR_IS: // integer string
case EVR_AS: // age string
......@@ -2242,21 +2356,46 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_UT: // unlimited text
case EVR_PN: // person name
case EVR_UI: // unique identifier
case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR
{
Uint8* data = NULL;
if (element.getUint8Array(data) == EC_Normal)
{
visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
const Uint32 length = element.getLength();
Uint32 l = 0;
while (l < length &&
data[l] != 0)
{
l++;
}
if (l == length)
{
// Not a null-terminated plain string
visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
}
else
{
std::string ignored;
std::string s(reinterpret_cast<const char*>(data), l);
ITagVisitor::Action action = visitor.VisitString
(ignored, parentTags, parentIndexes, tag, vr,
Toolbox::ConvertToUtf8(s, encoding));
if (action != ITagVisitor::Action_None)
{
LOG(WARNING) << "Cannot replace this string tag: "
<< FromDcmtkBridge::GetTagName(element)
<< " (" << tag.Format() << ")";
}
}
}
else
{
visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
}
break;
return;
}
/**
......@@ -2264,68 +2403,122 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
**/
case EVR_SL: // signed long
{
DcmSignedLong& content = dynamic_cast<DcmSignedLong&>(element);
std::vector<int64_t> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Sint32 f;
if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good())
if (content.getSint32(f, i).good())
{
visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
break;
}
case EVR_SS: // signed short
{
DcmSignedShort& content = dynamic_cast<DcmSignedShort&>(element);
std::vector<int64_t> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Sint16 f;
if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good())
if (content.getSint16(f, i).good())
{
visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
break;
}
case EVR_UL: // unsigned long
{
DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
std::vector<int64_t> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Uint32 f;
if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good())
if (content.getUint32(f, i).good())
{
visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
break;
}
case EVR_US: // unsigned short
{
DcmUnsignedShort& content = dynamic_cast<DcmUnsignedShort&>(element);
std::vector<int64_t> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Uint16 f;
if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good())
if (content.getUint16(f, i).good())
{
visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
break;
}
case EVR_FL: // float single-precision
{
DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
std::vector<double> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Float32 f;
if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good())
if (content.getFloat32(f, i).good())
{
visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
break;
}
case EVR_FD: // float double-precision
{
DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
std::vector<double> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
Float64 f;
if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good())
if (content.getFloat64(f, i).good())
{
visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
values.push_back(f);
}
}
visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
break;
}
......@@ -2336,13 +2529,23 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_AT:
{
DcmTagKey tagKey;
if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tagKey, 0).good())
DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
std::vector<DicomTag> values;
values.reserve(content.getVM());
for (unsigned long i = 0; i < content.getVM(); i++)
{
DcmTagKey f;
if (content.getTagVal(f, i).good())
{
DicomTag t(tagKey.getGroup(), tagKey.getElement());
visitor.VisitAttribute(parentTags, parentIndexes, tag, vr, t);
DicomTag t(f.getGroup(), f.getElement());
values.push_back(t);
}
}
assert(vr == ValueRepresentation_AttributeTag);
visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
break;
}
......@@ -2353,7 +2556,9 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
**/
case EVR_SQ: // sequence of items
{
return;
}
/**
......@@ -2374,8 +2579,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
case EVR_pixelItem: // used internally for pixel items in a compressed image
case EVR_PixelData: // used internally for uncompressed pixeld data
case EVR_OverlayData: // used internally for overlay data
visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
{
visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
return;
}
/**
......@@ -2418,6 +2625,12 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
// etc. are not." The following dynamic_cast is thus OK.
DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
if (sequence.card() == 0)
{
visitor.VisitEmptySequence(parentTags, parentIndexes, tag);
}
else
{
std::vector<DicomTag> tags = parentTags;
std::vector<size_t> indexes = parentIndexes;
tags.push_back(tag);
......@@ -2431,6 +2644,7 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
}
}
}
}
void FromDcmtkBridge::Apply(DcmItem& dataset,
......
......@@ -53,36 +53,46 @@ namespace Orthanc
{
}
virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
// Visiting a DICOM element that is internal to DCMTK
virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr) = 0;
virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
// SQ
virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag) = 0;
// SL, SS, UL, US
virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const void* data,
size_t size) = 0;
const std::vector<int64_t>& values) = 0;
virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
// FL, FD
virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
int64_t value) = 0;
const std::vector<double>& values) = 0;
virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
// AT
virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
double value) = 0;
const std::vector<DicomTag>& values) = 0;
virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
// OB, OD, OF, OL, OW, UN
virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
const DicomTag& tag,
ValueRepresentation vr,
const DicomTag& value) = 0;
const void* data,
size_t size) = 0;
// Visiting an UTF-8 string
virtual Action VisitString(std::string& newValue,
const std::vector<DicomTag>& parentTags,
const std::vector<size_t>& parentIndexes,
......
......@@ -59,6 +59,8 @@ namespace Orthanc
static const char* const MIME_WOFF = "application/x-font-woff";
static const char* const MIME_XML_2 = "text/xml";
static const char* const MIME_ZIP = "application/zip";
static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
// This function is autogenerated by the script
// "Resources/GenerateErrorCodes.py"
......@@ -1104,6 +1106,16 @@ namespace Orthanc
case MimeType_Woff:
return MIME_WOFF;
case MimeType_PrometheusText:
// https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
return "text/plain; version=0.0.4";
case MimeType_DicomWebJson:
return MIME_DICOM_WEB_JSON;
case MimeType_DicomWebXml:
return MIME_DICOM_WEB_XML;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
......@@ -1712,6 +1724,14 @@ namespace Orthanc
{
return MimeType_Woff;
}
else if (mime == MIME_DICOM_WEB_JSON)
{
return MimeType_DicomWebJson;
}
else if (mime == MIME_DICOM_WEB_XML)
{
return MimeType_DicomWebXml;
}
else
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
......
......@@ -105,7 +105,10 @@ namespace Orthanc
MimeType_WebAssembly,
MimeType_Xml,
MimeType_Woff, // Web Open Font Format
MimeType_Zip
MimeType_Zip,
MimeType_PrometheusText, // Prometheus text-based exposition format (for metrics)
MimeType_DicomWebJson,
MimeType_DicomWebXml
};
......
......@@ -35,16 +35,39 @@
#include "StorageAccessor.h"
#include "../Compression/ZlibCompressor.h"
#include "../MetricsRegistry.h"
#include "../OrthancException.h"
#include "../Toolbox.h"
#include "../Toolbox.h"
#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
# include "../HttpServer/HttpStreamTranscoder.h"
#endif
static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms";
static const std::string METRICS_READ = "orthanc_storage_read_duration_ms";
static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms";
namespace Orthanc
{
class StorageAccessor::MetricsTimer : public boost::noncopyable
{
private:
std::auto_ptr<MetricsRegistry::Timer> timer_;
public:
MetricsTimer(StorageAccessor& that,
const std::string& name)
{
if (that.metrics_ != NULL)
{
timer_.reset(new MetricsRegistry::Timer(*that.metrics_, name));
}
}
};
FileInfo StorageAccessor::Write(const void* data,
size_t size,
FileContentType type,
......@@ -64,6 +87,8 @@ namespace Orthanc
{
case CompressionType_None:
{
MetricsTimer timer(*this, METRICS_CREATE);
area_.Create(uuid, data, size, type);
return FileInfo(uuid, type, size, md5);
}
......@@ -82,6 +107,9 @@ namespace Orthanc
Toolbox::ComputeMD5(compressedMD5, compressed);
}
{
MetricsTimer timer(*this, METRICS_CREATE);
if (compressed.size() > 0)
{
area_.Create(uuid, &compressed[0], compressed.size(), type);
......@@ -90,6 +118,7 @@ namespace Orthanc
{
area_.Create(uuid, NULL, 0, type);
}
}
return FileInfo(uuid, type, size, md5,
CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
......@@ -108,6 +137,7 @@ namespace Orthanc
{
case CompressionType_None:
{
MetricsTimer timer(*this, METRICS_READ);
area_.Read(content, info.GetUuid(), info.GetContentType());
break;
}
......@@ -117,7 +147,12 @@ namespace Orthanc
ZlibCompressor zlib;
std::string compressed;
{
MetricsTimer timer(*this, METRICS_READ);
area_.Read(compressed, info.GetUuid(), info.GetContentType());
}
IBufferCompressor::Uncompress(content, zlib, compressed);
break;
}
......@@ -132,17 +167,19 @@ namespace Orthanc
}
void StorageAccessor::Read(Json::Value& content,
void StorageAccessor::ReadRaw(std::string& content,
const FileInfo& info)
{
std::string s;
Read(s, info);
MetricsTimer timer(*this, METRICS_READ);
area_.Read(content, info.GetUuid(), info.GetContentType());
}
Json::Reader reader;
if (!reader.parse(s, content))
void StorageAccessor::Remove(const std::string& fileUuid,
FileContentType type)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
MetricsTimer timer(*this, METRICS_REMOVE);
area_.Remove(fileUuid, type);
}
......@@ -151,7 +188,11 @@ namespace Orthanc
const FileInfo& info,
const std::string& mime)
{
{
MetricsTimer timer(*this, METRICS_READ);
area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType());
}
sender.SetContentType(mime);
const char* extension;
......
......@@ -61,14 +61,23 @@
#include <string>
#include <boost/noncopyable.hpp>
#include <stdint.h>
#include <json/value.h>
namespace Orthanc
{
class MetricsRegistry;
/**
* This class handles the compression/decompression of the raw files
* contained in the storage area, and monitors timing metrics (if
* enabled).
**/
class StorageAccessor : boost::noncopyable
{
private:
class MetricsTimer;
IStorageArea& area_;
MetricsRegistry* metrics_;
#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
void SetupSender(BufferHttpSender& sender,
......@@ -77,7 +86,16 @@ namespace Orthanc
#endif
public:
StorageAccessor(IStorageArea& area) : area_(area)
StorageAccessor(IStorageArea& area) :
area_(area),
metrics_(NULL)
{
}
StorageAccessor(IStorageArea& area,
MetricsRegistry& metrics) :
area_(area),
metrics_(&metrics)
{
}
......@@ -99,12 +117,15 @@ namespace Orthanc
void Read(std::string& content,
const FileInfo& info);
void Read(Json::Value& content,
void ReadRaw(std::string& content,
const FileInfo& info);
void Remove(const std::string& fileUuid,
FileContentType type);
void Remove(const FileInfo& info)
{
area_.Remove(info.GetUuid(), info.GetContentType());
Remove(info.GetUuid(), info.GetContentType());
}
#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
......
......@@ -46,6 +46,13 @@
#include <boost/lexical_cast.hpp>
#if ORTHANC_ENABLE_CIVETWEB == 1
# if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
# error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
# endif
#endif
namespace Orthanc
{
HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
......@@ -177,6 +184,10 @@ namespace Orthanc
{
s += "Connection: keep-alive\r\n";
}
else
{
s += "Connection: close\r\n";
}
for (std::list<std::string>::const_iterator
it = headers_.begin(); it != headers_.end(); ++it)
......@@ -428,13 +439,28 @@ namespace Orthanc
throw OrthancException(ErrorCode_NotImplemented,
"Multipart answers are not implemented together "
"with keep-alive connections if using Mongoose");
#else
#elif ORTHANC_ENABLE_CIVETWEB == 1
# if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
// Turn off Keep-Alive for multipart answers
// https://github.com/civetweb/civetweb/issues/727
stream_.DisableKeepAlive();
header += "Connection: close\r\n";
# else
// The function "mg_disable_keep_alive()" is not available,
// let's continue with Keep-Alive. Performance of WADO-RS will
// decrease.
header += "Connection: keep-alive\r\n";
# endif
#else
# error Please support your embedded Web server here
#endif
}
else
{
header += "Connection: close\r\n";
}
// Possibly add the cookies
for (std::list<std::string>::const_iterator
......
......@@ -47,6 +47,9 @@
#elif ORTHANC_ENABLE_CIVETWEB == 1
# include <civetweb.h>
# define MONGOOSE_USE_CALLBACKS 1
# if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
# error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
# endif
#else
# error "Either Mongoose or Civetweb must be enabled to compile this file"
......@@ -114,8 +117,18 @@ namespace Orthanc
#if ORTHANC_ENABLE_MONGOOSE == 1
throw OrthancException(ErrorCode_NotImplemented,
"Only available if using CivetWeb");
#elif ORTHANC_ENABLE_CIVETWEB == 1
# if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
mg_disable_keep_alive(connection_);
# else
# warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly
throw OrthancException(ErrorCode_NotImplemented,
"Only available if using a patched version of CivetWeb");
# endif
#else
# error Please support your embedded Web server here
#endif
}
};
......@@ -1189,6 +1202,8 @@ namespace Orthanc
Stop();
threadsCount_ = threads;
LOG(INFO) << "The embedded HTTP server will use " << threads << " threads";
}
......
......@@ -1379,7 +1379,18 @@ namespace Orthanc
for (Json::Value::Members::const_iterator it = members.begin();
it != members.end(); ++it)
{
std::auto_ptr<JobHandler> job(new JobHandler(unserializer, s[JOBS][*it], *it));
std::auto_ptr<JobHandler> job;
try
{
job.reset(new JobHandler(unserializer, s[JOBS][*it], *it));
}
catch (OrthancException& e)
{
LOG(WARNING) << "Cannot unserialize one job from previous execution, "
<< "skipping it: " << e.What();
continue;
}
const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime();
......@@ -1398,4 +1409,49 @@ namespace Orthanc
}
}
}
void JobsRegistry::GetStatistics(unsigned int& pending,
unsigned int& running,
unsigned int& success,
unsigned int& failed)
{
boost::mutex::scoped_lock lock(mutex_);
CheckInvariants();
pending = 0;
running = 0;
success = 0;
failed = 0;
for (JobsIndex::const_iterator it = jobsIndex_.begin();
it != jobsIndex_.end(); ++it)
{
JobHandler& job = *it->second;
switch (job.GetState())
{
case JobState_Retry:
case JobState_Pending:
pending ++;
break;
case JobState_Paused:
case JobState_Running:
running ++;
break;
case JobState_Success:
success ++;
break;
case JobState_Failure:
failed ++;
break;
default:
throw OrthancException(ErrorCode_InternalError);
}
}
}
}
......@@ -199,6 +199,11 @@ namespace Orthanc
void ResetObserver();
void GetStatistics(unsigned int& pending,
unsigned int& running,
unsigned int& success,
unsigned int& errors);
class RunningJob : public boost::noncopyable
{
private:
......
/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2019 Osimis S.A., Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* In addition, as a special exception, the copyright holders of this
* program give permission to link the code of its release with the
* OpenSSL project's "OpenSSL" library (or with modified versions of it
* that use the same license as the "OpenSSL" library), and distribute
* the linked executables. You must obey the GNU General Public License
* in all respects for all of the code used other than "OpenSSL". If you
* modify file(s) with this exception, you may extend this exception to
* your version of the file(s), but you are not obligated to do so. If
* you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files
* in the program, then also delete it here.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#include "PrecompiledHeaders.h"
#include "MetricsRegistry.h"
#include "OrthancException.h"
#include "ChunkedBuffer.h"
namespace Orthanc
{
static const boost::posix_time::ptime GetNow()
{
return boost::posix_time::microsec_clock::universal_time();
}
class MetricsRegistry::Item
{
private:
MetricsType type_;
boost::posix_time::ptime time_;
bool hasValue_;
float value_;
void Touch(float value,
const boost::posix_time::ptime& now)
{
hasValue_ = true;
value_ = value;
time_ = now;
}
void Touch(float value)
{
Touch(value, GetNow());
}
void UpdateMax(float value,
int duration)
{
if (hasValue_)
{
const boost::posix_time::ptime now = GetNow();
if (value > value_ ||
(now - time_).total_seconds() > duration)
{
Touch(value, now);
}
}
else
{
Touch(value);
}
}
void UpdateMin(float value,
int duration)
{
if (hasValue_)
{
const boost::posix_time::ptime now = GetNow();
if (value < value_ ||
(now - time_).total_seconds() > duration)
{
Touch(value, now);
}
}
else
{
Touch(value);
}
}
public:
Item(MetricsType type) :
type_(type),
hasValue_(false)
{
}
MetricsType GetType() const
{
return type_;
}
void Update(float value)
{
switch (type_)
{
case MetricsType_Default:
Touch(value);
break;
case MetricsType_MaxOver10Seconds:
UpdateMax(value, 10);
break;
case MetricsType_MaxOver1Minute:
UpdateMax(value, 60);
break;
case MetricsType_MinOver10Seconds:
UpdateMin(value, 10);
break;
case MetricsType_MinOver1Minute:
UpdateMin(value, 60);
break;
default:
throw OrthancException(ErrorCode_NotImplemented);
}
}
bool HasValue() const
{
return hasValue_;
}
const boost::posix_time::ptime& GetTime() const
{
if (hasValue_)
{
return time_;
}
else
{
throw OrthancException(ErrorCode_BadSequenceOfCalls);
}
}
float GetValue() const
{
if (hasValue_)
{
return value_;
}
else
{
throw OrthancException(ErrorCode_BadSequenceOfCalls);
}
}
};
MetricsRegistry::~MetricsRegistry()
{
for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
{
assert(it->second != NULL);
delete it->second;
}
}
void MetricsRegistry::SetEnabled(bool enabled)
{
boost::mutex::scoped_lock lock(mutex_);
enabled_ = enabled;
}
void MetricsRegistry::Register(const std::string& name,
MetricsType type)
{
boost::mutex::scoped_lock lock(mutex_);
Content::iterator found = content_.find(name);
if (found == content_.end())
{
content_[name] = new Item(type);
}
else
{
assert(found->second != NULL);
// This metrics already exists: Only recreate it if there is a
// mismatch in the type of metrics
if (found->second->GetType() != type)
{
delete found->second;
found->second = new Item(type);
}
}
}
void MetricsRegistry::SetValueInternal(const std::string& name,
float value,
MetricsType type)
{
boost::mutex::scoped_lock lock(mutex_);
Content::iterator found = content_.find(name);
if (found == content_.end())
{
std::auto_ptr<Item> item(new Item(type));
item->Update(value);
content_[name] = item.release();
}
else
{
assert(found->second != NULL);
found->second->Update(value);
}
}
MetricsType MetricsRegistry::GetMetricsType(const std::string& name)
{
boost::mutex::scoped_lock lock(mutex_);
Content::const_iterator found = content_.find(name);
if (found == content_.end())
{
throw OrthancException(ErrorCode_InexistentItem);
}
else
{
assert(found->second != NULL);
return found->second->GetType();
}
}
void MetricsRegistry::ExportPrometheusText(std::string& s)
{
// https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));
boost::mutex::scoped_lock lock(mutex_);
s.clear();
if (!enabled_)
{
return;
}
ChunkedBuffer buffer;
for (Content::const_iterator it = content_.begin();
it != content_.end(); ++it)
{
assert(it->second != NULL);
if (it->second->HasValue())
{
boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;
std::string line = (it->first + " " +
boost::lexical_cast<std::string>(it->second->GetValue()) + " " +
boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");
buffer.AddChunk(line);
}
}
buffer.Flatten(s);
}
void MetricsRegistry::SharedMetrics::Add(float delta)
{
boost::mutex::scoped_lock lock(mutex_);
value_ += delta;
registry_.SetValue(name_, value_);
}
void MetricsRegistry::Timer::Start()
{
if (registry_.IsEnabled())
{
active_ = true;
start_ = GetNow();
}
else
{
active_ = false;
}
}
MetricsRegistry::Timer::~Timer()
{
if (active_)
{
boost::posix_time::time_duration diff = GetNow() - start_;
registry_.SetValue(name_, diff.total_milliseconds(), type_);
}
}
}
/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2019 Osimis S.A., Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* In addition, as a special exception, the copyright holders of this
* program give permission to link the code of its release with the
* OpenSSL project's "OpenSSL" library (or with modified versions of it
* that use the same license as the "OpenSSL" library), and distribute
* the linked executables. You must obey the GNU General Public License
* in all respects for all of the code used other than "OpenSSL". If you
* modify file(s) with this exception, you may extend this exception to
* your version of the file(s), but you are not obligated to do so. If
* you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files
* in the program, then also delete it here.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#pragma once
#if !defined(ORTHANC_SANDBOXED)
# error The macro ORTHANC_SANDBOXED must be defined
#endif
#if ORTHANC_SANDBOXED == 1
# error The class MetricsRegistry cannot be used in sandboxed environments
#endif
#include <boost/thread/mutex.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
namespace Orthanc
{
enum MetricsType
{
MetricsType_Default,
MetricsType_MaxOver10Seconds,
MetricsType_MaxOver1Minute,
MetricsType_MinOver10Seconds,
MetricsType_MinOver1Minute
};
class MetricsRegistry : public boost::noncopyable
{
private:
class Item;
typedef std::map<std::string, Item*> Content;
bool enabled_;
boost::mutex mutex_;
Content content_;
void SetValueInternal(const std::string& name,
float value,
MetricsType type);
public:
MetricsRegistry() :
enabled_(true)
{
}
~MetricsRegistry();
bool IsEnabled() const
{
return enabled_;
}
void SetEnabled(bool enabled);
void Register(const std::string& name,
MetricsType type);
void SetValue(const std::string& name,
float value,
MetricsType type)
{
// Inlining to avoid loosing time if metrics are disabled
if (enabled_)
{
SetValueInternal(name, value, type);
}
}
void SetValue(const std::string& name,
float value)
{
SetValue(name, value, MetricsType_Default);
}
MetricsType GetMetricsType(const std::string& name);
// https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
void ExportPrometheusText(std::string& s);
class SharedMetrics : public boost::noncopyable
{
private:
boost::mutex mutex_;
MetricsRegistry& registry_;
std::string name_;
float value_;
public:
SharedMetrics(MetricsRegistry& registry,
const std::string& name,
MetricsType type) :
registry_(registry),
name_(name),
value_(0)
{
}
void Add(float delta);
};
class ActiveCounter : public boost::noncopyable
{
private:
SharedMetrics& metrics_;
public:
ActiveCounter(SharedMetrics& metrics) :
metrics_(metrics)
{
metrics_.Add(1);
}
~ActiveCounter()
{
metrics_.Add(-1);
}
};
class Timer : public boost::noncopyable
{
private:
MetricsRegistry& registry_;
std::string name_;
MetricsType type_;
bool active_;
boost::posix_time::ptime start_;
void Start();
public:
Timer(MetricsRegistry& registry,
const std::string& name) :
registry_(registry),
name_(name),
type_(MetricsType_MaxOver10Seconds)
{
Start();
}
Timer(MetricsRegistry& registry,
const std::string& name,
MetricsType type) :
registry_(registry),
name_(name),
type_(type)
{
Start();
}
~Timer();
};
};
}