Mercurial > hg > orthanc
changeset 5885:207371ec031e find-refactoring tip
integration mainline->find-refactoring
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 29 Nov 2024 10:54:20 +0100 |
parents | 92e5579681f2 (diff) 1e51e6299f7a (current diff) |
children | |
files | NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/UnitTestsSources/DicomMapTests.cpp |
diffstat | 115 files changed, 10306 insertions(+), 8869 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.clang-format Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,57 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: true +AlignTrailingComments: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 200 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ContinuationIndentWidth: 2 +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 2 +...
--- a/.hgignore Mon Nov 25 17:03:47 2024 +0100 +++ b/.hgignore Fri Nov 29 10:54:20 2024 +0100 @@ -7,6 +7,7 @@ .vscode/ *~ *.cmake.orig +.idea/ # when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore: .settings/
--- a/NEWS Mon Nov 25 17:03:47 2024 +0100 +++ b/NEWS Fri Nov 29 10:54:20 2024 +0100 @@ -1,12 +1,50 @@ Pending changes in the mainline =============================== +* TODO-FIND: complete the list of updated routes: + - /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1') + - /studies?since=x&limit=0 and sibbling routes: limit=0 now means "no limit" instead of "no results" + +General +------- + +* Database: + - Introduced database optimizations "ExtendedFind" to replace many small SQL queries + by a small number of large SQL queries to greatly reduce the cost of DB latency. + Furthermore, this "ExtendedFind" brings new sorting and filtering features to the + Rest API (TODO). + - Introduced database optimizations "ExtendedChanges" to allow filtering of /changes. + - Reduced the number of SQL queries when ingesting DICOM files. +* Introduced a new configuration "ReadOnly" to forbid an Orthanc instance to perform + any modifications in the Index DB or in the storage. + + REST API ------------ +-------- * API version upgraded to 26 * Improved parsing of multiple numerical values in DICOM tags. https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 +* in /system, added a new field "Capabilities" with new values: + - "HasExtendedChanges" for DB backend that provides this feature (the default SQLite DB + or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X). + - "HasExtendedFind" for DB backend that provides this feature (the default SQLite DB + or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X). +* With DB backend with "HasExtendedChanges" support, /changes now supports 2 more options: + - 'type' to filter the changes returned by the query + - 'to' to potentially cycle through changes in reverse order. + example: /changes?type=StableStudy&to=7584&limit=100 +* With DB backend with "HasExtendedFind" support, /tools/find now supports new options: + - 'OrderBy' to order by DICOM Tag or metadata value + - 'ParentPatient', 'ParentStudy', 'ParentSeries' to retrieve only descendants of an + Orthanc resource. + - 'MetadataQuery' to filter results based on metadata values. + - 'ResponseContent' to define what shall be included in the response for each returned + resource (e.g: Metadata, Children, ...) +* With DB backend with "HasExtendedFind" support, a new /tools/count-resources API route + is similar to tools/find but only returns the number of resources matching the criteria. +* With DB backend with "HasExtendedFind" support, usage of 'Limit' and 'Since in /tools/find + is not allowed if your query includes filtering on DICOM tags that are not stored in DB. * In DICOMWeb json, the "DS - Decimal String" values were represented by float numbers and they are now represented as strings to avoid introduction of long float representation (e.g 0.1429999999999 vs "0.143") and be more compliant with the DICOMWeb @@ -20,10 +58,9 @@ * DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing an outgoing SCU connexion when "DicomTlsRemoteCertificateRequired" is set to false. -* Introduced a new thread to update the statistics at regular interval for the - DB plugins that are implementing the UpdateAndGetStatistics function (currently only - PostgreSQL). This avoids very long update times in case you don't call /statistics - for a long period. +* Introduced a new thread to perform DB Housekeeping at regular interval (1s) for the + DB plugins requiring it (currently only PostgreSQL). E.g: This avoids very long update + times in case you don't call /statistics for a long period. * Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ... in very specific use-cases. * Fix C-Find queries not returning private tags in the modality worklist plugin. @@ -38,11 +75,27 @@ - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count * Upgraded dependencies for static builds: - curl 8.9.0 + - SQLite 3.46 - boost 1.86.0 * Added a new fallback when trying to decode a frame: transcode the file using the plugin before decoding the frame. This solves some issues with JP2K Lossy compression: https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117 -* Added a new warning that can be disabled in the configuration: W003_DecoderFailure +* Added new warnings that can be disabled in the configuration: + - W003_DecoderFailure + - W004_NoMainDicomTagsSignature + - W005_RequestingTagFromLowerResourceLevel + - W006_RequestingTagFromMetaHeader + - W007_MissingRequestedTagsNotReadFromDisk +* New default MainDicomTags are now stored in DB. Note that, in order to store these values + for resources that were ingested in Orthanc before this release, you would have to run + the Housekeeper plugin or call /reconstruct on every resources + - At Study Level: + - TimezoneOffsetFromUTC (used in QIDO-RS default queries) + - At Series Level: + - TimezoneOffsetFromUTC (used in QIDO-RS default queries) + - PerformedProcedureStepStartDate (used in QIDO-RS default queries) + - PerformedProcedureStepStartTime (used in QIDO-RS default queries) + - RequestAttributesSequence (used in QIDO-RS default queries)
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake Fri Nov 29 10:54:20 2024 +0100 @@ -37,11 +37,11 @@ if (SQLITE_STATIC) - SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100) - SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da") - SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3270100.zip") + SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100) + SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3") + SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip") - set(ORTHANC_SQLITE_VERSION 3027001) + set(ORTHANC_SQLITE_VERSION 3046001) DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Fri Nov 29 10:54:20 2024 +0100 @@ -262,6 +262,11 @@ "Name": "DuplicateResource", "Description": "Duplicate resource" }, + { + "Code": 47, + "Name": "IncompatibleConfigurations", + "Description": "Your configuration file contains configuration that are mutually incompatible" + }, @@ -337,7 +342,7 @@ { "Code": 1011, "Name": "SQLiteBindOutOfRange", - "Description": "SQLite: Bing a value while out of range (serious error)", + "Description": "SQLite: Bind a value while out of range (serious error)", "SQLite": true }, {
--- a/OrthancFramework/Resources/Graveyard/EclipseCodingStyle.xml Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,167 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<profiles version="1"> -<profile kind="CodeFormatterProfile" name="Orthanc" version="1"> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_in_empty_block" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.lineSplit" value="80"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_member_access" value="0"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_base_types" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.keep_else_statement_on_same_line" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_constructor_initializer_list" value="0"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_exception_specification" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_base_types" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_access_specifier" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_exception_specification" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_arguments" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_case" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.comment.min_distance_between_code_and_line_comment" value="1"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_expressions_in_array_initializer" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_declarator_list" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_bracket" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.tabulation.size" value="2"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_else_in_if_statement" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_enumerator_list" value="49"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_declarator_list" value="16"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_empty_lines" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.put_empty_statement_on_new_line" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_method_declaration" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_arguments" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_base_clause" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_breaks_compare_to_cases" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.join_wrapped_lines" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_declarator_list" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_arguments_in_method_invocation" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.comment.never_indent_line_comments_on_first_column" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_brackets" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_bracket" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_parameters_in_method_declaration" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_type_declaration" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_assignment_operator" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_arguments" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_expression_list" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_parameters" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.continuation_indentation" value="2"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_expression_list" value="0"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_parameters" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_binary_operator" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression" value="34"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.format_guardian_clause_on_one_line" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_extra_spaces" value="0"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_compare_to_type_header" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_namespace_header" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_compact_if" value="16"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_assignment_operator" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_assignment" value="16"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression_chain" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_parameters" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_expression_list" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_exception_specification" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_binary_operator" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_identifier_in_function_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_base_clause_in_type_declaration" value="80"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_exception_specification" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_declaration_compare_to_template_header" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_body" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_binary_expression" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_block" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_arguments" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_parameters" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.tabulation.char" value="space"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_parameters" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_colon_in_constructor_initializer_list" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block_in_case" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.compact_else_if" value="true"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_template_declaration" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_base_clause" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.keep_then_statement_on_same_line" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_switch" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.alignment_for_overloaded_left_shift_chain" value="18"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.keep_imple_if_on_one_line" value="false"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.indentation.size" value="2"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_namespace_declaration" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_arguments" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.brace_position_for_array_initializer" value="next_line"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_namespace_declaration" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_bracket" value="do not insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_while_in_do_statement" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_parameters" value="insert"/> -<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_arguments" value="do not insert"/> -</profile> -</profiles>
--- a/OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,192 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - - DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag) - { - DcmTag key(tag.GetGroup(), tag.GetElement()); - - if (tag.IsPrivate()) - { - // This raises BitBucket issue 140 (Modifying private tags with - // REST API changes VR from LO to UN) - // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=140 - LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags " - "are considered as having a binary value representation"; - return new DcmOtherByteOtherWord(key); - } - else if (IsBinaryTag(key)) - { - return new DcmOtherByteOtherWord(key); - } - - switch (key.getEVR()) - { - // http://support.dcmtk.org/docs/dcvr_8h-source.html - - /** - * 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 - case EVR_UN: // unknown value representation - case EVR_ox: // OB or OW depending on context - throw OrthancException(ErrorCode_InternalError); - - - /** - * String types. - * http://support.dcmtk.org/docs/classDcmByteString.html - **/ - - case EVR_AS: // age string - return new DcmAgeString(key); - - case EVR_AE: // application entity title - return new DcmApplicationEntity(key); - - case EVR_CS: // code string - return new DcmCodeString(key); - - case EVR_DA: // date string - return new DcmDate(key); - - case EVR_DT: // date time string - return new DcmDateTime(key); - - case EVR_DS: // decimal string - return new DcmDecimalString(key); - - case EVR_IS: // integer string - return new DcmIntegerString(key); - - case EVR_TM: // time string - return new DcmTime(key); - - case EVR_UI: // unique identifier - return new DcmUniqueIdentifier(key); - - case EVR_ST: // short text - return new DcmShortText(key); - - case EVR_LO: // long string - return new DcmLongString(key); - - case EVR_LT: // long text - return new DcmLongText(key); - - case EVR_UT: // unlimited text - return new DcmUnlimitedText(key); - - case EVR_SH: // short string - return new DcmShortString(key); - - 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 - **/ - - case EVR_SL: // signed long - return new DcmSignedLong(key); - - case EVR_SS: // signed short - return new DcmSignedShort(key); - - case EVR_UL: // unsigned long - return new DcmUnsignedLong(key); - - case EVR_US: // unsigned short - return new DcmUnsignedShort(key); - - case EVR_FL: // float single-precision - return new DcmFloatingPointSingle(key); - - case EVR_FD: // float double-precision - return new DcmFloatingPointDouble(key); - - - /** - * Sequence types, should never occur at this point. - **/ - - case EVR_SQ: // sequence of items - throw OrthancException(ErrorCode_ParameterOutOfRange); - - - /** - * TODO - **/ - - case EVR_AT: // attribute tag - throw OrthancException(ErrorCode_NotImplemented); - - - /** - * Internal to DCMTK. - **/ - - case EVR_xs: // SS or US depending on context - case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) - case EVR_na: // na="not applicable", for data which has no VR - case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor - case EVR_item: // used internally for items - case EVR_metainfo: // used internally for meta info datasets - case EVR_dataset: // used internally for datasets - case EVR_fileFormat: // used internally for DICOM files - case EVR_dicomDir: // used internally for DICOMDIR objects - case EVR_dirRecord: // used internally for DICOMDIR records - case EVR_pixelSQ: // used internally for pixel sequences in a compressed image - case EVR_pixelItem: // used internally for pixel items in a compressed image - case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) - case EVR_PixelData: // used internally for uncompressed pixeld data - case EVR_OverlayData: // used internally for overlay data - case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR - default: - break; - } - - throw OrthancException(ErrorCode_InternalError); - }
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "../ICommand.h" - -#include <list> -#include <cstddef> - -namespace Orthanc -{ - class BagOfTasks : public boost::noncopyable - { - private: - typedef std::list<ICommand*> Tasks; - - Tasks tasks_; - - public: - ~BagOfTasks() - { - for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it) - { - delete *it; - } - } - - ICommand* Pop() - { - ICommand* task = tasks_.front(); - tasks_.pop_front(); - return task; - } - - void Push(ICommand* task) // Takes ownership - { - if (task != NULL) - { - tasks_.push_back(task); - } - } - - size_t GetSize() const - { - return tasks_.size(); - } - - bool IsEmpty() const - { - return tasks_.empty(); - } - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,268 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeaders.h" -#include "BagOfTasksProcessor.h" - -#include "../Logging.h" -#include "../OrthancException.h" - -#include <stdio.h> - -namespace Orthanc -{ - class BagOfTasksProcessor::Task : public IDynamicObject - { - private: - uint64_t bag_; - std::auto_ptr<ICommand> command_; - - public: - Task(uint64_t bag, - ICommand* command) : - bag_(bag), - command_(command) - { - } - - bool Execute() - { - try - { - return command_->Execute(); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What(); - return false; - } - catch (std::runtime_error& e) - { - LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what(); - return false; - } - catch (...) - { - LOG(ERROR) << "Native exception while processing a bag of tasks"; - return false; - } - } - - uint64_t GetBag() - { - return bag_; - } - }; - - - void BagOfTasksProcessor::SignalProgress(Task& task, - Bag& bag) - { - assert(bag.done_ < bag.size_); - - bag.done_ += 1; - - if (bag.done_ == bag.size_) - { - exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running); - bagFinished_.notify_all(); - } - } - - void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that) - { - while (that->continue_) - { - std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100)); - if (obj.get() != NULL) - { - Task& task = *dynamic_cast<Task*>(obj.get()); - - { - boost::mutex::scoped_lock lock(that->mutex_); - - Bags::iterator bag = that->bags_.find(task.GetBag()); - assert(bag != that->bags_.end()); - assert(bag->second.done_ < bag->second.size_); - - if (bag->second.status_ != BagStatus_Running) - { - // Do not execute this task, as its parent bag of tasks - // has failed or is tagged as canceled - that->SignalProgress(task, bag->second); - continue; - } - } - - bool success = task.Execute(); - - { - boost::mutex::scoped_lock lock(that->mutex_); - - Bags::iterator bag = that->bags_.find(task.GetBag()); - assert(bag != that->bags_.end()); - - if (!success) - { - bag->second.status_ = BagStatus_Failed; - } - - that->SignalProgress(task, bag->second); - } - } - } - } - - - void BagOfTasksProcessor::Cancel(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - Bags::iterator it = bags_.find(bag); - if (it != bags_.end()) - { - it->second.status_ = BagStatus_Canceled; - } - } - - - bool BagOfTasksProcessor::Join(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - while (continue_) - { - ExitStatus::iterator it = exitStatus_.find(bag); - if (it == exitStatus_.end()) // The bag is still running - { - bagFinished_.wait(lock); - } - else - { - bool status = it->second; - exitStatus_.erase(it); - return status; - } - } - - return false; // The processor is stopping - } - - - float BagOfTasksProcessor::GetProgress(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - Bags::const_iterator it = bags_.find(bag); - if (it == bags_.end()) - { - // The bag of tasks has finished - return 1.0f; - } - else - { - return (static_cast<float>(it->second.done_) / - static_cast<float>(it->second.size_)); - } - } - - - bool BagOfTasksProcessor::Handle::Join() - { - if (hasJoined_) - { - return status_; - } - else - { - status_ = that_.Join(bag_); - hasJoined_ = true; - return status_; - } - } - - - BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : - countBags_(0), - continue_(true) - { - if (countThreads == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - threads_.resize(countThreads); - - for (size_t i = 0; i < threads_.size(); i++) - { - threads_[i] = new boost::thread(Worker, this); - } - } - - - BagOfTasksProcessor::~BagOfTasksProcessor() - { - continue_ = false; - - bagFinished_.notify_all(); // Wakes up all the pending "Join()" - - for (size_t i = 0; i < threads_.size(); i++) - { - if (threads_[i]) - { - if (threads_[i]->joinable()) - { - threads_[i]->join(); - } - - delete threads_[i]; - threads_[i] = NULL; - } - } - } - - - BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks) - { - if (tasks.GetSize() == 0) - { - return new Handle(*this, 0, true); - } - - boost::mutex::scoped_lock lock(mutex_); - - uint64_t id = countBags_; - countBags_ += 1; - - Bag bag(tasks.GetSize()); - bags_[id] = bag; - - while (!tasks.IsEmpty()) - { - queue_.Enqueue(new Task(id, tasks.Pop())); - } - - return new Handle(*this, id, false); - } -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "BagOfTasks.h" -#include "SharedMessageQueue.h" - -#include <stdint.h> -#include <map> - -namespace Orthanc -{ - class BagOfTasksProcessor : public boost::noncopyable - { - private: - enum BagStatus - { - BagStatus_Running, - BagStatus_Canceled, - BagStatus_Failed - }; - - - struct Bag - { - size_t size_; - size_t done_; - BagStatus status_; - - Bag() : - size_(0), - done_(0), - status_(BagStatus_Failed) - { - } - - explicit Bag(size_t size) : - size_(size), - done_(0), - status_(BagStatus_Running) - { - } - }; - - class Task; - - - typedef std::map<uint64_t, Bag> Bags; - typedef std::map<uint64_t, bool> ExitStatus; - - SharedMessageQueue queue_; - - boost::mutex mutex_; - uint64_t countBags_; - Bags bags_; - std::vector<boost::thread*> threads_; - ExitStatus exitStatus_; - bool continue_; - - boost::condition_variable bagFinished_; - - static void Worker(BagOfTasksProcessor* that); - - void Cancel(int64_t bag); - - bool Join(int64_t bag); - - float GetProgress(int64_t bag); - - void SignalProgress(Task& task, - Bag& bag); - - public: - class Handle : public boost::noncopyable - { - friend class BagOfTasksProcessor; - - private: - BagOfTasksProcessor& that_; - uint64_t bag_; - bool hasJoined_; - bool status_; - - Handle(BagOfTasksProcessor& that, - uint64_t bag, - bool empty) : - that_(that), - bag_(bag), - hasJoined_(empty) - { - } - - public: - ~Handle() - { - Join(); - } - - void Cancel() - { - that_.Cancel(bag_); - } - - bool Join(); - - float GetProgress() - { - return that_.GetProgress(bag_); - } - }; - - - explicit BagOfTasksProcessor(size_t countThreads); - - ~BagOfTasksProcessor(); - - Handle* Submit(BagOfTasks& tasks); - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "IDynamicObject.h" - -namespace Orthanc -{ - /** - * This class is the base class for the "Command" design pattern. - * http://en.wikipedia.org/wiki/Command_pattern - **/ - class ICommand : public IDynamicObject - { - public: - virtual bool Execute() = 0; - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - class ILockable : public boost::noncopyable - { - friend class Locker; - - protected: - virtual void Lock() = 0; - - virtual void Unlock() = 0; - - public: - virtual ~ILockable() - { - } - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Locker.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "ILockable.h" - -namespace Orthanc -{ - class Locker : public boost::noncopyable - { - private: - ILockable& lockable_; - - public: - Locker(ILockable& lockable) : lockable_(lockable) - { - lockable_.Lock(); - } - - virtual ~Locker() - { - lockable_.Unlock(); - } - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeaders.h" -#include "Mutex.h" - -#include "../OrthancException.h" - -#if defined(_WIN32) -#include <windows.h> -#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) -#include <pthread.h> -#else -#error Support your platform here -#endif - -namespace Orthanc -{ -#if defined (_WIN32) - - struct Mutex::PImpl - { - CRITICAL_SECTION criticalSection_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - ::InitializeCriticalSection(&pimpl_->criticalSection_); - } - - Mutex::~Mutex() - { - ::DeleteCriticalSection(&pimpl_->criticalSection_); - delete pimpl_; - } - - void Mutex::Lock() - { - ::EnterCriticalSection(&pimpl_->criticalSection_); - } - - void Mutex::Unlock() - { - ::LeaveCriticalSection(&pimpl_->criticalSection_); - } - - -#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) - - struct Mutex::PImpl - { - pthread_mutex_t mutex_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - - if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0) - { - delete pimpl_; - throw OrthancException(ErrorCode_InternalError); - } - } - - Mutex::~Mutex() - { - pthread_mutex_destroy(&pimpl_->mutex_); - delete pimpl_; - } - - void Mutex::Lock() - { - if (pthread_mutex_lock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - - void Mutex::Unlock() - { - if (pthread_mutex_unlock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - -#else -#error Support your plateform here -#endif -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "ILockable.h" - -namespace Orthanc -{ - class Mutex : public ILockable - { - private: - struct PImpl; - - PImpl *pimpl_; - - protected: - virtual void Lock(); - - virtual void Unlock(); - - public: - Mutex(); - - ~Mutex(); - }; -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,117 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeaders.h" -#include "ReaderWriterLock.h" - -#include <boost/thread/shared_mutex.hpp> - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation - // modules. - - class ReaderLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock_shared(); - } - - virtual void Unlock() - { - lock_.unlock_shared(); - } - - public: - explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - }; - - - class WriterLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock(); - } - - virtual void Unlock() - { - lock_.unlock(); - } - - public: - explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - }; - } - - struct ReaderWriterLock::PImpl - { - boost::shared_mutex lock_; - ReaderLockable reader_; - WriterLockable writer_; - - PImpl() : reader_(lock_), writer_(lock_) - { - } - }; - - - ReaderWriterLock::ReaderWriterLock() - { - pimpl_ = new PImpl; - } - - - ReaderWriterLock::~ReaderWriterLock() - { - delete pimpl_; - } - - - ILockable& ReaderWriterLock::ForReader() - { - return pimpl_->reader_; - } - - - ILockable& ReaderWriterLock::ForWriter() - { - return pimpl_->writer_; - } -}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "ILockable.h" - -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - class ReaderWriterLock : public boost::noncopyable - { - private: - struct PImpl; - - PImpl *pimpl_; - - public: - ReaderWriterLock(); - - virtual ~ReaderWriterLock(); - - ILockable& ForReader(); - - ILockable& ForWriter(); - }; -}
--- a/OrthancFramework/Resources/Graveyard/TestTranscoding.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,991 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/>. - **/ - - - bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmFileFormat& dicom, - DicomTransferSyntax syntax) - { - E_TransferSyntax xfer; - if (!LookupDcmtkTransferSyntax(xfer, syntax)) - { - return false; - } - else if (!dicom.validateMetaInfo(xfer).good()) - { - throw OrthancException(ErrorCode_InternalError, - "Cannot setup the transfer syntax to write a DICOM instance"); - } - else - { - return SaveToMemoryBufferInternal(buffer, dicom, xfer); - } - } - - - bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmFileFormat& dicom) - { - E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer(); - if (xfer == EXS_Unknown) - { - throw OrthancException(ErrorCode_InternalError, - "Cannot write a DICOM instance with unknown transfer syntax"); - } - else if (!dicom.validateMetaInfo(xfer).good()) - { - throw OrthancException(ErrorCode_InternalError, - "Cannot setup the transfer syntax to write a DICOM instance"); - } - else - { - return SaveToMemoryBufferInternal(buffer, dicom, xfer); - } - } - - - - - -#include <dcmtk/dcmdata/dcostrmb.h> -#include <dcmtk/dcmdata/dcpixel.h> -#include <dcmtk/dcmdata/dcpxitem.h> - -#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" - -namespace Orthanc -{ - class IParsedDicomImage : public boost::noncopyable - { - public: - virtual ~IParsedDicomImage() - { - } - - virtual DicomTransferSyntax GetTransferSyntax() = 0; - - virtual std::string GetSopClassUid() = 0; - - virtual std::string GetSopInstanceUid() = 0; - - virtual unsigned int GetFramesCount() = 0; - - // Can return NULL, for compressed transfer syntaxes - virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0; - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) = 0; - - virtual void WriteToMemoryBuffer(std::string& target) = 0; - }; - - - class IDicomImageReader : public boost::noncopyable - { - public: - virtual ~IDicomImageReader() - { - } - - virtual IParsedDicomImage* Read(const void* data, - size_t size) = 0; - - virtual IParsedDicomImage* Transcode(const void* data, - size_t size, - DicomTransferSyntax syntax, - bool allowNewSopInstanceUid) = 0; - }; - - - class DcmtkImageReader : public IDicomImageReader - { - private: - class Image : public IParsedDicomImage - { - private: - std::unique_ptr<DcmFileFormat> dicom_; - std::unique_ptr<DicomFrameIndex> index_; - DicomTransferSyntax transferSyntax_; - std::string sopClassUid_; - std::string sopInstanceUid_; - - static std::string GetStringTag(DcmDataset& dataset, - const DcmTagKey& tag) - { - const char* value = NULL; - - if (!dataset.findAndGetString(tag, value).good() || - value == NULL) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing SOP class/instance UID in DICOM instance"); - } - else - { - return std::string(value); - } - } - - public: - Image(DcmFileFormat* dicom, - DicomTransferSyntax syntax) : - dicom_(dicom), - transferSyntax_(syntax) - { - if (dicom == NULL || - dicom_->getDataset() == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - - DcmDataset& dataset = *dicom_->getDataset(); - index_.reset(new DicomFrameIndex(dataset)); - - sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); - sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); - } - - virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE - { - return transferSyntax_; - } - - virtual std::string GetSopClassUid() ORTHANC_OVERRIDE - { - return sopClassUid_; - } - - virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE - { - return sopInstanceUid_; - } - - virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE - { - return index_->GetFramesCount(); - } - - virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE - { - assert(dicom_.get() != NULL); - if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_)) - { - throw OrthancException(ErrorCode_InternalError, - "Cannot write the DICOM instance to a memory buffer"); - } - } - - virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE - { - assert(dicom_.get() != NULL && - dicom_->getDataset() != NULL); - return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); - } - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) ORTHANC_OVERRIDE - { - assert(index_.get() != NULL); - index_->GetRawFrame(target, frame); - } - }; - - unsigned int lossyQuality_; - - static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom) - { - if (dicom.getDataset() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - DcmDataset& dataset = *dicom.getDataset(); - - E_TransferSyntax xfer = dataset.getCurrentXfer(); - if (xfer == EXS_Unknown) - { - dataset.updateOriginalXfer(); - xfer = dataset.getCurrentXfer(); - if (xfer == EXS_Unknown) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot determine the transfer syntax of the DICOM instance"); - } - } - - DicomTransferSyntax syntax; - if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer)) - { - return syntax; - } - else - { - throw OrthancException( - ErrorCode_BadFileFormat, - "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer)); - } - } - - - static uint16_t GetBitsStored(DcmFileFormat& dicom) - { - if (dicom.getDataset() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - uint16_t bitsStored; - if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good()) - { - return bitsStored; - } - else - { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing \"Bits Stored\" tag in DICOM instance"); - } - } - - - public: - DcmtkImageReader() : - lossyQuality_(90) - { - } - - void SetLossyQuality(unsigned int quality) - { - if (quality <= 0 || - quality > 100) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - lossyQuality_ = quality; - } - } - - unsigned int GetLossyQuality() const - { - return lossyQuality_; - } - - virtual IParsedDicomImage* Read(const void* data, - size_t size) - { - std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); - if (dicom.get() == NULL) - { - throw OrthancException(ErrorCode_BadFileFormat); - } - - DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom); - - return new Image(dicom.release(), transferSyntax); - } - - virtual IParsedDicomImage* Transcode(const void* data, - size_t size, - DicomTransferSyntax syntax, - bool allowNewSopInstanceUid) - { - std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); - if (dicom.get() == NULL) - { - throw OrthancException(ErrorCode_BadFileFormat); - } - - const uint16_t bitsStored = GetBitsStored(*dicom); - - if (syntax == DetectTransferSyntax(*dicom)) - { - // No transcoding is needed - return new Image(dicom.release(), syntax); - } - - if (syntax == DicomTransferSyntax_LittleEndianImplicit && - FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) - { - return new Image(dicom.release(), syntax); - } - - if (syntax == DicomTransferSyntax_LittleEndianExplicit && - FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) - { - return new Image(dicom.release(), syntax); - } - - if (syntax == DicomTransferSyntax_BigEndianExplicit && - FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) - { - return new Image(dicom.release(), syntax); - } - - if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && - FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) - { - return new Image(dicom.release(), syntax); - } - -#if ORTHANC_ENABLE_JPEG == 1 - if (syntax == DicomTransferSyntax_JPEGProcess1 && - allowNewSopInstanceUid && - bitsStored == 8) - { - DJ_RPLossy rpLossy(lossyQuality_); - - if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy)) - { - return new Image(dicom.release(), syntax); - } - } -#endif - -#if ORTHANC_ENABLE_JPEG == 1 - if (syntax == DicomTransferSyntax_JPEGProcess2_4 && - allowNewSopInstanceUid && - bitsStored <= 12) - { - DJ_RPLossy rpLossy(lossyQuality_); - if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy)) - { - return new Image(dicom.release(), syntax); - } - } -#endif - - //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader"; - return NULL; - } - }; - - - - class IDicomTranscoder1 : public boost::noncopyable - { - public: - virtual ~IDicomTranscoder1() - { - } - - virtual DcmFileFormat& GetDicom() = 0; - - virtual DicomTransferSyntax GetTransferSyntax() = 0; - - virtual std::string GetSopClassUid() = 0; - - virtual std::string GetSopInstanceUid() = 0; - - virtual unsigned int GetFramesCount() = 0; - - virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) = 0; - - // NB: Transcoding can change the value of "GetSopInstanceUid()" - // and "GetTransferSyntax()" if lossy compression is applied - virtual bool Transcode(std::string& target, - DicomTransferSyntax syntax, - bool allowNewSopInstanceUid) = 0; - - virtual void WriteToMemoryBuffer(std::string& target) = 0; - }; - - - class DcmtkTranscoder2 : public IDicomTranscoder1 - { - private: - std::unique_ptr<DcmFileFormat> dicom_; - std::unique_ptr<DicomFrameIndex> index_; - DicomTransferSyntax transferSyntax_; - std::string sopClassUid_; - std::string sopInstanceUid_; - uint16_t bitsStored_; - unsigned int lossyQuality_; - - static std::string GetStringTag(DcmDataset& dataset, - const DcmTagKey& tag) - { - const char* value = NULL; - - if (!dataset.findAndGetString(tag, value).good() || - value == NULL) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing SOP class/instance UID in DICOM instance"); - } - else - { - return std::string(value); - } - } - - void Setup(DcmFileFormat* dicom) - { - lossyQuality_ = 90; - - dicom_.reset(dicom); - - if (dicom == NULL || - dicom_->getDataset() == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - - DcmDataset& dataset = *dicom_->getDataset(); - index_.reset(new DicomFrameIndex(dataset)); - - E_TransferSyntax xfer = dataset.getCurrentXfer(); - if (xfer == EXS_Unknown) - { - dataset.updateOriginalXfer(); - xfer = dataset.getCurrentXfer(); - if (xfer == EXS_Unknown) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot determine the transfer syntax of the DICOM instance"); - } - } - - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) - { - throw OrthancException( - ErrorCode_BadFileFormat, - "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer)); - } - - if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good()) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing \"Bits Stored\" tag in DICOM instance"); - } - - sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); - sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); - } - - public: - DcmtkTranscoder2(DcmFileFormat* dicom) // Takes ownership - { - Setup(dicom); - } - - DcmtkTranscoder2(const void* dicom, - size_t size) - { - Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); - } - - void SetLossyQuality(unsigned int quality) - { - if (quality <= 0 || - quality > 100) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - lossyQuality_ = quality; - } - } - - unsigned int GetLossyQuality() const - { - return lossyQuality_; - } - - unsigned int GetBitsStored() const - { - return bitsStored_; - } - - virtual DcmFileFormat& GetDicom() - { - assert(dicom_ != NULL); - return *dicom_; - } - - virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE - { - return transferSyntax_; - } - - virtual std::string GetSopClassUid() ORTHANC_OVERRIDE - { - return sopClassUid_; - } - - virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE - { - return sopInstanceUid_; - } - - virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE - { - return index_->GetFramesCount(); - } - - virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE - { - if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_)) - { - throw OrthancException(ErrorCode_InternalError, - "Cannot write the DICOM instance to a memory buffer"); - } - } - - virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE - { - assert(dicom_->getDataset() != NULL); - return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); - } - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) ORTHANC_OVERRIDE - { - index_->GetRawFrame(target, frame); - } - - virtual bool Transcode(std::string& target, - DicomTransferSyntax syntax, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE - { - assert(dicom_ != NULL && - dicom_->getDataset() != NULL); - - if (syntax == GetTransferSyntax()) - { - printf("NO TRANSCODING\n"); - - // No change in the transfer syntax => simply serialize the current dataset - WriteToMemoryBuffer(target); - return true; - } - - printf(">> %d\n", bitsStored_); - - if (syntax == DicomTransferSyntax_LittleEndianImplicit && - FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit; - return true; - } - - if (syntax == DicomTransferSyntax_LittleEndianExplicit && - FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit; - return true; - } - - if (syntax == DicomTransferSyntax_BigEndianExplicit && - FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_BigEndianExplicit; - return true; - } - - if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && - FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit; - return true; - } - -#if ORTHANC_ENABLE_JPEG == 1 - if (syntax == DicomTransferSyntax_JPEGProcess1 && - allowNewSopInstanceUid && - GetBitsStored() == 8) - { - DJ_RPLossy rpLossy(lossyQuality_); - - if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_JPEGProcess1; - sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); - return true; - } - } -#endif - -#if ORTHANC_ENABLE_JPEG == 1 - if (syntax == DicomTransferSyntax_JPEGProcess2_4 && - allowNewSopInstanceUid && - GetBitsStored() <= 12) - { - DJ_RPLossy rpLossy(lossyQuality_); - if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) - { - transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4; - sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); - return true; - } - } -#endif - - return false; - } - }; -} - - - - -#include <boost/filesystem.hpp> - - -static void TestFile(const std::string& path) -{ - static unsigned int count = 0; - count++; - - - printf("** %s\n", path.c_str()); - - std::string s; - SystemToolbox::ReadFile(s, path); - - Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size()); - - /*if (transcoder.GetBitsStored() != 8) // TODO - return; */ - - { - char buf[1024]; - sprintf(buf, "/tmp/source-%06d.dcm", count); - printf(">> %s\n", buf); - Orthanc::SystemToolbox::WriteFile(s, buf); - } - - printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), - transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), - transcoder.GetFramesCount(), transcoder.GetTransferSyntax()); - - for (size_t i = 0; i < transcoder.GetFramesCount(); i++) - { - std::string f; - transcoder.GetCompressedFrame(f, i); - - if (i == 0) - { - char buf[1024]; - sprintf(buf, "/tmp/frame-%06d.raw", count); - printf(">> %s\n", buf); - Orthanc::SystemToolbox::WriteFile(f, buf); - } - } - - { - std::string t; - transcoder.WriteToMemoryBuffer(t); - - Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); - printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size()); - } - - { - std::string a = transcoder.GetSopInstanceUid(); - DicomTransferSyntax b = transcoder.GetTransferSyntax(); - - DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4; - //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit; - - std::string t; - bool ok = transcoder.Transcode(t, syntax, true); - printf("Transcoding: %d\n", ok); - - if (ok) - { - printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str()); - printf("[%s] => [%s]\n", GetTransferSyntaxUid(b), - GetTransferSyntaxUid(transcoder.GetTransferSyntax())); - - { - char buf[1024]; - sprintf(buf, "/tmp/transcoded-%06d.dcm", count); - printf(">> %s\n", buf); - Orthanc::SystemToolbox::WriteFile(t, buf); - } - - Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); - printf(" => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size()); - } - } - - printf("\n"); -} - -TEST(Toto, DISABLED_Transcode) -{ - //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); - - if (1) - { - const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; - - for (boost::filesystem::directory_iterator it(PATH); - it != boost::filesystem::directory_iterator(); ++it) - { - if (boost::filesystem::is_regular_file(it->status())) - { - TestFile(it->path().string()); - } - } - } - - if (0) - { - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); - } - - if (0) - { - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); - } -} - - -TEST(Toto, DISABLED_Transcode2) -{ - for (int i = 0; i <= DicomTransferSyntax_XML; i++) - { - DicomTransferSyntax a = (DicomTransferSyntax) i; - - std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + - std::string(GetTransferSyntaxUid(a)) + ".dcm"); - if (Orthanc::SystemToolbox::IsRegularFile(path)) - { - printf("\n======= %s\n", GetTransferSyntaxUid(a)); - - std::string source; - Orthanc::SystemToolbox::ReadFile(source, path); - - DcmtkImageReader reader; - - { - std::unique_ptr<IParsedDicomImage> image( - reader.Read(source.c_str(), source.size())); - ASSERT_TRUE(image.get() != NULL); - ASSERT_EQ(a, image->GetTransferSyntax()); - - std::string target; - image->WriteToMemoryBuffer(target); - } - - for (int j = 0; j <= DicomTransferSyntax_XML; j++) - { - DicomTransferSyntax b = (DicomTransferSyntax) j; - //if (a == b) continue; - - std::unique_ptr<IParsedDicomImage> image( - reader.Transcode(source.c_str(), source.size(), b, true)); - if (image.get() != NULL) - { - printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); - - std::string target; - image->WriteToMemoryBuffer(target); - - char buf[1024]; - sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); - - SystemToolbox::WriteFile(target, buf); - } - else if (a != DicomTransferSyntax_JPEG2000 && - a != DicomTransferSyntax_JPEG2000LosslessOnly) - { - ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit && - b != DicomTransferSyntax_LittleEndianExplicit && - b != DicomTransferSyntax_BigEndianExplicit && - b != DicomTransferSyntax_DeflatedLittleEndianExplicit); - } - } - } - } -} - - -#include "../Core/DicomNetworking/DicomAssociation.h" -#include "../Core/DicomNetworking/DicomControlUserConnection.h" -#include "../Core/DicomNetworking/DicomStoreUserConnection.h" - -TEST(Toto, DISABLED_DicomAssociation) -{ - DicomAssociationParameters params; - params.SetLocalApplicationEntityTitle("ORTHANC"); - params.SetRemoteApplicationEntityTitle("PACS"); - params.SetRemotePort(2001); - -#if 0 - DicomAssociation assoc; - assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass); - assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass); - assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, - DicomTransferSyntax_JPEGProcess1); - assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, - DicomTransferSyntax_JPEGProcess2_4); - assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, - DicomTransferSyntax_JPEG2000); - - assoc.Open(params); - - int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage); - printf(">> %d\n", presID); - - std::map<DicomTransferSyntax, uint8_t> pc; - printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage)); - - for (std::map<DicomTransferSyntax, uint8_t>::const_iterator - it = pc.begin(); it != pc.end(); ++it) - { - printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second); - } -#else - { - DicomControlUserConnection assoc(params); - - try - { - printf(">> %d\n", assoc.Echo()); - } - catch (OrthancException&) - { - } - } - - params.SetRemoteApplicationEntityTitle("PACS"); - params.SetRemotePort(2000); - - { - DicomControlUserConnection assoc(params); - printf(">> %d\n", assoc.Echo()); - } - -#endif -} - -static void TestTranscode(DicomStoreUserConnection& scu, - const std::string& sopClassUid, - DicomTransferSyntax transferSyntax) -{ - std::set<DicomTransferSyntax> accepted; - - scu.LookupTranscoding(accepted, sopClassUid, transferSyntax); - if (accepted.empty()) - { - throw OrthancException(ErrorCode_NetworkProtocol, - "The SOP class is not supported by the remote modality"); - } - - { - unsigned int count = 0; - for (std::set<DicomTransferSyntax>::const_iterator - it = accepted.begin(); it != accepted.end(); ++it) - { - LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid - << " / " << GetTransferSyntaxUid(*it); - } - } - - if (accepted.find(transferSyntax) != accepted.end()) - { - printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax)); - } - else - { - // Transcoding - only in Orthanc >= 1.7.0 - - const DicomTransferSyntax uncompressed[] = { - DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax - DicomTransferSyntax_LittleEndianExplicit, - DicomTransferSyntax_BigEndianExplicit - }; - - bool found = false; - for (size_t i = 0; i < 3; i++) - { - if (accepted.find(uncompressed[i]) != accepted.end()) - { - printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i])); - found = true; - break; - } - } - - if (!found) - { - printf("**** KO KO KO\n"); - } - } -} - - -TEST(Toto, DISABLED_Store) -{ - DicomAssociationParameters params; - params.SetLocalApplicationEntityTitle("ORTHANC"); - params.SetRemoteApplicationEntityTitle("STORESCP"); - params.SetRemotePort(2000); - - DicomStoreUserConnection assoc(params); - assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1); - assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4); - //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); - - //assoc.SetUncompressedSyntaxesProposed(false); // Necessary for transcoding - assoc.SetCommonClassesProposed(false); - assoc.SetRetiredBigEndianProposed(true); - TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); - TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); - TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); -} - - -TEST(Toto, DISABLED_Store2) -{ - DicomAssociationParameters params; - params.SetLocalApplicationEntityTitle("ORTHANC"); - params.SetRemoteApplicationEntityTitle("STORESCP"); - params.SetRemotePort(2000); - - DicomStoreUserConnection assoc(params); - //assoc.SetCommonClassesProposed(false); - assoc.SetRetiredBigEndianProposed(true); - - std::string s; - Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm"); - - std::string c, i; - assoc.Store(c, i, s.c_str(), s.size()); - printf("[%s] [%s]\n", c.c_str(), i.c_str()); -} -
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -95,7 +95,8 @@ } else if (v.IsSequence()) { - s = "(sequence)"; + //s = "(sequence)"; + s = "(sequence) " + v.GetSequenceContent().toStyledString(); } else {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -55,9 +55,9 @@ // These lists have a specific signature. When a resource does not have // the metadata "MainDicomTagsSignature", we'll assume that they were stored // with an Orthanc prior to 1.11. It is therefore very important that you never - // change these lists ! + // change these lists ! Update ResetDefaultMainDicomTags instead. - static const DicomTag DEFAULT_PATIENT_MAIN_DICOM_TAGS[] = + static const DicomTag DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1010), "PatientAge" }, // { DicomTag(0x0010, 0x1040), "PatientAddress" }, @@ -66,9 +66,11 @@ DICOM_TAG_PATIENT_SEX, DICOM_TAG_OTHER_PATIENT_IDS, DICOM_TAG_PATIENT_ID + + // don't add tags here, check ResetDefaultMainDicomTags instead }; - static const DicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] = + static const DicomTag DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1020), "PatientSize" }, // { DicomTag(0x0010, 0x1030), "PatientWeight" }, @@ -84,9 +86,11 @@ DICOM_TAG_INSTITUTION_NAME, DICOM_TAG_REQUESTING_PHYSICIAN, DICOM_TAG_REFERRING_PHYSICIAN_NAME + + // don't add tags here, check ResetDefaultMainDicomTags instead }; - static const DicomTag DEFAULT_SERIES_MAIN_DICOM_TAGS[] = + static const DicomTag DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1080), "MilitaryRank" }, DICOM_TAG_SERIES_DATE, @@ -113,9 +117,11 @@ DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, DICOM_TAG_CONTRAST_BOLUS_AGENT + + // don't add tags here, check ResetDefaultMainDicomTags instead }; - static const DicomTag DEFAULT_INSTANCE_MAIN_DICOM_TAGS[] = + static const DicomTag DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS[] = { DICOM_TAG_INSTANCE_CREATION_DATE, DICOM_TAG_INSTANCE_CREATION_TIME, @@ -138,6 +144,8 @@ * indexed in the database by an older version of Orthanc. **/ DICOM_TAG_IMAGE_ORIENTATION_PATIENT // New in Orthanc 1.4.2 + + // don't add tags here, check ResetDefaultMainDicomTags instead }; class DicomMap::MainDicomTagsConfiguration : public boost::noncopyable @@ -187,23 +195,23 @@ switch (level) { case ResourceType_Patient: - tags = DEFAULT_PATIENT_MAIN_DICOM_TAGS; - size = sizeof(DEFAULT_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag); + tags = DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag); break; case ResourceType_Study: - tags = DEFAULT_STUDY_MAIN_DICOM_TAGS; - size = sizeof(DEFAULT_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag); + tags = DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag); break; case ResourceType_Series: - tags = DEFAULT_SERIES_MAIN_DICOM_TAGS; - size = sizeof(DEFAULT_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag); + tags = DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag); break; case ResourceType_Instance: - tags = DEFAULT_INSTANCE_MAIN_DICOM_TAGS; - size = sizeof(DEFAULT_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag); + tags = DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag); break; default: @@ -247,7 +255,7 @@ if (existingLevelTags.find(tag) != existingLevelTags.end()) { - throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined"); + throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false); } existingLevelTags.insert(tag); @@ -287,6 +295,17 @@ defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study]; defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series]; defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance]; + + // only add new tags here ! + // introduced in v 1.12.5 + AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Study); // used in default QIDO-RS queries + + AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Series); // used in default QIDO-RS queries + AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE, ResourceType_Series); // used in default QIDO-RS queries + AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME, ResourceType_Series); // used in default QIDO-RS queries + AddMainDicomTagInternal(DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE, ResourceType_Series); // used in default QIDO-RS queries + + // TODO-FIND: remove it from metadata when adding it ! AddMainDicomTagInternal(DICOM_TAG_SOP_CLASS_UID, ResourceType_Instance); // previously saved in a metadata; makes more sense to store it in a DICOM tag } void AddMainDicomTag(const DicomTag& tag, @@ -328,7 +347,7 @@ return signatures_[level]; } - std::string GetDefaultMainDicomTagsSignature(ResourceType level) + std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level) { #if !defined(__EMSCRIPTEN__) ReaderLock lock(mutex_); @@ -667,13 +686,16 @@ } - void DicomMap::CopyTagIfExists(const DicomMap& source, + bool DicomMap::CopyTagIfExists(const DicomMap& source, const DicomTag& tag) { if (source.HasTag(tag)) { SetValue(tag, source.GetValue(tag)); + return true; } + + return false; } @@ -778,6 +800,17 @@ return false; } + bool DicomMap::HasMetaInformationTags(const std::set<DicomTag>& tags) + { + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + if (it->GetGroup() == 0x0002) + { + return true; + } + } + return false; + } void DicomMap::GetMainDicomTags(std::set<DicomTag>& target, ResourceType level) @@ -805,9 +838,9 @@ return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level); } - std::string DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level) + std::string DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level) { - return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level); + return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignatureFrom1_11(level); } void DicomMap::GetTags(std::set<DicomTag>& tags) const
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Fri Nov 29 10:54:20 2024 +0100 @@ -129,7 +129,7 @@ static void SetupFindInstanceTemplate(DicomMap& result); - void CopyTagIfExists(const DicomMap& source, + bool CopyTagIfExists(const DicomMap& source, const DicomTag& tag); static bool IsMainDicomTag(const DicomTag& tag, ResourceType level); @@ -146,13 +146,15 @@ static bool HasComputedTags(const std::set<DicomTag>& tags); + static bool HasMetaInformationTags(const std::set<DicomTag>& tags); + static void GetMainDicomTags(std::set<DicomTag>& target, ResourceType level); // returns a string uniquely identifying the list of main dicom tags for a level static std::string GetMainDicomTagsSignature(ResourceType level); - static std::string GetDefaultMainDicomTagsSignature(ResourceType level); + static std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level); static void GetAllMainDicomTags(std::set<DicomTag>& target);
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Fri Nov 29 10:54:20 2024 +0100 @@ -157,7 +157,10 @@ static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032); static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090); static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070); + static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE(0x0040, 0x0244); + static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME(0x0040, 0x0245); static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254); + static const DicomTag DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE(0x0040, 0x0275); 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);
--- a/OrthancFramework/Sources/Enumerations.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/Enumerations.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -186,6 +186,9 @@ case ErrorCode_DuplicateResource: return "Duplicate resource"; + case ErrorCode_IncompatibleConfigurations: + return "Your configuration file contains configuration that are mutually incompatible"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -220,7 +223,7 @@ return "SQLite: Cannot step over a cached statement"; case ErrorCode_SQLiteBindOutOfRange: - return "SQLite: Bing a value while out of range (serious error)"; + return "SQLite: Bind a value while out of range (serious error)"; case ErrorCode_SQLitePrepareStatement: return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/Sources/Enumerations.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Fri Nov 29 10:54:20 2024 +0100 @@ -172,6 +172,7 @@ ErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, ErrorCode_ForbiddenAccess = 45 /*!< Access to a resource is forbidden */, ErrorCode_DuplicateResource = 46 /*!< Duplicate resource */, + ErrorCode_IncompatibleConfigurations = 47 /*!< Your configuration file contains configuration that are mutually incompatible */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -183,7 +184,7 @@ ErrorCode_SQLiteFlush = 1008 /*!< SQLite: Unable to flush the database */, ErrorCode_SQLiteCannotRun = 1009 /*!< SQLite: Cannot run a cached statement */, ErrorCode_SQLiteCannotStep = 1010 /*!< SQLite: Cannot step over a cached statement */, - ErrorCode_SQLiteBindOutOfRange = 1011 /*!< SQLite: Bing a value while out of range (serious error) */, + ErrorCode_SQLiteBindOutOfRange = 1011 /*!< SQLite: Bind a value while out of range (serious error) */, ErrorCode_SQLitePrepareStatement = 1012 /*!< SQLite: Cannot prepare a cached statement */, ErrorCode_SQLiteTransactionAlreadyStarted = 1013 /*!< SQLite: Beginning the same transaction twice */, ErrorCode_SQLiteTransactionCommit = 1014 /*!< SQLite: Failure when committing the transaction */,
--- a/OrthancFramework/Sources/SQLite/Connection.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/SQLite/Connection.h Fri Nov 29 10:54:20 2024 +0100 @@ -57,6 +57,7 @@ #endif #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__) +#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql) namespace Orthanc {
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h Fri Nov 29 10:54:20 2024 +0100 @@ -129,7 +129,7 @@ return "SQLite: Cannot step over a cached statement"; case ErrorCode_SQLiteBindOutOfRange: - return "SQLite: Bing a value while out of range (serious error)"; + return "SQLite: Bind a value while out of range (serious error)"; case ErrorCode_SQLitePrepareStatement: return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -57,12 +57,24 @@ { } + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line, + const std::string& statement) : + file_(file), + line_(line), + statement_(statement) + { + } + bool StatementId::operator< (const StatementId& other) const { if (line_ != other.line_) return line_ < other.line_; - return strcmp(file_, other.file_) < 0; + if (strcmp(file_, other.file_) < 0) + return true; + + return statement_ < other.statement_; } } }
--- a/OrthancFramework/Sources/SQLite/StatementId.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Fri Nov 29 10:54:20 2024 +0100 @@ -55,6 +55,7 @@ private: const char* file_; int line_; + std::string statement_; StatementId(); // Forbidden @@ -62,6 +63,10 @@ StatementId(const char* file, int line); + StatementId(const char* file, + int line, + const std::string& statement); + bool operator< (const StatementId& other) const; }; }
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -148,10 +148,10 @@ TEST_F(DicomMapMainTagsTests, Signatures) { - std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Patient); - std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Study); - std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Series); - std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Instance); + std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient); + std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Study); + std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Series); + std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Instance); ASSERT_NE(defaultInstanceSignature, defaultPatientSignature); ASSERT_NE(defaultSeriesSignature, defaultStudySignature); @@ -162,11 +162,11 @@ std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series); std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); - // at start, default and current signature should be equal - ASSERT_EQ(defaultPatientSignature, patientSignature); - ASSERT_EQ(defaultStudySignature, studySignature); - ASSERT_EQ(defaultSeriesSignature, seriesSignature); - ASSERT_EQ(defaultInstanceSignature, instanceSignature); + // // at start, default and current signature should be equal !! This is not true anymore since we have added new MainDicomTags in 1.12.5 + // ASSERT_EQ(defaultPatientSignature, patientSignature); + // ASSERT_EQ(defaultStudySignature, studySignature); + // ASSERT_EQ(defaultSeriesSignature, seriesSignature); + // ASSERT_EQ(defaultInstanceSignature, instanceSignature); DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance); instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); @@ -266,6 +266,7 @@ if (level == ResourceType_Study && (*it == DicomTag(0x0008, 0x0080) || /* InstitutionName, from Visit identification module, related to Visit */ *it == DicomTag(0x0032, 0x1032) || /* RequestingPhysician, from Imaging Service Request module, related to Study */ + *it == DicomTag(0x0008, 0x0201) || /* TimezoneOffsetFromUTC */ *it == DicomTag(0x0032, 0x1060))) /* RequestedProcedureDescription, from Requested Procedure module, related to Study */ { ok = true; @@ -284,6 +285,7 @@ *it == DicomTag(0x0054, 0x0101) || /* NumberOfTimeSlices, from PET Series module */ *it == DicomTag(0x0054, 0x1000) || /* SeriesType, from PET Series module */ *it == DicomTag(0x0018, 0x1400) || /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0008, 0x0201) || /* TimezoneOffsetFromUTC */ *it == DicomTag(0x0018, 0x0010))) /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */ { ok = true; @@ -1096,7 +1098,7 @@ std::set<DicomTag> tags; m.GetTags(tags); - // This corresponds to the values of DEFAULT_PATIENT_MAIN_DICOM_TAGS + // This corresponds to the values of DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS ASSERT_EQ(5u, tags.size()); ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
--- a/OrthancServer/CMakeLists.txt Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/CMakeLists.txt Fri Nov 29 10:54:20 2024 +0100 @@ -90,11 +90,16 @@ set(ORTHANC_SERVER_SOURCES ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/MainDicomTagsRegistry.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp @@ -119,7 +124,10 @@ ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp - ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp + ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraint.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraints.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseMetadataConstraint.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/HierarchicalMatcher.cpp @@ -326,7 +334,6 @@ add_definitions( -DORTHANC_BUILD_UNIT_TESTS=1 - -DORTHANC_BUILDING_SERVER_LIBRARY=1 # Macros for the plugins -DHAS_ORTHANC_EXCEPTION=0
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -564,7 +564,7 @@ virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, @@ -806,10 +806,10 @@ } - virtual void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - int64_t since, - uint32_t limit) ORTHANC_OVERRIDE + virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target, + ResourceType resourceType, + int64_t since, + uint32_t limit) ORTHANC_OVERRIDE { if (that_.extensions_.getAllPublicIdsWithLimit != NULL) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -46,7 +46,8 @@ namespace Orthanc { - class OrthancPluginDatabaseV3::Transaction : public BaseDatabaseWrapper::BaseTransaction + class OrthancPluginDatabaseV3::Transaction : + public BaseDatabaseWrapper::BaseTransaction { private: OrthancPluginDatabaseV3& that_; @@ -386,10 +387,10 @@ } - virtual void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - int64_t since, - uint32_t limit) ORTHANC_OVERRIDE + virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target, + ResourceType resourceType, + int64_t since, + uint32_t limit) ORTHANC_OVERRIDE { CheckSuccess(that_.backend_.getAllPublicIdsWithLimit( transaction_, Plugins::Convert(resourceType), @@ -798,7 +799,7 @@ virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, // Can be NULL if not needed - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -31,10 +31,12 @@ #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/Compatibility/GenericFind.h" #include "../../Sources/Database/ResourcesContent.h" #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/ServerToolbox.h" #include "PluginsEnumerations.h" +#include "../../Sources/Database/MainDicomTagsRegistry.h" #include "OrthancDatabasePlugin.pb.h" // Auto-generated file @@ -134,6 +136,228 @@ } + static void Convert(DatabasePluginMessages::DatabaseConstraint& target, + const DatabaseDicomTagConstraint& source) + { + target.set_level(Convert(source.GetLevel())); + target.set_tag_group(source.GetTag().GetGroup()); + target.set_tag_element(source.GetTag().GetElement()); + target.set_is_identifier_tag(source.IsIdentifier()); + target.set_is_case_sensitive(source.IsCaseSensitive()); + target.set_is_mandatory(source.IsMandatory()); + + target.mutable_values()->Reserve(source.GetValuesCount()); + for (size_t j = 0; j < source.GetValuesCount(); j++) + { + target.add_values(source.GetValue(j)); + } + + switch (source.GetConstraintType()) + { + case ConstraintType_Equal: + target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL); + break; + + case ConstraintType_SmallerOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL); + break; + + case ConstraintType_GreaterOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL); + break; + + case ConstraintType_Wildcard: + target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD); + break; + + case ConstraintType_List: + target.set_type(DatabasePluginMessages::CONSTRAINT_LIST); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static void Convert(DatabasePluginMessages::DatabaseMetadataConstraint& target, + const DatabaseMetadataConstraint& source) + { + target.set_metadata(source.GetMetadata()); + target.set_is_case_sensitive(source.IsCaseSensitive()); + target.set_is_mandatory(source.IsMandatory()); + + target.mutable_values()->Reserve(source.GetValuesCount()); + for (size_t j = 0; j < source.GetValuesCount(); j++) + { + target.add_values(source.GetValue(j)); + } + + switch (source.GetConstraintType()) + { + case ConstraintType_Equal: + target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL); + break; + + case ConstraintType_SmallerOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL); + break; + + case ConstraintType_GreaterOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL); + break; + + case ConstraintType_Wildcard: + target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD); + break; + + case ConstraintType_List: + target.set_type(DatabasePluginMessages::CONSTRAINT_LIST); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static void Convert(DatabasePluginMessages::Find_Request_Ordering& target, + const FindRequest::Ordering& source) + { + switch (source.GetKeyType()) + { + case FindRequest::KeyType_DicomTag: + { + ResourceType tagLevel; + DicomTagType tagType; + MainDicomTagsRegistry registry; + + registry.LookupTag(tagLevel, tagType, source.GetDicomTag()); + + target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_DICOM_TAG); + target.set_tag_group(source.GetDicomTag().GetGroup()); + target.set_tag_element(source.GetDicomTag().GetElement()); + target.set_is_identifier_tag(tagType == DicomTagType_Identifier); + target.set_tag_level(Convert(tagLevel)); + + }; break; + + case FindRequest::KeyType_Metadata: + target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_METADATA); + target.set_metadata(source.GetMetadataType()); + + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (source.GetDirection()) + { + case FindRequest::OrderingDirection_Ascending: + target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_ASC); + break; + + case FindRequest::OrderingDirection_Descending: + target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_DESC); + + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint) + { + switch (constraint) + { + case LabelsConstraint_All: + return DatabasePluginMessages::LABELS_CONSTRAINT_ALL; + + case LabelsConstraint_Any: + return DatabasePluginMessages::LABELS_CONSTRAINT_ANY; + + case LabelsConstraint_None: + return DatabasePluginMessages::LABELS_CONSTRAINT_NONE; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static void Convert(DatabasePluginMessages::Find_Request_ChildrenSpecification& target, + const FindRequest::ChildrenSpecification& source) + { + target.set_retrieve_identifiers(source.IsRetrieveIdentifiers()); + target.set_retrieve_count(source.IsRetrieveCount()); + + for (std::set<MetadataType>::const_iterator it = source.GetMetadata().begin(); it != source.GetMetadata().end(); ++it) + { + target.add_retrieve_metadata(*it); + } + + for (std::set<DicomTag>::const_iterator it = source.GetMainDicomTags().begin(); it != source.GetMainDicomTags().end(); ++it) + { + DatabasePluginMessages::Find_Request_Tag* tag = target.add_retrieve_main_dicom_tags(); + tag->set_group(it->GetGroup()); + tag->set_element(it->GetElement()); + } + } + + + static void Convert(FindResponse::Resource& target, + ResourceType level, + const DatabasePluginMessages::Find_Response_ResourceContent& source) + { + for (int i = 0; i < source.main_dicom_tags().size(); i++) + { + target.AddStringDicomTag(level, source.main_dicom_tags(i).group(), + source.main_dicom_tags(i).element(), source.main_dicom_tags(i).value()); + } + + for (int i = 0; i < source.metadata().size(); i++) + { + target.AddMetadata(level, static_cast<MetadataType>(source.metadata(i).key()), + source.metadata(i).value(), source.metadata(i).revision()); + } + } + + + static void Convert(FindResponse::Resource& target, + ResourceType level, + const DatabasePluginMessages::Find_Response_ChildrenContent& source) + { + for (int i = 0; i < source.identifiers().size(); i++) + { + target.AddChildIdentifier(level, source.identifiers(i)); + } + + target.SetChildrenCount(level, source.count()); + + for (int i = 0; i < source.main_dicom_tags().size(); i++) + { + const DicomTag tag(source.main_dicom_tags(i).group(), source.main_dicom_tags(i).element()); + + for (int j = 0; j < source.main_dicom_tags(i).values().size(); j++) + { + target.AddChildrenMainDicomTagValue(level, tag, source.main_dicom_tags(i).values(j)); + } + } + + for (int i = 0; i < source.metadata().size(); i++) + { + MetadataType key = static_cast<MetadataType>(source.metadata(i).key()); + + for (int j = 0; j < source.metadata(i).values().size(); j++) + { + target.AddChildrenMetadataValue(level, key, source.metadata(i).values(j)); + } + } + } + + static void Execute(DatabasePluginMessages::Response& response, const OrthancPluginDatabaseV4& database, const DatabasePluginMessages::Request& request) @@ -178,7 +402,9 @@ } - class OrthancPluginDatabaseV4::Transaction : public IDatabaseWrapper::ITransaction + class OrthancPluginDatabaseV4::Transaction : + public IDatabaseWrapper::ITransaction, + public IDatabaseWrapper::ICompatibilityTransaction { private: OrthancPluginDatabaseV4& database_; @@ -453,10 +679,10 @@ } - virtual void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - int64_t since, - uint32_t limit) ORTHANC_OVERRIDE + virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target, + ResourceType resourceType, + int64_t since, + uint32_t limit) ORTHANC_OVERRIDE { DatabasePluginMessages::TransactionRequest request; request.mutable_get_all_public_ids_with_limits()->set_resource_type(Convert(resourceType)); @@ -495,6 +721,37 @@ } } + virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& changeTypes) ORTHANC_OVERRIDE + { + assert(database_.GetDatabaseCapabilities().HasExtendedChanges()); + + DatabasePluginMessages::TransactionRequest request; + DatabasePluginMessages::TransactionResponse response; + + request.mutable_get_changes_extended()->set_since(since); + request.mutable_get_changes_extended()->set_limit(limit); + request.mutable_get_changes_extended()->set_to(to); + for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it) + { + request.mutable_get_changes_extended()->add_change_type(*it); + } + + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES_EXTENDED, request); + + done = response.get_changes_extended().done(); + + target.clear(); + for (int i = 0; i < response.get_changes_extended().changes().size(); i++) + { + target.push_back(Convert(response.get_changes_extended().changes(i))); + } + } + virtual void GetChildrenInternalId(std::list<int64_t>& target, int64_t id) ORTHANC_OVERRIDE @@ -806,6 +1063,12 @@ uncompressedSize = response.update_and_get_statistics().total_uncompressed_size(); } + virtual void PerformDbHousekeeping() ORTHANC_OVERRIDE + { + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_PERFORM_DB_HOUSEKEEPING); + } + virtual bool LookupMetadata(std::string& target, int64_t& revision, int64_t id, @@ -972,10 +1235,10 @@ return response.is_disk_size_above().result(); } - + virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, // Can be NULL if not needed - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, @@ -996,47 +1259,7 @@ for (size_t i = 0; i < lookup.GetSize(); i++) { - const DatabaseConstraint& source = lookup.GetConstraint(i); - - DatabasePluginMessages::DatabaseConstraint* target = request.mutable_lookup_resources()->add_lookup(); - target->set_level(Convert(source.GetLevel())); - target->set_tag_group(source.GetTag().GetGroup()); - target->set_tag_element(source.GetTag().GetElement()); - target->set_is_identifier_tag(source.IsIdentifier()); - target->set_is_case_sensitive(source.IsCaseSensitive()); - target->set_is_mandatory(source.IsMandatory()); - - target->mutable_values()->Reserve(source.GetValuesCount()); - for (size_t j = 0; j < source.GetValuesCount(); j++) - { - target->add_values(source.GetValue(j)); - } - - switch (source.GetConstraintType()) - { - case ConstraintType_Equal: - target->set_type(DatabasePluginMessages::CONSTRAINT_EQUAL); - break; - - case ConstraintType_SmallerOrEqual: - target->set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL); - break; - - case ConstraintType_GreaterOrEqual: - target->set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL); - break; - - case ConstraintType_Wildcard: - target->set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD); - break; - - case ConstraintType_List: - target->set_type(DatabasePluginMessages::CONSTRAINT_LIST); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + Convert(*request.mutable_lookup_resources()->add_lookup(), lookup.GetConstraint(i)); } for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) @@ -1044,23 +1267,7 @@ request.mutable_lookup_resources()->add_labels(*it); } - switch (labelsConstraint) - { - case LabelsConstraint_All: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL); - break; - - case LabelsConstraint_Any: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY); - break; - - case LabelsConstraint_None: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + request.mutable_lookup_resources()->set_labels_constraint(Convert(labelsConstraint)); DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request); @@ -1278,6 +1485,322 @@ { ListLabelsInternal(target, false, -1); } + + + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + DatabasePluginMessages::TransactionRequest dbRequest; + dbRequest.mutable_find()->set_level(Convert(request.GetLevel())); + + if (request.GetOrthancIdentifiers().HasPatientId()) + { + dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId()); + } + + if (request.GetOrthancIdentifiers().HasStudyId()) + { + dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId()); + } + + if (request.GetOrthancIdentifiers().HasSeriesId()) + { + dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId()); + } + + if (request.GetOrthancIdentifiers().HasInstanceId()) + { + dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId()); + } + + for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++) + { + Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i)); + } + + for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it) + { + Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); + } + + for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it) + { + dbRequest.mutable_find()->add_labels(*it); + } + + dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint())); + + DatabasePluginMessages::TransactionResponse dbResponse; + ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_COUNT_RESOURCES, dbRequest); + + count = dbResponse.count_resources().count(); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + DatabasePluginMessages::TransactionRequest dbRequest; + dbRequest.mutable_find()->set_level(Convert(request.GetLevel())); + + if (request.GetOrthancIdentifiers().HasPatientId()) + { + dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId()); + } + + if (request.GetOrthancIdentifiers().HasStudyId()) + { + dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId()); + } + + if (request.GetOrthancIdentifiers().HasSeriesId()) + { + dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId()); + } + + if (request.GetOrthancIdentifiers().HasInstanceId()) + { + dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId()); + } + + for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++) + { + Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i)); + } + + for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it) + { + Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); + } + + for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it) + { + Convert(*dbRequest.mutable_find()->add_ordering(), *(*it)); + } + + if (request.HasLimits()) + { + dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince()); + dbRequest.mutable_find()->mutable_limits()->set_count(request.GetLimitsCount()); + } + + for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it) + { + dbRequest.mutable_find()->add_labels(*it); + } + + dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint())); + + dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata()); + dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels()); + dbRequest.mutable_find()->set_retrieve_attachments(request.IsRetrieveAttachments()); + dbRequest.mutable_find()->set_retrieve_parent_identifier(request.IsRetrieveParentIdentifier()); + + if (request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(false); + } + else + { + dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(request.IsRetrieveOneInstanceMetadataAndAttachments()); + } + + if (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Study).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Study).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Series).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Series).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Patient) + { + Convert(*dbRequest.mutable_find()->mutable_children_studies(), request.GetChildrenSpecification(ResourceType_Study)); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study) + { + Convert(*dbRequest.mutable_find()->mutable_children_series(), request.GetChildrenSpecification(ResourceType_Series)); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series) + { + Convert(*dbRequest.mutable_find()->mutable_children_instances(), request.GetChildrenSpecification(ResourceType_Instance)); + } + + DatabasePluginMessages::TransactionResponse dbResponse; + ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_FIND, dbRequest); + + for (int i = 0; i < dbResponse.find().size(); i++) + { + const DatabasePluginMessages::Find_Response& source = dbResponse.find(i); + + std::unique_ptr<FindResponse::Resource> target( + new FindResponse::Resource(request.GetLevel(), source.internal_id(), source.public_id())); + + if (request.IsRetrieveParentIdentifier()) + { + target->SetParentIdentifier(source.parent_public_id()); + } + + for (int i = 0; i < source.labels().size(); i++) + { + target->AddLabel(source.labels(i)); + } + + if (source.attachments().size() != source.attachments_revisions().size()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + for (int i = 0; i < source.attachments().size(); i++) + { + target->AddAttachment(Convert(source.attachments(i)), source.attachments_revisions(i)); + } + + Convert(*target, ResourceType_Patient, source.patient_content()); + + if (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Study, source.study_content()); + } + + if (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Series, source.series_content()); + } + + if (request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Instance, source.instance_content()); + } + + if (request.GetLevel() == ResourceType_Patient) + { + Convert(*target, ResourceType_Study, source.children_studies_content()); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study) + { + Convert(*target, ResourceType_Series, source.children_series_content()); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series) + { + Convert(*target, ResourceType_Instance, source.children_instances_content()); + } + + if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + std::map<MetadataType, std::string> metadata; + for (int i = 0; i < source.one_instance_metadata().size(); i++) + { + MetadataType key = static_cast<MetadataType>(source.one_instance_metadata(i).key()); + if (metadata.find(key) == metadata.end()) + { + metadata[key] = source.one_instance_metadata(i).value(); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + std::map<FileContentType, FileInfo> attachments; + + for (int i = 0; i < source.one_instance_attachments().size(); i++) + { + FileInfo info(Convert(source.one_instance_attachments(i))); + if (attachments.find(info.GetContentType()) == attachments.end()) + { + attachments[info.GetContentType()] = info; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + target->SetOneInstanceMetadataAndAttachments(source.one_instance_public_id(), metadata, attachments); + } + + response.Add(target.release()); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + virtual void ExecuteFind(std::list<std::string>& identifiers, + const Capabilities& capabilities, + const FindRequest& request) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + // The integrated version of "ExecuteFind()" should have been called + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Compatibility::GenericFind find(*this, *this); + find.ExecuteFind(identifiers, capabilities, request); + } + } + + + virtual void ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + // The integrated version of "ExecuteFind()" should have been called + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Compatibility::GenericFind find(*this, *this); + find.ExecuteExpand(response, capabilities, request, identifier); + } + } }; @@ -1364,8 +1887,11 @@ dbCapabilities_.SetRevisionsSupport(systemInfo.supports_revisions()); dbCapabilities_.SetLabelsSupport(systemInfo.supports_labels()); dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property()); - dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics()); + dbCapabilities_.SetHasUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics()); dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); + dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes()); + dbCapabilities_.SetHasFindSupport(systemInfo.supports_find()); + dbCapabilities_.SetHasDbHousekeeping(systemInfo.has_db_housekeeping()); } open_ = true; @@ -1492,4 +2018,10 @@ return dbCapabilities_; } } + + + bool OrthancPluginDatabaseV4::HasIntegratedFind() const + { + return dbCapabilities_.HasFindSupport(); + } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Fri Nov 29 10:54:20 2024 +0100 @@ -93,6 +93,8 @@ virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -4515,9 +4515,8 @@ PImpl::ServerContextReference lock(*pimpl_); std::string s; - int64_t revision; // unused if (lock.GetContext().GetIndex().LookupMetadata( - s, revision, params.instanceId, + s, params.instanceId, ResourceType_Instance, MetadataType_Instance_PixelDataVR)) { hasPixelData = true; @@ -4536,7 +4535,7 @@ } } else if (lock.GetContext().GetIndex().LookupMetadata( - s, revision, params.instanceId, + s, params.instanceId, ResourceType_Instance, MetadataType_Instance_PixelDataOffset)) { // This file was stored by an older version of Orthanc,
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -595,5 +595,74 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType Convert(OrthancPluginResourceType type) + { + switch (type) + { + case OrthancPluginResourceType_Patient: + return ResourceType_Patient; + + case OrthancPluginResourceType_Study: + return ResourceType_Study; + + case OrthancPluginResourceType_Series: + return ResourceType_Series; + + case OrthancPluginResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + OrthancPluginConstraintType Convert(ConstraintType constraint) + { + switch (constraint) + { + case ConstraintType_Equal: + return OrthancPluginConstraintType_Equal; + + case ConstraintType_GreaterOrEqual: + return OrthancPluginConstraintType_GreaterOrEqual; + + case ConstraintType_SmallerOrEqual: + return OrthancPluginConstraintType_SmallerOrEqual; + + case ConstraintType_Wildcard: + return OrthancPluginConstraintType_Wildcard; + + case ConstraintType_List: + return OrthancPluginConstraintType_List; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } } }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Fri Nov 29 10:54:20 2024 +0100 @@ -25,15 +25,7 @@ #if ORTHANC_ENABLE_PLUGINS == 1 -/** - * NB: Conversions to/from "OrthancPluginConstraintType" and - * "OrthancPluginResourceType" are located in file - * "../../Sources/Search/DatabaseConstraint.h" to be shared with the - * "orthanc-databases" project. - **/ - #include "../../../OrthancFramework/Sources/MetricsRegistry.h" -#include "../../Sources/Search/DatabaseConstraint.h" #include "../../Sources/ServerEnumerations.h" #include "../Include/orthanc/OrthancCPlugin.h" @@ -75,6 +67,12 @@ StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason); MetricsUpdatePolicy Convert(OrthancPluginMetricsType type); + + OrthancPluginResourceType Convert(ResourceType type); + + ResourceType Convert(OrthancPluginResourceType type); + + OrthancPluginConstraintType Convert(ConstraintType constraint); } }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Nov 29 10:54:20 2024 +0100 @@ -121,7 +121,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 4 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 5 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -262,6 +262,7 @@ OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, OrthancPluginErrorCode_ForbiddenAccess = 45 /*!< Access to a resource is forbidden */, OrthancPluginErrorCode_DuplicateResource = 46 /*!< Duplicate resource */, + OrthancPluginErrorCode_IncompatibleConfigurations = 47 /*!< Your configuration file contains configuration that are mutually incompatible */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -273,7 +274,7 @@ OrthancPluginErrorCode_SQLiteFlush = 1008 /*!< SQLite: Unable to flush the database */, OrthancPluginErrorCode_SQLiteCannotRun = 1009 /*!< SQLite: Cannot run a cached statement */, OrthancPluginErrorCode_SQLiteCannotStep = 1010 /*!< SQLite: Cannot step over a cached statement */, - OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011 /*!< SQLite: Bing a value while out of range (serious error) */, + OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011 /*!< SQLite: Bind a value while out of range (serious error) */, OrthancPluginErrorCode_SQLitePrepareStatement = 1012 /*!< SQLite: Cannot prepare a cached statement */, OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013 /*!< SQLite: Beginning the same transaction twice */, OrthancPluginErrorCode_SQLiteTransactionCommit = 1014 /*!< SQLite: Failure when committing the transaction */, @@ -748,6 +749,7 @@ /** * The supported types of changes that can be signaled to the change callback. + * Note: this enum is not used to store changes in the DB ! * @ingroup Callbacks **/ typedef enum
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri Nov 29 10:54:20 2024 +0100 @@ -78,6 +78,16 @@ LABELS_CONSTRAINT_NONE = 2; } +enum OrderingKeyType { + ORDERING_KEY_TYPE_DICOM_TAG = 0; + ORDERING_KEY_TYPE_METADATA = 1; +} + +enum OrderingDirection { + ORDERING_DIRECTION_ASC = 0; + ORDERING_DIRECTION_DESC = 1; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -109,6 +119,13 @@ repeated string values = 8; } +message DatabaseMetadataConstraint { + int32 metadata = 1; + bool is_case_sensitive = 2; + bool is_mandatory = 3; + ConstraintType type = 4; + repeated string values = 5; +} /** * Database-level operations. @@ -141,6 +158,9 @@ bool supports_increment_global_property = 5; bool has_update_and_get_statistics = 6; bool has_measure_latency = 7; + bool supports_find = 8; // New in Orthanc 1.12.5 + bool has_extended_changes = 9; // New in Orthanc 1.12.5 + bool has_db_housekeeping = 10; // New in Orthanc 1.12.5 } } @@ -288,11 +308,15 @@ OPERATION_GET_CHILDREN_METADATA = 42; OPERATION_GET_LAST_CHANGE_INDEX = 43; OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44; - OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 - OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 - OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 - OPERATION_INCREMENT_GLOBAL_PROPERTY = 48; // New in Orthanc 1.12.3 - OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.3 + OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 + OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 + OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 + OPERATION_INCREMENT_GLOBAL_PROPERTY = 48; // New in Orthanc 1.12.3 + OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.3 + OPERATION_FIND = 50; // New in Orthanc 1.12.5 + OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 + OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 + OPERATION_PERFORM_DB_HOUSEKEEPING = 53; // New in Orthanc 1.12.5 } message Rollback { @@ -413,6 +437,19 @@ } } +message GetChangesExtended { + message Request { + int64 since = 1; + int64 to = 2; + repeated int32 change_type = 3; + uint32 limit = 4; + } + message Response { + repeated ServerIndexChange changes = 1; + bool done = 2; + } +} + message GetChildrenInternalId { message Request { int64 id = 1; @@ -670,6 +707,13 @@ } } +message PerformDbHousekeeping { + message Request { + } + message Response { + } +} + message ClearMainDicomTags { message Request { int64 id = 1; @@ -824,6 +868,123 @@ } } +message Find { // New in Orthanc 1.12.5 + message Request { // This corresponds to "FindRequest" in C++ + message Tag { + uint32 group = 1; + uint32 element = 2; + } + message Limits { + uint64 since = 1; + uint64 count = 2; + } + message ParentSpecification { + bool retrieve_main_dicom_tags = 1; + bool retrieve_metadata = 2; + } + message ChildrenSpecification { + bool retrieve_identifiers = 1; + repeated int32 retrieve_metadata = 2; + repeated Tag retrieve_main_dicom_tags = 3; + bool retrieve_count = 4; + } + message Ordering { + OrderingKeyType key_type = 1; + OrderingDirection direction = 2; + uint32 tag_group = 3; + uint32 tag_element = 4; + bool is_identifier_tag = 5; + ResourceType tag_level = 6; + int32 metadata = 7; + } + + // Part 1 of the request: Constraints + ResourceType level = 1; + string orthanc_id_patient = 2; // optional - GetOrthancIdentifiers().GetPatientId(); + string orthanc_id_study = 3; // optional - GetOrthancIdentifiers().GetStudyId(); + string orthanc_id_series = 4; // optional - GetOrthancIdentifiers().GetSeriesId(); + string orthanc_id_instance = 5; // optional - GetOrthancIdentifiers().GetInstanceId(); + repeated DatabaseConstraint dicom_tag_constraints = 6; + Limits limits = 7; // optional + repeated string labels = 8; + LabelsConstraintType labels_constraint = 9; + repeated Ordering ordering = 10; + repeated DatabaseMetadataConstraint metadata_constraints = 11; + + + // Part 2 of the request: What is to be retrieved + bool retrieve_main_dicom_tags = 100; + bool retrieve_metadata = 101; + bool retrieve_labels = 102; + bool retrieve_attachments = 103; + bool retrieve_parent_identifier = 104; + bool retrieve_one_instance_metadata_and_attachments = 105; + ParentSpecification parent_patient = 106; + ParentSpecification parent_study = 107; + ParentSpecification parent_series = 108; + ChildrenSpecification children_studies = 109; + ChildrenSpecification children_series = 110; + ChildrenSpecification children_instances = 111; + } + + message Response { // This corresponds to "FindResponse" in C++ + message Tag { + uint32 group = 1; + uint32 element = 2; + string value = 3; + } + message Metadata { + int32 key = 1; + string value = 2; + int64 revision = 3; + } + message MultipleTags { + uint32 group = 1; + uint32 element = 2; + repeated string values = 3; + } + message MultipleMetadata { + int32 key = 1; + repeated string values = 2; + } + message ResourceContent { + repeated Tag main_dicom_tags = 1; + repeated Metadata metadata = 2; + } + message ChildrenContent { + repeated string identifiers = 1; + repeated MultipleTags main_dicom_tags = 2; + repeated MultipleMetadata metadata = 3; + uint64 count = 4; + } + + int64 internal_id = 1; + string public_id = 2; + string parent_public_id = 3; // optional + repeated string labels = 4; + repeated FileInfo attachments = 5; + ResourceContent patient_content = 6; + ResourceContent study_content = 7; + ResourceContent series_content = 8; + ResourceContent instance_content = 9; + ChildrenContent children_studies_content = 10; + ChildrenContent children_series_content = 11; + ChildrenContent children_instances_content = 12; + string one_instance_public_id = 13; + repeated Metadata one_instance_metadata = 14; + repeated FileInfo one_instance_attachments = 15; + repeated int64 attachments_revisions = 16; + } +} + +message CountResources +{ + message Response + { + uint64 count = 1; + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -878,6 +1039,10 @@ ListLabels.Request list_labels = 147; IncrementGlobalProperty.Request increment_global_property = 148; UpdateAndGetStatistics.Request update_and_get_statistics = 149; + Find.Request find = 150; + GetChangesExtended.Request get_changes_extended = 151; + Find.Request count_resources = 152; + PerformDbHousekeeping.Request perform_db_housekeeping = 153; } message TransactionResponse { @@ -931,6 +1096,10 @@ ListLabels.Response list_labels = 147; IncrementGlobalProperty.Response increment_global_property = 148; UpdateAndGetStatistics.Response update_and_get_statistics = 149; + repeated Find.Response find = 150; // One message per found resources + GetChangesExtended.Response get_changes_extended = 151; + CountResources.Response count_resources = 152; + PerformDbHousekeeping.Response perform_db_housekeeping = 153; } enum RequestType {
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt Fri Nov 29 10:54:20 2024 +0100 @@ -60,7 +60,6 @@ -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}" -DORTHANC_ENABLE_LOGGING=1 -DORTHANC_ENABLE_PLUGINS=1 - -DORTHANC_BUILDING_SERVER_LIBRARY=0 ) include_directories(
--- a/OrthancServer/Resources/Configuration.json Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Resources/Configuration.json Fri Nov 29 10:54:20 2024 +0100 @@ -985,12 +985,43 @@ // saved with another "ExtraMainDicomTags" configuration which means that // your response might be incomplete/inconsistent. // You should call patients|studies|series|instances/../reconstruct to rebuild - // the DB. You may also check for the "Housekeeper" plugin + // the DB. You may also check for the "Housekeeper" plugin. "W002_InconsistentDicomTagsInDb": true, // Display a warning message when Orthanc and its plugins are unable // to decode a frame (new in Orthanc 1.12.5). - "W003_DecoderFailure": true - } + "W003_DecoderFailure": true, + + // Display a warning when the MainDicomTagsSignature metadata has not been + // found which means that the resource has been saved with a version prior + // to 1.11.0. + // You should call patients|studies|series|instances/../reconstruct to rebuild + // the DB. You may also check for the "Housekeeper" plugin. + // (new in Orthanc 1.12.5) + "W004_NoMainDicomTagsSignature": true, + + // Display a warning when a user performs a find request and requests a tag + // from a lower resource level; e.g. when requesting "StudyDescription" at + // Patient level. + // (new in Orthanc 1.12.5) + "W005_RequestingTagFromLowerResourceLevel": true, + + // Display a warning when a user performs a find request and requests a tag + // from the DICOM Meta Header. + // (new in Orthanc 1.12.5) + "W006_RequestingTagFromMetaHeader": true, + + // Display a warning when a user requests a tag that can not be read from disk + // because "StorageAccessOnFind" is set to "Never". + // (new in Orthanc 1.12.5) + "W007_MissingRequestedTagsNotReadFromDisk": true + }, + + // Configure Orthanc in read only mode. + // In this mode, many Orthanc features that requires a write access to the + // Index DB or the disk storage won't be available at all. + // (new in Orthanc 1.12.5) + "ReadOnly" : false + }
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,205 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "LookupIdentifierQuery.h" - -#include "../../Core/DicomParsing/FromDcmtkBridge.h" -#include "../../Core/OrthancException.h" -#include "../ServerToolbox.h" -#include "SetOfResources.h" - -#include <cassert> - - - -namespace Orthanc -{ - LookupIdentifierQuery::SingleConstraint:: - SingleConstraint(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) : - tag_(tag), - type_(type), - value_(ServerToolbox::NormalizeIdentifier(value)) - { - } - - - LookupIdentifierQuery::RangeConstraint:: - RangeConstraint(const DicomTag& tag, - const std::string& start, - const std::string& end) : - tag_(tag), - start_(ServerToolbox::NormalizeIdentifier(start)), - end_(ServerToolbox::NormalizeIdentifier(end)) - { - } - - - LookupIdentifierQuery::Disjunction::~Disjunction() - { - for (size_t i = 0; i < singleConstraints_.size(); i++) - { - delete singleConstraints_[i]; - } - - for (size_t i = 0; i < rangeConstraints_.size(); i++) - { - delete rangeConstraints_[i]; - } - } - - - void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - singleConstraints_.push_back(new SingleConstraint(tag, type, value)); - } - - - void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag, - const std::string& start, - const std::string& end) - { - rangeConstraints_.push_back(new RangeConstraint(tag, start, end)); - } - - - LookupIdentifierQuery::~LookupIdentifierQuery() - { - for (Disjunctions::iterator it = disjunctions_.begin(); - it != disjunctions_.end(); ++it) - { - delete *it; - } - } - - - bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag) - { - return ServerToolbox::IsIdentifier(tag, level_); - } - - - void LookupIdentifierQuery::AddConstraint(DicomTag tag, - IdentifierConstraintType type, - const std::string& value) - { - assert(IsIdentifier(tag)); - disjunctions_.push_back(new Disjunction); - disjunctions_.back()->Add(tag, type, value); - } - - - void LookupIdentifierQuery::AddRange(DicomTag tag, - const std::string& start, - const std::string& end) - { - assert(IsIdentifier(tag)); - disjunctions_.push_back(new Disjunction); - disjunctions_.back()->AddRange(tag, start, end); - } - - - LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction() - { - disjunctions_.push_back(new Disjunction); - return *disjunctions_.back(); - } - - - void LookupIdentifierQuery::Apply(std::list<std::string>& result, - IDatabaseWrapper& database) - { - SetOfResources resources(database, level_); - Apply(resources, database); - - resources.Flatten(result); - } - - - void LookupIdentifierQuery::Apply(SetOfResources& result, - IDatabaseWrapper& database) - { - for (size_t i = 0; i < disjunctions_.size(); i++) - { - std::list<int64_t> a; - - for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++) - { - const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j); - std::list<int64_t> b; - database.LookupIdentifier(b, level_, constraint.GetTag(), - constraint.GetType(), constraint.GetValue()); - - a.splice(a.end(), b); - } - - for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++) - { - const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j); - std::list<int64_t> b; - database.LookupIdentifierRange(b, level_, constraint.GetTag(), - constraint.GetStart(), constraint.GetEnd()); - - a.splice(a.end(), b); - } - - result.Intersect(a); - } - } - - - void LookupIdentifierQuery::Print(std::ostream& s) const - { - s << "Constraint: " << std::endl; - for (Disjunctions::const_iterator - it = disjunctions_.begin(); it != disjunctions_.end(); ++it) - { - if (it == disjunctions_.begin()) - s << " "; - else - s << "OR "; - - for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++) - { - const SingleConstraint& c = (*it)->GetSingleConstraint(j); - s << FromDcmtkBridge::GetTagName(c.GetTag(), ""); - - switch (c.GetType()) - { - case IdentifierConstraintType_Equal: s << " == "; break; - case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break; - case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break; - case IdentifierConstraintType_Wildcard: s << " ~= "; break; - default: - s << " ? "; - } - - s << c.GetValue() << std::endl; - } - } - } -}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,197 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 - -#include "../IDatabaseWrapper.h" - -#include "SetOfResources.h" - -#include <vector> -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - /** - * Primitive for wildcard matching, as defined in DICOM: - * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4 - * - * "Any occurrence of an "*" or a "?", then "*" shall match any - * sequence of characters (including a zero length value) and "?" - * shall match any single character. This matching is case - * sensitive, except for Attributes with an PN Value - * Representation (e.g., Patient Name (0010,0010))." - * - * Pay attention to the fact that "*" (resp. "?") generally - * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The - * values "%", "_", "\" should in the user request should - * respectively be escaped as "\%", "\_" and "\\". - * - * This matching must be case sensitive: The special case of PN VR - * is taken into consideration by normalizing the query string in - * method "NormalizeIdentifier()". - **/ - - class LookupIdentifierQuery : public boost::noncopyable - { - // This class encodes a conjunction ("AND") of disjunctions. Each - // disjunction represents an "OR" of several constraints. - - public: - class SingleConstraint - { - private: - DicomTag tag_; - IdentifierConstraintType type_; - std::string value_; - - public: - SingleConstraint(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - const DicomTag& GetTag() const - { - return tag_; - } - - IdentifierConstraintType GetType() const - { - return type_; - } - - const std::string& GetValue() const - { - return value_; - } - }; - - - class RangeConstraint - { - private: - DicomTag tag_; - std::string start_; - std::string end_; - - public: - RangeConstraint(const DicomTag& tag, - const std::string& start, - const std::string& end); - - const DicomTag& GetTag() const - { - return tag_; - } - - const std::string& GetStart() const - { - return start_; - } - - const std::string& GetEnd() const - { - return end_; - } - }; - - - class Disjunction : public boost::noncopyable - { - private: - std::vector<SingleConstraint*> singleConstraints_; - std::vector<RangeConstraint*> rangeConstraints_; - - public: - ~Disjunction(); - - void Add(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - void AddRange(const DicomTag& tag, - const std::string& start, - const std::string& end); - - size_t GetSingleConstraintsCount() const - { - return singleConstraints_.size(); - } - - const SingleConstraint& GetSingleConstraint(size_t i) const - { - return *singleConstraints_[i]; - } - - size_t GetRangeConstraintsCount() const - { - return rangeConstraints_.size(); - } - - const RangeConstraint& GetRangeConstraint(size_t i) const - { - return *rangeConstraints_[i]; - } - }; - - - private: - typedef std::vector<Disjunction*> Disjunctions; - - ResourceType level_; - Disjunctions disjunctions_; - - public: - LookupIdentifierQuery(ResourceType level) : level_(level) - { - } - - ~LookupIdentifierQuery(); - - bool IsIdentifier(const DicomTag& tag); - - void AddConstraint(DicomTag tag, - IdentifierConstraintType type, - const std::string& value); - - void AddRange(DicomTag tag, - const std::string& start, - const std::string& end); - - Disjunction& AddDisjunction(); - - ResourceType GetLevel() const - { - return level_; - } - - // The database must be locked - void Apply(std::list<std::string>& result, - IDatabaseWrapper& database); - - void Apply(SetOfResources& result, - IDatabaseWrapper& database); - - void Print(std::ostream& s) const; - }; -}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,469 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "LookupResource.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/FileStorage/StorageAccessor.h" -#include "../ServerToolbox.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" - - -namespace Orthanc -{ - static bool DoesDicomMapMatch(const DicomMap& dicom, - const DicomTag& tag, - const IFindConstraint& constraint) - { - const DicomValue* value = dicom.TestAndGetValue(tag); - - return (value != NULL && - !value->IsNull() && - !value->IsBinary() && - constraint.Match(value->GetContent())); - } - - - LookupResource::Level::Level(ResourceType level) : level_(level) - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - identifiers_.insert(tags[i]); - } - - DicomMap::LoadMainDicomTags(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (identifiers_.find(tags[i]) == identifiers_.end()) - { - mainTags_.insert(tags[i]); - } - } - } - - LookupResource::Level::~Level() - { - for (Constraints::iterator it = mainTagsConstraints_.begin(); - it != mainTagsConstraints_.end(); ++it) - { - delete it->second; - } - - for (Constraints::iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - delete it->second; - } - } - - bool LookupResource::Level::Add(const DicomTag& tag, - std::auto_ptr<IFindConstraint>& constraint) - { - if (identifiers_.find(tag) != identifiers_.end()) - { - if (level_ == ResourceType_Patient) - { - // The filters on the patient level must be cloned to the study level - identifiersConstraints_[tag] = constraint->Clone(); - } - else - { - identifiersConstraints_[tag] = constraint.release(); - } - - return true; - } - else if (mainTags_.find(tag) != mainTags_.end()) - { - if (level_ == ResourceType_Patient) - { - // The filters on the patient level must be cloned to the study level - mainTagsConstraints_[tag] = constraint->Clone(); - } - else - { - mainTagsConstraints_[tag] = constraint.release(); - } - - return true; - } - else - { - // This is not a main DICOM tag - return false; - } - } - - - bool LookupResource::Level::IsMatch(const DicomMap& dicom) const - { - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - for (Constraints::const_iterator it = mainTagsConstraints_.begin(); - it != mainTagsConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - return true; - } - - - LookupResource::LookupResource(ResourceType level) : level_(level) - { - switch (level) - { - case ResourceType_Patient: - levels_[ResourceType_Patient] = new Level(ResourceType_Patient); - break; - - case ResourceType_Instance: - levels_[ResourceType_Instance] = new Level(ResourceType_Instance); - // Do not add "break" here - - case ResourceType_Series: - levels_[ResourceType_Series] = new Level(ResourceType_Series); - // Do not add "break" here - - case ResourceType_Study: - levels_[ResourceType_Study] = new Level(ResourceType_Study); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - LookupResource::~LookupResource() - { - for (Levels::iterator it = levels_.begin(); - it != levels_.end(); ++it) - { - delete it->second; - } - - for (Constraints::iterator it = unoptimizedConstraints_.begin(); - it != unoptimizedConstraints_.end(); ++it) - { - delete it->second; - } - } - - - - bool LookupResource::AddInternal(ResourceType level, - const DicomTag& tag, - std::auto_ptr<IFindConstraint>& constraint) - { - Levels::iterator it = levels_.find(level); - if (it != levels_.end()) - { - if (it->second->Add(tag, constraint)) - { - return true; - } - } - - return false; - } - - - void LookupResource::Add(const DicomTag& tag, - IFindConstraint* constraint) - { - std::auto_ptr<IFindConstraint> c(constraint); - - if (!AddInternal(ResourceType_Patient, tag, c) && - !AddInternal(ResourceType_Study, tag, c) && - !AddInternal(ResourceType_Series, tag, c) && - !AddInternal(ResourceType_Instance, tag, c)) - { - unoptimizedConstraints_[tag] = c.release(); - } - } - - - static bool Match(const DicomMap& tags, - const DicomTag& tag, - const IFindConstraint& constraint) - { - const DicomValue* value = tags.TestAndGetValue(tag); - - if (value == NULL || - value->IsNull() || - value->IsBinary()) - { - return false; - } - else - { - return constraint.Match(value->GetContent()); - } - } - - - void LookupResource::Level::Apply(SetOfResources& candidates, - IDatabaseWrapper& database) const - { - // First, use the indexed identifiers - LookupIdentifierQuery query(level_); - - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - it->second->Setup(query, it->first); - } - - query.Apply(candidates, database); - - /*{ - query.Print(std::cout); - std::list<int64_t> source; - candidates.Flatten(source); - printf("=> %d\n", source.size()); - }*/ - - // Secondly, filter using the main DICOM tags - if (!identifiersConstraints_.empty() || - !mainTagsConstraints_.empty()) - { - std::list<int64_t> source; - candidates.Flatten(source); - candidates.Clear(); - - std::list<int64_t> filtered; - for (std::list<int64_t>::const_iterator candidate = source.begin(); - candidate != source.end(); ++candidate) - { - DicomMap tags; - database.GetMainDicomTags(tags, *candidate); - - bool match = true; - - // Re-apply the identifier constraints, as their "Setup" - // method is less restrictive than their "Match" method - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - match && it != identifiersConstraints_.end(); ++it) - { - if (!Match(tags, it->first, *it->second)) - { - match = false; - } - } - - for (Constraints::const_iterator it = mainTagsConstraints_.begin(); - match && it != mainTagsConstraints_.end(); ++it) - { - if (!Match(tags, it->first, *it->second)) - { - match = false; - } - } - - if (match) - { - filtered.push_back(*candidate); - } - } - - candidates.Intersect(filtered); - } - } - - - - bool LookupResource::IsMatch(const DicomMap& dicom) const - { - for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it) - { - if (!it->second->IsMatch(dicom)) - { - return false; - } - } - - for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); - it != unoptimizedConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - return true; - } - - - void LookupResource::ApplyLevel(SetOfResources& candidates, - ResourceType level, - IDatabaseWrapper& database) const - { - Levels::const_iterator it = levels_.find(level); - if (it != levels_.end()) - { - it->second->Apply(candidates, database); - } - - if (level == ResourceType_Study && - modalitiesInStudy_.get() != NULL) - { - // There is a constraint on the "ModalitiesInStudy" DICOM - // extension. Check out whether one child series has one of the - // allowed modalities - std::list<int64_t> allStudies, matchingStudies; - candidates.Flatten(allStudies); - - for (std::list<int64_t>::const_iterator - study = allStudies.begin(); study != allStudies.end(); ++study) - { - std::list<int64_t> childrenSeries; - database.GetChildrenInternalId(childrenSeries, *study); - - for (std::list<int64_t>::const_iterator - series = childrenSeries.begin(); series != childrenSeries.end(); ++series) - { - DicomMap tags; - database.GetMainDicomTags(tags, *series); - - const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - if (modalitiesInStudy_->Match(value->GetContent())) - { - matchingStudies.push_back(*study); - break; - } - } - } - } - - candidates.Intersect(matchingStudies); - } - } - - - void LookupResource::FindCandidates(std::list<int64_t>& result, - IDatabaseWrapper& database) const - { - ResourceType startingLevel; - if (level_ == ResourceType_Patient) - { - startingLevel = ResourceType_Patient; - } - else - { - startingLevel = ResourceType_Study; - } - - SetOfResources candidates(database, startingLevel); - - switch (level_) - { - case ResourceType_Patient: - ApplyLevel(candidates, ResourceType_Patient, database); - break; - - case ResourceType_Study: - ApplyLevel(candidates, ResourceType_Study, database); - break; - - case ResourceType_Series: - ApplyLevel(candidates, ResourceType_Study, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Series, database); - break; - - case ResourceType_Instance: - ApplyLevel(candidates, ResourceType_Study, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Series, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Instance, database); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - candidates.Flatten(result); - } - - - void LookupResource::SetModalitiesInStudy(const std::string& modalities) - { - modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */)); - - std::vector<std::string> items; - Toolbox::TokenizeString(items, modalities, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - modalitiesInStudy_->AddAllowedValue(items[i]); - } - } - - - void LookupResource::AddDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) - { - SetModalitiesInStudy(dicomQuery); - } - else - { - Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive)); - } - } - -}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 - -#include "ListConstraint.h" -#include "SetOfResources.h" - -#include <memory> - -namespace Orthanc -{ - class LookupResource : public boost::noncopyable - { - private: - typedef std::map<DicomTag, IFindConstraint*> Constraints; - - class Level - { - private: - ResourceType level_; - std::set<DicomTag> identifiers_; - std::set<DicomTag> mainTags_; - Constraints identifiersConstraints_; - Constraints mainTagsConstraints_; - - public: - Level(ResourceType level); - - ~Level(); - - bool Add(const DicomTag& tag, - std::auto_ptr<IFindConstraint>& constraint); - - void Apply(SetOfResources& candidates, - IDatabaseWrapper& database) const; - - bool IsMatch(const DicomMap& dicom) const; - }; - - typedef std::map<ResourceType, Level*> Levels; - - ResourceType level_; - Levels levels_; - Constraints unoptimizedConstraints_; // Constraints on non-main DICOM tags - std::auto_ptr<ListConstraint> modalitiesInStudy_; - - bool AddInternal(ResourceType level, - const DicomTag& tag, - std::auto_ptr<IFindConstraint>& constraint); - - void ApplyLevel(SetOfResources& candidates, - ResourceType level, - IDatabaseWrapper& database) const; - - public: - LookupResource(ResourceType level); - - ~LookupResource(); - - ResourceType GetLevel() const - { - return level_; - } - - void SetModalitiesInStudy(const std::string& modalities); - - void Add(const DicomTag& tag, - IFindConstraint* constraint); // Takes ownership - - void AddDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive); - - void FindCandidates(std::list<int64_t>& result, - IDatabaseWrapper& database) const; - - bool HasOnlyMainDicomTags() const - { - return unoptimizedConstraints_.empty(); - } - - bool IsMatch(const DicomMap& dicom) const; - }; -}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -cmake_minimum_required(VERSION 2.8) - -project(SampleDatabasePlugin) - -# Parameters of the build -SET(SAMPLE_DATABASE_VERSION "0.0" CACHE STRING "Version of the plugin") -SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(STANDALONE_BUILD ON) - -# Advanced parameters to fine-tune linking against system libraries -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite") - -include(${CMAKE_SOURCE_DIR}/../../../Plugins/Samples/Common/OrthancPlugins.cmake) - -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) -set(ENABLE_SQLITE ON) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) - -EmbedResources( - --system-exception # Use "std::runtime_error" instead of "OrthancException" for embedded resources - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/../../../Sources/Database/PrepareDatabase.sql - ) - -message("Setting the version of the plugin to ${SAMPLE_DATABASE_VERSION}") - -add_definitions( - -DORTHANC_SQLITE_STANDALONE=1 - -DORTHANC_ENABLE_PLUGINS=1 - -DORTHANC_SANDBOXED=0 - -DSAMPLE_DATABASE_VERSION="${SAMPLE_DATABASE_VERSION}" - ) - -add_library(SampleDatabase SHARED - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Connection.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/FunctionContext.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Statement.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/StatementId.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/StatementReference.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Transaction.cpp - ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp - ${CMAKE_SOURCE_DIR}/../../../Plugins/Engine/PluginsEnumerations.cpp - - Database.cpp - Plugin.cpp - DatabaseWrapperBase.cpp - - ${BOOST_SOURCES} - ${JSONCPP_SOURCES} - ${SQLITE_SOURCES} - ${AUTOGENERATED_SOURCES} - ) - -set_target_properties(SampleDatabase PROPERTIES - VERSION ${SAMPLE_DATABASE_VERSION} - SOVERSION ${SAMPLE_DATABASE_VERSION}) - -install( - TARGETS SampleDatabase - RUNTIME DESTINATION lib # Destination for Windows - LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux - )
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,555 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 "Database.h" - -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" - -#include <EmbeddedResources.h> -#include <boost/lexical_cast.hpp> - - -namespace Internals -{ - class SignalFileDeleted : public Orthanc::SQLite::IScalarFunction - { - private: - OrthancPlugins::DatabaseBackendOutput& output_; - - public: - SignalFileDeleted(OrthancPlugins::DatabaseBackendOutput& output) : output_(output) - { - } - - virtual const char* GetName() const - { - return "SignalFileDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 7; - } - - virtual void Compute(Orthanc::SQLite::FunctionContext& context) - { - std::string uncompressedMD5, compressedMD5; - - if (!context.IsNullValue(5)) - { - uncompressedMD5 = context.GetStringValue(5); - } - - if (!context.IsNullValue(6)) - { - compressedMD5 = context.GetStringValue(6); - } - - output_.SignalDeletedAttachment(context.GetStringValue(0), - context.GetIntValue(1), - context.GetInt64Value(2), - uncompressedMD5, - context.GetIntValue(3), - context.GetInt64Value(4), - compressedMD5); - } - }; - - - class SignalResourceDeleted : public Orthanc::SQLite::IScalarFunction - { - private: - OrthancPlugins::DatabaseBackendOutput& output_; - - public: - SignalResourceDeleted(OrthancPlugins::DatabaseBackendOutput& output) : output_(output) - { - } - - virtual const char* GetName() const - { - return "SignalResourceDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(Orthanc::SQLite::FunctionContext& context) - { - output_.SignalDeletedResource(context.GetStringValue(0), - Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1)))); - } - }; -} - - -class Database::SignalRemainingAncestor : public Orthanc::SQLite::IScalarFunction -{ -private: - bool hasRemainingAncestor_; - std::string remainingPublicId_; - OrthancPluginResourceType remainingType_; - -public: - SignalRemainingAncestor() : - hasRemainingAncestor_(false), - remainingType_(OrthancPluginResourceType_Instance) // Some dummy value - { - } - - void Reset() - { - hasRemainingAncestor_ = false; - } - - virtual const char* GetName() const - { - return "SignalRemainingAncestor"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(Orthanc::SQLite::FunctionContext& context) - { - if (!hasRemainingAncestor_ || - remainingType_ >= context.GetIntValue(1)) - { - hasRemainingAncestor_ = true; - remainingPublicId_ = context.GetStringValue(0); - remainingType_ = Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1))); - } - } - - bool HasRemainingAncestor() const - { - return hasRemainingAncestor_; - } - - const std::string& GetRemainingAncestorId() const - { - assert(hasRemainingAncestor_); - return remainingPublicId_; - } - - OrthancPluginResourceType GetRemainingAncestorType() const - { - assert(hasRemainingAncestor_); - return remainingType_; - } -}; - - - -Database::Database(const std::string& path) : - path_(path), - base_(db_), - signalRemainingAncestor_(NULL) -{ -} - - -void Database::Open() -{ - db_.Open(path_); - - db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); - - // http://www.sqlite.org/pragma.html - db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); - db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); - db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); - db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); - //db_.Execute("PRAGMA TEMP_STORE=memory"); - - if (!db_.DoesTableExist("GlobalProperties")) - { - std::string query; - Orthanc::EmbeddedResources::GetFileResource(query, Orthanc::EmbeddedResources::PREPARE_DATABASE); - db_.Execute(query); - } - - signalRemainingAncestor_ = new SignalRemainingAncestor; - db_.Register(signalRemainingAncestor_); - db_.Register(new Internals::SignalFileDeleted(GetOutput())); - db_.Register(new Internals::SignalResourceDeleted(GetOutput())); -} - - -void Database::Close() -{ - db_.Close(); -} - - -void Database::AddAttachment(int64_t id, - const OrthancPluginAttachment& attachment) -{ - Orthanc::FileInfo info(attachment.uuid, - static_cast<Orthanc::FileContentType>(attachment.contentType), - attachment.uncompressedSize, - attachment.uncompressedHash, - static_cast<Orthanc::CompressionType>(attachment.compressionType), - attachment.compressedSize, - attachment.compressedHash); - base_.AddAttachment(id, info); -} - - -void Database::DeleteResource(int64_t id) -{ - signalRemainingAncestor_->Reset(); - - Orthanc::SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); - s.BindInt64(0, id); - s.Run(); - - if (signalRemainingAncestor_->HasRemainingAncestor()) - { - GetOutput().SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorId(), - signalRemainingAncestor_->GetRemainingAncestorType()); - } -} - - -static void Answer(OrthancPlugins::DatabaseBackendOutput& output, - const Orthanc::ServerIndexChange& change) -{ - output.AnswerChange(change.GetSeq(), - change.GetChangeType(), - Orthanc::Plugins::Convert(change.GetResourceType()), - change.GetPublicId(), - change.GetDate()); -} - - -static void Answer(OrthancPlugins::DatabaseBackendOutput& output, - const Orthanc::ExportedResource& resource) -{ - output.AnswerExportedResource(resource.GetSeq(), - Orthanc::Plugins::Convert(resource.GetResourceType()), - resource.GetPublicId(), - resource.GetModality(), - resource.GetDate(), - resource.GetPatientId(), - resource.GetStudyInstanceUid(), - resource.GetSeriesInstanceUid(), - resource.GetSopInstanceUid()); -} - - -void Database::GetChanges(bool& done /*out*/, - int64_t since, - uint32_t maxResults) -{ - typedef std::list<Orthanc::ServerIndexChange> Changes; - - Changes changes; - base_.GetChanges(changes, done, since, maxResults); - - for (Changes::const_iterator it = changes.begin(); it != changes.end(); ++it) - { - Answer(GetOutput(), *it); - } -} - - -void Database::GetExportedResources(bool& done /*out*/, - int64_t since, - uint32_t maxResults) -{ - typedef std::list<Orthanc::ExportedResource> Resources; - - Resources resources; - base_.GetExportedResources(resources, done, since, maxResults); - - for (Resources::const_iterator it = resources.begin(); it != resources.end(); ++it) - { - Answer(GetOutput(), *it); - } -} - - -void Database::GetLastChange() -{ - std::list<Orthanc::ServerIndexChange> change; - Orthanc::ErrorCode code = base_.GetLastChange(change); - - if (code != Orthanc::ErrorCode_Success) - { - throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code)); - } - - if (!change.empty()) - { - Answer(GetOutput(), change.front()); - } -} - - -void Database::GetLastExportedResource() -{ - std::list<Orthanc::ExportedResource> resource; - base_.GetLastExportedResource(resource); - - if (!resource.empty()) - { - Answer(GetOutput(), resource.front()); - } -} - - -void Database::GetMainDicomTags(int64_t id) -{ - Orthanc::DicomMap tags; - base_.GetMainDicomTags(tags, id); - - Orthanc::DicomArray arr(tags); - for (size_t i = 0; i < arr.GetSize(); i++) - { - GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(), - arr.GetElement(i).GetTag().GetElement(), - arr.GetElement(i).GetValue().GetContent()); - } -} - - -std::string Database::GetPublicId(int64_t resourceId) -{ - std::string id; - if (base_.GetPublicId(id, resourceId)) - { - return id; - } - else - { - throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_UnknownResource); - } -} - - -OrthancPluginResourceType Database::GetResourceType(int64_t resourceId) -{ - Orthanc::ResourceType result; - Orthanc::ErrorCode code = base_.GetResourceType(result, resourceId); - - if (code == Orthanc::ErrorCode_Success) - { - return Orthanc::Plugins::Convert(result); - } - else - { - throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code)); - } -} - - - -template <typename I> -static void ConvertList(std::list<int32_t>& target, - const std::list<I>& source) -{ - for (typename std::list<I>::const_iterator - it = source.begin(); it != source.end(); ++it) - { - target.push_back(*it); - } -} - - -void Database::ListAvailableMetadata(std::list<int32_t>& target /*out*/, - int64_t id) -{ - std::list<Orthanc::MetadataType> tmp; - base_.ListAvailableMetadata(tmp, id); - ConvertList(target, tmp); -} - - -void Database::ListAvailableAttachments(std::list<int32_t>& target /*out*/, - int64_t id) -{ - std::list<Orthanc::FileContentType> tmp; - base_.ListAvailableAttachments(tmp, id); - ConvertList(target, tmp); -} - - -void Database::LogChange(const OrthancPluginChange& change) -{ - int64_t id; - OrthancPluginResourceType type; - if (!LookupResource(id, type, change.publicId) || - type != change.resourceType) - { - throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_DatabasePlugin); - } - - Orthanc::ServerIndexChange tmp(change.seq, - static_cast<Orthanc::ChangeType>(change.changeType), - Orthanc::Plugins::Convert(change.resourceType), - change.publicId, - change.date); - - base_.LogChange(id, tmp); -} - - -void Database::LogExportedResource(const OrthancPluginExportedResource& resource) -{ - Orthanc::ExportedResource tmp(resource.seq, - Orthanc::Plugins::Convert(resource.resourceType), - resource.publicId, - resource.modality, - resource.date, - resource.patientId, - resource.studyInstanceUid, - resource.seriesInstanceUid, - resource.sopInstanceUid); - - base_.LogExportedResource(tmp); -} - - -bool Database::LookupAttachment(int64_t id, - int32_t contentType) -{ - Orthanc::FileInfo attachment; - if (base_.LookupAttachment(attachment, id, static_cast<Orthanc::FileContentType>(contentType))) - { - GetOutput().AnswerAttachment(attachment.GetUuid(), - attachment.GetContentType(), - attachment.GetUncompressedSize(), - attachment.GetUncompressedMD5(), - attachment.GetCompressionType(), - attachment.GetCompressedSize(), - attachment.GetCompressedMD5()); - return true; - } - else - { - return false; - } -} - - -bool Database::LookupParent(int64_t& parentId /*out*/, - int64_t resourceId) -{ - bool found; - Orthanc::ErrorCode code = base_.LookupParent(found, parentId, resourceId); - - if (code == Orthanc::ErrorCode_Success) - { - return found; - } - else - { - throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code)); - } -} - - -bool Database::LookupResource(int64_t& id /*out*/, - OrthancPluginResourceType& type /*out*/, - const char* publicId) -{ - Orthanc::ResourceType tmp; - if (base_.LookupResource(id, tmp, publicId)) - { - type = Orthanc::Plugins::Convert(tmp); - return true; - } - else - { - return false; - } -} - - -void Database::StartTransaction() -{ - transaction_.reset(new Orthanc::SQLite::Transaction(db_)); - transaction_->Begin(); -} - - -void Database::RollbackTransaction() -{ - transaction_->Rollback(); - transaction_.reset(NULL); -} - - -void Database::CommitTransaction() -{ - transaction_->Commit(); - transaction_.reset(NULL); -} - - -uint32_t Database::GetDatabaseVersion() -{ - std::string version; - - if (!LookupGlobalProperty(version, Orthanc::GlobalProperty_DatabaseSchemaVersion)) - { - throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError); - } - - try - { - return boost::lexical_cast<uint32_t>(version); - } - catch (boost::bad_lexical_cast&) - { - throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError); - } -} - - -void Database::UpgradeDatabase(uint32_t targetVersion, - OrthancPluginStorageArea* storageArea) -{ - if (targetVersion == 6) - { - OrthancPluginErrorCode code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, - OrthancPluginResourceType_Study); - if (code == OrthancPluginErrorCode_Success) - { - code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, - OrthancPluginResourceType_Series); - } - - if (code != OrthancPluginErrorCode_Success) - { - throw OrthancPlugins::DatabaseException(code); - } - - base_.SetGlobalProperty(Orthanc::GlobalProperty_DatabaseSchemaVersion, "6"); - } -}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,275 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 - -#include "OrthancCppDatabasePlugin.h" - -#include "../../../../OrthancFramework/Sources/SQLite/Connection.h" -#include "../../../../OrthancFramework/Sources/SQLite/Transaction.h" -#include "../../../Plugins/Engine/PluginsEnumerations.h" -#include "DatabaseWrapperBase.h" - -#include <memory> - -class Database : public OrthancPlugins::IDatabaseBackend -{ -private: - class SignalRemainingAncestor; - - std::string path_; - Orthanc::SQLite::Connection db_; - Orthanc::DatabaseWrapperBase base_; - SignalRemainingAncestor* signalRemainingAncestor_; - - std::auto_ptr<Orthanc::SQLite::Transaction> transaction_; - -public: - Database(const std::string& path); - - virtual void Open(); - - virtual void Close(); - - virtual void AddAttachment(int64_t id, - const OrthancPluginAttachment& attachment); - - virtual void AttachChild(int64_t parent, - int64_t child) - { - base_.AttachChild(parent, child); - } - - virtual void ClearChanges() - { - db_.Execute("DELETE FROM Changes"); - } - - virtual void ClearExportedResources() - { - db_.Execute("DELETE FROM ExportedResources"); - } - - virtual int64_t CreateResource(const char* publicId, - OrthancPluginResourceType type) - { - return base_.CreateResource(publicId, Orthanc::Plugins::Convert(type)); - } - - virtual void DeleteAttachment(int64_t id, - int32_t attachment) - { - base_.DeleteAttachment(id, static_cast<Orthanc::FileContentType>(attachment)); - } - - virtual void DeleteMetadata(int64_t id, - int32_t metadataType) - { - base_.DeleteMetadata(id, static_cast<Orthanc::MetadataType>(metadataType)); - } - - virtual void DeleteResource(int64_t id); - - virtual void GetAllInternalIds(std::list<int64_t>& target, - OrthancPluginResourceType resourceType) - { - base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType)); - } - - virtual void GetAllPublicIds(std::list<std::string>& target, - OrthancPluginResourceType resourceType) - { - base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType)); - } - - virtual void GetAllPublicIds(std::list<std::string>& target, - OrthancPluginResourceType resourceType, - uint64_t since, - uint64_t limit) - { - base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType), since, limit); - } - - virtual void GetChanges(bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/, - int64_t id) - { - base_.GetChildrenInternalId(target, id); - } - - virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/, - int64_t id) - { - base_.GetChildrenPublicId(target, id); - } - - virtual void GetExportedResources(bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetLastChange(); - - virtual void GetLastExportedResource(); - - virtual void GetMainDicomTags(int64_t id); - - virtual std::string GetPublicId(int64_t resourceId); - - virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) - { - return base_.GetResourceCount(Orthanc::Plugins::Convert(resourceType)); - } - - virtual OrthancPluginResourceType GetResourceType(int64_t resourceId); - - virtual uint64_t GetTotalCompressedSize() - { - return base_.GetTotalCompressedSize(); - } - - virtual uint64_t GetTotalUncompressedSize() - { - return base_.GetTotalUncompressedSize(); - } - - virtual bool IsExistingResource(int64_t internalId) - { - return base_.IsExistingResource(internalId); - } - - virtual bool IsProtectedPatient(int64_t internalId) - { - return base_.IsProtectedPatient(internalId); - } - - virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/, - int64_t id); - - virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/, - int64_t id); - - virtual void LogChange(const OrthancPluginChange& change); - - virtual void LogExportedResource(const OrthancPluginExportedResource& resource); - - virtual bool LookupAttachment(int64_t id, - int32_t contentType); - - virtual bool LookupGlobalProperty(std::string& target /*out*/, - int32_t property) - { - return base_.LookupGlobalProperty(target, static_cast<Orthanc::GlobalProperty>(property)); - } - - virtual void LookupIdentifier(std::list<int64_t>& target /*out*/, - OrthancPluginResourceType level, - uint16_t group, - uint16_t element, - OrthancPluginIdentifierConstraint constraint, - const char* value) - { - base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level), - Orthanc::DicomTag(group, element), - Orthanc::Plugins::Convert(constraint), value); - } - - virtual bool LookupMetadata(std::string& target /*out*/, - int64_t id, - int32_t metadataType) - { - return base_.LookupMetadata(target, id, static_cast<Orthanc::MetadataType>(metadataType)); - } - - virtual bool LookupParent(int64_t& parentId /*out*/, - int64_t resourceId); - - virtual bool LookupResource(int64_t& id /*out*/, - OrthancPluginResourceType& type /*out*/, - const char* publicId); - - virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/) - { - return base_.SelectPatientToRecycle(internalId); - } - - virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/, - int64_t patientIdToAvoid) - { - return base_.SelectPatientToRecycle(internalId, patientIdToAvoid); - } - - - virtual void SetGlobalProperty(int32_t property, - const char* value) - { - base_.SetGlobalProperty(static_cast<Orthanc::GlobalProperty>(property), value); - } - - virtual void SetMainDicomTag(int64_t id, - uint16_t group, - uint16_t element, - const char* value) - { - base_.SetMainDicomTag(id, Orthanc::DicomTag(group, element), value); - } - - virtual void SetIdentifierTag(int64_t id, - uint16_t group, - uint16_t element, - const char* value) - { - base_.SetIdentifierTag(id, Orthanc::DicomTag(group, element), value); - } - - virtual void SetMetadata(int64_t id, - int32_t metadataType, - const char* value) - { - base_.SetMetadata(id, static_cast<Orthanc::MetadataType>(metadataType), value); - } - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected) - { - base_.SetProtectedPatient(internalId, isProtected); - } - - virtual void StartTransaction(); - - virtual void RollbackTransaction(); - - virtual void CommitTransaction(); - - virtual uint32_t GetDatabaseVersion(); - - virtual void UpgradeDatabase(uint32_t targetVersion, - OrthancPluginStorageArea* storageArea); - - virtual void ClearMainDicomTags(int64_t internalId) - { - base_.ClearMainDicomTags(internalId); - } -};
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,747 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 "../../../Sources/PrecompiledHeadersServer.h" -#include "DatabaseWrapperBase.h" - -#include <stdio.h> -#include <memory> - -namespace Orthanc -{ - void DatabaseWrapperBase::SetGlobalProperty(GlobalProperty property, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); - s.BindInt(0, property); - s.BindString(1, value); - s.Run(); - } - - bool DatabaseWrapperBase::LookupGlobalProperty(std::string& target, - GlobalProperty property) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM GlobalProperties WHERE property=?"); - s.BindInt(0, property); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - int64_t DatabaseWrapperBase::CreateResource(const std::string& publicId, - ResourceType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); - s.BindInt(0, type); - s.BindString(1, publicId); - s.Run(); - return db_.GetLastInsertRowId(); - } - - bool DatabaseWrapperBase::LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); - s.BindString(0, publicId); - - if (!s.Step()) - { - return false; - } - else - { - id = s.ColumnInt(0); - type = static_cast<ResourceType>(s.ColumnInt(1)); - - // Check whether there is a single resource with this public id - assert(!s.Step()); - - return true; - } - } - - ErrorCode DatabaseWrapperBase::LookupParent(bool& found, - int64_t& parentId, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT parentId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - return ErrorCode_UnknownResource; - } - - if (s.ColumnIsNull(0)) - { - found = false; - } - else - { - found = true; - parentId = s.ColumnInt(0); - } - - return ErrorCode_Success; - } - - bool DatabaseWrapperBase::GetPublicId(std::string& result, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - return false; - } - else - { - result = s.ColumnString(0); - return true; - } - } - - - ErrorCode DatabaseWrapperBase::GetResourceType(ResourceType& result, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT resourceType FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (s.Step()) - { - result = static_cast<ResourceType>(s.ColumnInt(0)); - return ErrorCode_Success; - } - else - { - return ErrorCode_UnknownResource; - } - } - - - void DatabaseWrapperBase::AttachChild(int64_t parent, - int64_t child) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); - s.BindInt64(0, parent); - s.BindInt64(1, child); - s.Run(); - } - - - void DatabaseWrapperBase::SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.BindString(2, value); - s.Run(); - } - - void DatabaseWrapperBase::DeleteMetadata(int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.Run(); - } - - bool DatabaseWrapperBase::LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - void DatabaseWrapperBase::ListAvailableMetadata(std::list<MetadataType>& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast<MetadataType>(s.ColumnInt(0))); - } - } - - - void DatabaseWrapperBase::AddAttachment(int64_t id, - const FileInfo& attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, attachment.GetContentType()); - s.BindString(2, attachment.GetUuid()); - s.BindInt64(3, attachment.GetCompressedSize()); - s.BindInt64(4, attachment.GetUncompressedSize()); - s.BindInt(5, attachment.GetCompressionType()); - s.BindString(6, attachment.GetUncompressedMD5()); - s.BindString(7, attachment.GetCompressedMD5()); - s.Run(); - } - - - void DatabaseWrapperBase::DeleteAttachment(int64_t id, - FileContentType attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, attachment); - s.Run(); - } - - - - void DatabaseWrapperBase::ListAvailableAttachments(std::list<FileContentType>& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT fileType FROM AttachedFiles WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast<FileContentType>(s.ColumnInt(0))); - } - } - - bool DatabaseWrapperBase::LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, contentType); - - if (!s.Step()) - { - return false; - } - else - { - attachment = FileInfo(s.ColumnString(0), - contentType, - s.ColumnInt64(1), - s.ColumnString(4), - static_cast<CompressionType>(s.ColumnInt(2)), - s.ColumnInt64(3), - s.ColumnString(5)); - return true; - } - } - - - void DatabaseWrapperBase::ClearMainDicomTags(int64_t id) - { - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - } - - - void DatabaseWrapperBase::SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapperBase::SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapperBase::GetMainDicomTags(DicomMap& map, - int64_t id) - { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3), false); - } - } - - - - void DatabaseWrapperBase::GetChildrenPublicId(std::list<std::string>& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapperBase::GetChildrenInternalId(std::list<int64_t>& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapperBase::LogChange(int64_t internalId, - const ServerIndexChange& change) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, change.GetChangeType()); - s.BindInt64(1, internalId); - s.BindInt(2, change.GetResourceType()); - s.BindString(3, change.GetDate()); - s.Run(); - } - - - ErrorCode DatabaseWrapperBase::GetChangesInternal(std::list<ServerIndexChange>& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1)); - ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3)); - const std::string& date = s.ColumnString(4); - - int64_t internalId = s.ColumnInt64(2); - std::string publicId; - if (!GetPublicId(publicId, internalId)) - { - return ErrorCode_UnknownResource; - } - - target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); - } - - done = !(target.size() == maxResults && s.Step()); - return ErrorCode_Success; - } - - - ErrorCode DatabaseWrapperBase::GetChanges(std::list<ServerIndexChange>& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - return GetChangesInternal(target, done, s, maxResults); - } - - ErrorCode DatabaseWrapperBase::GetLastChange(std::list<ServerIndexChange>& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); - return GetChangesInternal(target, done, s, 1); - } - - - void DatabaseWrapperBase::LogExportedResource(const ExportedResource& resource) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); - - s.BindInt(0, resource.GetResourceType()); - s.BindString(1, resource.GetPublicId()); - s.BindString(2, resource.GetModality()); - s.BindString(3, resource.GetPatientId()); - s.BindString(4, resource.GetStudyInstanceUid()); - s.BindString(5, resource.GetSeriesInstanceUid()); - s.BindString(6, resource.GetSopInstanceUid()); - s.BindString(7, resource.GetDate()); - s.Run(); - } - - - void DatabaseWrapperBase::GetExportedResourcesInternal(std::list<ExportedResource>& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1)); - std::string publicId = s.ColumnString(2); - - ExportedResource resource(seq, - resourceType, - publicId, - s.ColumnString(3), // modality - s.ColumnString(8), // date - s.ColumnString(4), // patient ID - s.ColumnString(5), // study instance UID - s.ColumnString(6), // series instance UID - s.ColumnString(7)); // sop instance UID - - target.push_back(resource); - } - - done = !(target.size() == maxResults && s.Step()); - } - - - void DatabaseWrapperBase::GetExportedResources(std::list<ExportedResource>& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetExportedResourcesInternal(target, done, s, maxResults); - } - - - void DatabaseWrapperBase::GetLastExportedResource(std::list<ExportedResource>& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); - GetExportedResourcesInternal(target, done, s, 1); - } - - - - uint64_t DatabaseWrapperBase::GetTotalCompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapperBase::GetTotalUncompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); - } - - void DatabaseWrapperBase::GetAllInternalIds(std::list<int64_t>& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; - } - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?"); - s.BindInt(0, resourceType); - s.BindInt64(1, limit); - s.BindInt64(2, since); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - uint64_t DatabaseWrapperBase::GetResourceCount(ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - if (!s.Step()) - { - return 0; - } - else - { - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - return c; - } - } - - - bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder " - "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); - s.BindInt64(0, patientIdToAvoid); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - bool DatabaseWrapperBase::IsProtectedPatient(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); - s.BindInt64(0, internalId); - return !s.Step(); - } - - void DatabaseWrapperBase::SetProtectedPatient(int64_t internalId, - bool isProtected) - { - if (isProtected) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); - s.BindInt64(0, internalId); - s.Run(); - } - else if (IsProtectedPatient(internalId)) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); - s.BindInt64(0, internalId); - s.Run(); - } - else - { - // Nothing to do: The patient is already unprotected - } - } - - - - bool DatabaseWrapperBase::IsExistingResource(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM Resources WHERE internalId=?"); - s.BindInt64(0, internalId); - return s.Step(); - } - - - - void DatabaseWrapperBase::LookupIdentifier(std::list<int64_t>& target, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND "); - - std::auto_ptr<SQLite::Statement> s; - - switch (type) - { - case IdentifierConstraintType_GreaterOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); - break; - - case IdentifierConstraintType_SmallerOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); - break; - - case IdentifierConstraintType_Wildcard: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); - break; - - case IdentifierConstraintType_Equal: - default: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); - break; - } - - assert(s.get() != NULL); - - s->BindInt(0, level); - s->BindInt(1, tag.GetGroup()); - s->BindInt(2, tag.GetElement()); - s->BindString(3, value); - - target.clear(); - - while (s->Step()) - { - target.push_back(s->ColumnInt64(0)); - } - } - - - void DatabaseWrapperBase::LookupIdentifierRange(std::list<int64_t>& target, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - { - SQLite::Statement statement(db_, SQLITE_FROM_HERE, - "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?"); - - statement.BindInt(0, level); - statement.BindInt(1, tag.GetGroup()); - statement.BindInt(2, tag.GetElement()); - statement.BindString(3, start); - statement.BindString(4, end); - - target.clear(); - - while (statement.Step()) - { - target.push_back(statement.ColumnInt64(0)); - } - } -}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,200 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 - -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" -#include "../../../../OrthancFramework/Sources/Enumerations.h" -#include "../../../../OrthancFramework/Sources/FileStorage/FileInfo.h" -#include "../../../../OrthancFramework/Sources/SQLite/Connection.h" -#include "../../../Sources/ExportedResource.h" -#include "../../../Sources/ServerIndexChange.h" -#include "../../../Sources/ServerEnumerations.h" - -#include <list> - - -namespace Orthanc -{ - /** - * This class is shared between the Orthanc core and the sample - * database plugin whose code is in - * "../Plugins/Samples/DatabasePlugin". - **/ - class DatabaseWrapperBase - { - private: - SQLite::Connection& db_; - - ErrorCode GetChangesInternal(std::list<ServerIndexChange>& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void GetExportedResourcesInternal(std::list<ExportedResource>& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - public: - DatabaseWrapperBase(SQLite::Connection& db) : db_(db) - { - } - - void SetGlobalProperty(GlobalProperty property, - const std::string& value); - - bool LookupGlobalProperty(std::string& target, - GlobalProperty property); - - int64_t CreateResource(const std::string& publicId, - ResourceType type); - - bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId); - - ErrorCode LookupParent(bool& found, - int64_t& parentId, - int64_t resourceId); - - bool GetPublicId(std::string& result, - int64_t resourceId); - - ErrorCode GetResourceType(ResourceType& result, - int64_t resourceId); - - void AttachChild(int64_t parent, - int64_t child); - - void SetMetadata(int64_t id, - MetadataType type, - const std::string& value); - - void DeleteMetadata(int64_t id, - MetadataType type); - - bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type); - - void ListAvailableMetadata(std::list<MetadataType>& target, - int64_t id); - - void AddAttachment(int64_t id, - const FileInfo& attachment); - - void DeleteAttachment(int64_t id, - FileContentType attachment); - - void ListAvailableAttachments(std::list<FileContentType>& target, - int64_t id); - - bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType); - - - void ClearMainDicomTags(int64_t id); - - - void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - void GetMainDicomTags(DicomMap& map, - int64_t id); - - void GetChildrenPublicId(std::list<std::string>& target, - int64_t id); - - void GetChildrenInternalId(std::list<int64_t>& target, - int64_t id); - - void LogChange(int64_t internalId, - const ServerIndexChange& change); - - ErrorCode GetChanges(std::list<ServerIndexChange>& target, - bool& done, - int64_t since, - uint32_t maxResults); - - ErrorCode GetLastChange(std::list<ServerIndexChange>& target); - - void LogExportedResource(const ExportedResource& resource); - - void GetExportedResources(std::list<ExportedResource>& target, - bool& done, - int64_t since, - uint32_t maxResults); - - void GetLastExportedResource(std::list<ExportedResource>& target); - - uint64_t GetTotalCompressedSize(); - - uint64_t GetTotalUncompressedSize(); - - void GetAllInternalIds(std::list<int64_t>& target, - ResourceType resourceType); - - void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType); - - void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - size_t since, - size_t limit); - - uint64_t GetResourceCount(ResourceType resourceType); - - bool SelectPatientToRecycle(int64_t& internalId); - - bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); - - bool IsProtectedPatient(int64_t internalId); - - void SetProtectedPatient(int64_t internalId, - bool isProtected); - - bool IsExistingResource(int64_t internalId); - - void LookupIdentifier(std::list<int64_t>& result, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - void LookupIdentifierRange(std::list<int64_t>& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end); - }; -} -
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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 "Database.h" - -#include <memory> -#include <iostream> -#include <boost/algorithm/string/predicate.hpp> - -static OrthancPluginContext* context_ = NULL; -static std::auto_ptr<OrthancPlugins::IDatabaseBackend> backend_; - - -extern "C" -{ - ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) - { - context_ = c; - OrthancPluginLogWarning(context_, "Sample plugin is initializing"); - - /* Check the version of the Orthanc core */ - if (OrthancPluginCheckVersion(c) == 0) - { - char info[256]; - sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", - c->orthancVersion, - ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); - OrthancPluginLogError(context_, info); - return -1; - } - - std::string path = "SampleDatabase.sqlite"; - uint32_t argCount = OrthancPluginGetCommandLineArgumentsCount(context_); - for (uint32_t i = 0; i < argCount; i++) - { - char* tmp = OrthancPluginGetCommandLineArgument(context_, i); - std::string argument(tmp); - OrthancPluginFreeString(context_, tmp); - - if (boost::starts_with(argument, "--database=")) - { - path = argument.substr(11); - } - } - - std::string s = "Using the following SQLite database: " + path; - OrthancPluginLogWarning(context_, s.c_str()); - - backend_.reset(new Database(path)); - OrthancPlugins::DatabaseBackendAdapter::Register(context_, *backend_); - - return 0; - } - - ORTHANC_PLUGINS_API void OrthancPluginFinalize() - { - backend_.reset(NULL); - } - - ORTHANC_PLUGINS_API const char* OrthancPluginGetName() - { - return "sample-database"; - } - - - ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() - { - return "1.0"; - } -}
--- a/OrthancServer/Resources/Graveyard/SetupAnonymization2011.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,258 +0,0 @@ - /** - * This is a manual implementation by Alain Mazy. Only kept for reference. - * https://bitbucket.org/sjodogne/orthanc/commits/c6defdc4c611fca2ab528ba2c6937a742e0329a8?at=issue-46-anonymization - **/ - - void DicomModification::SetupAnonymization2011() - { - // This is Table E.1-1 from PS 3.15-2011 - DICOM Part 15: Security and System Management Profiles - // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2011/11_15pu.pdf - - removals_.insert(DicomTag(0x0000, 0x1000)); // Affected SOP Instance UID - removals_.insert(DicomTag(0x0000, 0x1001)); // Requested SOP Instance UID - removals_.insert(DicomTag(0x0002, 0x0003)); // Media Storage SOP Instance UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances - removals_.insert(DicomTag(0x0004, 0x1511)); // Referenced SOP Instance UID in File - removals_.insert(DicomTag(0x0008, 0x0010)); // Irradiation Event UID - removals_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID - //removals_.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set in Apply() - clearings_.insert(DicomTag(0x0008, 0x0020)); // Study Date - clearings_.insert(DicomTag(0x0008, 0x0021)); // Series Date - clearings_.insert(DicomTag(0x0008, 0x0030)); // Study Time - clearings_.insert(DicomTag(0x0008, 0x0031)); // Series Time - removals_.insert(DicomTag(0x0008, 0x0022)); // Acquisition Date - removals_.insert(DicomTag(0x0008, 0x0023)); // Content Date - removals_.insert(DicomTag(0x0008, 0x0024)); // Overlay Date - removals_.insert(DicomTag(0x0008, 0x0025)); // Curve Date - removals_.insert(DicomTag(0x0008, 0x002a)); // Acquisition DateTime - removals_.insert(DicomTag(0x0008, 0x0032)); // Acquisition Time - removals_.insert(DicomTag(0x0008, 0x0033)); // Content Time - removals_.insert(DicomTag(0x0008, 0x0034)); // Overlay Time - removals_.insert(DicomTag(0x0008, 0x0035)); // Curve Time - removals_.insert(DicomTag(0x0008, 0x0050)); // Accession Number - removals_.insert(DicomTag(0x0008, 0x0058)); // Failed SOP Instance UID List - removals_.insert(DicomTag(0x0008, 0x0080)); // Institution Name - removals_.insert(DicomTag(0x0008, 0x0081)); // Institution Address - removals_.insert(DicomTag(0x0008, 0x0082)); // Institution Code Sequence - 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, 0x0096)); // Referring Physician's Identification Sequence - removals_.insert(DicomTag(0x0008, 0x010d)); // Context Group Extension Creator UID - removals_.insert(DicomTag(0x0008, 0x0201)); // Timezone Offset From UTC - removals_.insert(DicomTag(0x0008, 0x0300)); // Current Patient Location - 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, 0x1049)); // Physician(s) of Record Identification Sequence - removals_.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name - removals_.insert(DicomTag(0x0008, 0x1052)); // Performing Physicians Identification Sequence - removals_.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study - removals_.insert(DicomTag(0x0008, 0x1062)); // Physician Reading Study Identification Sequence - removals_.insert(DicomTag(0x0008, 0x1070)); // Operators' Name - removals_.insert(DicomTag(0x0008, 0x1072)); // Operators' Identification Sequence - removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description - removals_.insert(DicomTag(0x0008, 0x1084)); // Admitting Diagnoses Code Sequence - removals_.insert(DicomTag(0x0008, 0x1110)); // Referenced Study Sequence - removals_.insert(DicomTag(0x0008, 0x1111)); // Referenced Performed Procedure Step Sequence - removals_.insert(DicomTag(0x0008, 0x1120)); // Referenced Patient Sequence - removals_.insert(DicomTag(0x0008, 0x1140)); // Referenced Image Sequence - removals_.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID - removals_.insert(DicomTag(0x0008, 0x1195)); // Transaction UID - removals_.insert(DicomTag(0x0008, 0x2111)); // Derivation Description - removals_.insert(DicomTag(0x0008, 0x2112)); // Source Image Sequence - removals_.insert(DicomTag(0x0008, 0x4000)); // Identifying Comments - removals_.insert(DicomTag(0x0008, 0x9123)); // Creator Version UID - //removals_.insert(DicomTag(0x0010, 0x0010)); // Patient's Name => cf. below (*) - //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 - clearings_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex - removals_.insert(DicomTag(0x0010, 0x0050)); // Patient's Insurance Plan Code Sequence - removals_.insert(DicomTag(0x0010, 0x0101)); // Patient's Primary Language Code Sequence - removals_.insert(DicomTag(0x0010, 0x0102)); // Patient's Primary Language Modifier Code Sequence - removals_.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids - removals_.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names - removals_.insert(DicomTag(0x0010, 0x1002)); // Other Patient IDs Sequence - removals_.insert(DicomTag(0x0010, 0x1005)); // Patient's Birth Name - 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, 0x1040)); // Patient's Address - removals_.insert(DicomTag(0x0010, 0x1050)); // Insurance Plan Identification - removals_.insert(DicomTag(0x0010, 0x1060)); // Patient's Mother's Birth Name - removals_.insert(DicomTag(0x0010, 0x1080)); // Military Rank - removals_.insert(DicomTag(0x0010, 0x1081)); // Branch of Service - removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator - removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts - removals_.insert(DicomTag(0x0010, 0x2110)); // Allergies - removals_.insert(DicomTag(0x0010, 0x2150)); // Country of Residence - removals_.insert(DicomTag(0x0010, 0x2152)); // Region of Residence - removals_.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers - removals_.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group - removals_.insert(DicomTag(0x0010, 0x2180)); // Occupation - removals_.insert(DicomTag(0x0010, 0x21a0)); // Smoking Status - removals_.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History - removals_.insert(DicomTag(0x0010, 0x21c0)); // Pregnancy Status - removals_.insert(DicomTag(0x0010, 0x21d0)); // Last Menstrual Date - removals_.insert(DicomTag(0x0010, 0x21f0)); // Patient's Religious Preference - removals_.insert(DicomTag(0x0010, 0x2203)); // Patient's Sex Neutered - removals_.insert(DicomTag(0x0010, 0x2297)); // Responsible Person - removals_.insert(DicomTag(0x0010, 0x2299)); // Responsible Organization - removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments - removals_.insert(DicomTag(0x0018, 0x0010)); // Contrast Bolus Agent - removals_.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number - removals_.insert(DicomTag(0x0018, 0x1002)); // Device UID - removals_.insert(DicomTag(0x0018, 0x1004)); // Plate ID - removals_.insert(DicomTag(0x0018, 0x1005)); // Generator ID - removals_.insert(DicomTag(0x0018, 0x1007)); // Cassette ID - removals_.insert(DicomTag(0x0018, 0x1008)); // Gantry ID - removals_.insert(DicomTag(0x0018, 0x1030)); // Protocol Name - removals_.insert(DicomTag(0x0018, 0x1400)); // Acquisition Device Processing Description - removals_.insert(DicomTag(0x0018, 0x4000)); // Acquisition Comments - removals_.insert(DicomTag(0x0018, 0x700a)); // Detector ID - removals_.insert(DicomTag(0x0018, 0xa003)); // Contribution Description - removals_.insert(DicomTag(0x0018, 0x9424)); // Acquisition Protocol Description - //removals_.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => set in Apply() - //removals_.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => set in Apply() - 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, 0x3401)); // Modifying Device ID - removals_.insert(DicomTag(0x0020, 0x3404)); // Modifying Device Manufacturer - removals_.insert(DicomTag(0x0020, 0x3406)); // Modified Image Description - removals_.insert(DicomTag(0x0020, 0x4000)); // Image Comments - removals_.insert(DicomTag(0x0020, 0x9158)); // Frame Comments - removals_.insert(DicomTag(0x0020, 0x9161)); // Concatenation UID - removals_.insert(DicomTag(0x0020, 0x9164)); // Dimension Organization UID - //removals_.insert(DicomTag(0x0028, 0x1199)); // Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances - //removals_.insert(DicomTag(0x0028, 0x1214)); // Large Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances - removals_.insert(DicomTag(0x0028, 0x4000)); // Image Presentation Comments - removals_.insert(DicomTag(0x0032, 0x0012)); // Study ID Issuer - removals_.insert(DicomTag(0x0032, 0x1020)); // Scheduled Study Location - removals_.insert(DicomTag(0x0032, 0x1021)); // Scheduled Study Location AE Title - removals_.insert(DicomTag(0x0032, 0x1030)); // Reason for Study - removals_.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician - removals_.insert(DicomTag(0x0032, 0x1033)); // Requesting Service - removals_.insert(DicomTag(0x0032, 0x1060)); // Requesting Procedure Description - removals_.insert(DicomTag(0x0032, 0x1070)); // Requested Contrast Agent - removals_.insert(DicomTag(0x0032, 0x4000)); // Study Comments - removals_.insert(DicomTag(0x0038, 0x0010)); // Admission ID - removals_.insert(DicomTag(0x0038, 0x0011)); // Issuer of Admission ID - removals_.insert(DicomTag(0x0038, 0x001e)); // Scheduled Patient Institution Residence - removals_.insert(DicomTag(0x0038, 0x0020)); // Admitting Date - removals_.insert(DicomTag(0x0038, 0x0021)); // Admitting Time - removals_.insert(DicomTag(0x0038, 0x0040)); // Discharge Diagnosis Description - removals_.insert(DicomTag(0x0038, 0x0050)); // Special Needs - removals_.insert(DicomTag(0x0038, 0x0060)); // Service Episode ID - removals_.insert(DicomTag(0x0038, 0x0061)); // Issuer of Service Episode ID - removals_.insert(DicomTag(0x0038, 0x0062)); // Service Episode Description - removals_.insert(DicomTag(0x0038, 0x0400)); // Patient's Institution Residence - removals_.insert(DicomTag(0x0038, 0x0500)); // Patient State - removals_.insert(DicomTag(0x0038, 0x4000)); // Visit Comments - removals_.insert(DicomTag(0x0038, 0x1234)); // Referenced Patient Alias Sequence - removals_.insert(DicomTag(0x0040, 0x0001)); // Scheduled Station AE Title - removals_.insert(DicomTag(0x0040, 0x0002)); // Scheduled Procedure Step Start Date - removals_.insert(DicomTag(0x0040, 0x0003)); // Scheduled Procedure Step Start Time - removals_.insert(DicomTag(0x0040, 0x0004)); // Scheduled Procedure Step End Date - removals_.insert(DicomTag(0x0040, 0x0005)); // Scheduled Procedure Step End Time - removals_.insert(DicomTag(0x0040, 0x0006)); // Scheduled Performing Physician Name - removals_.insert(DicomTag(0x0040, 0x0007)); // Scheduled Procedure Step Description - removals_.insert(DicomTag(0x0040, 0x000b)); // Scheduled Performing Physician Identification Sequence - removals_.insert(DicomTag(0x0040, 0x0010)); // Scheduled Station Name - removals_.insert(DicomTag(0x0040, 0x0011)); // Scheduled Procedure Step Location - removals_.insert(DicomTag(0x0040, 0x0012)); // Pre-Medication - removals_.insert(DicomTag(0x0040, 0x0241)); // Performed Station AE Title - removals_.insert(DicomTag(0x0040, 0x0242)); // Performed Station Name - removals_.insert(DicomTag(0x0040, 0x0243)); // Performed Location - removals_.insert(DicomTag(0x0040, 0x0244)); // Performed Procedure Step Start Date - removals_.insert(DicomTag(0x0040, 0x0245)); // Performed Procedure Step Start Time - removals_.insert(DicomTag(0x0040, 0x0248)); // Performed Station Name Code Sequence - removals_.insert(DicomTag(0x0040, 0x0253)); // Performed Procedure Step ID - removals_.insert(DicomTag(0x0040, 0x0254)); // Performed Procedure Step Description - removals_.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence - removals_.insert(DicomTag(0x0040, 0x0280)); // Comments on Performed Procedure Step - removals_.insert(DicomTag(0x0040, 0x0555)); // Acquisition Context Sequence - removals_.insert(DicomTag(0x0040, 0x1001)); // Requested Procedure ID - removals_.insert(DicomTag(0x0040, 0x1010)); // Names of Intended Recipient of Results - removals_.insert(DicomTag(0x0040, 0x1011)); // Intended Recipient of Results Identification Sequence - removals_.insert(DicomTag(0x0040, 0x1004)); // Patient Transport Arrangements - removals_.insert(DicomTag(0x0040, 0x1005)); // Requested Procedure Location - removals_.insert(DicomTag(0x0040, 0x1101)); // Person Identification Code Sequence - removals_.insert(DicomTag(0x0040, 0x1102)); // Person Address - removals_.insert(DicomTag(0x0040, 0x1103)); // Person Telephone Numbers - removals_.insert(DicomTag(0x0040, 0x1400)); // Requested Procedure Comments - removals_.insert(DicomTag(0x0040, 0x2001)); // Reason for Imaging Service Request - removals_.insert(DicomTag(0x0040, 0x2008)); // Order Entered By - removals_.insert(DicomTag(0x0040, 0x2009)); // Order Enterer Location - removals_.insert(DicomTag(0x0040, 0x2010)); // Order Callback Phone Number - removals_.insert(DicomTag(0x0040, 0x2016)); // Placer Order Number of Imaging Service Request - removals_.insert(DicomTag(0x0040, 0x2017)); // Filler Order Number of Imaging Service Request - removals_.insert(DicomTag(0x0040, 0x2400)); // Imaging Service Request Comments - removals_.insert(DicomTag(0x0040, 0x4023)); // Referenced General Purpose Scheduled Procedure Step Transaction UID - removals_.insert(DicomTag(0x0040, 0x4025)); // Scheduled Station Name Code Sequence - removals_.insert(DicomTag(0x0040, 0x4027)); // Scheduled Station Geographic Location Code Sequence - removals_.insert(DicomTag(0x0040, 0x4030)); // Performed Station Geographic Location Code Sequence - removals_.insert(DicomTag(0x0040, 0x4034)); // Scheduled Human Performers Sequence - removals_.insert(DicomTag(0x0040, 0x4035)); // Actual Human Performers Sequence - removals_.insert(DicomTag(0x0040, 0x4036)); // Human Performers Organization - removals_.insert(DicomTag(0x0040, 0x4037)); // Human Performers Name - removals_.insert(DicomTag(0x0040, 0xa027)); // Verifying Organization - removals_.insert(DicomTag(0x0040, 0xa073)); // Verifying Observer Sequence - removals_.insert(DicomTag(0x0040, 0xa075)); // Verifying Observer Name - removals_.insert(DicomTag(0x0040, 0xa078)); // Author Observer Sequence - removals_.insert(DicomTag(0x0040, 0xa07a)); // Participant Sequence - removals_.insert(DicomTag(0x0040, 0xa07c)); // Custodial Organization Sequence - removals_.insert(DicomTag(0x0040, 0xa088)); // Verifying Observer Identification Code Sequence - removals_.insert(DicomTag(0x0040, 0xa123)); // Person Name - removals_.insert(DicomTag(0x0040, 0xa124)); // UID - removals_.insert(DicomTag(0x0040, 0xa730)); // Content Sequence - removals_.insert(DicomTag(0x0040, 0x3001)); // Confidentiality Constraint on Patient Data Description - removals_.insert(DicomTag(0x0040, 0xdb0c)); // Template Extension Organization UID - removals_.insert(DicomTag(0x0040, 0xdb0d)); // Template Extension Creator UID - removals_.insert(DicomTag(0x0070, 0x0001)); // Graphic Annotation Sequence - removals_.insert(DicomTag(0x0070, 0x0084)); // Content Creator's Name - removals_.insert(DicomTag(0x0070, 0x0086)); // Content Creator's Identification Code Sequence - removals_.insert(DicomTag(0x0070, 0x031a)); // Fiducial UID - removals_.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID - removals_.insert(DicomTag(0x0088, 0x0200)); // Icon Image Sequence - removals_.insert(DicomTag(0x0088, 0x0904)); // Topic Title - removals_.insert(DicomTag(0x0088, 0x0906)); // Topic Subject - removals_.insert(DicomTag(0x0088, 0x0910)); // Topic Author - removals_.insert(DicomTag(0x0088, 0x0912)); // Topic Key Words - removals_.insert(DicomTag(0x0400, 0x0100)); // Digital Signature UID - removals_.insert(DicomTag(0x0400, 0x0402)); // Referenced Digital Signature Sequence - removals_.insert(DicomTag(0x0400, 0x0403)); // Referenced SOP Instance MAC Sequence - removals_.insert(DicomTag(0x0400, 0x0404)); // MAC - removals_.insert(DicomTag(0x0400, 0x0550)); // Modified Attributes Sequence - removals_.insert(DicomTag(0x0400, 0x0561)); // Original Attributes Sequence - removals_.insert(DicomTag(0x2030, 0x0020)); // Text String - removals_.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID - removals_.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID - removals_.insert(DicomTag(0x300a, 0x0013)); // Dose Reference UID - removals_.insert(DicomTag(0x300e, 0x0008)); // Reviewer Name - removals_.insert(DicomTag(0x4000, 0x0010)); // Arbitrary - removals_.insert(DicomTag(0x4000, 0x4000)); // Text Comments - removals_.insert(DicomTag(0x4008, 0x0042)); // Results ID Issuer - removals_.insert(DicomTag(0x4008, 0x0102)); // Interpretation Recorder - removals_.insert(DicomTag(0x4008, 0x010a)); // Interpretation Transcriber - removals_.insert(DicomTag(0x4008, 0x010b)); // Interpretation Text - removals_.insert(DicomTag(0x4008, 0x010c)); // Interpretation Author - removals_.insert(DicomTag(0x4008, 0x0111)); // Interpretation Approver Sequence - removals_.insert(DicomTag(0x4008, 0x0114)); // Physician Approving Interpretation - removals_.insert(DicomTag(0x4008, 0x0115)); // Interpretation Diagnosis Description - removals_.insert(DicomTag(0x4008, 0x0118)); // Results Distribution List Sequence - removals_.insert(DicomTag(0x4008, 0x0119)); // Distribution Name - removals_.insert(DicomTag(0x4008, 0x011a)); // Distribution Address - removals_.insert(DicomTag(0x4008, 0x0202)); // Interpretation ID Issuer - removals_.insert(DicomTag(0x4008, 0x0300)); // Impressions - removals_.insert(DicomTag(0x4008, 0x4000)); // Results Comments - removals_.insert(DicomTag(0xfffa, 0xfffa)); // Digital Signature Sequence - removals_.insert(DicomTag(0xfffc, 0xfffc)); // Data Set Trailing Padding - //removals_.insert(DicomTag(0x60xx, 0x4000)); // Overlay Comments => TODO - //removals_.insert(DicomTag(0x60xx, 0x3000)); // Overlay Data => TODO - - // Set the DeidentificationMethod tag - ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2011); - }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/ImplementationNotes/DatabasesClassHierarchy.txt Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,30 @@ +The main object to access the DB is the ServerIndex class that is accessible from the ServerContext. + +ServerIndex inherits from StatelessDatabaseOperations. + +StatelessDatabaseOperations owns an IDatabaseWrapper member (db). +StatelessDatabaseOperations has 2 internal Transaction classes (ReadOnlyTransactions and ReadWriteTransactions) that implements the DB +operations by calling the methods from IDatabaseWrapper:ITransaction. + +IDatabaseWrapper has 2 direct derived classes: +- BaseDatabaseWrapper which simply provides a "not implemented" implementation of new methods to its derived classes: + - OrthancPluginDatabase that is a legacy plugin interface + - OrthancPluginDatabaseV3 that is a legacy plugin interface + - SQLiteDatabaseWrapper that is used by the default SQLite DB in Orthanc +- OrthancPluginDatabaseV4 that is the latest plugin interface and uses protobuf + +When you add a new method in the DB (e.g: UpdateAndGetStatistics with a new signature), you must: +- define it as a member of StatelessDatabaseOperations +- define it as a member of StatelessDatabaseOperations::ReadWriteTransactions or StatelessDatabaseOperations::ReadOnlyTransactions +- define it as a member of IDatabaseWrapper:ITransaction +- define it in OrthancDatabasePlugin.proto (new request + new response + new message) +- define it in OrthancPluginDatabaseV4 +- define a NotImplemented default implementation in BaseDatabaseWrapper +- optionally define it in SQLiteDatabaseWrapper if it can be implemented in SQLite +- very likely define it as a DbCapabilities in IDatabaseWrapper::DbCapabilities (e.g: Has/SetHasUpdateAndGetStatistics()) such that the Orthanc + core knows if it can use it or not. + +Then, in the orthanc-databases repo, you should: +- define it as a virtual member of IDatabaseBackend +- define it as a member of IndexBackend +- add a handler for the new protobuf message in DatabaseBackendAdapterV4
--- a/OrthancServer/Resources/Orthanc.doxygen Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Resources/Orthanc.doxygen Fri Nov 29 10:54:20 2024 +0100 @@ -755,6 +755,7 @@ # Note: If this tag is empty the current directory is searched. INPUT = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \ + @CMAKE_SOURCE_DIR@/Plugins/Engine \ @CMAKE_SOURCE_DIR@/Sources # This tag can be used to specify the character encoding of the source files
--- a/OrthancServer/Resources/RunCppCheck.sh Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Resources/RunCppCheck.sh Fri Nov 29 10:54:20 2024 +0100 @@ -16,8 +16,8 @@ stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 -stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:374 -stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:378 +stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65 +stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:480 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191 stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361 @@ -34,9 +34,9 @@ useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026 -assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:290 -assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:389 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3663 +assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 +assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3527 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454 EOF @@ -55,7 +55,6 @@ -DJSONCPP_VERSION_MAJOR=1 \ -DJSONCPP_VERSION_MINOR=0 \ -DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0 \ - -DORTHANC_BUILDING_SERVER_LIBRARY=1 \ -DORTHANC_BUILD_UNIT_TESTS=1 \ -DORTHANC_ENABLE_BASE64=1 \ -DORTHANC_ENABLE_CIVETWEB=1 \
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -24,6 +24,7 @@ #include "BaseDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "Compatibility/GenericFind.h" namespace Orthanc { @@ -45,6 +46,56 @@ throw OrthancException(ErrorCode_NotImplemented); // Not supported } + void BaseDatabaseWrapper::BaseTransaction::PerformDbHousekeeping() + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + void BaseDatabaseWrapper::BaseTransaction::GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list<std::string>& identifiers, + const Capabilities& capabilities, + const FindRequest& request) + { + Compatibility::GenericFind find(*this, *this); + find.ExecuteFind(identifiers, capabilities, request); + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + Compatibility::GenericFind find(*this, *this); + find.ExecuteExpand(response, capabilities, request, identifier); + } + uint64_t BaseDatabaseWrapper::MeasureLatency() {
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Fri Nov 29 10:54:20 2024 +0100 @@ -34,7 +34,9 @@ class BaseDatabaseWrapper : public IDatabaseWrapper { public: - class BaseTransaction : public IDatabaseWrapper::ITransaction + class BaseTransaction : + public IDatabaseWrapper::ITransaction, + public IDatabaseWrapper::ICompatibilityTransaction { public: virtual int64_t IncrementGlobalProperty(GlobalProperty property, @@ -47,8 +49,39 @@ int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) ORTHANC_OVERRIDE; + + virtual void PerformDbHousekeeping() ORTHANC_OVERRIDE; + + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(std::list<std::string>& identifiers, + const Capabilities& capabilities, + const FindRequest& request) ORTHANC_OVERRIDE; + + virtual void ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE; + + virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE; }; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE + { + return false; + } }; }
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -74,7 +74,7 @@ } } - void Add(const DatabaseConstraint& constraint) + void Add(const DatabaseDicomTagConstraint& constraint) { constraints_.push_back(new DicomTagConstraint(constraint)); } @@ -84,7 +84,7 @@ static void ApplyIdentifierConstraint(SetOfResources& candidates, ILookupResources& compatibility, - const DatabaseConstraint& constraint, + const DatabaseDicomTagConstraint& constraint, ResourceType level) { std::list<int64_t> matches; @@ -134,8 +134,8 @@ static void ApplyIdentifierRange(SetOfResources& candidates, ILookupResources& compatibility, - const DatabaseConstraint& smaller, - const DatabaseConstraint& greater, + const DatabaseDicomTagConstraint& smaller, + const DatabaseDicomTagConstraint& greater, ResourceType level) { assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual && @@ -153,10 +153,10 @@ static void ApplyLevel(SetOfResources& candidates, IDatabaseWrapper::ITransaction& transaction, ILookupResources& compatibility, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType level) { - typedef std::set<const DatabaseConstraint*> SetOfConstraints; + typedef std::set<const DatabaseDicomTagConstraint*> SetOfConstraints; typedef std::map<DicomTag, SetOfConstraints> Identifiers; // (1) Select which constraints apply to this level, and split @@ -168,7 +168,7 @@ for (size_t i = 0; i < lookup.GetSize(); i++) { - const DatabaseConstraint& constraint = lookup.GetConstraint(i); + const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i); if (constraint.GetLevel() == level) { @@ -191,8 +191,8 @@ { // Check whether some range constraint over identifiers is // present at this level - const DatabaseConstraint* smaller = NULL; - const DatabaseConstraint* greater = NULL; + const DatabaseDicomTagConstraint* smaller = NULL; + const DatabaseDicomTagConstraint* greater = NULL; for (SetOfConstraints::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) @@ -308,7 +308,7 @@ void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, size_t limit) {
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h Fri Nov 29 10:54:20 2024 +0100 @@ -46,7 +46,7 @@ void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, size_t limit); };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,677 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" + +#include <stack> + + +namespace Orthanc +{ + namespace Compatibility + { + static bool IsRequestWithoutContraint(const FindRequest& request) + { + return (request.GetDicomTagConstraints().IsEmpty() && + request.GetMetadataConstraintsCount() == 0 && + request.GetLabels().empty() && + request.GetOrdering().empty()); + } + + static void GetChildren(std::list<int64_t>& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<int64_t> tmp; + transaction.GetChildrenInternalId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildren(std::list<std::string>& target, + IDatabaseWrapper::ICompatibilityTransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<std::string> tmp; + transaction.GetChildrenPublicId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildrenIdentifiers(std::list<std::string>& children, + IDatabaseWrapper::ITransaction& transaction, + IDatabaseWrapper::ICompatibilityTransaction& compatibilityTransaction, + const OrthancIdentifiers& identifiers, + ResourceType topLevel, + ResourceType bottomLevel) + { + if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) || + topLevel == bottomLevel) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::list<int64_t> currentResources; + ResourceType currentLevel; + + { + int64_t id; + if (!transaction.LookupResource(id, currentLevel, identifiers.GetLevel(topLevel)) || + currentLevel != topLevel) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + currentResources.push_back(id); + } + + while (currentLevel != bottomLevel) + { + ResourceType nextLevel = GetChildResourceType(currentLevel); + if (nextLevel == bottomLevel) + { + GetChildren(children, compatibilityTransaction, currentResources); + } + else + { + std::list<int64_t> nextResources; + GetChildren(nextResources, transaction, currentResources); + currentResources.swap(nextResources); + } + + currentLevel = nextLevel; + } + } + + void GenericFind::ExecuteFind(std::list<std::string>& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request) + { + if (!request.GetLabels().empty() && + !capabilities.HasLabelsSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); + } + + if (!request.GetOrdering().empty()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support ordering"); + } + + if (IsRequestWithoutContraint(request) && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + if (!request.HasLimits()) + { + transaction_.GetAllPublicIds(identifiers, request.GetLevel()); + } + else if (request.GetLimitsCount() != 0) + { + compatibilityTransaction_.GetAllPublicIdsCompatibility(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount()); + } + else + { + // Starting with Orthanc 1.12.5, "limit=0" means "no limit" + std::list<std::string> tmp; + transaction_.GetAllPublicIds(tmp, request.GetLevel()); + + size_t count = 0; + for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) + { + if (count >= request.GetLimitsSince()) + { + identifiers.push_back(*it); + } + + count++; + } + } + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Patient && + request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetPatientId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Study && + !request.GetOrthancIdentifiers().HasPatientId() && + request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetStudyId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Series && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetSeriesId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Instance && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetInstanceId()); + } + else if (IsRequestWithoutContraint(request) && + (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) && + request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_, + request.GetOrthancIdentifiers(), ResourceType_Patient, request.GetLevel()); + } + else if (IsRequestWithoutContraint(request) && + (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) && + !request.GetOrthancIdentifiers().HasPatientId() && + request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_, + request.GetOrthancIdentifiers(), ResourceType_Study, request.GetLevel()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Instance && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_, + request.GetOrthancIdentifiers(), ResourceType_Series, request.GetLevel()); + } + else if (request.GetMetadataConstraintsCount() == 0 && + request.GetOrdering().empty() && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + transaction_.ApplyLookupResources(identifiers, NULL /* TODO-FIND: Could the "instancesId" information be exploited? */, + request.GetDicomTagConstraints(), request.GetLevel(), request.GetLabels(), + request.GetLabelsConstraint(), request.HasLimits() ? request.GetLimitsCount() : 0); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void GenericFind::RetrieveMainDicomTags(FindResponse::Resource& target, + ResourceType level, + int64_t internalId) + { + DicomMap m; + transaction_.GetMainDicomTags(m, internalId); + + DicomArray a(m); + for (size_t i = 0; i < a.GetSize(); i++) + { + const DicomElement& element = a.GetElement(i); + if (element.GetValue().IsString()) + { + target.AddStringDicomTag(level, element.GetTag().GetGroup(), + element.GetTag().GetElement(), element.GetValue().GetContent()); + } + else + { + throw OrthancException(ErrorCode_BadParameterType); + } + } + } + + + static ResourceType GetTopLevelOfInterest(const FindRequest& request) + { + switch (request.GetLevel()) + { + case ResourceType_Patient: + return ResourceType_Patient; + + case ResourceType_Study: + if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else + { + return ResourceType_Study; + } + + case ResourceType_Series: + if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else + { + return ResourceType_Series; + } + + case ResourceType_Instance: + if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else if (request.GetParentSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else + { + return ResourceType_Instance; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static ResourceType GetBottomLevelOfInterest(const FindRequest& request) + { + switch (request.GetLevel()) + { + case ResourceType_Patient: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else if (request.GetChildrenSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else + { + return ResourceType_Patient; + } + + case ResourceType_Study: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else + { + return ResourceType_Study; + } + + case ResourceType_Series: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else + { + return ResourceType_Series; + } + + case ResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void GenericFind::ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + int64_t internalId; + ResourceType level; + std::string parent; + + if (request.IsRetrieveParentIdentifier()) + { + if (!compatibilityTransaction_.LookupResourceAndParent(internalId, level, parent, identifier)) + { + return; // The resource is not available anymore + } + + if (level == ResourceType_Patient) + { + if (!parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else + { + if (parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + else + { + if (!transaction_.LookupResource(internalId, level, identifier)) + { + return; // The resource is not available anymore + } + } + + if (level != request.GetLevel()) + { + throw OrthancException(ErrorCode_UnknownResource, "Wrong resource level for this ID"); // this might happen e.g if you call /instances/... with a series instance id + } + + std::unique_ptr<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier)); + + if (request.IsRetrieveParentIdentifier()) + { + assert(!parent.empty()); + resource->SetParentIdentifier(parent); + } + + if (request.IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, level, internalId); + } + + if (request.IsRetrieveMetadata()) + { + std::map<MetadataType, std::string> metadata; + transaction_.GetAllMetadata(metadata, internalId); + + for (std::map<MetadataType, std::string>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + if (request.IsRetrieveMetadataRevisions() && + capabilities.HasRevisionsSupport()) + { + std::string value; + int64_t revision; + if (transaction_.LookupMetadata(value, revision, internalId, it->first) && + value == it->second) + { + resource->AddMetadata(level, it->first, it->second, revision); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else + { + resource->AddMetadata(level, it->first, it->second, 0 /* revision not requested */); + } + } + } + + { + const ResourceType topLevel = GetTopLevelOfInterest(request); + + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != topLevel) + { + int64_t parentId; + if (transaction_.LookupParent(parentId, currentId)) + { + currentId = parentId; + currentLevel = GetParentResourceType(currentLevel); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, currentLevel, currentId); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMetadata()) + { + std::map<MetadataType, std::string> metadata; + transaction_.GetAllMetadata(metadata, currentId); + + for (std::map<MetadataType, std::string>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + resource->AddMetadata(currentLevel, it->first, it->second, 0 /* revision not request */); + } + } + } + } + + if (capabilities.HasLabelsSupport() && + request.IsRetrieveLabels()) + { + compatibilityTransaction_.ListLabels(resource->GetLabels(), internalId); + } + + if (request.IsRetrieveAttachments()) + { + std::set<FileContentType> attachments; + transaction_.ListAvailableAttachments(attachments, internalId); + + for (std::set<FileContentType>::const_iterator it = attachments.begin(); it != attachments.end(); ++it) + { + FileInfo info; + int64_t revision; + if (transaction_.LookupAttachment(info, revision, internalId, *it) && + info.GetContentType() == *it) + { + resource->AddAttachment(info, revision); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + { + const ResourceType bottomLevel = GetBottomLevelOfInterest(request); + + std::list<int64_t> currentIds; + currentIds.push_back(internalId); + + ResourceType currentLevel = level; + + while (currentLevel != bottomLevel) + { + ResourceType childrenLevel = GetChildResourceType(currentLevel); + + if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers() || + request.GetChildrenSpecification(childrenLevel).IsRetrieveCount()) + { + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<std::string> ids; + compatibilityTransaction_.GetChildrenPublicId(ids, *it); + + for (std::list<std::string>::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2) + { + resource->AddChildIdentifier(childrenLevel, *it2); + } + resource->IncrementChildrenCount(childrenLevel, ids.size()); + } + } + + const std::set<MetadataType>& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata(); + + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + for (std::list<int64_t>::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2) + { + std::list<std::string> values; + transaction_.GetChildrenMetadata(values, *it2, *it); + + for (std::list<std::string>::const_iterator it3 = values.begin(); it3 != values.end(); ++it3) + { + resource->AddChildrenMetadataValue(childrenLevel, *it, *it3); + } + } + } + + const std::set<DicomTag>& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags(); + + if (childrenLevel != bottomLevel || + !mainDicomTags.empty()) + { + std::list<int64_t> childrenIds; + + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<int64_t> tmp; + transaction_.GetChildrenInternalId(tmp, *it); + + childrenIds.splice(childrenIds.end(), tmp); + } + + if (!mainDicomTags.empty()) + { + for (std::list<int64_t>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it) + { + DicomMap m; + transaction_.GetMainDicomTags(m, *it); + + for (std::set<DicomTag>::const_iterator it2 = mainDicomTags.begin(); it2 != mainDicomTags.end(); ++it2) + { + std::string value; + if (m.LookupStringValue(value, *it2, false /* no binary allowed */)) + { + resource->AddChildrenMainDicomTagValue(childrenLevel, *it2, value); + } + } + } + } + + currentIds = childrenIds; + } + else + { + currentIds.clear(); + } + + currentLevel = childrenLevel; + } + } + + if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != ResourceType_Instance) + { + std::list<int64_t> children; + transaction_.GetChildrenInternalId(children, currentId); + if (children.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + currentId = children.front(); + currentLevel = GetChildResourceType(currentLevel); + } + } + + std::map<MetadataType, std::string> metadata; + transaction_.GetAllMetadata(metadata, currentId); + + std::set<FileContentType> attachmentsType; + transaction_.ListAvailableAttachments(attachmentsType, currentId); + + std::map<FileContentType, FileInfo> attachments; + for (std::set<FileContentType>::const_iterator it = attachmentsType.begin(); it != attachmentsType.end(); ++it) + { + FileInfo info; + int64_t revision; // Unused in this case + if (transaction_.LookupAttachment(info, revision, currentId, *it)) + { + attachments[*it] = info; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + resource->SetOneInstanceMetadataAndAttachments(transaction_.GetPublicId(currentId), metadata, attachments); + } + + response.Add(resource.release()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + class GenericFind : public boost::noncopyable + { + private: + IDatabaseWrapper::ITransaction& transaction_; + IDatabaseWrapper::ICompatibilityTransaction& compatibilityTransaction_; + + void RetrieveMainDicomTags(FindResponse::Resource& target, + ResourceType level, + int64_t internalId); + + public: + explicit GenericFind(IDatabaseWrapper::ITransaction& transaction, + IDatabaseWrapper::ICompatibilityTransaction& compatibilityTransaction) : + transaction_(transaction), + compatibilityTransaction_(compatibilityTransaction) + { + } + + void ExecuteFind(std::list<std::string>& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request); + + void ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier); + }; + } +}
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -35,7 +35,7 @@ ILookupResources& compatibility, std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, size_t limit) {
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h Fri Nov 29 10:54:20 2024 +0100 @@ -60,7 +60,7 @@ ILookupResources& compatibility, std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, size_t limit); };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,285 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include "MainDicomTagsRegistry.h" + +#include <cassert> + + +namespace Orthanc +{ + FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_) || + level == level_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + return retrieveParentPatient_; + + case ResourceType_Study: + return retrieveParentStudy_; + + case ResourceType_Series: + return retrieveParentSeries_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindRequest::ChildrenSpecification& FindRequest::GetChildrenSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level_, level) || + level == level_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Study: + return retrieveChildrenStudies_; + + case ResourceType_Series: + return retrieveChildrenSeries_; + + case ResourceType_Instance: + return retrieveChildrenInstances_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindRequest::FindRequest(ResourceType level) : + level_(level), + hasLimits_(false), + limitsSince_(0), + limitsCount_(0), + labelsConstraint_(LabelsConstraint_All), + retrieveMainDicomTags_(false), + retrieveMetadata_(false), + retrieveMetadataRevisions_(false), + retrieveLabels_(false), + retrieveAttachments_(false), + retrieveParentIdentifier_(false), + retrieveOneInstanceMetadataAndAttachments_(false) + { + } + + + FindRequest::~FindRequest() + { + for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + + for (std::deque<DatabaseMetadataConstraint*>::iterator it = metadataConstraints_.begin(); it != metadataConstraints_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + + void FindRequest::SetOrthancId(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetOrthancPatientId(id); + break; + + case ResourceType_Study: + SetOrthancStudyId(id); + break; + + case ResourceType_Series: + SetOrthancSeriesId(id); + break; + + case ResourceType_Instance: + SetOrthancInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindRequest::SetOrthancPatientId(const std::string& id) + { + orthancIdentifiers_.SetPatientId(id); + } + + + void FindRequest::SetOrthancStudyId(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetStudyId(id); + } + } + + + void FindRequest::SetOrthancSeriesId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetSeriesId(id); + } + } + + + void FindRequest::SetOrthancInstanceId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetInstanceId(id); + } + } + + + void FindRequest::SetLimits(uint64_t since, + uint64_t count) + { + hasLimits_ = true; + limitsSince_ = since; + limitsCount_ = count; + } + + + uint64_t FindRequest::GetLimitsSince() const + { + if (hasLimits_) + { + return limitsSince_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + uint64_t FindRequest::GetLimitsCount() const + { + if (hasLimits_) + { + return limitsCount_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindRequest::AddOrdering(const DicomTag& tag, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(tag), direction)); + } + + + void FindRequest::AddOrdering(MetadataType metadataType, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(metadataType), direction)); + } + + + void FindRequest::AddMetadataConstraint(DatabaseMetadataConstraint* constraint) + { + metadataConstraints_.push_back(constraint); + } + + + void FindRequest::SetRetrieveParentIdentifier(bool retrieve) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + retrieveParentIdentifier_ = retrieve; + } + } + + + void FindRequest::SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve) + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + retrieveOneInstanceMetadataAndAttachments_ = retrieve; + } + } + + + bool FindRequest::IsRetrieveOneInstanceMetadataAndAttachments() const + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return retrieveOneInstanceMetadataAndAttachments_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,450 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../Search/DatabaseDicomTagConstraints.h" +#include "../Search/DatabaseMetadataConstraint.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" + +#include <deque> +#include <map> +#include <set> +#include <cassert> +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class MainDicomTagsRegistry; + + class FindRequest : public boost::noncopyable + { + public: + enum KeyType // used for ordering and filters + { + KeyType_DicomTag, + KeyType_Metadata + }; + + + enum OrderingDirection + { + OrderingDirection_Ascending, + OrderingDirection_Descending + }; + + + class Key + { + private: + KeyType type_; + DicomTag dicomTag_; + MetadataType metadata_; + + // TODO-FIND: to execute the query, we actually need: + // ResourceType level_; + // DicomTagType dicomTagType_; + // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind + + public: + explicit Key(const DicomTag& dicomTag) : + type_(KeyType_DicomTag), + dicomTag_(dicomTag), + metadata_(MetadataType_EndUser) + { + } + + explicit Key(MetadataType metadata) : + type_(KeyType_Metadata), + dicomTag_(0, 0), + metadata_(metadata) + { + } + + KeyType GetType() const + { + return type_; + } + + const DicomTag& GetDicomTag() const + { + assert(GetType() == KeyType_DicomTag); + return dicomTag_; + } + + MetadataType GetMetadataType() const + { + assert(GetType() == KeyType_Metadata); + return metadata_; + } + }; + + class Ordering : public boost::noncopyable + { + private: + OrderingDirection direction_; + Key key_; + + public: + Ordering(const Key& key, + OrderingDirection direction) : + direction_(direction), + key_(key) + { + } + + KeyType GetKeyType() const + { + return key_.GetType(); + } + + OrderingDirection GetDirection() const + { + return direction_; + } + + MetadataType GetMetadataType() const + { + return key_.GetMetadataType(); + } + + DicomTag GetDicomTag() const + { + return key_.GetDicomTag(); + } + }; + + + class ParentSpecification : public boost::noncopyable + { + private: + bool mainDicomTags_; + bool metadata_; + + public: + ParentSpecification() : + mainDicomTags_(false), + metadata_(false) + { + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + mainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return mainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + metadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return metadata_; + } + + bool IsOfInterest() const + { + return (mainDicomTags_ || metadata_); + } + }; + + + class ChildrenSpecification : public boost::noncopyable + { + private: + bool identifiers_; + std::set<MetadataType> metadata_; + std::set<DicomTag> mainDicomTags_; + bool count_; + + public: + ChildrenSpecification() : + identifiers_(false), + count_(false) + { + } + + void SetRetrieveIdentifiers(bool retrieve) + { + identifiers_ = retrieve; + } + + bool IsRetrieveIdentifiers() const + { + return identifiers_; + } + + void SetRetrieveCount(bool retrieve) + { + count_ = retrieve; + } + + bool IsRetrieveCount() const + { + return count_; + } + + void AddMetadata(MetadataType metadata) + { + metadata_.insert(metadata); + } + + const std::set<MetadataType>& GetMetadata() const + { + return metadata_; + } + + void AddMainDicomTag(const DicomTag& tag) + { + mainDicomTags_.insert(tag); + } + + const std::set<DicomTag>& GetMainDicomTags() const + { + return mainDicomTags_; + } + + bool IsOfInterest() const + { + return (identifiers_ || !metadata_.empty() || !mainDicomTags_.empty() || count_); + } + }; + + + private: + // filter & ordering fields + ResourceType level_; // The level of the response (the filtering on tags, labels and metadata also happens at this level) + OrthancIdentifiers orthancIdentifiers_; // The response must belong to this Orthanc resources hierarchy + DatabaseDicomTagConstraints dicomTagConstraints_; // All tags filters (note: the order is not important) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set<std::string> labels_; + LabelsConstraint labelsConstraint_; + + std::deque<Ordering*> ordering_; // The ordering criteria (note: the order is important !) + std::deque<DatabaseMetadataConstraint*> metadataConstraints_; // All metadata filters (note: the order is not important) + + bool retrieveMainDicomTags_; + bool retrieveMetadata_; + bool retrieveMetadataRevisions_; + bool retrieveLabels_; + bool retrieveAttachments_; + bool retrieveParentIdentifier_; + ParentSpecification retrieveParentPatient_; + ParentSpecification retrieveParentStudy_; + ParentSpecification retrieveParentSeries_; + ChildrenSpecification retrieveChildrenStudies_; + ChildrenSpecification retrieveChildrenSeries_; + ChildrenSpecification retrieveChildrenInstances_; + bool retrieveOneInstanceMetadataAndAttachments_; + + std::unique_ptr<MainDicomTagsRegistry> mainDicomTagsRegistry_; + + public: + explicit FindRequest(ResourceType level); + + ~FindRequest(); + + ResourceType GetLevel() const + { + return level_; + } + + void SetOrthancId(ResourceType level, + const std::string& id); + + void SetOrthancPatientId(const std::string& id); + + void SetOrthancStudyId(const std::string& id); + + void SetOrthancSeriesId(const std::string& id); + + void SetOrthancInstanceId(const std::string& id); + + const OrthancIdentifiers& GetOrthancIdentifiers() const + { + return orthancIdentifiers_; + } + + DatabaseDicomTagConstraints& GetDicomTagConstraints() + { + return dicomTagConstraints_; + } + + const DatabaseDicomTagConstraints& GetDicomTagConstraints() const + { + return dicomTagConstraints_; + } + + size_t GetMetadataConstraintsCount() const + { + return metadataConstraints_.size(); + } + + void ClearLimits() + { + hasLimits_ = false; + } + + void SetLimits(uint64_t since, + uint64_t count); + + bool HasLimits() const + { + return hasLimits_; + } + + uint64_t GetLimitsSince() const; + + uint64_t GetLimitsCount() const; + + void AddOrdering(const DicomTag& tag, + OrderingDirection direction); + + void AddOrdering(MetadataType metadataType, + OrderingDirection direction); + + const std::deque<Ordering*>& GetOrdering() const + { + return ordering_; + } + + void AddMetadataConstraint(DatabaseMetadataConstraint* constraint); + + const std::deque<DatabaseMetadataConstraint*>& GetMetadataConstraint() const + { + return metadataConstraints_; + } + + void SetLabels(const std::set<std::string>& labels) + { + labels_ = labels; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsConstraint_; + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + labelsConstraint_ = constraint; + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + retrieveMainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return retrieveMainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + retrieveMetadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return retrieveMetadata_; + } + + void SetRetrieveMetadataRevisions(bool retrieve) + { + retrieveMetadataRevisions_ = retrieve; + } + + bool IsRetrieveMetadataRevisions() const + { + return retrieveMetadataRevisions_; + } + + void SetRetrieveLabels(bool retrieve) + { + retrieveLabels_ = retrieve; + } + + bool IsRetrieveLabels() const + { + return retrieveLabels_; + } + + void SetRetrieveAttachments(bool retrieve) + { + retrieveAttachments_ = retrieve; + } + + bool IsRetrieveAttachments() const + { + return retrieveAttachments_; + } + + void SetRetrieveParentIdentifier(bool retrieve); + + bool IsRetrieveParentIdentifier() const + { + return retrieveParentIdentifier_; + } + + ParentSpecification& GetParentSpecification(ResourceType level); + + const ParentSpecification& GetParentSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetParentSpecification(level); + } + + ChildrenSpecification& GetChildrenSpecification(ResourceType level); + + const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetChildrenSpecification(level); + } + + void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve); + + bool IsRetrieveOneInstanceMetadataAndAttachments() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,904 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable + { + public: + enum ValueType + { + ValueType_String, + ValueType_Null + }; + + private: + ValueType type_; + std::string value_; + + public: + DicomValue(ValueType type, + const std::string& value) : + type_(type), + value_(value) + { + } + + ValueType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + switch (type_) + { + case ValueType_Null: + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case ValueType_String: + return value_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel() + { + for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group, + uint16_t element) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, ""); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const + { + for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + + switch (it->second->GetType()) + { + case DicomValue::ValueType_String: + target.SetValue(it->first, it->second->GetValue(), false /* not binary */); + break; + + case DicomValue::ValueType_Null: + target.SetNullValue(it->first); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + FindResponse::ChildrenInformation::~ChildrenInformation() + { + for (MetadataValues::iterator it = metadataValues_.begin(); it != metadataValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + for (MainDicomTagValues::iterator it = mainDicomTagValues_.begin(); it != mainDicomTagValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::ChildrenInformation::AddIdentifier(const std::string& identifier) + { + // The same identifier can be added through AddChildIdentifier and through AddOneInstanceIdentifier + identifiers_.insert(identifier); + } + + + void FindResponse::ChildrenInformation::AddMetadataValue(MetadataType metadata, + const std::string& value) + { + MetadataValues::iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + std::set<std::string> s; + s.insert(value); + metadataValues_[metadata] = new std::set<std::string>(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const + { + MetadataValues::const_iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + void FindResponse::ChildrenInformation::AddMainDicomTagValue(const DicomTag& tag, + const std::string& value) + { + MainDicomTagValues::iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + std::set<std::string> s; + s.insert(value); + mainDicomTagValues_[tag] = new std::set<std::string>(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const + { + MainDicomTagValues::const_iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + FindResponse::ChildrenInformation& FindResponse::Resource::GetChildrenInformation(ResourceType level) + { + switch (level) + { + case ResourceType_Study: + if (level_ == ResourceType_Patient) + { + return childrenStudiesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Series: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + return childrenSeriesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Instance: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + return childrenInstancesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindResponse::MainDicomTagsAtLevel& FindResponse::Resource::GetMainDicomTagsAtLevel(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return mainDicomTagsPatient_; + + case ResourceType_Study: + return mainDicomTagsStudy_; + + case ResourceType_Series: + return mainDicomTagsSeries_; + + case ResourceType_Instance: + return mainDicomTagsInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::GetAllMainDicomTags(DicomMap& target) const + { + switch (level_) + { + // Don't reorder or add "break" below + case ResourceType_Instance: + mainDicomTagsInstance_.Export(target); + + case ResourceType_Series: + mainDicomTagsSeries_.Export(target); + + case ResourceType_Study: + mainDicomTagsStudy_.Export(target); + + case ResourceType_Patient: + mainDicomTagsPatient_.Export(target); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value, + int64_t revision) + { + std::map<MetadataType, MetadataContent>& m = GetMetadata(level); + + if (m.find(metadata) != m.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + m[metadata] = MetadataContent(value, revision); + } + } + + + std::map<MetadataType, FindResponse::MetadataContent>& FindResponse::Resource::GetMetadata(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return metadataPatient_; + + case ResourceType_Study: + return metadataStudy_; + + case ResourceType_Series: + return metadataSeries_; + + case ResourceType_Instance: + return metadataInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool FindResponse::Resource::LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const + { + const std::map<MetadataType, MetadataContent>& m = GetMetadata(level); + + std::map<MetadataType, MetadataContent>::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second.GetValue(); + return true; + } + } + + + bool FindResponse::Resource::LookupMetadata(std::string& value, + int64_t& revision, + ResourceType level, + MetadataType metadata) const + { + const std::map<MetadataType, MetadataContent>& m = GetMetadata(level); + + std::map<MetadataType, MetadataContent>::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second.GetValue(); + revision = found->second.GetRevision(); + return true; + } + } + + + void FindResponse::Resource::SetParentIdentifier(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parentIdentifier_.reset(new std::string(id)); + } + } + + + const std::string& FindResponse::Resource::GetParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + return *parentIdentifier_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::HasParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return parentIdentifier_.get() != NULL; + } + } + + + void FindResponse::Resource::AddLabel(const std::string& label) + { + if (labels_.find(label) == labels_.end()) + { + labels_.insert(label); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::Resource::AddAttachment(const FileInfo& attachment, + int64_t revision) + { + if (attachments_.find(attachment.GetContentType()) == attachments_.end() && + revisions_.find(attachment.GetContentType()) == revisions_.end()) + { + attachments_[attachment.GetContentType()] = attachment; + revisions_[attachment.GetContentType()] = revision; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::LookupAttachment(FileInfo& target, + int64_t& revision, + FileContentType type) const + { + std::map<FileContentType, FileInfo>::const_iterator it = attachments_.find(type); + std::map<FileContentType, int64_t>::const_iterator it2 = revisions_.find(type); + + if (it != attachments_.end()) + { + if (it2 != revisions_.end()) + { + target = it->second; + revision = it2->second; + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + if (it2 == revisions_.end()) + { + return false; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void FindResponse::Resource::SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId, + const std::map<MetadataType, std::string>& metadata, + const std::map<FileContentType, FileInfo>& attachments) + { + if (hasOneInstanceMetadataAndAttachments_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (instancePublicId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + hasOneInstanceMetadataAndAttachments_ = true; + oneInstancePublicId_ = instancePublicId; + oneInstanceMetadata_ = metadata; + oneInstanceAttachments_ = attachments; + } + } + + + void FindResponse::Resource::SetOneInstancePublicId(const std::string& instancePublicId) + { + SetOneInstanceMetadataAndAttachments(instancePublicId, std::map<MetadataType, std::string>(), + std::map<FileContentType, FileInfo>()); + } + + + void FindResponse::Resource::AddOneInstanceMetadata(MetadataType metadata, + const std::string& value) + { + if (hasOneInstanceMetadataAndAttachments_) + { + if (oneInstanceMetadata_.find(metadata) == oneInstanceMetadata_.end()) + { + oneInstanceMetadata_[metadata] = value; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Metadata already exists"); + } + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::Resource::AddOneInstanceAttachment(const FileInfo& attachment) + { + if (hasOneInstanceMetadataAndAttachments_) + { + if (oneInstanceAttachments_.find(attachment.GetContentType()) == oneInstanceAttachments_.end()) + { + oneInstanceAttachments_[attachment.GetContentType()] = attachment; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Attachment already exists"); + } + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& FindResponse::Resource::GetOneInstancePublicId() const + { + if (hasOneInstanceMetadataAndAttachments_) + { + return oneInstancePublicId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::map<MetadataType, std::string>& FindResponse::Resource::GetOneInstanceMetadata() const + { + if (hasOneInstanceMetadataAndAttachments_) + { + return oneInstanceMetadata_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::map<FileContentType, FileInfo>& FindResponse::Resource::GetOneInstanceAttachments() const + { + if (hasOneInstanceMetadataAndAttachments_) + { + return oneInstanceAttachments_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + static void DebugDicomMap(Json::Value& target, + const DicomMap& m) + { + DicomArray a(m); + for (size_t i = 0; i < a.GetSize(); i++) + { + if (a.GetElement(i).GetValue().IsNull()) + { + target[a.GetElement(i).GetTag().Format()] = Json::nullValue; + } + else if (a.GetElement(i).GetValue().IsString()) + { + target[a.GetElement(i).GetTag().Format()] = a.GetElement(i).GetValue().GetContent(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + static void DebugMetadata(Json::Value& target, + const std::map<MetadataType, std::string>& m) + { + target = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second; + } + } + + + static void DebugMetadata(Json::Value& target, + const std::map<MetadataType, FindResponse::MetadataContent>& m) + { + target = Json::objectValue; + + for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator it = m.begin(); it != m.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second.GetValue(); + } + } + + + static void DebugAddAttachment(Json::Value& target, + const FileInfo& info) + { + Json::Value u = Json::arrayValue; + u.append(info.GetUuid()); + u.append(static_cast<Json::UInt64>(info.GetUncompressedSize())); + target[EnumerationToString(info.GetContentType())] = u; + } + + + static void DebugAttachments(Json::Value& target, + const std::map<FileContentType, FileInfo>& attachments) + { + target = Json::objectValue; + for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin(); + it != attachments.end(); ++it) + { + if (it->first != it->second.GetContentType()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + DebugAddAttachment(target, it->second); + } + } + } + + + static void DebugSetOfStrings(Json::Value& target, + const std::set<std::string>& values) + { + target = Json::arrayValue; + for (std::set<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + target.append(*it); + } + } + + + void FindResponse::Resource::DebugExport(Json::Value& target, + const FindRequest& request) const + { + target = Json::objectValue; + + target["Level"] = EnumerationToString(GetLevel()); + target["ID"] = GetIdentifier(); + + if (request.IsRetrieveParentIdentifier()) + { + target["ParentID"] = GetParentIdentifier(); + } + + if (request.IsRetrieveMainDicomTags()) + { + DicomMap m; + GetMainDicomTags(m, request.GetLevel()); + DebugDicomMap(target[EnumerationToString(GetLevel())]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata()) + { + DebugMetadata(target[EnumerationToString(GetLevel())]["Metadata"], GetMetadata(request.GetLevel())); + } + + static const ResourceType levels[4] = { ResourceType_Patient, ResourceType_Study, ResourceType_Series, ResourceType_Instance }; + + for (size_t i = 0; i < 4; i++) + { + const char* level = EnumerationToString(levels[i]); + + if (levels[i] != request.GetLevel() && + IsResourceLevelAboveOrEqual(levels[i], request.GetLevel())) + { + if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags()) + { + DicomMap m; + GetMainDicomTags(m, levels[i]); + DebugDicomMap(target[level]["MainDicomTags"], m); + } + + if (request.GetParentSpecification(levels[i]).IsRetrieveMetadata()) + { + DebugMetadata(target[level]["Metadata"], GetMetadata(levels[i])); + } + } + + if (levels[i] != request.GetLevel() && + IsResourceLevelAboveOrEqual(request.GetLevel(), levels[i])) + { + if (request.GetChildrenSpecification(levels[i]).IsRetrieveIdentifiers()) + { + DebugSetOfStrings(target[level]["Identifiers"], GetChildrenInformation(levels[i]).GetIdentifiers()); + } + + const std::set<MetadataType>& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata(); + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMetadataValues(values, *it); + DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values); + } + + const std::set<DicomTag>& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags(); + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMainDicomTagValues(values, *it); + DebugSetOfStrings(target[level]["MainDicomTags"][it->Format()], values); + } + } + } + + if (request.IsRetrieveLabels()) + { + DebugSetOfStrings(target["Labels"], labels_); + } + + if (request.IsRetrieveAttachments()) + { + DebugAttachments(target["Attachments"], attachments_); + } + + if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + DebugMetadata(target["OneInstance"]["Metadata"], GetOneInstanceMetadata()); + DebugAttachments(target["OneInstance"]["Attachments"], GetOneInstanceAttachments()); + } + } + + + FindResponse::~FindResponse() + { + for (size_t i = 0; i < items_.size(); i++) + { + assert(items_[i] != NULL); + delete items_[i]; + } + } + + + void FindResponse::Add(Resource* item /* takes ownership */) + { + std::unique_ptr<Resource> protection(item); + + if (item == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (!items_.empty() && + items_[0]->GetLevel() != item->GetLevel()) + { + throw OrthancException(ErrorCode_BadParameterType, "A find response must only contain resources of the same type"); + } + else + { + const std::string& id = item->GetIdentifier(); + int64_t internalId = item->GetInternalId(); + + if (identifierIndex_.find(id) == identifierIndex_.end() && internalIdIndex_.find(internalId) == internalIdIndex_.end()) + { + items_.push_back(protection.release()); + identifierIndex_[id] = item; + internalIdIndex_[internalId] = item; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "This resource has already been added: " + id + "/" + boost::lexical_cast<std::string>(internalId)); + } + } + } + + + const FindResponse::Resource& FindResponse::GetResourceByIndex(size_t index) const + { + if (index >= items_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(items_[index] != NULL); + return *items_[index]; + } + } + + + FindResponse::Resource& FindResponse::GetResourceByIdentifier(const std::string& id) + { + IdentifierIndex::const_iterator found = identifierIndex_.find(id); + + if (found == identifierIndex_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return *found->second; + } + } + + + FindResponse::Resource& FindResponse::GetResourceByInternalId(int64_t internalId) + { + InternalIdIndex::const_iterator found = internalIdIndex_.find(internalId); + + if (found == internalIdIndex_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return *found->second; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,435 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "FindRequest.h" + +#include <boost/noncopyable.hpp> +#include <deque> +#include <map> +#include <set> +#include <list> + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + private: + class MainDicomTagsAtLevel : public boost::noncopyable + { + private: + class DicomValue; + + typedef std::map<DicomTag, DicomValue*> MainDicomTags; + + MainDicomTags mainDicomTags_; + + public: + ~MainDicomTagsAtLevel(); + + void AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value); + + // The "Null" value could be used in the future to indicate a + // value that is not available, typically a new "ExtraMainDicomTag" + void AddNullDicomTag(uint16_t group, + uint16_t element); + + void Export(DicomMap& target) const; + }; + + class ChildrenInformation : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::set<std::string>* > MetadataValues; + typedef std::map<DicomTag, std::set<std::string>* > MainDicomTagValues; + + std::set<std::string> identifiers_; + uint64_t count_; + MetadataValues metadataValues_; + MainDicomTagValues mainDicomTagValues_; + + public: + ChildrenInformation() + : count_(0) + { + } + + ~ChildrenInformation(); + + void AddIdentifier(const std::string& identifier); + + const std::set<std::string>& GetIdentifiers() const + { + return identifiers_; + } + + void SetCount(uint64_t count) + { + count_ = count; + } + + void IncrementCount(uint64_t count) + { + count_ += count; + } + + uint64_t GetCount() const + { + return count_; + } + + void AddMetadataValue(MetadataType metadata, + const std::string& value); + + void GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const; + + void AddMainDicomTagValue(const DicomTag& tag, + const std::string& value); + + void GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const; + }; + + + public: + class MetadataContent + { + private: + std::string value_; + int64_t revision_; + + public: + MetadataContent() : + revision_(0) + { + } + + MetadataContent(const std::string& value, + int64_t revision) : + value_(value), + revision_(revision) + { + } + + MetadataContent(const std::string& value) : + value_(value), + revision_(0) + { + } + + const std::string& GetValue() const + { + return value_; + } + + int64_t GetRevision() const + { + return revision_; + } + + void SetRevision(int64_t revision) + { + revision_ = revision; + } + }; + + + class Resource : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::list<std::string>*> ChildrenMetadata; + + ResourceType level_; + int64_t internalId_; // Internal ID of the resource in the database + std::string identifier_; + std::unique_ptr<std::string> parentIdentifier_; + MainDicomTagsAtLevel mainDicomTagsPatient_; + MainDicomTagsAtLevel mainDicomTagsStudy_; + MainDicomTagsAtLevel mainDicomTagsSeries_; + MainDicomTagsAtLevel mainDicomTagsInstance_; + std::map<MetadataType, MetadataContent> metadataPatient_; + std::map<MetadataType, MetadataContent> metadataStudy_; + std::map<MetadataType, MetadataContent> metadataSeries_; + std::map<MetadataType, MetadataContent> metadataInstance_; + ChildrenInformation childrenStudiesInformation_; + ChildrenInformation childrenSeriesInformation_; + ChildrenInformation childrenInstancesInformation_; + std::set<std::string> labels_; + std::map<FileContentType, FileInfo> attachments_; + std::map<FileContentType, int64_t> revisions_; + bool hasOneInstanceMetadataAndAttachments_; + std::string oneInstancePublicId_; + std::map<MetadataType, std::string> oneInstanceMetadata_; + std::map<FileContentType, FileInfo> oneInstanceAttachments_; + + MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level); + + const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level); + } + + ChildrenInformation& GetChildrenInformation(ResourceType level); + + const ChildrenInformation& GetChildrenInformation(ResourceType level) const + { + return const_cast<Resource&>(*this).GetChildrenInformation(level); + } + + public: + Resource(ResourceType level, + int64_t internalId, + const std::string& identifier) : + level_(level), + internalId_(internalId), + identifier_(identifier), + hasOneInstanceMetadataAndAttachments_(false) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + int64_t GetInternalId() const + { + return internalId_; + } + + const std::string& GetIdentifier() const + { + return identifier_; + } + + void SetParentIdentifier(const std::string& id); + + const std::string& GetParentIdentifier() const; + + bool HasParentIdentifier() const; + + void AddStringDicomTag(ResourceType level, + uint16_t group, + uint16_t element, + const std::string& value) + { + GetMainDicomTagsAtLevel(level).AddStringDicomTag(group, element, value); + } + + void AddNullDicomTag(ResourceType level, + uint16_t group, + uint16_t element) + { + GetMainDicomTagsAtLevel(level).AddNullDicomTag(group, element); + } + + void GetMainDicomTags(DicomMap& target, + ResourceType level) const + { + GetMainDicomTagsAtLevel(level).Export(target); + } + + void GetAllMainDicomTags(DicomMap& target) const; + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value, + int64_t revision); + + std::map<MetadataType, MetadataContent>& GetMetadata(ResourceType level); + + const std::map<MetadataType, MetadataContent>& GetMetadata(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMetadata(level); + } + + bool LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const; + + bool LookupMetadata(std::string& value, + int64_t& revision, + ResourceType level, + MetadataType metadata) const; + + void AddChildIdentifier(ResourceType level, + const std::string& childId) + { + GetChildrenInformation(level).AddIdentifier(childId); + } + + const std::set<std::string>& GetChildrenIdentifiers(ResourceType level) const + { + return GetChildrenInformation(level).GetIdentifiers(); + } + + void SetChildrenCount(ResourceType level, + uint64_t count) + { + GetChildrenInformation(level).SetCount(count); + } + + void IncrementChildrenCount(ResourceType level, + uint64_t count) + { + GetChildrenInformation(level).IncrementCount(count); + } + + uint64_t GetChildrenCount(ResourceType level) const + { + return GetChildrenInformation(level).GetCount(); + } + + void AddChildrenMetadataValue(ResourceType level, + MetadataType metadata, + const std::string& value) + { + GetChildrenInformation(level).AddMetadataValue(metadata, value); + } + + void GetChildrenMetadataValues(std::set<std::string>& values, + ResourceType level, + MetadataType metadata) const + { + GetChildrenInformation(level).GetMetadataValues(values, metadata); + } + + void AddChildrenMainDicomTagValue(ResourceType level, + const DicomTag& tag, + const std::string& value) + { + GetChildrenInformation(level).AddMainDicomTagValue(tag, value); + } + + void GetChildrenMainDicomTagValues(std::set<std::string>& values, + ResourceType level, + const DicomTag& tag) const + { + GetChildrenInformation(level).GetMainDicomTagValues(values, tag); + } + + void AddLabel(const std::string& label); + + std::set<std::string>& GetLabels() + { + return labels_; + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment, + int64_t revision); + + bool LookupAttachment(FileInfo& target, + int64_t& revision, + FileContentType type) const; + + const std::map<FileContentType, FileInfo>& GetAttachments() const + { + return attachments_; + } + + void SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId, + const std::map<MetadataType, std::string>& metadata, + const std::map<FileContentType, FileInfo>& attachments); + + void SetOneInstancePublicId(const std::string& instancePublicId); + + void AddOneInstanceMetadata(MetadataType metadata, + const std::string& value); + + void AddOneInstanceAttachment(const FileInfo& attachment); + + bool HasOneInstanceMetadataAndAttachments() const + { + return hasOneInstanceMetadataAndAttachments_; + } + + const std::string& GetOneInstancePublicId() const; + + const std::map<MetadataType, std::string>& GetOneInstanceMetadata() const; + + const std::map<FileContentType, FileInfo>& GetOneInstanceAttachments() const; + + void DebugExport(Json::Value& target, + const FindRequest& request) const; + }; + + private: + typedef std::map<std::string, Resource*> IdentifierIndex; + typedef std::map<int64_t, Resource*> InternalIdIndex; + + std::deque<Resource*> items_; + IdentifierIndex identifierIndex_; + InternalIdIndex internalIdIndex_; + + public: + ~FindResponse(); + + void Add(Resource* item /* takes ownership */); + + size_t GetSize() const + { + return items_.size(); + } + + const Resource& GetResourceByIndex(size_t index) const; + + Resource& GetResourceByIdentifier(const std::string& id); + + Resource& GetResourceByInternalId(int64_t internalId); + + const Resource& GetResourceByIdentifier(const std::string& id) const + { + return const_cast<FindResponse&>(*this).GetResourceByIdentifier(id); + } + + const Resource& GetResourceByInternalId(int64_t internalId) const + { + return const_cast<FindResponse&>(*this).GetResourceByInternalId(internalId); + } + + bool HasResource(const std::string& id) const + { + return (identifierIndex_.find(id) != identifierIndex_.end()); + } + + bool HasResource(int64_t& internalId) const + { + return (internalIdIndex_.find(internalId) != internalIdIndex_.end()); + } + }; +}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Nov 29 10:54:20 2024 +0100 @@ -29,6 +29,8 @@ #include "../ExportedResource.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerIndexChange.h" +#include "FindRequest.h" +#include "FindResponse.h" #include "IDatabaseListener.h" #include <list> @@ -37,7 +39,7 @@ namespace Orthanc { - class DatabaseConstraints; + class DatabaseDicomTagConstraints; class ResourcesContent; class IDatabaseWrapper : public boost::noncopyable @@ -52,6 +54,9 @@ bool hasAtomicIncrementGlobalProperty_; bool hasUpdateAndGetStatistics_; bool hasMeasureLatency_; + bool hasFindSupport_; + bool hasExtendedChanges_; + bool hasDbHousekeeping_; public: Capabilities() : @@ -60,7 +65,10 @@ hasLabelsSupport_(false), hasAtomicIncrementGlobalProperty_(false), hasUpdateAndGetStatistics_(false), - hasMeasureLatency_(false) + hasMeasureLatency_(false), + hasFindSupport_(false), + hasExtendedChanges_(false), + hasDbHousekeeping_(false) { } @@ -94,6 +102,16 @@ return hasLabelsSupport_; } + void SetHasExtendedChanges(bool value) + { + hasExtendedChanges_ = value; + } + + bool HasExtendedChanges() const + { + return hasExtendedChanges_; + } + void SetAtomicIncrementGlobalProperty(bool value) { hasAtomicIncrementGlobalProperty_ = value; @@ -104,7 +122,7 @@ return hasAtomicIncrementGlobalProperty_; } - void SetUpdateAndGetStatistics(bool value) + void SetHasUpdateAndGetStatistics(bool value) { hasUpdateAndGetStatistics_ = value; } @@ -114,6 +132,16 @@ return hasUpdateAndGetStatistics_; } + void SetHasDbHousekeeping(bool value) + { + hasDbHousekeeping_ = value; + } + + bool HasDbHousekeeping() const + { + return hasDbHousekeeping_; + } + void SetMeasureLatency(bool value) { hasMeasureLatency_ = value; @@ -123,6 +151,16 @@ { return hasMeasureLatency_; } + + void SetHasFindSupport(bool value) + { + hasFindSupport_ = value; + } + + bool HasFindSupport() const + { + return hasFindSupport_; + } }; @@ -176,11 +214,6 @@ virtual void GetAllPublicIds(std::list<std::string>& target, ResourceType resourceType) = 0; - virtual void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - int64_t since, - uint32_t limit) = 0; - virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/, bool& done /*out*/, int64_t since, @@ -189,9 +222,6 @@ virtual void GetChildrenInternalId(std::list<int64_t>& target, int64_t id) = 0; - virtual void GetChildrenPublicId(std::list<std::string>& target, - int64_t id) = 0; - virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/, bool& done /*out*/, int64_t since, @@ -282,7 +312,7 @@ virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, // Can be NULL if not needed - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, @@ -315,16 +345,6 @@ /** - * Primitives introduced in Orthanc 1.5.4 - **/ - - virtual bool LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) = 0; - - - /** * Primitives introduced in Orthanc 1.12.0 **/ @@ -334,10 +354,6 @@ virtual void RemoveLabel(int64_t resource, const std::string& label) = 0; - // List the labels of one single resource - virtual void ListLabels(std::set<std::string>& target, - int64_t resource) = 0; - // List all the labels that are present in any resource virtual void ListAllLabels(std::set<std::string>& target) = 0; @@ -345,12 +361,92 @@ int64_t increment, bool shared) = 0; + // New in Orthanc 1.12.3 virtual void UpdateAndGetStatistics(int64_t& patientsCount, int64_t& studiesCount, int64_t& seriesCount, int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) = 0; + + /** + * Primitives introduced in Orthanc 1.12.5 + **/ + + // This is only implemented if "HasIntegratedFind()" is "true" + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) = 0; + + // This is only implemented if "HasIntegratedFind()" is "true" + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) = 0; + + // This is only implemented if "HasIntegratedFind()" is "false" + virtual void ExecuteFind(std::list<std::string>& identifiers, + const Capabilities& capabilities, + const FindRequest& request) = 0; + + /** + * This is only implemented if "HasIntegratedFind()" is + * "false". In this flavor, the resource of interest might have + * been deleted, as the expansion is not done in the same + * transaction as the "ExecuteFind()". In such cases, the + * wrapper should not throw an exception, but simply ignore the + * request to expand the resource (i.e., "response" must not be + * modified). + **/ + virtual void ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) = 0; + + // New in Orthanc 1.12.5 + virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType) = 0; + + // New in Orthanc 1.12.5 + virtual void PerformDbHousekeeping() = 0; + }; + + + // TODO-FIND: Could this interface be removed? + class ICompatibilityTransaction : public boost::noncopyable + { + public: + virtual ~ICompatibilityTransaction() + { + } + + virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target, + ResourceType resourceType, + int64_t since, + uint32_t limit) = 0; + + virtual void GetChildrenPublicId(std::list<std::string>& target, + int64_t id) = 0; + + /** + * Primitives introduced in Orthanc 1.5.4 + **/ + + virtual bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) = 0; + + /** + * Primitives introduced in Orthanc 1.12.0 + **/ + + // List the labels of one single resource + virtual void ListLabels(std::set<std::string>& target, + int64_t resource) = 0; }; @@ -375,5 +471,9 @@ virtual const Capabilities GetDatabaseCapabilities() const = 0; virtual uint64_t MeasureLatency() = 0; + + // Returns "true" iff. the database engine supports the + // simultaneous find and expansion of resources. + virtual bool HasIntegratedFind() const = 0; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,142 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "MainDicomTagsRegistry.h" + +#include "../ServerToolbox.h" + +namespace Orthanc +{ + void MainDicomTagsRegistry::LoadTags(ResourceType level) + { + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); + } + else + { + // These patient-level tags are copied in the study level + assert(level == ResourceType_Study && + (tags[i] == DICOM_TAG_PATIENT_ID || + tags[i] == DICOM_TAG_PATIENT_NAME || + tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); + } + } + } + + { + std::set<DicomTag> tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set<DicomTag>::const_iterator + tag = tags.begin(); tag != tags.end(); ++tag) + { + if (registry_.find(*tag) == registry_.end()) + { + registry_[*tag] = TagInfo(level, DicomTagType_Main); + } + } + } + } + + + MainDicomTagsRegistry::MainDicomTagsRegistry() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + + void MainDicomTagsRegistry::LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const + { + Registry::const_iterator it = registry_.find(tag); + + if (it == registry_.end()) + { + // Default values + level = ResourceType_Instance; + type = DicomTagType_Generic; + } + else + { + level = it->second.GetLevel(); + type = it->second.GetType(); + } + } + + + bool MainDicomTagsRegistry::NormalizeLookup(DatabaseDicomTagConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const + { + bool isEquivalentLookup = true; + + target.Clear(); + + for (size_t i = 0; i < source.GetConstraintsCount(); i++) + { + ResourceType level; + DicomTagType type; + + LookupTag(level, type, source.GetConstraint(i).GetTag()); + + if (type == DicomTagType_Identifier || + type == DicomTagType_Main) + { + // Use the fact that patient-level tags are copied at the study level + if (level == ResourceType_Patient && + queryLevel != ResourceType_Patient) + { + level = ResourceType_Study; + } + + bool isEquivalentConstraint; + target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type)); + + if (!isEquivalentConstraint) + { + isEquivalentLookup = false; + } + } + else + { + isEquivalentLookup = false; + } + } + + return isEquivalentLookup; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,89 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../Search/DatabaseLookup.h" +#include "../Search/DatabaseDicomTagConstraints.h" + +#include <boost/noncopyable.hpp> + + +namespace Orthanc +{ + class MainDicomTagsRegistry : public boost::noncopyable + { + private: + class TagInfo + { + private: + ResourceType level_; + DicomTagType type_; + + public: + TagInfo() + { + } + + TagInfo(ResourceType level, + DicomTagType type) : + level_(level), + type_(type) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + DicomTagType GetType() const + { + return type_; + } + }; + + typedef std::map<DicomTag, TagInfo> Registry; + + Registry registry_; + + void LoadTags(ResourceType level); + + public: + MainDicomTagsRegistry(); + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const; + + /** + * Returns "true" iff. the normalized lookup is the same as the + * original DatabaseLookup. If "false" is returned, the target + * constraints are less strict than the original DatabaseLookup, + * so more resources will match them. + **/ + bool NormalizeLookup(DatabaseDicomTagConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,248 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other) + { + if (other.HasPatientId()) + { + SetPatientId(other.GetPatientId()); + } + + if (other.HasStudyId()) + { + SetStudyId(other.GetStudyId()); + } + + if (other.HasSeriesId()) + { + SetSeriesId(other.GetSeriesId()); + } + + if (other.HasInstanceId()) + { + SetInstanceId(other.GetInstanceId()); + } + } + + + void OrthancIdentifiers::SetPatientId(const std::string& id) + { + if (HasPatientId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + patientId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetPatientId() const + { + if (HasPatientId()) + { + return *patientId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetStudyId(const std::string& id) + { + if (HasStudyId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + studyId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetStudyId() const + { + if (HasStudyId()) + { + return *studyId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetSeriesId(const std::string& id) + { + if (HasSeriesId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + seriesId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetSeriesId() const + { + if (HasSeriesId()) + { + return *seriesId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetInstanceId(const std::string& id) + { + if (HasInstanceId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instanceId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetInstanceId() const + { + if (HasInstanceId()) + { + return *instanceId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ResourceType OrthancIdentifiers::DetectLevel() const + { + if (HasPatientId() && + !HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Patient; + } + else if (// HasPatientId() && + HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Study; + } + else if (// HasPatientId() && + // HasStudyId() && + HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Series; + } + else if (// HasPatientId() && + // HasStudyId() && + // HasSeriesId() && + HasInstanceId()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + void OrthancIdentifiers::SetLevel(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetPatientId(id); + break; + + case ResourceType_Study: + SetStudyId(id); + break; + + case ResourceType_Series: + SetSeriesId(id); + break; + + case ResourceType_Instance: + SetInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + std::string OrthancIdentifiers::GetLevel(ResourceType level) const + { + switch (level) + { + case ResourceType_Patient: + return GetPatientId(); + + case ResourceType_Study: + return GetStudyId(); + + case ResourceType_Series: + return GetSeriesId(); + + case ResourceType_Instance: + return GetInstanceId(); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + bool OrthancIdentifiers::IsDefined() const + { + return HasPatientId() || HasStudyId() || HasSeriesId() || HasInstanceId(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,95 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include <boost/noncopyable.hpp> +#include <string> + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr<std::string> patientId_; + std::unique_ptr<std::string> studyId_; + std::unique_ptr<std::string> seriesId_; + std::unique_ptr<std::string> instanceId_; + + public: + OrthancIdentifiers() + { + } + + OrthancIdentifiers(const OrthancIdentifiers& other); + + void SetPatientId(const std::string& id); + + bool HasPatientId() const + { + return patientId_.get() != NULL; + } + + const std::string& GetPatientId() const; + + void SetStudyId(const std::string& id); + + bool HasStudyId() const + { + return studyId_.get() != NULL; + } + + const std::string& GetStudyId() const; + + void SetSeriesId(const std::string& id); + + bool HasSeriesId() const + { + return seriesId_.get() != NULL; + } + + const std::string& GetSeriesId() const; + + void SetInstanceId(const std::string& id); + + bool HasInstanceId() const + { + return instanceId_.get() != NULL; + } + + const std::string& GetInstanceId() const; + + ResourceType DetectLevel() const; + + void SetLevel(ResourceType level, + const std::string& id); + + std::string GetLevel(ResourceType level) const; + + bool IsDefined() const; + }; +}
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -29,6 +29,7 @@ #include "../../../OrthancFramework/Sources/SQLite/Transaction.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerToolbox.h" +#include "Compatibility/GenericFind.h" #include "Compatibility/ICreateInstance.h" #include "Compatibility/IGetChildrenMetadata.h" #include "Compatibility/ILookupResourceAndParent.h" @@ -42,6 +43,53 @@ namespace Orthanc { + static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec) + { + std::set<std::string> metadataTypes; + for (std::set<MetadataType>::const_iterator it = childrenSpec.GetMetadata().begin(); it != childrenSpec.GetMetadata().end(); ++it) + { + metadataTypes.insert(boost::lexical_cast<std::string>(*it)); + } + std::string joinedMetadataTypes; + Orthanc::Toolbox::JoinStrings(joinedMetadataTypes, metadataTypes, ", "); + + return joinedMetadataTypes; + } + + static std::string JoinRequestedTags(const FindRequest::ChildrenSpecification& childrenSpec) + { + // note: SQLite does not seem to support (tagGroup, tagElement) in ((x, y), (z, w)) in complex subqueries. + // Therefore, since we expect the requested tag list to be short, we write it as + // ((tagGroup = x AND tagElement = y ) OR (tagGroup = z AND tagElement = w)) + + std::string sql = " ("; + std::set<std::string> tags; + for (std::set<DicomTag>::const_iterator it = childrenSpec.GetMainDicomTags().begin(); it != childrenSpec.GetMainDicomTags().end(); ++it) + { + tags.insert("(tagGroup = " + boost::lexical_cast<std::string>(it->GetGroup()) + + " AND tagElement = " + boost::lexical_cast<std::string>(it->GetElement()) + ")"); + } + std::string joinedTags; + Orthanc::Toolbox::JoinStrings(joinedTags, tags, " OR "); + + sql += joinedTags + ") "; + return sql; + } + + static std::string JoinChanges(const std::set<ChangeType>& changeTypes) + { + std::set<std::string> changeTypesString; + for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it) + { + changeTypesString.insert(boost::lexical_cast<std::string>(static_cast<uint32_t>(*it))); + } + + std::string joinedChangesTypes; + Orthanc::Toolbox::JoinStrings(joinedChangesTypes, changeTypesString, ", "); + + return joinedChangesTypes; + } + class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter { private: @@ -64,6 +112,28 @@ return "ESCAPE '\\'"; } + virtual std::string FormatLimits(uint64_t since, uint64_t count) ORTHANC_OVERRIDE + { + std::string sql; + + if (count > 0) + { + sql += " LIMIT " + boost::lexical_cast<std::string>(count); + } + + if (since > 0) + { + if (count == 0) + { + sql += " LIMIT -1"; // In SQLite, "OFFSET" cannot appear without "LIMIT" + } + + sql += " OFFSET " + boost::lexical_cast<std::string>(since); + } + + return sql; + } + virtual bool IsEscapeBrackets() const ORTHANC_OVERRIDE { return false; @@ -234,11 +304,12 @@ void GetChangesInternal(std::list<ServerIndexChange>& target, bool& done, SQLite::Statement& s, - uint32_t limit) + uint32_t limit, + bool returnFirstResults) // the statement usually returns limit+1 results while we only need the limit results -> we need to know which ones to return, the firsts or the lasts { target.clear(); - while (target.size() < limit && s.Step()) + while (s.Step()) { int64_t seq = s.ColumnInt64(0); ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1)); @@ -251,7 +322,22 @@ target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); } - done = !(target.size() == limit && s.Step()); + done = target.size() <= limit; // 'done' means "there are no more other changes of this type in that direction (depending on since/to)" + + // if we have retrieved more changes than requested -> cleanup + if (target.size() > limit) + { + assert(target.size() == limit+1); // the statement should only request 1 element more + + if (returnFirstResults) + { + target.pop_back(); + } + else + { + target.pop_front(); + } + } } @@ -340,7 +426,7 @@ virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, @@ -351,7 +437,7 @@ std::string sql; LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit); - sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; + sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; // TODO-FIND: use a CTE (or is this method obsolete ?) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); @@ -381,6 +467,753 @@ } } +#define C0_QUERY_ID 0 +#define C1_INTERNAL_ID 1 +#define C2_ROW_NUMBER 2 +#define C3_STRING_1 3 +#define C4_STRING_2 4 +#define C5_STRING_3 5 +#define C6_INT_1 6 +#define C7_INT_2 7 +#define C8_BIG_INT_1 8 +#define C9_BIG_INT_2 9 + +#define QUERY_LOOKUP 1 +#define QUERY_MAIN_DICOM_TAGS 2 +#define QUERY_ATTACHMENTS 3 +#define QUERY_METADATA 4 +#define QUERY_LABELS 5 +#define QUERY_PARENT_MAIN_DICOM_TAGS 10 +#define QUERY_PARENT_IDENTIFIER 11 +#define QUERY_PARENT_METADATA 12 +#define QUERY_GRAND_PARENT_MAIN_DICOM_TAGS 15 +#define QUERY_GRAND_PARENT_METADATA 16 +#define QUERY_CHILDREN_IDENTIFIERS 20 +#define QUERY_CHILDREN_MAIN_DICOM_TAGS 21 +#define QUERY_CHILDREN_METADATA 22 +#define QUERY_CHILDREN_COUNT 23 +#define QUERY_GRAND_CHILDREN_IDENTIFIERS 30 +#define QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS 31 +#define QUERY_GRAND_CHILDREN_METADATA 32 +#define QUERY_GRAND_CHILDREN_COUNT 33 +#define QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS 40 +#define QUERY_GRAND_GRAND_CHILDREN_COUNT 41 +#define QUERY_ONE_INSTANCE_IDENTIFIER 50 +#define QUERY_ONE_INSTANCE_METADATA 51 +#define QUERY_ONE_INSTANCE_ATTACHMENTS 52 + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + LookupFormatter formatter; + std::string sql; + + std::string lookupSql; + LookupFormatter::Apply(lookupSql, formatter, request); + + // base query, retrieve the ordered internalId and publicId of the selected resources + sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup"; + SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql); + formatter.Bind(s); + + s.Step(); + count = s.ColumnInt64(0); + } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + LookupFormatter formatter; + std::string sql; + const ResourceType requestLevel = request.GetLevel(); + + std::string lookupSql; + LookupFormatter::Apply(lookupSql, formatter, request); + + // base query, retrieve the ordered internalId and publicId of the selected resources + sql = "WITH Lookup AS (" + lookupSql + ") "; + + // in SQLite, all CTEs must be created at the beginning of the query, you can not define local CTE inside subqueries + // need one instance info ? (part 1: create the CTE) + if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + // Here, we create a nested CTE 'OneInstance' with one instance ID to join with metadata and main + sql += ", OneInstance AS"; + + switch (requestLevel) + { + case ResourceType_Series: + { + sql+= " (SELECT Lookup.internalId AS parentInternalId, childLevel.publicId AS instancePublicId, childLevel.internalId AS instanceInternalId" + " FROM Resources AS childLevel " + " INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) "; + break; + } + + case ResourceType_Study: + { + sql+= " (SELECT Lookup.internalId AS parentInternalId, grandChildLevel.publicId AS instancePublicId, grandChildLevel.internalId AS instanceInternalId" + " FROM Resources AS grandChildLevel " + " INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId " + " INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) "; + break; + } + + case ResourceType_Patient: + { + sql+= " (SELECT Lookup.internalId AS parentInternalId, grandGrandChildLevel.publicId AS instancePublicId, grandGrandChildLevel.internalId AS instanceInternalId" + " FROM Resources AS grandGrandChildLevel " + " INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId " + " INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId " + " INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) "; + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + sql += "SELECT " + " " TOSTRING(QUERY_LOOKUP) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " Lookup.rowNumber AS c2_rowNumber, " + " Lookup.publicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + " FROM Lookup "; + + // need one instance info ? (part 2: execute the queries) + if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + sql += " UNION SELECT" + " " TOSTRING(QUERY_ONE_INSTANCE_IDENTIFIER) " AS c0_queryId, " + " parentInternalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " instancePublicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " instanceInternalId AS c8_big_int1, " + " NULL AS c9_big_int2 " + " FROM OneInstance "; + + sql += " UNION SELECT" + " " TOSTRING(QUERY_ONE_INSTANCE_METADATA) " AS c0_queryId, " + " parentInternalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " Metadata.value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " Metadata.type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + " FROM OneInstance " + " INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId "; + + sql += " UNION SELECT" + " " TOSTRING(QUERY_ONE_INSTANCE_ATTACHMENTS) " AS c0_queryId, " + " parentInternalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " uuid AS c3_string1, " + " uncompressedMD5 AS c4_string2, " + " compressedMD5 AS c5_string3, " + " fileType AS c6_int1, " + " compressionType AS c7_int2, " + " compressedSize AS c8_big_int1, " + " uncompressedSize AS c9_big_int2 " + " FROM OneInstance " + " INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId "; + + } + + // need MainDicomTags from resource ? + if (request.IsRetrieveMainDicomTags()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_MAIN_DICOM_TAGS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " tagGroup AS c6_int1, " + " tagElement AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId "; + } + + // need resource metadata ? + if (request.IsRetrieveMetadata()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_METADATA) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Metadata ON Metadata.id = Lookup.internalId "; + } + + // need resource attachments ? + if (request.IsRetrieveAttachments()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_ATTACHMENTS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " uuid AS c3_string1, " + " uncompressedMD5 AS c4_string2, " + " compressedMD5 AS c5_string3, " + " fileType AS c6_int1, " + " compressionType AS c7_int2, " + " compressedSize AS c8_big_int1, " + " uncompressedSize AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId "; + } + + + // need resource labels ? + if (request.IsRetrieveLabels()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_LABELS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " label AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Labels ON Labels.id = Lookup.internalId "; + } + + if (requestLevel > ResourceType_Patient) + { + // need MainDicomTags from parent ? + if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMainDicomTags()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " tagGroup AS c6_int1, " + " tagElement AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " + "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId "; + } + + // need metadata from parent ? + if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMetadata()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_PARENT_METADATA) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " + "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId "; + } + + if (requestLevel > ResourceType_Study) + { + // need MainDicomTags from grandparent ? + if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMainDicomTags()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " tagGroup AS c6_int1, " + " tagElement AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " + "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " + "INNER JOIN MainDicomTags ON MainDicomTags.id = parentLevel.parentId "; + } + + // need metadata from grandparent ? + if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMetadata()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_PARENT_METADATA) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " + "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " + "INNER JOIN Metadata ON Metadata.id = parentLevel.parentId "; + } + } + } + + // need MainDicomTags from children ? + if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMainDicomTags().size() > 0) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " tagGroup AS c6_int1, " + " tagElement AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " + " INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); + } + + // need MainDicomTags from grandchildren ? + if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMainDicomTags().size() > 0) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " tagGroup AS c6_int1, " + " tagElement AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " + " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " + " INNER JOIN MainDicomTags ON MainDicomTags.id = grandChildLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))); + } + + // need parent identifier ? + if (request.IsRetrieveParentIdentifier()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_PARENT_IDENTIFIER) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " parentLevel.publicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId " + " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "; + } + + // need children metadata ? + if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMetadata().size() > 0) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_CHILDREN_METADATA) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " + " INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") "; + } + + // need grandchildren metadata ? + if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMetadata().size() > 0) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_CHILDREN_METADATA) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " value AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " type AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " + " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " + " INNER JOIN Metadata ON Metadata.id = grandChildLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))) + ") "; + } + + // need children identifiers ? + if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Study).IsRetrieveIdentifiers()) || + (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) || + (requestLevel == ResourceType_Series && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_CHILDREN_IDENTIFIERS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " childLevel.publicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "; + } + // no need to count if we have retrieved the list of identifiers + else if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Study).IsRetrieveCount()) || + (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveCount()) || + (requestLevel == ResourceType_Series && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount())) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_CHILDREN_COUNT) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " NULL AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " COUNT(*) AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId "; + } + + // need grandchildren identifiers ? + if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) || + (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " grandChildLevel.publicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " + "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "; + } + // no need to count if we have retrieved the list of identifiers + else if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveCount()) || + (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount())) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " NULL AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " COUNT(*) AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " + "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId "; + } + + // need grandgrandchildren identifiers ? + if (requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " grandGrandChildLevel.publicId AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " NULL AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " + "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " + "INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId "; + } + // no need to count if we have retrieved the list of identifiers + else if (requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount()) + { + sql += "UNION SELECT " + " " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_COUNT) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " NULL AS c2_rowNumber, " + " NULL AS c3_string1, " + " NULL AS c4_string2, " + " NULL AS c5_string3, " + " COUNT(*) AS c6_int1, " + " NULL AS c7_int2, " + " NULL AS c8_big_int1, " + " NULL AS c9_big_int2 " + "FROM Lookup " + "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " + "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " + "INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId GROUP BY Lookup.internalId "; + } + + + sql += " ORDER BY c0_queryId, c2_rowNumber"; // this is really important to make sure that the Lookup query is the first one to provide results since we use it to create the responses element ! + + SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql); + formatter.Bind(s); + + while (s.Step()) + { + int queryId = s.ColumnInt(C0_QUERY_ID); + int64_t internalId = s.ColumnInt64(C1_INTERNAL_ID); + + // LOG(INFO) << queryId << ": " << internalId; + // continue; + + assert(queryId == QUERY_LOOKUP || response.HasResource(internalId)); // the QUERY_LOOKUP must be read first and must create the response before any other query tries to populate the fields + + switch (queryId) + { + case QUERY_LOOKUP: + response.Add(new FindResponse::Resource(requestLevel, internalId, s.ColumnString(C3_STRING_1))); + break; + + case QUERY_LABELS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddLabel(s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_ATTACHMENTS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), + s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), + s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); + res.AddAttachment(file, 0 /* TODO - REVISIONS */); + }; break; + + case QUERY_MAIN_DICOM_TAGS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddStringDicomTag(requestLevel, + static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_PARENT_MAIN_DICOM_TAGS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), + static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), + static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_CHILDREN_MAIN_DICOM_TAGS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddMetadata(static_cast<ResourceType>(requestLevel), + static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + }; break; + + case QUERY_PARENT_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), + static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + }; break; + + case QUERY_GRAND_PARENT_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), + static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + }; break; + + case QUERY_CHILDREN_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), + static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_GRAND_CHILDREN_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), + static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_PARENT_IDENTIFIER: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.SetParentIdentifier(s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_CHILDREN_IDENTIFIERS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 1), + s.ColumnString(C3_STRING_1)); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1), + res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 1)).size()); + }; break; + + case QUERY_GRAND_CHILDREN_IDENTIFIERS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 2), + s.ColumnString(C3_STRING_1)); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2), + res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 2)).size()); + }; break; + + case QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 3), + s.ColumnString(C3_STRING_1)); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3), + res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 3)).size()); + }; break; + + case QUERY_CHILDREN_COUNT: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1), + static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + }; break; + + case QUERY_GRAND_CHILDREN_COUNT: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2), + static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + }; break; + + case QUERY_GRAND_GRAND_CHILDREN_COUNT: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3), + static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + }; break; + + case QUERY_ONE_INSTANCE_IDENTIFIER: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.SetOneInstancePublicId(s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_ONE_INSTANCE_METADATA: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1)); + }; break; + + case QUERY_ONE_INSTANCE_ATTACHMENTS: + { + FindResponse::Resource& res = response.GetResourceByInternalId(internalId); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), + s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), + s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); + res.AddOneInstanceAttachment(file); + }; break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } // From the "ICreateInstance" interface virtual void AttachChild(int64_t parent, @@ -510,22 +1343,16 @@ } - virtual void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - int64_t since, - uint32_t limit) ORTHANC_OVERRIDE + virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target, + ResourceType resourceType, + int64_t since, + uint32_t limit) ORTHANC_OVERRIDE { - if (limit == 0) - { - target.clear(); - return; - } - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE " "resourceType=? LIMIT ? OFFSET ?"); s.BindInt(0, resourceType); - s.BindInt64(1, limit); + s.BindInt64(1, limit == 0 ? -1 : limit); // In SQLite, setting "LIMIT" to "-1" means "no limit" s.BindInt64(2, since); target.clear(); @@ -541,10 +1368,72 @@ int64_t since, uint32_t limit) ORTHANC_OVERRIDE { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, limit + 1); - GetChangesInternal(target, done, s, limit); + std::set<ChangeType> filter; + GetChangesExtended(target, done, since, -1, limit, filter); + } + + virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE + { + std::vector<std::string> filters; + bool hasSince = false; + bool hasTo = false; + + if (since > 0) + { + hasSince = true; + filters.push_back("seq>?"); + } + if (to != -1) + { + hasTo = true; + filters.push_back("seq<=?"); + } + if (filterType.size() != 0) + { + filters.push_back("changeType IN ( " + JoinChanges(filterType) + " )"); + } + + std::string filtersString; + if (filters.size() > 0) + { + Toolbox::JoinStrings(filtersString, filters, " AND "); + filtersString = "WHERE " + filtersString; + } + + std::string sql; + bool returnFirstResults; + if (hasTo && !hasSince) + { + // in this case, we want the largest values in the LIMIT clause but we want them ordered in ascending order + sql = "SELECT * FROM (SELECT * FROM Changes " + filtersString + " ORDER BY seq DESC LIMIT ?) ORDER BY seq ASC"; + returnFirstResults = false; + } + else + { + // default query: we want the smallest values ordered in ascending order + sql = "SELECT * FROM Changes " + filtersString + " ORDER BY seq ASC LIMIT ?"; + returnFirstResults = true; + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql); + + int paramCounter = 0; + if (hasSince) + { + s.BindInt64(paramCounter++, since); + } + if (hasTo) + { + s.BindInt64(paramCounter++, to); + } + + s.BindInt(paramCounter++, limit + 1); // we take limit+1 because we use the +1 to know if "Done" must be set to true + GetChangesInternal(target, done, s, limit, returnFirstResults); } @@ -605,7 +1494,7 @@ { bool done; // Ignored SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); - GetChangesInternal(target, done, s, 1); + GetChangesInternal(target, done, s, 1, true); } @@ -1328,6 +2217,8 @@ // TODO: implement revisions in SQLite dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); + dbCapabilities_.SetHasExtendedChanges(true); + dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); db_.Open(path); } @@ -1340,6 +2231,8 @@ // TODO: implement revisions in SQLite dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); + dbCapabilities_.SetHasExtendedChanges(true); + dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); db_.OpenInMemory(); }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri Nov 29 10:54:20 2024 +0100 @@ -57,7 +57,8 @@ void GetChangesInternal(std::list<ServerIndexChange>& target, bool& done, SQLite::Statement& s, - uint32_t maxResults); + uint32_t maxResults, + bool returnFirstResults); void GetExportedResourcesInternal(std::list<ExportedResource>& target, bool& done, @@ -99,6 +100,12 @@ throw OrthancException(ErrorCode_NotImplemented); } + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE + { + //return true; // => This uses specialized SQL commands + return false; // => This uses Compatibility/GenericFind + } + /** * The "StartTransaction()" method is guaranteed to return a class * derived from "UnitTestsTransaction". The methods of
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -281,6 +281,10 @@ } target["Last"] = static_cast<int>(last); + if (!log.empty()) + { + target["First"] = static_cast<int>(log.front().GetSeq()); + } } @@ -300,141 +304,6 @@ } - class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable - { - private: - class TagInfo - { - private: - ResourceType level_; - DicomTagType type_; - - public: - TagInfo() - { - } - - TagInfo(ResourceType level, - DicomTagType type) : - level_(level), - type_(type) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomTagType GetType() const - { - return type_; - } - }; - - typedef std::map<DicomTag, TagInfo> Registry; - - - Registry registry_; - - void LoadTags(ResourceType level) - { - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (registry_.find(tags[i]) == registry_.end()) - { - registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); - } - else - { - // These patient-level tags are copied in the study level - assert(level == ResourceType_Study && - (tags[i] == DICOM_TAG_PATIENT_ID || - tags[i] == DICOM_TAG_PATIENT_NAME || - tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); - } - } - } - - { - std::set<DicomTag> tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set<DicomTag>::const_iterator - tag = tags.begin(); tag != tags.end(); ++tag) - { - if (registry_.find(*tag) == registry_.end()) - { - registry_[*tag] = TagInfo(level, DicomTagType_Main); - } - } - } - } - - void LookupTag(ResourceType& level, - DicomTagType& type, - const DicomTag& tag) const - { - Registry::const_iterator it = registry_.find(tag); - - if (it == registry_.end()) - { - // Default values - level = ResourceType_Instance; - type = DicomTagType_Generic; - } - else - { - level = it->second.GetLevel(); - type = it->second.GetType(); - } - } - - public: - MainDicomTagsRegistry() - { - LoadTags(ResourceType_Patient); - LoadTags(ResourceType_Study); - LoadTags(ResourceType_Series); - LoadTags(ResourceType_Instance); - } - - void NormalizeLookup(DatabaseConstraints& target, - const DatabaseLookup& source, - ResourceType queryLevel) const - { - target.Clear(); - - for (size_t i = 0; i < source.GetConstraintsCount(); i++) - { - ResourceType level; - DicomTagType type; - - LookupTag(level, type, source.GetConstraint(i).GetTag()); - - if (type == DicomTagType_Identifier || - type == DicomTagType_Main) - { - // Use the fact that patient-level tags are copied at the study level - if (level == ResourceType_Patient && - queryLevel != ResourceType_Patient) - { - level = ResourceType_Study; - } - - target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); - } - } - } - }; - - void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId, ChangeType changeType, ResourceType resourceType, @@ -612,6 +481,10 @@ else { assert(writeOperations != NULL); + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly, "The DB is trying to execute a ReadWrite transaction while Orthanc has been started in ReadOnly mode."); + } Transaction transaction(db_, *factory_, TransactionType_ReadWrite); { @@ -649,10 +522,11 @@ } - StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : + StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db, bool readOnly) : db_(db), mainDicomTagsRegistry_(new MainDicomTagsRegistry), - maxRetries_(0) + maxRetries_(0), + readOnly_(readOnly) { } @@ -706,284 +580,32 @@ { ApplyInternal(NULL, &operations); } - - - bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target, - const std::string& publicId, - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags) - { - class Operations : public ReadOnlyOperationsT6< - bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceFlags> + + + const FindResponse::Resource& StatelessDatabaseOperations::ExecuteSingleResource(FindResponse& response, + const FindRequest& request) + { + ExecuteFind(response, request); + + if (response.GetSize() == 0) { - private: - bool hasLabelsSupport_; - - static bool LookupStringMetadata(std::string& result, - const std::map<MetadataType, std::string>& metadata, - MetadataType type) - { - std::map<MetadataType, std::string>::const_iterator found = metadata.find(type); - - if (found == metadata.end()) - { - return false; - } - else - { - result = found->second; - return true; - } - } - - - static bool LookupIntegerMetadata(int64_t& result, - const std::map<MetadataType, std::string>& metadata, - MetadataType type) - { - std::string s; - if (!LookupStringMetadata(s, metadata, type)) - { - return false; - } - - try - { - result = boost::lexical_cast<int64_t>(s); - return true; - } - catch (boost::bad_lexical_cast&) - { - return false; - } - } - - - public: - explicit Operations(bool hasLabelsSupport) : - hasLabelsSupport_(hasLabelsSupport) - { - } - - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE + throw OrthancException(ErrorCode_UnknownResource); + } + else if (response.GetSize() == 1) + { + if (response.GetResourceByIndex(0).GetLevel() != request.GetLevel()) { - // Lookup for the requested resource - int64_t internalId; - ResourceType type; - std::string parent; - if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) || - type != tuple.get<3>()) - { - tuple.get<0>() = false; - } - else - { - ExpandedResource& target = tuple.get<1>(); - ExpandResourceFlags expandFlags = tuple.get<5>(); - - // Set information about the parent resource (if it exists) - if (type == ResourceType_Patient) - { - if (!parent.empty()) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - else - { - if (parent.empty()) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.parentId_ = parent; - } - - target.SetResource(type, tuple.get<2>()); - - if (expandFlags & ExpandResourceFlags_IncludeChildren) - { - // List the children resources - transaction.GetChildrenPublicId(target.childrenIds_, internalId); - } - - if (expandFlags & ExpandResourceFlags_IncludeMetadata) - { - // Extract the metadata - transaction.GetAllMetadata(target.metadata_, internalId); - - switch (type) - { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - { - int64_t i; - if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances)) - { - target.expectedNumberOfInstances_ = static_cast<int>(i); - target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i)); - } - else - { - target.expectedNumberOfInstances_ = -1; - target.status_ = EnumerationToString(SeriesStatus_Unknown); - } - - break; - } - - case ResourceType_Instance: - { - FileInfo attachment; - int64_t revision; // ignored - if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); - } - - target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize()); - target.fileUuid_ = attachment.GetUuid(); - - int64_t i; - if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries)) - { - target.indexInSeries_ = static_cast<int>(i); - } - else - { - target.indexInSeries_ = -1; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - // check the main dicom tags list has not changed since the resource was stored - target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type); - LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature); - } - - if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags) - { - // read all tags from DB - transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId); - - // read all main sequences from DB - std::string serializedSequences; - if (LookupStringMetadata(serializedSequences, target.metadata_, MetadataType_MainDicomSequences)) - { - Json::Value jsonMetadata; - Toolbox::ReadJson(jsonMetadata, serializedSequences); - - assert(jsonMetadata["Version"].asInt() == 1); - target.GetMainDicomTags().FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); - } - - // check if we have access to all requestedTags or if we must get tags from parents - const std::set<DicomTag>& requestedTags = tuple.get<4>(); - - if (requestedTags.size() > 0) - { - std::set<DicomTag> savedMainDicomTags; - - FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_); - - // read parent main dicom tags as long as we have not gathered all requested tags - ResourceType currentLevel = target.GetLevel(); - int64_t currentInternalId = internalId; - Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); - - while ((target.missingRequestedTags_.size() > 0) - && currentLevel != ResourceType_Patient) - { - currentLevel = GetParentResourceType(currentLevel); - - int64_t currentParentId; - if (!transaction.LookupParent(currentParentId, currentInternalId)) - { - break; - } - - std::map<MetadataType, std::string> parentMetadata; - transaction.GetAllMetadata(parentMetadata, currentParentId); - - std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel); - LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature); - - std::set<DicomTag> parentSavedMainDicomTags; - FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature); - - size_t previousMissingCount = target.missingRequestedTags_.size(); - Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); - Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); - - // read the parent tags from DB only if it reduces the number of missing tags - if (target.missingRequestedTags_.size() < previousMissingCount) - { - Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); - - DicomMap parentTags; - transaction.GetMainDicomTags(parentTags, currentParentId); - - target.GetMainDicomTags().Merge(parentTags); - } - - currentInternalId = currentParentId; - } - } - } - - if ((expandFlags & ExpandResourceFlags_IncludeLabels) && - hasLabelsSupport_) - { - transaction.ListLabels(target.labels_, internalId); - } - - std::string tmp; - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) - { - target.anonymizedFrom_ = tmp; - } - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_ModifiedFrom)) - { - target.modifiedFrom_ = tmp; - } - - if (type == ResourceType_Patient || - type == ResourceType_Study || - type == ResourceType_Series) - { - target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(type, internalId); - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate)) - { - target.lastUpdate_ = tmp; - } - } - else - { - target.isStable_ = false; - } - - tuple.get<0>() = true; - } + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + return response.GetResourceByIndex(0); } - }; - - bool found; - Operations operations(db_.GetDatabaseCapabilities().HasLabelsSupport()); - operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags); - return found; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } } @@ -991,110 +613,50 @@ const std::string& publicId, ResourceType level) { - class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType> + FindRequest request(level); + request.SetOrthancId(level, publicId); + request.SetRetrieveMetadata(true); + + FindResponse response; + std::map<MetadataType, FindResponse::MetadataContent> metadata = ExecuteSingleResource(response, request).GetMetadata(level); + + target.clear(); + for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - ResourceType type; - int64_t id; - if (!transaction.LookupResource(id, type, tuple.get<1>()) || - tuple.get<2>() != type) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else - { - transaction.GetAllMetadata(tuple.get<0>(), id); - } - } - }; - - Operations operations; - operations.Apply(*this, target, publicId, level); + target[it->first] = it->second.GetValue(); + } } bool StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment, int64_t& revision, - const std::string& instancePublicId, + ResourceType level, + const std::string& publicId, FileContentType contentType) { - class Operations : public ReadOnlyOperationsT5<bool&, FileInfo&, int64_t&, const std::string&, FileContentType> - { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - int64_t internalId; - ResourceType type; - if (!transaction.LookupResource(internalId, type, tuple.get<3>())) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>())) - { - assert(tuple.get<1>().GetContentType() == tuple.get<4>()); - tuple.get<0>() = true; - } - else - { - tuple.get<0>() = false; - } - } - }; - - bool found; - Operations operations; - operations.Apply(*this, found, attachment, revision, instancePublicId, contentType); - return found; + FindRequest request(level); + request.SetOrthancId(level, publicId); + request.SetRetrieveAttachments(true); + + FindResponse response; + return ExecuteSingleResource(response, request).LookupAttachment(attachment, revision, contentType); } void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target, ResourceType resourceType) { - class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType> + // This method is tested by "orthanc-tests/Plugins/WebDav/Run.py" + FindRequest request(resourceType); + + FindResponse response; + ExecuteFind(response, request); + + target.clear(); + for (size_t i = 0; i < response.GetSize(); i++) { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // TODO - CANDIDATE FOR "TransactionType_Implicit" - transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>()); - } - }; - - Operations operations; - operations.Apply(*this, target, resourceType); - } - - - void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target, - ResourceType resourceType, - size_t since, - uint32_t limit) - { - if (limit == 0) - { - target.clear(); - } - else - { - class Operations : public ReadOnlyOperationsT4<std::list<std::string>&, ResourceType, size_t, size_t> - { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // TODO - CANDIDATE FOR "TransactionType_Implicit" - transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); - } - }; - - Operations operations; - operations.Apply(*this, target, resourceType, since, limit); + target.push_back(response.GetResourceByIndex(i).GetIdentifier()); } } @@ -1168,7 +730,7 @@ } }; - if (GetDatabaseCapabilities().HasUpdateAndGetStatistics()) + if (GetDatabaseCapabilities().HasUpdateAndGetStatistics() && !IsReadOnly()) { Operations operations; Apply(operations); @@ -1218,6 +780,39 @@ } + void StatelessDatabaseOperations::GetChangesExtended(Json::Value& target, + int64_t since, + int64_t to, + unsigned int maxResults, + const std::set<ChangeType>& changeType) + { + class Operations : public ReadOnlyOperationsT5<Json::Value&, int64_t, int64_t, unsigned int, const std::set<ChangeType>&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + std::list<ServerIndexChange> changes; + bool done; + bool hasLast = false; + int64_t last = 0; + + transaction.GetChangesExtended(changes, done, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>()); + if (changes.empty()) + { + last = transaction.GetLastChangeIndex(); + hasLast = true; + } + + FormatLog(tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last); + } + }; + + Operations operations; + operations.Apply(*this, target, since, to, maxResults, changeType); + } + + void StatelessDatabaseOperations::GetLastChange(Json::Value& target) { class Operations : public ReadOnlyOperationsT1<Json::Value&> @@ -1325,43 +920,29 @@ void StatelessDatabaseOperations::GetChildren(std::list<std::string>& result, + ResourceType level, const std::string& publicId) { - class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&> + const ResourceType childLevel = GetChildResourceType(level); + + FindRequest request(level); + request.SetOrthancId(level, publicId); + request.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true); + + FindResponse response; + ExecuteFind(response, request); + + result.clear(); + + for (size_t i = 0; i < response.GetSize(); i++) { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE + const std::set<std::string>& children = response.GetResourceByIndex(i).GetChildrenIdentifiers(childLevel); + + for (std::set<std::string>::const_iterator it = children.begin(); it != children.end(); ++it) { - ResourceType type; - int64_t resource; - if (!transaction.LookupResource(resource, type, tuple.get<1>())) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else if (type == ResourceType_Instance) - { - // An instance cannot have a child - throw OrthancException(ErrorCode_BadParameterType); - } - else - { - std::list<int64_t> tmp; - transaction.GetChildrenInternalId(tmp, resource); - - tuple.get<0>().clear(); - - for (std::list<int64_t>::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - tuple.get<0>().push_back(transaction.GetPublicId(*it)); - } - } + result.push_back(*it); } - }; - - Operations operations; - operations.Apply(*this, result, publicId); + } } @@ -1427,36 +1008,33 @@ bool StatelessDatabaseOperations::LookupMetadata(std::string& target, + const std::string& publicId, + ResourceType expectedType, + MetadataType type) + { + FindRequest request(expectedType); + request.SetOrthancId(expectedType, publicId); + request.SetRetrieveMetadata(true); + request.SetRetrieveMetadataRevisions(false); // No need to retrieve revisions + + FindResponse response; + return ExecuteSingleResource(response, request).LookupMetadata(target, expectedType, type); + } + + + bool StatelessDatabaseOperations::LookupMetadata(std::string& target, int64_t& revision, const std::string& publicId, ResourceType expectedType, MetadataType type) { - class Operations : public ReadOnlyOperationsT6<bool&, std::string&, int64_t&, - const std::string&, ResourceType, MetadataType> - { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - ResourceType resourceType; - int64_t id; - if (!transaction.LookupResource(id, resourceType, tuple.get<3>()) || - resourceType != tuple.get<4>()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else - { - tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), tuple.get<2>(), id, tuple.get<5>()); - } - } - }; - - bool found; - Operations operations; - operations.Apply(*this, found, target, revision, publicId, expectedType, type); - return found; + FindRequest request(expectedType); + request.SetOrthancId(expectedType, publicId); + request.SetRetrieveMetadata(true); + request.SetRetrieveMetadataRevisions(true); // We are asked to retrieve revisions + + FindResponse response; + return ExecuteSingleResource(response, request).LookupMetadata(target, revision, expectedType, type); } @@ -1686,20 +1264,21 @@ DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); - DatabaseConstraints query; - query.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + DatabaseDicomTagConstraints query; + bool isIdentical; // unused + query.AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier)); class Operations : public IReadOnlyOperations { private: std::vector<std::string>& result_; - const DatabaseConstraints& query_; + const DatabaseDicomTagConstraints& query_; ResourceType level_; public: Operations(std::vector<std::string>& result, - const DatabaseConstraints& query, + const DatabaseDicomTagConstraints& query, ResourceType level) : result_(result), query_(query), @@ -1977,77 +1556,6 @@ } - void StatelessDatabaseOperations::ApplyLookupResources(std::vector<std::string>& resourcesId, - std::vector<std::string>* instancesId, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - uint32_t limit) - { - class Operations : public ReadOnlyOperationsT6<bool, const DatabaseConstraints&, ResourceType, - const std::set<std::string>&, LabelsConstraint, size_t> - { - private: - std::list<std::string> resourcesList_; - std::list<std::string> instancesList_; - - public: - const std::list<std::string>& GetResourcesList() const - { - return resourcesList_; - } - - const std::list<std::string>& GetInstancesList() const - { - return instancesList_; - } - - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // TODO - CANDIDATE FOR "TransactionType_Implicit" - if (tuple.get<0>()) - { - transaction.ApplyLookupResources( - resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); - } - else - { - transaction.ApplyLookupResources( - resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); - } - } - }; - - if (!labels.empty() && - !db_.GetDatabaseCapabilities().HasLabelsSupport()) - { - throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); - } - - for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) - { - ServerToolbox::CheckValidLabel(*it); - } - - DatabaseConstraints normalized; - - assert(mainDicomTagsRegistry_.get() != NULL); - mainDicomTagsRegistry_->NormalizeLookup(normalized, lookup, queryLevel); - - Operations operations; - operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit); - - CopyListToVector(resourcesId, operations.GetResourcesList()); - - if (instancesId != NULL) - { - CopyListToVector(*instancesId, operations.GetInstancesList()); - } - } - - bool StatelessDatabaseOperations::DeleteResource(Json::Value& remainingAncestor, const std::string& uuid, ResourceType expectedType) @@ -2515,7 +2023,7 @@ catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; + << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; oldValue = 0; } @@ -2814,8 +2322,8 @@ public: explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel) - : limitToThisLevelDicomTags_(limitToThisLevelDicomTags), - limitToLevel_(limitToLevel) + : limitToThisLevelDicomTags_(limitToThisLevelDicomTags), + limitToLevel_(limitToLevel) { OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom); hasher_.reset(new DicomInstanceHasher(summary_)); @@ -2917,8 +2425,8 @@ } - bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize, - uint64_t addedInstanceSize) + bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize, + uint64_t addedInstanceSize) { if (maximumStorageSize != 0) { @@ -2939,7 +2447,7 @@ return false; } - bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, + bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, const std::string& patientId) { if (maximumPatientCount != 0) @@ -3042,7 +2550,7 @@ }; if (maximumStorageMode == MaxStorageMode_Recycle - && (maximumStorageSize != 0 || maximumPatientCount != 0)) + && (maximumStorageSize != 0 || maximumPatientCount != 0)) { Operations operations(maximumStorageSize, maximumPatientCount); Apply(operations); @@ -3106,9 +2614,9 @@ } static void SetMainDicomSequenceMetadata(ResourcesContent& content, - int64_t resource, - const DicomMap& dicomSummary, - ResourceType level) + int64_t resource, + const DicomMap& dicomSummary, + ResourceType level) { std::string serialized; GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level); @@ -3301,7 +2809,7 @@ // Ensure there is enough room in the storage for the new instance uint64_t instanceSize = 0; for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) + it != attachments_.end(); ++it) { instanceSize += it->GetCompressedSize(); } @@ -3330,7 +2838,7 @@ // Attach the files to the newly created instance for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) + it != attachments_.end(); ++it) { if (isReconstruct_) { @@ -3688,28 +3196,12 @@ const std::string& publicId, ResourceType level) { - class Operations : public ReadOnlyOperationsT3<std::set<std::string>&, const std::string&, ResourceType> - { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - ResourceType type; - int64_t id; - if (!transaction.LookupResource(id, type, tuple.get<1>()) || - tuple.get<2>() != type) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else - { - transaction.ListLabels(tuple.get<0>(), id); - } - } - }; - - Operations operations; - operations.Apply(*this, target, publicId, level); + FindRequest request(level); + request.SetOrthancId(level, publicId); + request.SetRetrieveLabels(true); + + FindResponse response; + target = ExecuteSingleResource(response, request).GetLabels(); } @@ -3806,4 +3298,134 @@ boost::shared_lock<boost::shared_mutex> lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + bool StatelessDatabaseOperations::HasExtendedChanges() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasExtendedChanges(); + } + + bool StatelessDatabaseOperations::HasFindSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasFindSupport(); + } + + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, + const FindRequest& request) + { + class IntegratedCount : public ReadOnlyOperationsT3<uint64_t&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteCount(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities(); + + if (db_.HasIntegratedFind()) + { + IntegratedCount operations; + operations.Apply(*this, count, request, capabilities); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class IntegratedFind : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class FindStage : public ReadOnlyOperationsT3<std::list<std::string>&, const IDatabaseWrapper::Capabilities&, const FindRequest& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class ExpandStage : public ReadOnlyOperationsT4<FindResponse&, const IDatabaseWrapper::Capabilities&, const FindRequest&, const std::string&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + } + }; + + IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities(); + + if (db_.HasIntegratedFind()) + { + /** + * In this flavor, the "find" and the "expand" phases are + * executed in one single transaction. + **/ + IntegratedFind operations; + operations.Apply(*this, response, request, capabilities); + } + else + { + /** + * In this flavor, the "find" and the "expand" phases for each + * found resource are executed in distinct transactions. This is + * the compatibility mode equivalent to Orthanc <= 1.12.3. + **/ + std::list<std::string> identifiers; + + FindStage find; + find.Apply(*this, identifiers, capabilities, request); + + ExpandStage expand; + + for (std::list<std::string>::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it) + { + /** + * Not that the resource might have been deleted (as we are in + * another transaction). The database engine must ignore such + * error cases. + **/ + expand.Apply(*this, response, capabilities, request, *it); + } + } + } + + void StatelessDatabaseOperations::PerformDbHousekeeping() + { + class Operations : public IReadWriteOperations + { + public: + Operations() + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.PerformDbHousekeeping(); + } + }; + + Operations operations; + Apply(operations); + } + }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Nov 29 10:54:20 2024 +0100 @@ -25,8 +25,9 @@ #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../DicomInstanceOrigin.h" #include "IDatabaseWrapper.h" -#include "../DicomInstanceOrigin.h" +#include "MainDicomTagsRegistry.h" #include <boost/shared_ptr.hpp> #include <boost/thread/shared_mutex.hpp> @@ -38,91 +39,6 @@ class ParsedDicomFile; struct ServerIndexChange; - class ExpandedResource : public boost::noncopyable - { - private: - std::string id_; - ResourceType level_; - DicomMap tags_; // all main tags and main sequences from DB - - public: - std::string mainDicomTagsSignature_; - std::string parentId_; - std::list<std::string> childrenIds_; - std::map<MetadataType, std::string> metadata_; - std::string anonymizedFrom_; - std::string modifiedFrom_; - std::string lastUpdate_; - std::set<DicomTag> missingRequestedTags_; - - // for patients/studies/series - bool isStable_; - - // for series only - int expectedNumberOfInstances_; - std::string status_; - - // for instances only - size_t fileSize_; - std::string fileUuid_; - int indexInSeries_; - - // New in Orthanc 1.12.0 - std::set<std::string> labels_; - - public: - // TODO - Cleanup - ExpandedResource() : - level_(ResourceType_Instance), - isStable_(false), - expectedNumberOfInstances_(0), - fileSize_(0), - indexInSeries_(0) - { - } - - void SetResource(ResourceType level, - const std::string& id) - { - level_ = level; - id_ = id; - } - - const std::string& GetPublicId() const - { - return id_; - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomMap& GetMainDicomTags() - { - return tags_; - } - - const DicomMap& GetMainDicomTags() const - { - return tags_; - } - }; - - enum ExpandResourceFlags - { - ExpandResourceFlags_None = 0, - ExpandResourceFlags_IncludeMetadata = (1 << 0), - ExpandResourceFlags_IncludeChildren = (1 << 1), - ExpandResourceFlags_IncludeMainDicomTags = (1 << 2), - ExpandResourceFlags_IncludeLabels = (1 << 3), - - ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata | - ExpandResourceFlags_IncludeChildren | - ExpandResourceFlags_IncludeMainDicomTags | - ExpandResourceFlags_IncludeLabels) - }; - class StatelessDatabaseOperations : public boost::noncopyable { public: @@ -209,7 +125,7 @@ void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, // Can be NULL if not needed - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, // New in Orthanc 1.12.0 LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 @@ -225,20 +141,6 @@ transaction_.GetAllMetadata(target, id); } - void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType) - { - return transaction_.GetAllPublicIds(target, resourceType); - } - - void GetAllPublicIds(std::list<std::string>& target, - ResourceType resourceType, - size_t since, - uint32_t limit) - { - return transaction_.GetAllPublicIds(target, resourceType, since, limit); - } - void GetChanges(std::list<ServerIndexChange>& target /*out*/, bool& done /*out*/, int64_t since, @@ -247,18 +149,22 @@ transaction_.GetChanges(target, done, since, limit); } + void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType) + { + transaction_.GetChangesExtended(target, done, since, to, limit, filterType); + } + void GetChildrenInternalId(std::list<int64_t>& target, int64_t id) { transaction_.GetChildrenInternalId(target, id); } - void GetChildrenPublicId(std::list<std::string>& target, - int64_t id) - { - transaction_.GetChildrenPublicId(target, id); - } - void GetExportedResources(std::list<ExportedResource>& target /*out*/, bool& done /*out*/, int64_t since, @@ -359,25 +265,46 @@ { return transaction_.LookupResource(id, type, publicId); } - - bool LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) - { - return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId); - } - - void ListLabels(std::set<std::string>& target, - int64_t id) - { - transaction_.ListLabels(target, id); - } void ListAllLabels(std::set<std::string>& target) { transaction_.ListAllLabels(target); } + + bool HasReachedMaxStorageSize(uint64_t maximumStorageSize, + uint64_t addedInstanceSize); + + bool HasReachedMaxPatientCount(unsigned int maximumPatientCount, + const std::string& patientId); + + void ExecuteCount(uint64_t& count, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteCount(count, request, capabilities); + } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteFind(response, request, capabilities); + } + + void ExecuteFind(std::list<std::string>& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request) + { + transaction_.ExecuteFind(identifiers, capabilities, request); + } + + void ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + transaction_.ExecuteExpand(response, capabilities, request, identifier); + } }; @@ -473,6 +400,11 @@ return transaction_.UpdateAndGetStatistics(patientsCount, studiesCount, seriesCount, instancesCount, compressedSize, uncompressedSize); } + void PerformDbHousekeeping() + { + return transaction_.PerformDbHousekeeping(); + } + void SetMetadata(int64_t id, MetadataType type, const std::string& value, @@ -497,12 +429,6 @@ uint64_t addedInstanceSize, const std::string& newPatientId); - bool HasReachedMaxStorageSize(uint64_t maximumStorageSize, - uint64_t addedInstanceSize); - - bool HasReachedMaxPatientCount(unsigned int maximumPatientCount, - const std::string& patientId); - bool IsRecyclingNeeded(uint64_t maximumStorageSize, unsigned int maximumPatients, uint64_t addedInstanceSize, @@ -545,7 +471,6 @@ private: - class MainDicomTagsRegistry; class Transaction; IDatabaseWrapper& db_; @@ -555,17 +480,26 @@ boost::shared_mutex mutex_; std::unique_ptr<ITransactionContextFactory> factory_; unsigned int maxRetries_; + bool readOnly_; void ApplyInternal(IReadOnlyOperations* readOperations, IReadWriteOperations* writeOperations); + const FindResponse::Resource &ExecuteSingleResource(FindResponse &response, + const FindRequest &request); + protected: void StandaloneRecycling(MaxStorageMode maximumStorageMode, uint64_t maximumStorageSize, unsigned int maximumPatientCount); + bool IsReadOnly() + { + return readOnly_; + } + public: - explicit StatelessDatabaseOperations(IDatabaseWrapper& database); + explicit StatelessDatabaseOperations(IDatabaseWrapper& database, bool readOnly); void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */); @@ -592,12 +526,6 @@ void Apply(IReadWriteOperations& operations); - bool ExpandResource(ExpandedResource& target, - const std::string& publicId, - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags); - void GetAllMetadata(std::map<MetadataType, std::string>& target, const std::string& publicId, ResourceType level); @@ -605,11 +533,6 @@ void GetAllUuids(std::list<std::string>& target, ResourceType resourceType); - void GetAllUuids(std::list<std::string>& target, - ResourceType resourceType, - size_t since, - uint32_t limit); - void GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, /* out */ uint64_t& countPatients, @@ -619,15 +542,26 @@ bool LookupAttachment(FileInfo& attachment, int64_t& revision, - const std::string& instancePublicId, + ResourceType level, + const std::string& publicId, FileContentType contentType); void GetChanges(Json::Value& target, int64_t since, uint32_t limit); + void GetChangesExtended(Json::Value& target, + int64_t since, + int64_t to, + uint32_t limit, + const std::set<ChangeType>& filterType); + void GetLastChange(Json::Value& target); + bool HasExtendedChanges(); + + bool HasFindSupport(); + void GetExportedResources(Json::Value& target, int64_t since, uint32_t limit); @@ -637,12 +571,18 @@ bool IsProtectedPatient(const std::string& publicId); void GetChildren(std::list<std::string>& result, + ResourceType level, const std::string& publicId); void GetChildInstances(std::list<std::string>& result, const std::string& publicId); bool LookupMetadata(std::string& target, + const std::string& publicId, + ResourceType expectedType, + MetadataType type); + + bool LookupMetadata(std::string& target, int64_t& revision, const std::string& publicId, ResourceType expectedType, @@ -694,14 +634,6 @@ const std::string& publicId, ResourceType parentType); - void ApplyLookupResources(std::vector<std::string>& resourcesId, - std::vector<std::string>* instancesId, // Can be NULL if not needed - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - uint32_t limit); - bool DeleteResource(Json::Value& remainingAncestor /* out */, const std::string& uuid, ResourceType expectedType); @@ -798,5 +730,14 @@ const std::set<std::string>& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); + + void ExecuteCount(uint64_t& count, + const FindRequest& request); + + void PerformDbHousekeeping(); + }; }
--- a/OrthancServer/Sources/OrthancConfiguration.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -1161,6 +1161,22 @@ { warning = Warnings_003_DecoderFailure; } + else if (name == "W004_NoMainDicomTagsSignature") + { + warning = Warnings_004_NoMainDicomTagsSignature; + } + else if (name == "W005_RequestingTagFromLowerResourceLevel") + { + warning = Warnings_005_RequestingTagFromLowerResourceLevel; + } + else if (name == "W006_RequestingTagFromMetaHeader") + { + warning = Warnings_006_RequestingTagFromMetaHeader; + } + else if (name == "W007_MissingRequestedTagsNotReadFromDisk") + { + warning = Warnings_007_MissingRequestedTagsNotReadFromDisk; + } else { throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -30,6 +30,7 @@ #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" #include "../../OrthancFramework/Sources/MetricsRegistry.h" #include "OrthancConfiguration.h" +#include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerContext.h" #include "ServerToolbox.h" @@ -39,134 +40,48 @@ namespace Orthanc { - static void AddAnswer(DicomFindAnswers& answers, - ServerContext& context, - const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson, - ResourceType level, - const DicomArray& query, - const std::list<DicomTag>& sequencesToReturn, - const std::string& defaultPrivateCreator, - const std::map<uint16_t, std::string>& privateCreators, - const std::string& retrieveAet, - bool allowStorageAccess) + static void CopySequence(ParsedDicomFile& dicom, + const DicomTag& tag, + const Json::Value& source, + const std::string& defaultPrivateCreator, + const std::map<uint16_t, std::string>& privateCreators) { - ExpandedResource resource; - std::set<DicomTag> requestedTags; - - query.GetTags(requestedTags); - requestedTags.erase(DICOM_TAG_QUERY_RETRIEVE_LEVEL); // this is not part of the answer - - // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...). This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS - context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, - level, requestedTags, ExpandResourceFlags_IncludeMainDicomTags, allowStorageAccess); - - DicomMap result; + if (source.type() == Json::objectValue && + source.isMember("Type") && + source.isMember("Value") && + source["Type"].asString() == "Sequence" && + source["Value"].type() == Json::arrayValue) + { + Json::Value content = Json::arrayValue; - /** - * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2. - * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2 - * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ - **/ - result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet, false /* not binary */); - - for (size_t i = 0; i < query.GetSize(); i++) - { - if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) { - // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) - result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue()); + Json::Value item; + Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); + content.append(item); } - else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET) - { - // Do not include the encoding, this is handled by class ParsedDicomFile - } - else + + if (tag.IsPrivate()) { - const DicomTag& tag = query.GetElement(i).GetTag(); - const DicomValue* value = resource.GetMainDicomTags().TestAndGetValue(tag); + std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag.GetGroup()); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) + if (found != privateCreators.end()) { - result.SetValue(tag, value->GetContent(), false); + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); } else { - result.SetValue(tag, "", false); + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); } } + else + { + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); + } } - - if (result.GetSize() == 0 && - sequencesToReturn.empty()) - { - CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag"; - } - else if (sequencesToReturn.empty()) - { - answers.Add(result); - } - else if (dicomAsJson == NULL) - { - CLOG(WARNING, DICOM) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled"; - answers.Add(result); - } - else - { - ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), - true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators); - - for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin(); - tag != sequencesToReturn.end(); ++tag) - { - assert(dicomAsJson != NULL); - const Json::Value& source = (*dicomAsJson) [tag->Format()]; - - if (source.type() == Json::objectValue && - source.isMember("Type") && - source.isMember("Value") && - source["Type"].asString() == "Sequence" && - source["Value"].type() == Json::arrayValue) - { - Json::Value content = Json::arrayValue; - - for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) - { - Json::Value item; - Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); - content.append(item); - } - - if (tag->IsPrivate()) - { - std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup()); - - if (found != privateCreators.end()) - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); - } - else - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); - } - } - else - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); - } - } - } - - answers.Add(dicom); - } } - bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, ResourceType level, const DicomTag& tag, @@ -237,86 +152,122 @@ } - class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor + namespace { - private: - DicomFindAnswers& answers_; - ServerContext& context_; - ResourceType level_; - const DicomMap& query_; - DicomArray queryAsArray_; - const std::list<DicomTag>& sequencesToReturn_; - std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself - const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself - std::string retrieveAet_; - FindStorageAccessMode findStorageAccessMode_; + class LookupVisitorV2 : public ResourceFinder::IVisitor + { + private: + DicomFindAnswers& answers_; + DicomArray queryAsArray_; + const std::list<DicomTag>& sequencesToReturn_; + std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself + const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself + std::string retrieveAet_; + + public: + LookupVisitorV2(DicomFindAnswers& answers, + const DicomMap& query, + const std::list<DicomTag>& sequencesToReturn, + const std::map<uint16_t, std::string>& privateCreators) : + answers_(answers), + queryAsArray_(query), + sequencesToReturn_(sequencesToReturn), + privateCreators_(privateCreators) + { + answers_.SetComplete(false); - public: - LookupVisitor(DicomFindAnswers& answers, - ServerContext& context, - ResourceType level, - const DicomMap& query, - const std::list<DicomTag>& sequencesToReturn, - const std::map<uint16_t, std::string>& privateCreators, - FindStorageAccessMode findStorageAccessMode) : - answers_(answers), - context_(context), - level_(level), - query_(query), - queryAsArray_(query), - sequencesToReturn_(sequencesToReturn), - privateCreators_(privateCreators), - findStorageAccessMode_(findStorageAccessMode) - { - answers_.SetComplete(false); + { + OrthancConfiguration::ReaderLock lock; + defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); + retrieveAet_ = lock.GetConfiguration().GetOrthancAET(); + } + } + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { - OrthancConfiguration::ReaderLock lock; - defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); - retrieveAet_ = lock.GetConfiguration().GetOrthancAET(); - } - } + DicomMap resourceTags; + resource.GetAllMainDicomTags(resourceTags); + resourceTags.Merge(requestedTags); + + DicomMap result; + + /** + * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2. + * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2 + * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ + **/ + result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false /* not binary */); + + for (size_t i = 0; i < queryAsArray_.GetSize(); i++) + { + const DicomTag tag = queryAsArray_.GetElement(i).GetTag(); - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - // Ask the "DICOM-as-JSON" attachment only if sequences are to - // be returned OR if "query_" contains non-main DICOM tags! - - DicomMap withoutSpecialTags; - withoutSpecialTags.Assign(query_); + if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + { + // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) + result.SetValue(tag, queryAsArray_.GetElement(i).GetValue()); + } + else if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + // Do not include the encoding, this is handled by class ParsedDicomFile + } + else + { + const DicomValue* value = resourceTags.TestAndGetValue(tag); - // Check out "ComputeCounters()" - withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY); + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + result.SetValue(tag, "", false); + } + else + { + result.SetValue(tag, value->GetContent(), false); + } + } + } - // Check out "AddAnswer()" - withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); - withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL); - - return (!sequencesToReturn_.empty() || - !withoutSpecialTags.HasOnlyMainDicomTags()); - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - answers_.SetComplete(true); - } + if (result.GetSize() == 0 && + sequencesToReturn_.empty()) + { + CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag"; + } + else if (sequencesToReturn_.empty()) + { + answers_.Add(result); + } + else + { + ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), + true /* be permissive, cf. issue #136 */, defaultPrivateCreator_, privateCreators_); - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) ORTHANC_OVERRIDE - { - AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_, - defaultPrivateCreator_, privateCreators_, retrieveAet_, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); - } - }; + for (std::list<DicomTag>::const_iterator tag = sequencesToReturn_.begin(); + tag != sequencesToReturn_.end(); ++tag) + { + const DicomValue* value = resourceTags.TestAndGetValue(*tag); + if (value != NULL && + value->IsSequence()) + { + CopySequence(dicom, *tag, value->GetSequenceContent(), defaultPrivateCreator_, privateCreators_); + } + else + { + dicom.Replace(*tag, std::string(""), false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator_); + } + } + + answers_.Add(dicom); + } + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + answers_.SetComplete(true); + } + }; + } void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, @@ -396,7 +347,6 @@ throw OrthancException(ErrorCode_NotImplemented); } - DicomArray query(*filteredInput); CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level); @@ -410,9 +360,12 @@ } } + std::set<DicomTag> requestedTags; + for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin(); it != sequencesToReturn.end(); ++it) { + requestedTags.insert(*it); CLOG(INFO, DICOM) << " (" << it->Format() << ") " << FromDcmtkBridge::GetTagName(*it, "") << " : sequence tag whose content will be copied"; @@ -441,18 +394,26 @@ const DicomTag tag = element.GetTag(); // remove tags that are not used for matching - if (element.GetValue().IsNull() || - tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || + if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || tag == DICOM_TAG_SPECIFIC_CHARACTER_SET || tag == DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC) // time zone is not directly used for matching. Once we support "Timezone query adjustment", we may use it to adjust date-time filters but for now, just ignore it { continue; } + requestedTags.insert(tag); + + if (element.GetValue().IsNull()) + { + // There is no constraint on this tag + continue; + } + std::string value = element.GetValue().GetContent(); if (value.size() == 0) { // An empty string corresponds to an universal constraint, so we ignore it + requestedTags.insert(tag); continue; } @@ -483,11 +444,12 @@ * Run the query. **/ - size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; - + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(lookup); + finder.AddRequestedTags(requestedTags); - LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode()); - context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); + LookupVisitorV2 visitor(answers, *filteredInput, sequencesToReturn, privateCreators); + finder.Execute(visitor, context_); }
--- a/OrthancServer/Sources/OrthancInitialization.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -261,7 +261,17 @@ { LOG(INFO) << " - " << tagName; } - DicomMap::AddMainDicomTag(tag, level); + + try + { + DicomMap::AddMainDicomTag(tag, level); + } + catch(OrthancException& e) + { + LOG(WARNING) << " - !!! " << tagName << " is already defined as a standard MainDicomTags, it is useless to include it in the ExtraMainDicomTags"; + } + + } } }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -270,7 +270,16 @@ RegisterAnonymizeModify(); RegisterArchive(); - Register("/instances", UploadDicomFile); + if (!context_.IsReadOnly()) + { + Register("/instances", UploadDicomFile); + } + else + { + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating POST /instances route"; + } + + // Auto-generated directories Register("/tools", RestApi::AutoListChildren); @@ -492,11 +501,15 @@ static const std::string GET_SHORT = "short"; static const std::string GET_REQUESTED_TAGS_OLD = "requestedTags"; // This was the only option in Orthanc <= 1.12.3 static const std::string GET_REQUESTED_TAGS = "requested-tags"; + static const std::string GET_RESPONSE_CONTENT = "response-content"; + static const std::string GET_EXPAND = "expand"; static const std::string POST_SIMPLIFY = "Simplify"; static const std::string POST_FULL = "Full"; static const std::string POST_SHORT = "Short"; static const std::string POST_REQUESTED_TAGS = "RequestedTags"; + static const std::string POST_RESPONSE_CONTENT = "ResponseContent"; + static const std::string POST_EXPAND = "Expand"; static const std::string DOCUMENT_SIMPLIFY = "report the DICOM tags in human-readable format (using the symbolic name of the tags)"; @@ -625,19 +638,100 @@ } catch (OrthancException& ex) { - throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails()); + throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requested-tags argument: ") + ex.What() + " " + ex.GetDetails()); } } } - void OrthancRestApi::DocumentRequestedTags(RestApiGetCall& call) + void OrthancRestApi::DocumentRequestedTags(RestApiCall& call) { + if (call.GetMethod() == HttpMethod_Get) + { call.GetDocumentation().SetHttpGetArgument(GET_REQUESTED_TAGS, RestApiCallDocumentation::Type_String, "If present, list the DICOM Tags you want to list in the response. This argument is a semi-column separated list " "of DICOM Tags identifiers; e.g: '" + GET_REQUESTED_TAGS + "=0010,0010;PatientBirthDate'. " "The tags requested tags are returned in the 'RequestedTags' field in the response. " "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " - "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return ", false); + "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " + "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false); + } + else if (call.GetMethod() == HttpMethod_Post) + { + call.GetDocumentation().SetRequestField(POST_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings, + "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true). " + "The tags requested tags are returned in the 'RequestedTags' field in the response. " + "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " + "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " + "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } } + void OrthancRestApi::GetResponseContentAndExpand(ResponseContentFlags& responseContent, + const RestApiGetCall& call) + { + if (call.HasArgument(GET_RESPONSE_CONTENT)) + { + std::string s = call.GetArgument(GET_RESPONSE_CONTENT, ""); + responseContent = ResponseContentFlags_Default; + + if (!s.empty()) + { + std::set<std::string> splitResponseContent; + Toolbox::SplitString(splitResponseContent, s, ';'); + + for (std::set<std::string>::const_iterator it = splitResponseContent.begin(); it != splitResponseContent.end(); ++it) + { + responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(*it)); + } + } + } + else if (call.HasArgument(GET_EXPAND) && call.GetBooleanArgument("expand", true)) + { + responseContent = ResponseContentFlags_ExpandTrue; + } + else + { + responseContent = ResponseContentFlags_ID; + } + } + + void OrthancRestApi::DocumentResponseContentAndExpand(RestApiCall& call) + { + if (call.GetMethod() == HttpMethod_Get) + { + call.GetDocumentation().SetHttpGetArgument(GET_RESPONSE_CONTENT, RestApiCallDocumentation::Type_String, + "Defines the content of response for each returned resource. Allowed values are `MainDicomTags`, " + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "e.g: '" + GET_RESPONSE_CONTENT + "=MainDicomTags;Children " + "(new in Orthanc 1.12.5 - overrides `expand`)", false); + + call.GetDocumentation().SetHttpGetArgument(GET_EXPAND, RestApiCallDocumentation::Type_String, + "If present, retrieve detailed information about the individual resources", false); + + } + else if (call.GetMethod() == HttpMethod_Post) + { + call.GetDocumentation().SetRequestField(POST_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings, + "Defines the content of response for each returned resource. (this field, if present, overrides the \"Expand\" field). " + "Allowed values are `MainDicomTags`, " + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "(new in Orthanc 1.12.5)", false); + + call.GetDocumentation().SetRequestField(POST_EXPAND, RestApiCallDocumentation::Type_Boolean, + "If present, retrieve detailed information about the individual resources", false); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + } + + }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Fri Nov 29 10:54:20 2024 +0100 @@ -157,6 +157,11 @@ static void GetRequestedTags(std::set<DicomTag>& requestedTags, const RestApiGetCall& call); - static void DocumentRequestedTags(RestApiGetCall& call); + static void DocumentRequestedTags(RestApiCall& call); + + static void GetResponseContentAndExpand(ResponseContentFlags& responseContent, + const RestApiGetCall& call); + + static void DocumentResponseContentAndExpand(RestApiCall& call); }; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -29,14 +29,15 @@ namespace Orthanc { // Changes API -------------------------------------------------------------- + static const unsigned int DEFAULT_LIMIT = 100; + static const int64_t DEFAULT_TO = -1; - static void GetSinceAndLimit(int64_t& since, - unsigned int& limit, - bool& last, - const RestApiGetCall& call) + static void GetSinceToAndLimit(int64_t& since, + int64_t& to, + unsigned int& limit, + bool& last, + const RestApiGetCall& call) { - static const unsigned int DEFAULT_LIMIT = 100; - if (call.HasArgument("last")) { last = true; @@ -48,11 +49,13 @@ try { since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0")); + to = boost::lexical_cast<int64_t>(call.GetArgument("to", boost::lexical_cast<std::string>(DEFAULT_TO))); limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", boost::lexical_cast<std::string>(DEFAULT_LIMIT))); } catch (boost::bad_lexical_cast&) { since = 0; + to = DEFAULT_TO; limit = DEFAULT_LIMIT; return; } @@ -66,33 +69,65 @@ .SetTag("Tracking changes") .SetSummary("List changes") .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality. Please note that, when resources are deleted, their corresponding change entries are also removed from the Changes Log, which helps ensuring that this log does not grow indefinitely.") + .SetHttpGetArgument("last", RestApiCallDocumentation::Type_Number, "Request only the last change id (this argument must be used alone)", false) .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false) - .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false) + .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index excluded", false) + .SetHttpGetArgument("to", RestApiCallDocumentation::Type_Number, "Show only the resources till the provided index included (only available if your DB backend supports ExtendedChanges)", false) + .SetHttpGetArgument("type", RestApiCallDocumentation::Type_String, "Show only the changes of the provided type (only available if your DB backend supports ExtendedChanges)", false) .AddAnswerType(MimeType_Json, "The list of changes") .SetAnswerField("Changes", RestApiCallDocumentation::Type_JsonListOfObjects, "The individual changes") .SetAnswerField("Done", RestApiCallDocumentation::Type_Boolean, - "Whether the last reported change is the last of the full history") + "Whether the last reported change is the last of the full history.") .SetAnswerField("Last", RestApiCallDocumentation::Type_Number, "The index of the last reported change, can be used for the `since` argument in subsequent calls to this route") + .SetAnswerField("First", RestApiCallDocumentation::Type_Number, + "The index of the first reported change, its value-1 can be used for the `to` argument in subsequent calls to this route when browsing the changes in reverse order") .SetHttpGetSample("https://orthanc.uclouvain.be/demo/changes?since=0&limit=2", true); return; } ServerContext& context = OrthancRestApi::GetContext(call); - //std::string filter = GetArgument(getArguments, "filter", ""); - int64_t since; + int64_t since, to; + std::set<ChangeType> filterType; + unsigned int limit; bool last; - GetSinceAndLimit(since, limit, last, call); + GetSinceToAndLimit(since, to, limit, last, call); + + std::string filterArgument = call.GetArgument("type", "all"); + if (filterArgument != "all" && filterArgument != "All") + { + std::set<std::string> filterTypeStrings; + Toolbox::SplitString(filterTypeStrings, filterArgument, ';'); + + for (std::set<std::string>::const_iterator it = filterTypeStrings.begin(); it != filterTypeStrings.end(); ++it) + { + filterType.insert(StringToChangeType(*it)); + } + } Json::Value result; if (last) { context.GetIndex().GetLastChange(result); } + else if (context.GetIndex().HasExtendedChanges()) + { + context.GetIndex().GetChangesExtended(result, since, to, limit, filterType); + } else { + if (filterType.size() > 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to filter changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)"); + } + + if (to != DEFAULT_TO) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to use the 'to' parameter in /changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)"); + } + context.GetIndex().GetChanges(result, since, limit); } @@ -139,10 +174,10 @@ ServerContext& context = OrthancRestApi::GetContext(call); - int64_t since; + int64_t since, to; unsigned int limit; bool last; - GetSinceAndLimit(since, limit, last, call); + GetSinceToAndLimit(since, to, limit, last, call); Json::Value result; if (last)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -22,6 +22,8 @@ #include "../PrecompiledHeadersServer.h" +#include "../ResourceFinder.h" + #include "OrthancRestApi.h" #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" @@ -39,6 +41,7 @@ #include "../OrthancConfiguration.h" #include "../Search/DatabaseLookup.h" +#include "../Search/DatabaseMetadataConstraint.h" #include "../ServerContext.h" #include "../ServerToolbox.h" #include "../SliceOrdering.h" @@ -65,6 +68,14 @@ namespace Orthanc { + static ResourceType GetResourceTypeFromUri(const RestApiCall& call) + { + assert(!call.GetFullUri().empty()); + const std::string resourceType = call.GetFullUri() [0]; + return StringToResourceType(resourceType.c_str()); + } + + static std::string GetDocumentationSampleResource(ResourceType type) { switch (type) @@ -127,79 +138,30 @@ } + static bool ExpandResource(Json::Value& target, + ServerContext& context, + ResourceType level, + const std::string& identifier, + DicomToJsonFormat format, + bool retrieveMetadata) + { + ResponseContentFlags responseContent = ResponseContentFlags_ExpandTrue; + + if (retrieveMetadata) + { + responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | ResponseContentFlags_Metadata); + } + + ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode()); + finder.SetOrthancId(level, identifier); + finder.SetRetrieveMetadata(retrieveMetadata); + + return finder.ExecuteOneResource(target, context, format, retrieveMetadata); + } + + // List all the patients, studies, series or instances ---------------------- - static void AnswerListOfResources(RestApiOutput& output, - ServerContext& context, - const std::list<std::string>& resources, - const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource. - const std::map<std::string, boost::shared_ptr<DicomMap> >& resourcesMainDicomTags, // optional: all tags read from DB for a resource (current level and upper levels) - const std::map<std::string, boost::shared_ptr<Json::Value> >& resourcesDicomAsJson, // optional: the dicom-as-json for each resource - ResourceType level, - bool expand, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - Json::Value answer = Json::arrayValue; - - for (std::list<std::string>::const_iterator - resource = resources.begin(); resource != resources.end(); ++resource) - { - if (expand) - { - Json::Value expanded; - - std::map<std::string, std::string>::const_iterator instanceId = instancesIds.find(*resource); - if (instanceId != instancesIds.end()) // if it is found in instancesIds, it is also in resourcesDicomAsJson and mainDicomTags - { - // reuse data already collected before (e.g during lookup) - std::map<std::string, boost::shared_ptr<DicomMap> >::const_iterator mainDicomTags = resourcesMainDicomTags.find(*resource); - std::map<std::string, boost::shared_ptr<Json::Value> >::const_iterator dicomAsJson = resourcesDicomAsJson.find(*resource); - - context.ExpandResource(expanded, *resource, - *(mainDicomTags->second.get()), - instanceId->second, - dicomAsJson->second.get(), - level, format, requestedTags, allowStorageAccess); - } - else - { - context.ExpandResource(expanded, *resource, level, format, requestedTags, allowStorageAccess); - } - - if (expanded.type() == Json::objectValue) - { - answer.append(expanded); - } - } - else - { - answer.append(*resource); - } - } - - output.AnswerJson(answer); - } - - - static void AnswerListOfResources(RestApiOutput& output, - ServerContext& context, - const std::list<std::string>& resources, - ResourceType level, - bool expand, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - std::map<std::string, std::string> unusedInstancesIds; - std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags; - std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson; - - AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); - } - - template <enum ResourceType resourceType> static void ListResources(RestApiGetCall& call) { @@ -215,21 +177,23 @@ .SetDescription("List the Orthanc identifiers of all the available DICOM " + resources) .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false) .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false) - .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String, - "If present, retrieve detailed information about the individual " + resources, false) .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " "about the reported " + resources + " (if `expand` argument is provided)") .SetHttpGetSample("https://orthanc.uclouvain.be/demo/" + resources + "?since=0&limit=2", true); + OrthancRestApi::DocumentResponseContentAndExpand(call); return; } - - ServerIndex& index = OrthancRestApi::GetIndex(call); - ServerContext& context = OrthancRestApi::GetContext(call); - - std::list<std::string> result; + + // TODO-FIND: include the FindRequest options parsing like since, limit in a method (parse from get-arguments and from post payload) std::set<DicomTag> requestedTags; + ResponseContentFlags responseContent; + OrthancRestApi::GetRequestedTags(requestedTags, call); + OrthancRestApi::GetResponseContentAndExpand(responseContent, call); + + ResourceFinder finder(resourceType, responseContent, OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + finder.AddRequestedTags(requestedTags); if (call.HasArgument("limit") || call.HasArgument("since")) @@ -248,19 +212,16 @@ call.FlattenUri()); } - size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); - index.GetAllUuids(result, resourceType, since, limit); + uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", "")); + finder.SetLimitsSince(since); + finder.SetLimitsCount(limit); } - else - { - index.GetAllUuids(result, resourceType); - } - - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); + + Json::Value answer; + finder.Execute(answer, OrthancRestApi::GetContext(call), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } @@ -284,14 +245,17 @@ return; } - const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); - std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); + + ResourceFinder finder(resourceType, ResponseContentFlags_ExpandTrue, OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + finder.AddRequestedTags(requestedTags); + finder.SetOrthancId(resourceType, call.GetUriComponent("id", "")); + Json::Value json; - if (OrthancRestApi::GetContext(call).ExpandResource( - json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */)) + if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */)) { call.GetOutput().AnswerJson(json); } @@ -442,7 +406,7 @@ else { // return the attachment without any transcoding - context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); + context.AnswerAttachment(call.GetOutput(), ResourceType_Instance, publicId, FileContentType_Dicom); } } @@ -1619,12 +1583,13 @@ static void GetResourceStatistics(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get " + r + " statistics") .SetDescription("Get statistics about the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -1645,9 +1610,9 @@ "Size on the disk of the uncompressed DICOM instances associated with the " + r + ", expressed in bytes") .SetAnswerField("DicomUncompressedSizeMB", RestApiCallDocumentation::Type_Number, "Size on the disk of the uncompressed DICOM instances associated with the " + r + ", expressed in megabytes (MB)") - .SetHttpGetSample(GetDocumentationSampleResource(t) + "/statistics", true); - - switch (t) + .SetHttpGetSample(GetDocumentationSampleResource(level) + "/statistics", true); + + switch (level) { // Do NOT add "break" below this point! case ResourceType_Patient: @@ -1716,22 +1681,15 @@ // Handling of metadata ----------------------------------------------------- - static void CheckValidResourceType(const RestApiCall& call) - { - assert(!call.GetFullUri().empty()); - const std::string resourceType = call.GetFullUri() [0]; - StringToResourceType(resourceType.c_str()); - } - - static void ListMetadata(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("List metadata") .SetDescription("Get the list of metadata that are associated with the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -1741,13 +1699,12 @@ "If present, use the numeric identifier of the metadata instead of its symbolic name", false) .AddAnswerType(MimeType_Json, "JSON array containing the names of the available metadata, " "or JSON associative array mapping metadata to their values (if `expand` argument is provided)") - .SetHttpGetSample(GetDocumentationSampleResource(t) + "/metadata", true); + .SetHttpGetSample(GetDocumentationSampleResource(level) + "/metadata", true); return; } assert(!call.GetFullUri().empty()); const std::string publicId = call.GetUriComponent("id", ""); - ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); typedef std::map<MetadataType, std::string> Metadata; @@ -1879,12 +1836,13 @@ static void GetMetadata(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get metadata") .SetDescription("Get the value of a metadata that is associated with the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -1897,7 +1855,6 @@ assert(!call.GetFullUri().empty()); const std::string publicId = call.GetUriComponent("id", ""); - const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); std::string name = call.GetUriComponent("name", ""); MetadataType metadata = StringToMetadata(name); @@ -1926,12 +1883,13 @@ static void DeleteMetadata(RestApiDeleteCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Delete metadata") .SetDescription("Delete some metadata associated with the given DICOM " + r + ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).") @@ -1942,7 +1900,6 @@ return; } - CheckValidResourceType(call); const std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); @@ -1990,12 +1947,13 @@ static void SetMetadata(RestApiPutCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Set metadata") .SetDescription("Set the value of some metadata in the given DICOM " + r + ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).") @@ -2006,8 +1964,6 @@ return; } - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); MetadataType metadata = StringToMetadata(name); @@ -2055,23 +2011,22 @@ static void ListLabels(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("List labels") .SetDescription("Get the labels that are associated with the given " + r + " (new in Orthanc 1.12.0)") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .AddAnswerType(MimeType_Json, "JSON array containing the names of the labels") - .SetHttpGetSample(GetDocumentationSampleResource(t) + "/labels", true); + .SetHttpGetSample(GetDocumentationSampleResource(level) + "/labels", true); return; } - assert(!call.GetFullUri().empty()); const std::string publicId = call.GetUriComponent("id", ""); - ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); std::set<std::string> labels; OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); @@ -2089,12 +2044,13 @@ static void GetLabel(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Test label") .SetDescription("Test whether the " + r + " is associated with the given label") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -2103,11 +2059,7 @@ return; } - CheckValidResourceType(call); - - assert(!call.GetFullUri().empty()); const std::string publicId = call.GetUriComponent("id", ""); - const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); std::string label = call.GetUriComponent("label", ""); @@ -2123,12 +2075,13 @@ static void AddLabel(RestApiPutCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Add label") .SetDescription("Associate a label with a " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -2136,10 +2089,7 @@ return; } - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); - const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); std::string label = call.GetUriComponent("label", ""); OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Add); @@ -2150,12 +2100,13 @@ static void RemoveLabel(RestApiDeleteCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Remove label") .SetDescription("Remove a label associated with a " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -2163,10 +2114,7 @@ return; } - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); - const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); std::string label = call.GetUriComponent("label", ""); OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Remove); @@ -2179,26 +2127,26 @@ static void ListAttachments(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("List attachments") .SetDescription("Get the list of attachments that are associated with the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetHttpGetArgument("full", RestApiCallDocumentation::Type_String, "If present, retrieve the attachments list and their numerical ids", false) .AddAnswerType(MimeType_Json, "JSON array containing the names of the attachments") - .SetHttpGetSample(GetDocumentationSampleResource(t) + "/attachments", true); + .SetHttpGetSample(GetDocumentationSampleResource(level) + "/attachments", true); return; } - const std::string resourceType = call.GetFullUri() [0]; const std::string publicId = call.GetUriComponent("id", ""); std::set<FileContentType> attachments; - OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); + OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, level); Json::Value result; @@ -2207,7 +2155,7 @@ result = Json::objectValue; for (std::set<FileContentType>::const_iterator - it = attachments.begin(); it != attachments.end(); ++it) + it = attachments.begin(); it != attachments.end(); ++it) { std::string key = EnumerationToString(*it); result[key] = static_cast<uint16_t>(*it); @@ -2218,7 +2166,7 @@ result = Json::arrayValue; for (std::set<FileContentType>::const_iterator - it = attachments.begin(); it != attachments.end(); ++it) + it = attachments.begin(); it != attachments.end(); ++it) { result.append(EnumerationToString(*it)); } @@ -2240,16 +2188,15 @@ static bool GetAttachmentInfo(FileInfo& info, + ResourceType level, RestApiGetCall& call) { - CheckValidResourceType(call); - const std::string publicId = call.GetUriComponent("id", ""); const std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); int64_t revision; - if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType)) + if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, level, publicId, contentType)) { SetAttachmentETag(call.GetOutput(), revision, info); // New in Orthanc 1.9.2 @@ -2276,10 +2223,11 @@ static void GetAttachmentOperations(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag("Other") @@ -2291,7 +2239,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { Json::Value operations = Json::arrayValue; @@ -2330,12 +2278,13 @@ template <int uncompress> static void GetAttachmentData(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get attachment" + std::string(uncompress ? "" : " (no decompression)")) .SetDescription("Get the (binary) content of one attachment associated with the given " + r + std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`.")) @@ -2349,19 +2298,17 @@ ServerContext& context = OrthancRestApi::GetContext(call); - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); FileContentType type = StringToContentType(call.GetUriComponent("name", "")); FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { // NB: "SetAttachmentETag()" is already invoked by "GetAttachmentInfo()" if (uncompress) { - context.AnswerAttachment(call.GetOutput(), publicId, type); + context.AnswerAttachment(call.GetOutput(), level, publicId, type); } else { @@ -2369,7 +2316,7 @@ std::string content; std::string attachmentId; int64_t revision; - context.ReadAttachment(content, revision, attachmentId, publicId, type, false, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(content, revision, attachmentId, level, publicId, type, false, true /* skipCache when you absolutely need the compressed data */); int64_t userRevision; std::string userMD5; @@ -2390,13 +2337,14 @@ static void GetAttachmentSize(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment") .SetDescription("Get the size of one attachment associated with the given " + r) .AddAnswerType(MimeType_PlainText, "The size of the attachment"); @@ -2404,7 +2352,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText); } @@ -2412,13 +2360,14 @@ static void GetAttachmentInfo(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get info about the attachment") .SetDescription("Get all the information about the attachment associated with the given " + r) .AddAnswerType(MimeType_Json, "JSON object containing the information about the attachment") @@ -2427,7 +2376,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { Json::Value result = Json::objectValue; result["Uuid"] = info.GetUuid(); @@ -2443,13 +2392,14 @@ static void GetAttachmentCompressedSize(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment on disk") .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../size` iff `EnableStorage` is `true`.") @@ -2458,7 +2408,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText); } @@ -2467,13 +2417,14 @@ static void GetAttachmentMD5(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r) .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment"); @@ -2481,7 +2432,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call) && + if (GetAttachmentInfo(info, level, call) && info.GetUncompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText); @@ -2491,13 +2442,14 @@ static void GetAttachmentCompressedMD5(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment on disk") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../md5` iff `EnableStorage` is `true`.") @@ -2506,7 +2458,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call) && + if (GetAttachmentInfo(info, level, call) && info.GetCompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText); @@ -2516,12 +2468,13 @@ static void VerifyAttachment(RestApiPostCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Verify attachment") .SetDescription("Verify that the attachment is not corrupted, by validating its MD5 hash") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -2531,7 +2484,6 @@ } ServerContext& context = OrthancRestApi::GetContext(call); - CheckValidResourceType(call); std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); @@ -2539,7 +2491,7 @@ FileInfo info; int64_t revision; // Ignored - if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) || + if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, level, publicId, contentType) || info.GetCompressedMD5() == "" || info.GetUncompressedMD5() == "") { @@ -2553,7 +2505,7 @@ std::string data; std::string attachmentId; - context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(data, revision, attachmentId, level, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */); std::string actualMD5; Toolbox::ComputeMD5(actualMD5, data); @@ -2568,7 +2520,7 @@ } else { - context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(data, revision, attachmentId, level, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */); Toolbox::ComputeMD5(actualMD5, data); ok = (actualMD5 == info.GetUncompressedMD5()); } @@ -2588,12 +2540,13 @@ static void UploadAttachment(RestApiPutCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Set attachment") .SetDescription("Attach a file to the given DICOM " + r + ". This call will fail if trying to modify a system attachment (i.e. whose index is < 1024).") @@ -2606,7 +2559,6 @@ } ServerContext& context = OrthancRestApi::GetContext(call); - CheckValidResourceType(call); std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); @@ -2649,12 +2601,13 @@ static void DeleteAttachment(RestApiDeleteCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Delete attachment") .SetDescription("Delete an attachment associated with the given DICOM " + r + ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).") @@ -2665,8 +2618,6 @@ return; } - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); @@ -2738,12 +2689,13 @@ template <enum CompressionType compression> static void ChangeAttachmentCompression(RestApiPostCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary(compression == CompressionType_None ? "Uncompress attachment" : "Compress attachment") .SetDescription("Change the compression scheme that is used to store an attachment.") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") @@ -2751,26 +2703,25 @@ return; } - CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression); + OrthancRestApi::GetContext(call).ChangeAttachmentCompression(level, publicId, contentType, compression); call.GetOutput().AnswerBuffer("{}", MimeType_Json); } static void IsAttachmentCompressed(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); AddAttachmentDocumentation(call, r); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Is attachment compressed?") .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.") .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed"); @@ -2778,7 +2729,7 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + if (GetAttachmentInfo(info, level, call)) { std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; call.GetOutput().AnswerBuffer(answer, MimeType_PlainText); @@ -2891,20 +2842,21 @@ static void GetSharedTags(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full); - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get shared tags") .SetDescription("Extract the DICOM tags whose value is constant across all the child instances of " "the DICOM " + r + " whose Orthanc identifier is provided in the URL") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .AddAnswerType(MimeType_Json, "JSON object containing the values of the DICOM tags") - .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(t) + "/shared-tags", 5); + .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(level) + "/shared-tags", 5); return; } @@ -3082,70 +3034,13 @@ } - namespace + enum FindType { - class FindVisitor : public ServerContext::ILookupVisitor - { - private: - bool isComplete_; - std::list<std::string> resources_; - FindStorageAccessMode findStorageAccessMode_; - - // cache the data we used during lookup and that we could reuse when building the answers - std::map<std::string, std::string> instancesIds_; // the id of an instance for each found resource. - std::map<std::string, boost::shared_ptr<DicomMap> > resourcesMainDicomTags_; // all tags read from DB for a resource (current level and upper levels) - std::map<std::string, boost::shared_ptr<Json::Value> > resourcesDicomAsJson_; // the dicom-as-json for a resource - - DicomToJsonFormat format_; - - public: - explicit FindVisitor(DicomToJsonFormat format, FindStorageAccessMode findStorageAccessMode) : - isComplete_(false), - findStorageAccessMode_(findStorageAccessMode), - format_(format) - { - } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - isComplete_ = true; // Unused information as of Orthanc 1.5.0 - } - - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) ORTHANC_OVERRIDE - { - resources_.push_back(publicId); - instancesIds_[publicId] = instanceId; - resourcesMainDicomTags_[publicId].reset(mainDicomTags.Clone()); - if (dicomAsJson != NULL) - { - resourcesDicomAsJson_[publicId].reset(new Json::Value(*dicomAsJson)); // keep our own copy because we might reuse it between lookup and answers - } - else - { - resourcesDicomAsJson_[publicId] = boost::shared_ptr<Json::Value>(); - } - } - - void Answer(RestApiOutput& output, - ServerContext& context, - ResourceType level, - bool expand, - const std::set<DicomTag>& requestedTags) const - { - AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); - } - }; - } - - + FindType_Find, + FindType_Count + }; + + template <enum FindType requestType> static void Find(RestApiPostCall& call) { static const char* const KEY_CASE_SENSITIVE = "CaseSensitive"; @@ -3157,41 +3052,73 @@ static const char* const KEY_SINCE = "Since"; static const char* const KEY_LABELS = "Labels"; // New in Orthanc 1.12.0 static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint"; // New in Orthanc 1.12.0 + static const char* const KEY_ORDER_BY = "OrderBy"; // New in Orthanc 1.12.5 + static const char* const KEY_ORDER_BY_KEY = "Key"; // New in Orthanc 1.12.5 + static const char* const KEY_ORDER_BY_TYPE = "Type"; // New in Orthanc 1.12.5 + static const char* const KEY_ORDER_BY_DIRECTION = "Direction"; // New in Orthanc 1.12.5 + static const char* const KEY_PARENT_PATIENT = "ParentPatient"; // New in Orthanc 1.12.5 + static const char* const KEY_PARENT_STUDY = "ParentStudy"; // New in Orthanc 1.12.5 + static const char* const KEY_PARENT_SERIES = "ParentSeries"; // New in Orthanc 1.12.5 + static const char* const KEY_METADATA_QUERY = "MetadataQuery"; // New in Orthanc 1.12.5 + static const char* const KEY_RESPONSE_CONTENT = "ResponseContent"; // New in Orthanc 1.12.5 if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); - call.GetDocumentation() - .SetTag("System") - .SetSummary("Look for local resources") - .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, " - "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: " - "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc") + RestApiCallDocumentation& doc = call.GetDocumentation(); + + doc.SetTag("System") .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean, "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false) - .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean, - "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false) .SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String, "Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true) - .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number, - "Limit the number of reported resources", false) - .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number, - "Show only the resources since the provided index (in conjunction with `Limit`)", false) - .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings, - "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true). " - "The tags requested tags are returned in the 'RequestedTags' field in the response. " - "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " - "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " - "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false) .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the filter on the values of the DICOM tags", true) .SetRequestField(KEY_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings, "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true) .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String, "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true) - .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " - "about the reported resources (if `Expand` argument is `true`)"); + .SetRequestField(KEY_PARENT_PATIENT, RestApiCallDocumentation::Type_String, + "Limit the reported resources to descendants of this patient (new in Orthanc 1.12.5)", true) + .SetRequestField(KEY_PARENT_STUDY, RestApiCallDocumentation::Type_String, + "Limit the reported resources to descendants of this study (new in Orthanc 1.12.5)", true) + .SetRequestField(KEY_PARENT_SERIES, RestApiCallDocumentation::Type_String, + "Limit the reported resources to descendants of this series (new in Orthanc 1.12.5)", true) + .SetRequestField(KEY_METADATA_QUERY, RestApiCallDocumentation::Type_JsonObject, + "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true); + + switch (requestType) + { + case FindType_Find: + doc.SetSummary("Look for local resources") + .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, " + "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: " + "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc") + .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean, + "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false) + .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number, + "Limit the number of reported resources", false) + .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number, + "Show only the resources since the provided index (in conjunction with `Limit`)", false) + .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects, + "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true) + .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " + "about the reported resources (if `Expand` argument is `true`)"); + + OrthancRestApi::DocumentRequestedTags(call); + OrthancRestApi::DocumentResponseContentAndExpand(call); + break; + case FindType_Count: + doc.SetSummary("Count local resources") + .SetDescription("This URI can be used to count the resources that are matching criterias on the content of the local Orthanc server, " + "in a way that is similar to tools/find") + .AddAnswerType(MimeType_Json, "A JSON object with the `Count` of matching resources"); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + return; } @@ -3222,24 +3149,6 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" must be a Boolean"); } - else if (request.isMember(KEY_LIMIT) && - request[KEY_LIMIT].type() != Json::intValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer"); - } - else if (request.isMember(KEY_SINCE) && - request[KEY_SINCE].type() != Json::intValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_SINCE) + "\" must be an integer"); - } - else if (request.isMember(KEY_REQUESTED_TAGS) && - request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); - } else if (request.isMember(KEY_LABELS) && request[KEY_LABELS].type() != Json::arrayValue) { @@ -3252,121 +3161,344 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings"); } - else + else if (request.isMember(KEY_METADATA_QUERY) && + request[KEY_METADATA_QUERY].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_METADATA_QUERY) + "\" must be an JSON object"); + } + else if (request.isMember(KEY_PARENT_PATIENT) && + request[KEY_PARENT_PATIENT].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_PARENT_PATIENT) + "\" must be a string"); + } + else if (request.isMember(KEY_PARENT_STUDY) && + request[KEY_PARENT_STUDY].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_PARENT_STUDY) + "\" must be a string"); + } + else if (request.isMember(KEY_PARENT_SERIES) && + request[KEY_PARENT_SERIES].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string"); + } + else if (requestType == FindType_Find && request.isMember(KEY_LIMIT) && + request[KEY_LIMIT].type() != Json::intValue) { - bool expand = false; - if (request.isMember(KEY_EXPAND)) - { - expand = request[KEY_EXPAND].asBool(); - } - - bool caseSensitive = false; - if (request.isMember(KEY_CASE_SENSITIVE)) + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer"); + } + else if (requestType == FindType_Find && request.isMember(KEY_SINCE) && + request[KEY_SINCE].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_SINCE) + "\" must be an integer"); + } + else if (requestType == FindType_Find && request.isMember(KEY_REQUESTED_TAGS) && + request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); + } + else if (requestType == FindType_Find && request.isMember(KEY_RESPONSE_CONTENT) && + request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array"); + } + else if (requestType == FindType_Find && request.isMember(KEY_ORDER_BY) && + request[KEY_ORDER_BY].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array"); + } + else if (true) + { + ResponseContentFlags responseContent = ResponseContentFlags_ID; + + if (requestType == FindType_Find) { - caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); - } - - size_t limit = 0; - if (request.isMember(KEY_LIMIT)) - { - int tmp = request[KEY_LIMIT].asInt(); - if (tmp < 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); - } - - limit = static_cast<size_t>(tmp); - } - - size_t since = 0; - if (request.isMember(KEY_SINCE)) - { - int tmp = request[KEY_SINCE].asInt(); - if (tmp < 0) + if (request.isMember(KEY_RESPONSE_CONTENT)) { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); + responseContent = ResponseContentFlags_Default; + + for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i) + { + responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString())); + } } - - since = static_cast<size_t>(tmp); - } - - std::set<DicomTag> requestedTags; - - if (request.isMember(KEY_REQUESTED_TAGS)) - { - FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); - } - - ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); - - DatabaseLookup query; - - Json::Value::Members members = request[KEY_QUERY].getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - if (request[KEY_QUERY][members[i]].type() != Json::stringValue) + else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool()) { - throw OrthancException(ErrorCode_BadRequest, - "Tag \"" + members[i] + "\" must be associated with a string"); - } - - const std::string value = request[KEY_QUERY][members[i]].asString(); - - if (!value.empty()) - { - // An empty string corresponds to an universal constraint, - // so we ignore it. This mimics the behavior of class - // "OrthancFindRequestHandler" - query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), - value, caseSensitive, true); + responseContent = ResponseContentFlags_ExpandTrue; } } - - std::set<std::string> labels; - - if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 + else if (requestType == FindType_Count) { - for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + responseContent = ResponseContentFlags_INTERNAL_CountResources; + } + + const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); + + ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode()); + + DatabaseLookup dicomTagLookup; + + { // common query code + bool caseSensitive = false; + if (request.isMember(KEY_CASE_SENSITIVE)) { - if (request[KEY_LABELS][i].type() != Json::stringValue) + caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); + } + + { // DICOM Tag query + + Json::Value::Members members = request[KEY_QUERY].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request[KEY_QUERY][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" must be associated with a string"); + } + + const std::string value = request[KEY_QUERY][members[i]].asString(); + + if (!value.empty()) + { + // An empty string corresponds to an universal constraint, + // so we ignore it. This mimics the behavior of class + // "OrthancFindRequestHandler" + dicomTagLookup.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + value, caseSensitive, true); + } + } + + if (requestType == FindType_Count && !dicomTagLookup.HasOnlyMainDicomTags()) + { + throw OrthancException(ErrorCode_BadRequest, + "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database"); + } + + finder.SetDatabaseLookup(dicomTagLookup); + } + + { // Metadata query + Json::Value::Members members = request[KEY_METADATA_QUERY].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request[KEY_METADATA_QUERY][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" must be associated with a string"); + } + MetadataType metadata = StringToMetadata(members[i]); + + const std::string value = request[KEY_METADATA_QUERY][members[i]].asString(); + + if (!value.empty()) + { + if (value.find('\\') != std::string::npos) + { + std::vector<std::string> items; + Toolbox::TokenizeString(items, value, '\\'); + + finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_List, items, caseSensitive)); + } + else if (value.find('*') != std::string::npos || value.find('?') != std::string::npos) + { + finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Wildcard, value, caseSensitive)); + } + else + { + finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Equal, value, caseSensitive)); + } + } + } + } + + { // labels query + if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + { + if (request[KEY_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + } + else + { + finder.AddLabel(request[KEY_LABELS][i].asString()); + } + } + } + + finder.SetLabelsConstraint(LabelsConstraint_All); + + if (request.isMember(KEY_LABELS_CONSTRAINT)) + { + const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); + if (s == "All") + { + finder.SetLabelsConstraint(LabelsConstraint_All); + } + else if (s == "Any") + { + finder.SetLabelsConstraint(LabelsConstraint_Any); + } + else if (s == "None") + { + finder.SetLabelsConstraint(LabelsConstraint_None); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + } + } + } + + // parents query + if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5 + { + finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString()); + } + else if (request.isMember(KEY_PARENT_STUDY)) + { + finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString()); + } + else if (request.isMember(KEY_PARENT_SERIES)) + { + finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString()); + } + } + + // response + if (requestType == FindType_Find) + { + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human); + + finder.SetDatabaseLimits(context.GetDatabaseLimits(level)); + + if (((request.isMember(KEY_LIMIT) && request[KEY_LIMIT].asInt64() != 0) || + (request.isMember(KEY_SINCE) && request[KEY_SINCE].asInt64() != 0)) && + !dicomTagLookup.HasOnlyMainDicomTags()) + { + throw OrthancException(ErrorCode_BadRequest, + "Unable to use " + std::string(KEY_LIMIT) + " or " + std::string(KEY_SINCE) + " in tools/find when querying tags that are not stored as MainDicomTags in the Database"); + } + + if (request.isMember(KEY_LIMIT)) + { + int64_t tmp = request[KEY_LIMIT].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); + } + else if (tmp != 0) // This is for compatibility with Orthanc 1.12.4 + { + finder.SetLimitsCount(static_cast<uint64_t>(tmp)); + } + } + + if (request.isMember(KEY_SINCE)) + { + int64_t tmp = request[KEY_SINCE].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); } else { - labels.insert(request[KEY_LABELS][i].asString()); + finder.SetLimitsSince(static_cast<uint64_t>(tmp)); } } - } - - LabelsConstraint labelsConstraint = LabelsConstraint_All; - - if (request.isMember(KEY_LABELS_CONSTRAINT)) - { - const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); - if (s == "All") + + if (request.isMember(KEY_REQUESTED_TAGS)) { - labelsConstraint = LabelsConstraint_All; + std::set<DicomTag> requestedTags; + FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); + finder.AddRequestedTags(requestedTags); } - else if (s == "Any") + + if (request.isMember(KEY_ORDER_BY)) // New in Orthanc 1.12.5 { - labelsConstraint = LabelsConstraint_Any; - } - else if (s == "None") - { - labelsConstraint = LabelsConstraint_None; + for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++) + { + if (request[KEY_ORDER_BY][i].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects"); + } + else + { + const Json::Value& order = request[KEY_ORDER_BY][i]; + FindRequest::OrderingDirection direction; + std::string directionString; + std::string typeString; + + if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string"); + } + + if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); + } + + Toolbox::ToLowerCase(directionString, order[KEY_ORDER_BY_DIRECTION].asString()); + if (directionString == "asc") + { + direction = FindRequest::OrderingDirection_Ascending; + } + else if (directionString == "desc") + { + direction = FindRequest::OrderingDirection_Descending; + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); + } + + if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); + } + + Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString()); + if (typeString == "dicomtag") + { + DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString()); + finder.AddOrdering(tag, direction); + } + else if (typeString == "metadata") + { + MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString()); + finder.AddOrdering(metadata, direction); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); + } + } + } } - else - { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); - } + + Json::Value answer; + finder.Execute(answer, context, format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } - - FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode()); - context.Apply(visitor, query, level, labels, labelsConstraint, since, limit); - visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); + else if (requestType == FindType_Count) + { + uint64_t count = finder.Count(context); + Json::Value answer; + answer["Count"] = Json::Value::UInt64(count); + call.GetOutput().AnswerJson(answer); + } + } } @@ -3395,50 +3527,35 @@ return; } - ServerIndex& index = OrthancRestApi::GetIndex(call); - ServerContext& context = OrthancRestApi::GetContext(call); + const bool expand = (!call.HasArgument("expand") || + // this "expand" is the only one to have a false default value to keep backward compatibility + call.GetBooleanArgument("expand", false)); + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - std::list<std::string> a, b, c; - a.push_back(call.GetUriComponent("id", "")); - - ResourceType type = start; - while (type != end) - { - b.clear(); - - for (std::list<std::string>::const_iterator - it = a.begin(); it != a.end(); ++it) - { - index.GetChildren(c, *it); - b.splice(b.begin(), c); - } - - type = GetChildResourceType(type); - - a.clear(); - a.splice(a.begin(), b); - } - - AnswerListOfResources(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false), // this "expand" is the only one to have a false default value to keep backward compatibility - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); + ResourceFinder finder(end, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID), OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + finder.SetOrthancId(start, call.GetUriComponent("id", "")); + finder.AddRequestedTags(requestedTags); + + Json::Value answer; + finder.Execute(answer, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } static void GetChildInstancesTags(RestApiGetCall& call) { + const ResourceType level = GetResourceTypeFromUri(call); + if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full); - ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); - std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); call.GetDocumentation() - .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */)) .SetSummary("Get tags of instances") .SetDescription("Get the tags of all the child instances of the DICOM " + r + " whose Orthanc identifier is provided in the URL") @@ -3446,7 +3563,7 @@ .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings, "Also include the DICOM tags that are provided in this list, even if their associated value is long", false) .AddAnswerType(MimeType_Json, "JSON object associating the Orthanc identifiers of the instances, with the values of their DICOM tags") - .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(t) + "/instances-tags", 5); + .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(level) + "/instances-tags", 5); return; } @@ -3538,7 +3655,7 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); Json::Value resource; - if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */)) + if (ExpandResource(resource, OrthancRestApi::GetContext(call), currentType, current, format, false)) { call.GetOutput().AnswerJson(resource); } @@ -3798,6 +3915,7 @@ static void GetBulkChildren(std::set<std::string>& target, ServerIndex& index, + ResourceType level, const std::set<std::string>& source) { target.clear(); @@ -3806,7 +3924,7 @@ it = source.begin(); it != source.end(); ++it) { std::list<std::string> children; - index.GetChildren(children, *it); + index.GetChildren(children, level, *it); for (std::list<std::string>::const_iterator child = children.begin(); child != children.end(); ++child) @@ -3817,24 +3935,6 @@ } - static void AddMetadata(Json::Value& target, - ServerIndex& index, - const std::string& resource, - ResourceType level) - { - target = Json::objectValue; - - std::map<MetadataType, std::string> content; - index.GetAllMetadata(content, resource, level); - - for (std::map<MetadataType, std::string>::const_iterator - it = content.begin(); it != content.end(); ++it) - { - target[EnumerationToString(it->first)] = it->second; - } - } - - static void BulkContent(RestApiPostCall& call) { static const char* const LEVEL = "Level"; @@ -3911,11 +4011,11 @@ // Need to explore children std::set<std::string> current; current.insert(*it); - + for (;;) { std::set<std::string> children; - GetBulkChildren(children, index, current); + GetBulkChildren(children, index, type, current); type = GetChildResourceType(type); if (type == level) @@ -3972,15 +4072,8 @@ it = interest.begin(); it != interest.end(); ++it) { Json::Value item; - std::set<DicomTag> emptyRequestedTags; // not supported for bulk content - - if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + if (ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) { - if (metadata) - { - AddMetadata(item[METADATA], index, *it, level); - } - answer.append(item); } } @@ -3996,16 +4089,10 @@ { ResourceType level; Json::Value item; - std::set<DicomTag> emptyRequestedTags; // not supported for bulk content if (index.LookupResourceType(level, *it) && - OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) { - if (metadata) - { - AddMetadata(item[METADATA], index, *it, level); - } - answer.append(item); } else @@ -4073,13 +4160,23 @@ Register("/series", ListResources<ResourceType_Series>); Register("/studies", ListResources<ResourceType_Study>); - Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); + if (!context_.IsReadOnly()) + { + Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); + Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); + Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); + Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); + + Register("/tools/bulk-delete", BulkDelete); + } + else + { + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating DELETE routes"; + } + Register("/instances/{id}", GetSingleResource<ResourceType_Instance>); - Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); Register("/patients/{id}", GetSingleResource<ResourceType_Patient>); - Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); Register("/series/{id}", GetSingleResource<ResourceType_Series>); - Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); Register("/studies/{id}", GetSingleResource<ResourceType_Study>); Register("/instances/{id}/statistics", GetResourceStatistics); @@ -4124,7 +4221,16 @@ Register("/instances/{id}/numpy", GetNumpyInstance); // New in Orthanc 1.10.0 Register("/patients/{id}/protected", IsProtectedPatient); - Register("/patients/{id}/protected", SetPatientProtection); + + if (!context_.IsReadOnly()) + { + Register("/patients/{id}/protected", SetPatientProtection); + } + else + { + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT /patients/{id}/protected route"; + } + std::vector<std::string> resourceTypes; resourceTypes.push_back("patients"); @@ -4142,14 +4248,15 @@ // New in Orthanc 1.12.0 Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels); Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel); - Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel); - Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel); + + if (!context_.IsReadOnly()) + { + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel); + } Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments); - Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations); - Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment); - Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); @@ -4157,14 +4264,35 @@ Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/md5", GetAttachmentMD5); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/size", GetAttachmentSize); - Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/info", GetAttachmentInfo); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/verify-md5", VerifyAttachment); + + if (!context_.IsReadOnly()) + { + Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment); + Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment); + Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>); + Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>); + } } - Register("/tools/invalidate-tags", InvalidateTags); + if (context_.IsReadOnly()) + { + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT, POST and DELETE attachments routes"; + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT and DELETE labels routes"; + } + + if (!context_.IsReadOnly()) + { + Register("/tools/invalidate-tags", InvalidateTags); + } + Register("/tools/lookup", Lookup); - Register("/tools/find", Find); + Register("/tools/find", Find<FindType_Find>); + if (context_.GetIndex().HasFindSupport()) + { + Register("/tools/count-resources", Find<FindType_Count>); + } Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>); Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>); @@ -4189,13 +4317,19 @@ Register("/series/{id}/ordered-slices", OrderSlices); Register("/series/{id}/numpy", GetNumpySeries); // New in Orthanc 1.10.0 - Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>); - Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>); - Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>); - Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>); - Register("/tools/reconstruct", ReconstructAllResources); + if (!context_.IsReadOnly()) + { + Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>); + Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>); + Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>); + Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>); + Register("/tools/reconstruct", ReconstructAllResources); + } + else + { + LOG(WARNING) << "READ-ONLY SYSTEM: deactivating /reconstruct routes"; + } Register("/tools/bulk-content", BulkContent); - Register("/tools/bulk-delete", BulkDelete); - } + } }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -92,6 +92,10 @@ static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode"; static const char* const USER_METADATA = "UserMetadata"; static const char* const HAS_LABELS = "HasLabels"; + static const char* const CAPABILITIES = "Capabilities"; + static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges"; + static const char* const HAS_EXTENDED_FIND = "HasExtendedFind"; + static const char* const READ_ONLY = "ReadOnly"; if (call.IsDocumentation()) { @@ -138,6 +142,10 @@ "The configured UserMetadata (new in Orthanc 1.12.0)") .SetAnswerField(HAS_LABELS, RestApiCallDocumentation::Type_Boolean, "Whether the database back-end supports labels (new in Orthanc 1.12.0)") + .SetAnswerField(CAPABILITIES, RestApiCallDocumentation::Type_JsonObject, + "Whether the back-end supports optional features like 'HasExtendedChanges', 'HasExtendedFind' (new in Orthanc 1.12.5) ") + .SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean, + "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)") .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true); return; } @@ -169,6 +177,7 @@ result[STORAGE_AREA_PLUGIN] = Json::nullValue; result[DATABASE_BACKEND_PLUGIN] = Json::nullValue; + result[READ_ONLY] = context.IsReadOnly(); #if ORTHANC_ENABLE_PLUGINS == 1 result[PLUGINS_ENABLED] = true; @@ -196,6 +205,9 @@ GetUserMetadataConfiguration(result[USER_METADATA]); result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport(); + result[CAPABILITIES] = Json::objectValue; + result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges(); + result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport(); call.GetOutput().AnswerJson(result); }
--- a/OrthancServer/Sources/OrthancWebDav.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -28,6 +28,7 @@ #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h" #include "../../OrthancFramework/Sources/Logging.h" +#include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerContext.h" @@ -50,6 +51,20 @@ { return boost::posix_time::second_clock::universal_time(); } + + + static void ParseTime(boost::posix_time::ptime& target, + const std::string& value) + { + try + { + target = boost::posix_time::from_iso_string(value); + } + catch (std::exception& e) + { + target = GetNow(); + } + } static void LookupTime(boost::posix_time::ptime& target, @@ -59,117 +74,111 @@ MetadataType metadata) { std::string value; - int64_t revision; // Ignored - if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata)) + if (context.GetIndex().LookupMetadata(value, publicId, level, metadata)) { - try - { - target = boost::posix_time::from_iso_string(value); - return; - } - catch (std::exception& e) - { - } + ParseTime(target, value); } - - target = GetNow(); + else + { + target = GetNow(); + } } - class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor + class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor { private: - ServerContext& context_; - bool isComplete_; - Collection& target_; - ResourceType level_; + bool isComplete_; + Collection& target_; public: - DicomIdentifiersVisitor(ServerContext& context, - Collection& target, - ResourceType level) : - context_(context), + explicit DicomIdentifiersVisitorV2(Collection& target) : isComplete_(false), - target_(target), - level_(level) + target_(target) { } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - + virtual void MarkAsComplete() ORTHANC_OVERRIDE { isComplete_ = true; // TODO } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { - DicomTag tag(0, 0); - MetadataType timeMetadata; + DicomMap resourceTags; + resource.GetMainDicomTags(resourceTags, resource.GetLevel()); - switch (level_) + std::string uid; + bool hasUid; + + std::string time; + bool hasTime; + + switch (resource.GetLevel()) { case ResourceType_Study: - tag = DICOM_TAG_STUDY_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); break; case ResourceType_Series: - tag = DICOM_TAG_SERIES_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); break; - + case ResourceType_Instance: - tag = DICOM_TAG_SOP_INSTANCE_UID; - timeMetadata = MetadataType_Instance_ReceptionDate; + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SOP_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_Instance_ReceptionDate); break; default: throw OrthancException(ErrorCode_InternalError); } - - std::string s; - if (mainDicomTags.LookupStringValue(s, tag, false) && - !s.empty()) + + if (hasUid && + !uid.empty()) { - std::unique_ptr<Resource> resource; + std::unique_ptr<Resource> item; - if (level_ == ResourceType_Instance) + if (resource.GetLevel() == ResourceType_Instance) { FileInfo info; - int64_t revision; // Ignored - if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom)) + int64_t revision; + if (resource.LookupAttachment(info, revision, FileContentType_Dicom)) { - std::unique_ptr<File> f(new File(s + ".dcm")); + std::unique_ptr<File> f(new File(uid + ".dcm")); f->SetMimeType(MimeType_Dicom); f->SetContentLength(info.GetUncompressedSize()); - resource.reset(f.release()); + item.reset(f.release()); } } else { - resource.reset(new Folder(s)); + item.reset(new Folder(uid)); } - if (resource.get() != NULL) + if (item.get() != NULL) { - boost::posix_time::ptime t; - LookupTime(t, context_, publicId, level_, timeMetadata); - resource->SetCreationTime(t); - target_.AddResource(resource.release()); + if (hasTime) + { + boost::posix_time::ptime t; + ParseTime(t, time); + item->SetCreationTime(t); + } + else + { + item->SetCreationTime(GetNow()); + } + + target_.AddResource(item.release()); } } } }; - - class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor + + class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor { private: ServerContext& context_; @@ -178,9 +187,9 @@ boost::posix_time::ptime& time_; public: - DicomFileVisitor(ServerContext& context, - std::string& target, - boost::posix_time::ptime& time) : + DicomFileVisitorV2(ServerContext& context, + std::string& target, + boost::posix_time::ptime& time) : context_(context), success_(false), target_(target), @@ -193,19 +202,12 @@ return success_; } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { if (success_) { @@ -213,70 +215,18 @@ } else { - LookupTime(time_, context_, publicId, ResourceType_Instance, MetadataType_Instance_ReceptionDate); - context_.ReadDicom(target_, publicId); - success_ = true; - } - } - }; - - - class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor - { - private: - ServerContext& context_; - bool success_; - std::string& target_; - ResourceType level_; - - public: - OrthancJsonVisitor(ServerContext& context, - std::string& target, - ResourceType level) : - context_(context), - success_(false), - target_(target), - level_(level) - { - } - - bool IsSuccess() const - { - return success_; - } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - } - - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE - { - Json::Value resource; - std::set<DicomTag> emptyRequestedTags; // not supported for webdav - - if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, true /* allowStorageAccess */)) - { - if (success_) + std::string s; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate)) { - success_ = false; // Two matches => Error + ParseTime(time_, s); } else { - target_ = resource.toStyledString(); + time_ = GetNow(); + } - // Replace UNIX newlines with DOS newlines - boost::replace_all(target_, "\n", "\r\n"); - - success_ = true; - } + context_.ReadDicom(target_, resource.GetIdentifier()); + success_ = true; } } }; @@ -486,7 +436,7 @@ std::list<std::string> resources; try { - context_.GetIndex().GetChildren(resources, parentSeries_); + context_.GetIndex().GetChildren(resources, ResourceType_Series, parentSeries_); } catch (OrthancException&) { @@ -502,7 +452,7 @@ FileInfo info; int64_t revision; // Ignored - if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom)) + if (context_.GetIndex().LookupAttachment(info, revision, ResourceType_Instance, *it, FileContentType_Dicom)) { std::unique_ptr<File> resource(new File(*it + ".dcm")); resource->SetMimeType(MimeType_Dicom); @@ -555,7 +505,7 @@ std::list<std::string> resources; try { - context_.GetIndex().GetChildren(resources, parentSeries_); + context_.GetIndex().GetChildren(resources, ResourceType_Series, parentSeries_); } catch (OrthancException&) { @@ -873,6 +823,7 @@ class OrthancWebDav::SingleDicomResource : public ListOfResources { private: + ResourceType parentLevel_; std::string parentId_; protected: @@ -880,7 +831,7 @@ { try { - GetContext().GetIndex().GetChildren(resources, parentId_); + GetContext().GetIndex().GetChildren(resources, parentLevel_, parentId_); } catch (OrthancException&) { @@ -912,6 +863,7 @@ const std::string& parentId, const Templates& templates) : ListOfResources(context, level, templates), + parentLevel_(GetParentResourceType(level)), parentId_(parentId) { } @@ -955,7 +907,7 @@ std::string year_; std::string month_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::list<std::string>& resources_; @@ -966,21 +918,14 @@ { } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { - resources_.push_back(publicId); + resources_.push_back(resource.GetIdentifier()); } }; @@ -992,7 +937,10 @@ true /* case sensitive */, true /* mandatory tag */); Visitor visitor(resources); - GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, GetContext().GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, GetContext()); } virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE @@ -1025,7 +973,7 @@ std::string year_; const Templates& templates_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::set<std::string> months_; @@ -1036,20 +984,16 @@ return months_; } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { + DicomMap mainDicomTags; + resource.GetMainDicomTags(mainDicomTags, ResourceType_Study); + std::string s; if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) && s.size() == 8) @@ -1071,7 +1015,10 @@ true /* case sensitive */, true /* mandatory tag */); Visitor visitor; - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, context_); for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin(); it != visitor.GetMonths().end(); ++it) @@ -1165,7 +1112,7 @@ }; - class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor + class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor { private: ServerContext& context_; @@ -1179,22 +1126,15 @@ { } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags /* unused */, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { Json::Value info; - context_.DeleteResource(info, publicId, level_); + context_.DeleteResource(info, resource.GetIdentifier(), level_); } }; @@ -1425,12 +1365,11 @@ { DatabaseLookup query; ResourceType level; - size_t limit = 0; // By default, no limits if (path.size() == 1) { level = ResourceType_Study; - limit = 0; // TODO - Should we limit here? + // TODO - Should we limit here? } else if (path.size() == 2) { @@ -1455,9 +1394,32 @@ return false; } - DicomIdentifiersVisitor visitor(context_, collection, level); - context_.Apply(visitor, query, level, 0 /* since */, limit); - + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + finder.SetRetrieveMetadata(true); + + switch (level) + { + case ResourceType_Study: + finder.AddRequestedTag(DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + finder.AddRequestedTag(DICOM_TAG_SERIES_INSTANCE_UID); + break; + + case ResourceType_Instance: + finder.AddRequestedTag(DICOM_TAG_SOP_INSTANCE_UID); + finder.SetRetrieveAttachments(true); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DicomIdentifiersVisitorV2 visitor(collection); + finder.Execute(visitor, context_); + return true; } else if (path[0] == BY_PATIENTS || @@ -1478,6 +1440,33 @@ } + static bool GetOrthancJson(std::string& target, + ServerContext& context, + ResourceType level, + const DatabaseLookup& query) + { + ResourceFinder finder(level, ResponseContentFlags_ExpandTrue, context.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + + Json::Value expanded; + finder.Execute(expanded, context, DicomToJsonFormat_Human, false /* don't add "Metadata" */); + + if (expanded.size() != 1) + { + return false; + } + else + { + target = expanded[0].toStyledString(); + + // Replace UNIX newlines with DOS newlines + boost::replace_all(target, "\n", "\r\n"); + + return true; + } + } + + bool OrthancWebDav::GetFileContent(MimeType& mime, std::string& content, boost::posix_time::ptime& modificationTime, @@ -1495,12 +1484,9 @@ DatabaseLookup query; query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1], true /* case sensitive */, true /* mandatory tag */); - - OrthancJsonVisitor visitor(context_, content, ResourceType_Study); - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); mime = MimeType_Json; - return visitor.IsSuccess(); + return GetOrthancJson(content, context_, ResourceType_Study, query); } else if (path.size() == 4 && path[3] == SERIES_INFO) @@ -1511,11 +1497,8 @@ query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2], true /* case sensitive */, true /* mandatory tag */); - OrthancJsonVisitor visitor(context_, content, ResourceType_Series); - context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */); - mime = MimeType_Json; - return visitor.IsSuccess(); + return GetOrthancJson(content, context_, ResourceType_Series, query); } else if (path.size() == 4 && boost::ends_with(path[3], ".dcm")) @@ -1530,10 +1513,16 @@ query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid, true /* case sensitive */, true /* mandatory tag */); - DicomFileVisitor visitor(context_, content, modificationTime); - context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */); - mime = MimeType_Dicom; + + ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + finder.SetRetrieveMetadata(true); + finder.SetRetrieveAttachments(true); + + DicomFileVisitorV2 visitor(context_, content, modificationTime); + finder.Execute(visitor, context_); + return visitor.IsSuccess(); } else @@ -1655,7 +1644,10 @@ } DicomDeleteVisitor visitor(context_, level); - context_.Apply(visitor, query, level, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, context_); return true; } else
--- a/OrthancServer/Sources/OrthancWebDav.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancWebDav.h Fri Nov 29 10:54:20 2024 +0100 @@ -38,8 +38,8 @@ typedef std::map<ResourceType, std::string> Templates; class DicomDeleteVisitor; - class DicomFileVisitor; - class DicomIdentifiersVisitor; + class DicomFileVisitorV2; + class DicomIdentifiersVisitorV2; class InstancesOfSeries; class InternalNode; class ListOfResources; @@ -47,6 +47,7 @@ class ListOfStudiesByMonth; class ListOfStudiesByYear; class OrthancJsonVisitor; + class OrthancJsonVisitorV2; class ResourcesIndex; class RootNode; class SingleDicomResource;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,1327 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersServer.h" +#include "ResourceFinder.h" + +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/OrthancException.h" +#include "../../OrthancFramework/Sources/SerializationToolbox.h" +#include "OrthancConfiguration.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" +#include "ServerIndex.h" + + +namespace Orthanc +{ + static bool IsComputedTag(const DicomTag& tag) + { + return (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES || + tag == DICOM_TAG_SOP_CLASSES_IN_STUDY || + tag == DICOM_TAG_MODALITIES_IN_STUDY || + tag == DICOM_TAG_INSTANCE_AVAILABILITY); + } + + void ResourceFinder::ConfigureChildrenCountComputedTag(DicomTag tag, + ResourceType parentLevel, + ResourceType childLevel) + { + if (request_.GetLevel() == parentLevel) + { + requestedComputedTags_.insert(tag); + request_.GetChildrenSpecification(childLevel).SetRetrieveCount(true); + } + } + + + void ResourceFinder::InjectChildrenCountComputedTag(DicomMap& requestedTags, + DicomTag tag, + const FindResponse::Resource& resource, + ResourceType level) const + { + if (IsRequestedComputedTag(tag)) + { + requestedTags.SetValue(tag, boost::lexical_cast<std::string>(resource.GetChildrenCount(level)), false); + } + } + + + void ResourceFinder::InjectComputedTags(DicomMap& requestedTags, + const FindResponse::Resource& resource) const + { + switch (resource.GetLevel()) + { + case ResourceType_Patient: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, resource, ResourceType_Study); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Study: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, resource, ResourceType_Instance); + + if (IsRequestedComputedTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + std::set<std::string> modalities; + resource.GetChildrenMainDicomTagValues(modalities, ResourceType_Series, DICOM_TAG_MODALITY); + + std::string s; + Toolbox::JoinStrings(s, modalities, "\\"); + + requestedTags.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, s, false); + } + + if (IsRequestedComputedTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + { + std::set<std::string> classes; + resource.GetChildrenMetadataValues(classes, ResourceType_Instance, MetadataType_Instance_SopClassUid); + + std::string s; + Toolbox::JoinStrings(s, classes, "\\"); + + requestedTags.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, s, false); + } + + break; + + case ResourceType_Series: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Instance: + if (IsRequestedComputedTag(DICOM_TAG_INSTANCE_AVAILABILITY)) + { + requestedTags.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); + } + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + SeriesStatus ResourceFinder::GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource) + { + if (resource.GetLevel() != ResourceType_Series) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + std::string s; + if (!resource.LookupMetadata(s, ResourceType_Series, MetadataType_Series_ExpectedNumberOfInstances) || + !SerializationToolbox::ParseUnsignedInteger32(expectedNumberOfInstances, s)) + { + return SeriesStatus_Unknown; + } + + std::set<std::string> values; + resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries); + + std::set<int64_t> instances; + + for (std::set<std::string>::const_iterator + it = values.begin(); it != values.end(); ++it) + { + int64_t index; + + if (!SerializationToolbox::ParseInteger64(index, *it)) + { + return SeriesStatus_Unknown; + } + + if (index <= 0 || + index > static_cast<int64_t>(expectedNumberOfInstances)) + { + // Out-of-range instance index + return SeriesStatus_Inconsistent; + } + + if (instances.find(index) != instances.end()) + { + // Twice the same instance index + return SeriesStatus_Inconsistent; + } + + instances.insert(index); + } + + if (instances.size() == static_cast<size_t>(expectedNumberOfInstances)) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + static void GetMainDicomSequencesFromMetadata(DicomMap& target, const FindResponse::Resource& resource, ResourceType level) + { + // read all main sequences from DB + std::string serializedSequences; + if (resource.LookupMetadata(serializedSequences, level, MetadataType_MainDicomSequences)) + { + Json::Value jsonMetadata; + Toolbox::ReadJson(jsonMetadata, serializedSequences); + + if (jsonMetadata["Version"].asInt() == 1) + { + target.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + } + + + void ResourceFinder::Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index, + DicomToJsonFormat format) const + { + /** + * This method closely follows "SerializeExpandedResource()" in + * "ServerContext.cpp" from Orthanc 1.12.4. + **/ + + if (responseContent_ == ResponseContentFlags_ID) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (resource.GetLevel() != request_.GetLevel()) + { + throw OrthancException(ErrorCode_InternalError); + } + + target = Json::objectValue; + + target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); + target["ID"] = resource.GetIdentifier(); + + if (responseContent_ & ResponseContentFlags_Parent) + { + switch (resource.GetLevel()) + { + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.GetParentIdentifier(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + if ((responseContent_ & ResponseContentFlags_Children) && (resource.GetLevel() != ResourceType_Instance)) + { + const std::set<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel())); + + Json::Value c = Json::arrayValue; + for (std::set<std::string>::const_iterator + it = children.begin(); it != children.end(); ++it) + { + c.append(*it); + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + target["Studies"] = c; + break; + + case ResourceType_Study: + target["Series"] = c; + break; + + case ResourceType_Series: + target["Instances"] = c; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + { + if ((responseContent_ & ResponseContentFlags_Status) || (responseContent_ & ResponseContentFlags_MetadataLegacy) ) + { + uint32_t expectedNumberOfInstances; + SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource); + + if (responseContent_ & ResponseContentFlags_Status ) + { + target["Status"] = EnumerationToString(status); + } + + if (responseContent_ & ResponseContentFlags_MetadataLegacy) + { + static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances"; + + if (status == SeriesStatus_Unknown) + { + target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue; + } + else + { + target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances; + } + } + } + break; + } + + case ResourceType_Instance: + { + if (responseContent_ & ResponseContentFlags_AttachmentsLegacy) + { + FileInfo info; + int64_t revision; + if (resource.LookupAttachment(info, revision, FileContentType_Dicom)) + { + target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize()); + target["FileUuid"] = info.GetUuid(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + if (responseContent_ & ResponseContentFlags_MetadataLegacy) + { + static const char* const INDEX_IN_SERIES = "IndexInSeries"; + + std::string s; + uint32_t indexInSeries; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) && + SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s)) + { + target[INDEX_IN_SERIES] = indexInSeries; + } + else + { + target[INDEX_IN_SERIES] = Json::nullValue; + } + } + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string s; + if (responseContent_ & ResponseContentFlags_MetadataLegacy) + { + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom)) + { + target["AnonymizedFrom"] = s; + } + + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom)) + { + target["ModifiedFrom"] = s; + } + + if (resource.GetLevel() == ResourceType_Patient || + resource.GetLevel() == ResourceType_Study || + resource.GetLevel() == ResourceType_Series) + { + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate)) + { + target["LastUpdate"] = s; + } + } + } + + if (responseContent_ & ResponseContentFlags_IsStable) + { + if (resource.GetLevel() == ResourceType_Patient || + resource.GetLevel() == ResourceType_Study || + resource.GetLevel() == ResourceType_Series) + { + target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId()); + } + } + + if (responseContent_ & ResponseContentFlags_MainDicomTags) + { + DicomMap allMainDicomTags; + resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel()); + + // read all main sequences from DB + GetMainDicomSequencesFromMetadata(allMainDicomTags, resource, resource.GetLevel()); + + static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; + + // TODO-FIND : Ignore "null" values + + DicomMap levelMainDicomTags; + allMainDicomTags.ExtractResourceInformation(levelMainDicomTags, resource.GetLevel()); + + target[MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], levelMainDicomTags, format); + + if (resource.GetLevel() == ResourceType_Study) + { + DicomMap patientMainDicomTags; + allMainDicomTags.ExtractPatientInformation(patientMainDicomTags); + + target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); + } + } + + if (responseContent_ & ResponseContentFlags_Labels) + { + Json::Value labels = Json::arrayValue; + + for (std::set<std::string>::const_iterator + it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it) + { + labels.append(*it); + } + + target["Labels"] = labels; + } + + if (responseContent_ & ResponseContentFlags_Metadata) // new in Orthanc 1.12.4 + { + const std::map<MetadataType, FindResponse::MetadataContent>& m = resource.GetMetadata(resource.GetLevel()); + + Json::Value metadata = Json::objectValue; + + for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator it = m.begin(); it != m.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second.GetValue(); + } + + target["Metadata"] = metadata; + } + + if (responseContent_ & ResponseContentFlags_Attachments) // new in Orthanc 1.12.5 + { + const std::map<FileContentType, FileInfo>& attachments = resource.GetAttachments(); + + target["Attachments"] = Json::arrayValue; + + for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin(); it != attachments.end(); ++it) + { + Json::Value attachment = Json::objectValue; + attachment["Uuid"] = it->second.GetUuid(); + attachment["ContentType"] = it->second.GetContentType(); + attachment["UncompressedSize"] = Json::Value::UInt64(it->second.GetUncompressedSize()); + attachment["CompressedSize"] = Json::Value::UInt64(it->second.GetCompressedSize()); + attachment["UncompressedMD5"] = it->second.GetUncompressedMD5(); + attachment["CompressedMD5"] = it->second.GetCompressedMD5(); + + target["Attachments"].append(attachment); + } + } + } + + + void ResourceFinder::UpdateRequestLimits(ServerContext& context) + { + if (context.GetIndex().HasFindSupport()) // in this case, limits are fully implemented in DB + { + pagingMode_ = PagingMode_FullDatabase; + + if (hasLimitsSince_ || hasLimitsCount_) + { + pagingMode_ = PagingMode_FullDatabase; + if (databaseLimits_ != 0 && limitsCount_ > databaseLimits_) + { + LOG(WARNING) << "ResourceFinder: 'Limit' is larger than LimitFindResults/LimitFindInstances configurations, using limit fron the configuration file"; + limitsCount_ = databaseLimits_; + } + + request_.SetLimits(limitsSince_, limitsCount_); + } + else if (databaseLimits_ != 0) + { + request_.SetLimits(0, databaseLimits_); + } + + } + else + { + // By default, use manual paging + pagingMode_ = PagingMode_FullManual; + + if (databaseLimits_ != 0) + { + request_.SetLimits(0, databaseLimits_ + 1); + } + else + { + request_.ClearLimits(); + } + + if (lookup_.get() == NULL && + (hasLimitsSince_ || hasLimitsCount_)) + { + pagingMode_ = PagingMode_FullDatabase; + request_.SetLimits(limitsSince_, limitsCount_); + } + + if (lookup_.get() != NULL && + isSimpleLookup_ && + (hasLimitsSince_ || hasLimitsCount_)) + { + /** + * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only + * accept the "limit" argument. The "since" must be implemented + * manually. + **/ + + if (hasLimitsSince_ && + limitsSince_ != 0) + { + pagingMode_ = PagingMode_ManualSkip; + request_.SetLimits(0, limitsCount_ + limitsSince_); + } + else + { + pagingMode_ = PagingMode_FullDatabase; + request_.SetLimits(0, limitsCount_); + } + } + } + } + + + ResourceFinder::ResourceFinder(ResourceType level, + ResponseContentFlags responseContent, + FindStorageAccessMode storageAccessMode) : + request_(level), + databaseLimits_(0), + isSimpleLookup_(true), + pagingMode_(PagingMode_FullManual), + hasLimitsSince_(false), + hasLimitsCount_(false), + limitsSince_(0), + limitsCount_(0), + responseContent_(responseContent), + storageAccessMode_(storageAccessMode), + isWarning002Enabled_(false), + isWarning004Enabled_(false), + isWarning005Enabled_(false) + { + { + OrthancConfiguration::ReaderLock lock; + isWarning002Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb); + isWarning004Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature); + isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel); + } + + request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags); + request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy)); + request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels); + + switch (level) + { + case ResourceType_Patient: + request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children); + request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); + break; + + case ResourceType_Study: + request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children); + request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent); + request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); + break; + + case ResourceType_Series: + if (responseContent_ & ResponseContentFlags_Status) + { + request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus + } + request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children); + request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent); + request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); + break; + + case ResourceType_Instance: + request_.SetRetrieveAttachments((responseContent_ & ResponseContentFlags_AttachmentsLegacy) // for FileSize & FileUuid + || (responseContent_ & ResponseContentFlags_Attachments)); + request_.SetRetrieveParentIdentifier(true); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void ResourceFinder::SetDatabaseLimits(uint64_t limits) + { + databaseLimits_ = limits; + } + + + void ResourceFinder::SetLimitsSince(uint64_t since) + { + if (hasLimitsSince_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimitsSince_ = true; + limitsSince_ = since; + } + } + + + void ResourceFinder::SetLimitsCount(uint64_t count) + { + if (hasLimitsCount_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimitsCount_ = true; + limitsCount_ = count; + } + } + + + void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup) + { + MainDicomTagsRegistry registry; + + lookup_.reset(lookup.Clone()); + + for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) + { + DicomTag tag = lookup.GetConstraint(i).GetTag(); + if (IsComputedTag(tag)) + { + AddRequestedTag(tag); + } + else + { + ResourceType level; + DicomTagType tagType; + registry.LookupTag(level, tagType, tag); + if (tagType == DicomTagType_Generic) + { + AddRequestedTag(tag); + } + } + } + + isSimpleLookup_ = registry.NormalizeLookup(request_.GetDicomTagConstraints(), lookup, request_.GetLevel()); + + // "request_.GetDicomTagConstraints()" only contains constraints on main DICOM tags + + for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++) + { + const DatabaseDicomTagConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i); + if (constraint.GetLevel() == request_.GetLevel()) + { + request_.SetRetrieveMainDicomTags(true); + } + else if (IsResourceLevelAboveOrEqual(constraint.GetLevel(), request_.GetLevel())) + { + request_.GetParentSpecification(constraint.GetLevel()).SetRetrieveMainDicomTags(true); + } + else + { + LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel()) + << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level (" + << EnumerationToString(constraint.GetLevel()) << "), this will return no result"; + } + + if (IsComputedTag(constraint.GetTag())) + { + // Sanity check + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void ResourceFinder::AddRequestedTag(const DicomTag& tag) + { + requestedTags_.insert(tag); + + if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient)) + { + if (request_.GetLevel() == ResourceType_Patient) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + /** + * This comes from the fact that patient-level tags are copied + * at the study level, as implemented by "ResourcesContent::AddResource()". + **/ + if (request_.GetLevel() == ResourceType_Study) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true); // to get the MainDicomSequences + } + } + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Study)) + { + if (request_.GetLevel() == ResourceType_Patient) + { + if (isWarning005Enabled_) + { + LOG(WARNING) << "W005: Requested tag " << tag.Format() + << " should only be read at the study, series, or instance level"; + } + request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance + } + else + { + if (request_.GetLevel() == ResourceType_Study) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true); // to get the MainDicomSequences + } + } + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Series)) + { + if (request_.GetLevel() == ResourceType_Patient || + request_.GetLevel() == ResourceType_Study) + { + if (isWarning005Enabled_) + { + LOG(WARNING) << "W005: Requested tag " << tag.Format() + << " should only be read at the series or instance level"; + } + request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance + } + else + { + if (request_.GetLevel() == ResourceType_Series) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMetadata(true); // to get the MainDicomSequences + } + } + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Instance)) + { + if (request_.GetLevel() == ResourceType_Patient || + request_.GetLevel() == ResourceType_Study || + request_.GetLevel() == ResourceType_Series) + { + if (isWarning005Enabled_) + { + LOG(WARNING) << "W005: Requested tag " << tag.Format() + << " should only be read at the instance level"; + } + request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance + } + else + { + request_.SetRetrieveMainDicomTags(true); + } + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Study); + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Series); + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Instance); + } + else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Series); + } + else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Instance); + } + else if (tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Series, ResourceType_Instance); + } + else if (tag == DICOM_TAG_SOP_CLASSES_IN_STUDY) + { + requestedComputedTags_.insert(tag); + request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_SopClassUid); + } + else if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + requestedComputedTags_.insert(tag); + if (request_.GetLevel() < ResourceType_Series) + { + request_.GetChildrenSpecification(ResourceType_Series).AddMainDicomTag(DICOM_TAG_MODALITY); + } + else if (request_.GetLevel() == ResourceType_Instance) // this happens in QIDO-RS when searching for instances without specifying a StudyInstanceUID -> all Study level tags must be included in the response + { + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true); + } + } + else if (tag == DICOM_TAG_INSTANCE_AVAILABILITY) + { + requestedComputedTags_.insert(tag); + } + else + { + // This is neither a main DICOM tag, nor a computed DICOM tag: + // We might need to access a DICOM file or the MainDicomSequences metadata + + request_.SetRetrieveMetadata(true); + + if (request_.GetLevel() != ResourceType_Instance) + { + request_.SetRetrieveOneInstanceMetadataAndAttachments(true); + } + } + } + + + void ResourceFinder::AddRequestedTags(const std::set<DicomTag>& tags) + { + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + AddRequestedTag(*it); + } + } + + + static void InjectRequestedTags(DicomMap& target, + std::set<DicomTag>& remainingRequestedTags /* in & out */, + const FindResponse::Resource& resource, + ResourceType level/*, + const std::set<DicomTag>& tags*/) + { + if (!remainingRequestedTags.empty() && level <= resource.GetLevel()) + { + std::set<DicomTag> savedMainDicomTags; + + DicomMap m; + resource.GetMainDicomTags(m, level); // read DicomTags from DB + + if (resource.GetMetadata(level).size() > 0) + { + GetMainDicomSequencesFromMetadata(m, resource, level); // read DicomSequences from metadata + + // check which tags have been saved in DB; that's the way to know if they are missing because they were not saved or because they have no value + + std::string signature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(level); // default signature in case it's not in the metadata (= the signature for 1.11.0) + if (resource.LookupMetadata(signature, level, MetadataType_MainDicomTagsSignature)) + { + if (level == ResourceType_Study) // when we retrieve the study tags, we actually also get the patient tags that are also saved at study level but not included in the signature + { + signature += ";" + DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient); // append the default signature (from before 1.11.0) + } + + FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, signature); + } + } + + std::set<DicomTag> copiedTags; + for (std::set<DicomTag>::const_iterator it = remainingRequestedTags.begin(); it != remainingRequestedTags.end(); ++it) + { + if (target.CopyTagIfExists(m, *it)) + { + copiedTags.insert(*it); + } + else if (savedMainDicomTags.find(*it) != savedMainDicomTags.end()) // the tag should have been saved in DB but has no value so we consider it has been copied + { + copiedTags.insert(*it); + } + } + + Toolbox::RemoveSets(remainingRequestedTags, copiedTags); + } + } + + + static void ConvertMetadata(std::map<MetadataType, std::string>& converted, + const FindResponse::Resource& resource) + { + const std::map<MetadataType, FindResponse::MetadataContent> metadata = resource.GetMetadata(ResourceType_Instance); + + for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + converted[it->first] = it->second.GetValue(); + } + } + + + static void ReadMissingTagsFromStorageArea(DicomMap& requestedTags, + ServerContext& context, + const FindRequest& request, + const FindResponse::Resource& resource, + const std::set<DicomTag>& missingTags) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) + { + std::string missings; + FromDcmtkBridge::FormatListOfTags(missings, missingTags); + + LOG(WARNING) << "W001: Accessing DICOM tags from storage when accessing " + << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false) + << " " << resource.GetIdentifier() + << ": " << missings; + } + + // TODO-FIND: What do we do if the DICOM has been removed since the request? + // Do we fail, or do we skip the resource? + + Json::Value tmpDicomAsJson; + + if (request.GetLevel() == ResourceType_Instance && + request.IsRetrieveMetadata() && + request.IsRetrieveAttachments()) + { + LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetIdentifier(); + + std::map<MetadataType, std::string> converted; + ConvertMetadata(converted, resource); + + context.ReadDicomAsJson(tmpDicomAsJson, resource.GetIdentifier(), converted, + resource.GetAttachments(), missingTags /* ignoreTagLength */); + } + else if (request.GetLevel() != ResourceType_Instance && + request.IsRetrieveOneInstanceMetadataAndAttachments()) + { + LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetOneInstancePublicId(); + + context.ReadDicomAsJson(tmpDicomAsJson, resource.GetOneInstancePublicId(), resource.GetOneInstanceMetadata(), + resource.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */); + } + else + { + // TODO-FIND: This fallback shouldn't be necessary + + FindRequest requestDicomAttachment(request.GetLevel()); + requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier()); + + if (request.GetLevel() == ResourceType_Instance) + { + requestDicomAttachment.SetRetrieveMetadata(true); + requestDicomAttachment.SetRetrieveAttachments(true); + } + else + { + requestDicomAttachment.SetRetrieveOneInstanceMetadataAndAttachments(true); + } + + FindResponse responseDicomAttachment; + context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment); + + if (responseDicomAttachment.GetSize() != 1) + { + throw OrthancException(ErrorCode_InexistentFile); + } + else + { + const FindResponse::Resource& response = responseDicomAttachment.GetResourceByIndex(0); + const std::string instancePublicId = response.GetIdentifier(); + LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId; + + if (request.GetLevel() == ResourceType_Instance) + { + std::map<MetadataType, std::string> converted; + ConvertMetadata(converted, resource); + + context.ReadDicomAsJson(tmpDicomAsJson, response.GetIdentifier(), converted, + response.GetAttachments(), missingTags /* ignoreTagLength */); + } + else + { + context.ReadDicomAsJson(tmpDicomAsJson, response.GetOneInstancePublicId(), response.GetOneInstanceMetadata(), + response.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */); + } + } + } + + DicomMap tmpDicomMap; + tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); + + for (std::set<DicomTag>::const_iterator it = missingTags.begin(); it != missingTags.end(); ++it) + { + assert(!requestedTags.HasTag(*it)); + if (tmpDicomMap.HasTag(*it)) + { + requestedTags.SetValue(*it, tmpDicomMap.GetValue(*it)); + } + else + { + requestedTags.SetNullValue(*it); // TODO-FIND: Is this compatible with Orthanc <= 1.12.3? + } + } + } + + uint64_t ResourceFinder::Count(ServerContext& context) const + { + uint64_t count = 0; + context.GetIndex().ExecuteCount(count, request_); + return count; + } + + + void ResourceFinder::Execute(IVisitor& visitor, + ServerContext& context) + { + UpdateRequestLimits(context); + + bool isWarning002Enabled = false; + bool isWarning004Enabled = false; + bool isWarning006Enabled = false; + bool isWarning007Enabled = false; + + { + OrthancConfiguration::ReaderLock lock; + isWarning002Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb); + isWarning004Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature); + isWarning006Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_006_RequestingTagFromMetaHeader); + isWarning007Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_007_MissingRequestedTagsNotReadFromDisk); + } + + FindResponse response; + context.GetIndex().ExecuteFind(response, request_); + + bool complete; + + switch (pagingMode_) + { + case PagingMode_FullDatabase: + case PagingMode_ManualSkip: + complete = true; + break; + + case PagingMode_FullManual: + complete = (databaseLimits_ == 0 || + response.GetSize() <= databaseLimits_); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (lookup_.get() != NULL) + { + LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << response.GetSize(); + } + + size_t countResults = 0; + size_t skipped = 0; + + for (size_t i = 0; i < response.GetSize(); i++) + { + const FindResponse::Resource& resource = response.GetResourceByIndex(i); + +#if 0 + { + Json::Value v; + resource.DebugExport(v, request_); + std::cout << v.toStyledString(); + } +#endif + + DicomMap outRequestedTags; + + if (HasRequestedTags()) + { + std::set<DicomTag> remainingRequestedTags = requestedTags_; // at this point, all requested tags are "missing" + + InjectComputedTags(outRequestedTags, resource); + Toolbox::RemoveSets(remainingRequestedTags, requestedComputedTags_); + + InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Patient); + InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Study); + InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Series); + InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Instance); + + if (DicomMap::HasMetaInformationTags(remainingRequestedTags)) // we are not able to retrieve meta information in RequestedTags + { + std::set<DicomTag> metaTagsToRemove; + for (std::set<DicomTag>::const_iterator it = remainingRequestedTags.begin(); it != remainingRequestedTags.end(); ++it) + { + if (it->GetGroup() == 0x0002) + { + metaTagsToRemove.insert(*it); + } + } + + if (isWarning006Enabled) + { + std::string joinedMetaTags; + FromDcmtkBridge::FormatListOfTags(joinedMetaTags, metaTagsToRemove); + LOG(WARNING) << "W006: Unable to include tags from the Meta Header in 'RequestedTags'. Skipping them: " << joinedMetaTags; + } + + Toolbox::RemoveSets(remainingRequestedTags, metaTagsToRemove); + } + + + if (!remainingRequestedTags.empty() && + !DicomMap::HasOnlyComputedTags(remainingRequestedTags)) // if the only remaining tags are computed tags, it is worthless to read them from disk + { + // If a lookup tag is not available from DB, it is included in remainingRequestedTags and it will always be included in the answer too + // -> from 1.12.5, "StorageAccessOnFind": "Always" is actually equivalent to "StorageAccessOnFind": "Answers" + if (IsStorageAccessAllowed()) + { + ReadMissingTagsFromStorageArea(outRequestedTags, context, request_, resource, remainingRequestedTags); + } + else if (isWarning007Enabled) + { + std::string joinedTags; + FromDcmtkBridge::FormatListOfTags(joinedTags, remainingRequestedTags); + LOG(WARNING) << "W007: Unable to include requested tags since 'StorageAccessOnFind' does not allow accessing the storage to build answers: " << joinedTags; + } + } + + std::string mainDicomTagsSignature; + if (isWarning002Enabled && + resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature) && + mainDicomTagsSignature != DicomMap::GetMainDicomTagsSignature(resource.GetLevel())) + { + LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false) + << " has been stored with another version of Main Dicom Tags list, you should POST to /" + << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false) + << "/" << resource.GetIdentifier() + << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin. Some MainDicomTags might be missing from this answer."; + } + else if (isWarning004Enabled && + request_.IsRetrieveMetadata() && + !resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature)) + { + LOG(WARNING) << "W004: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false) + << " has been stored with an old Orthanc version and does not have a MainDicomTagsSignature, you should POST to /" + << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false) + << "/" << resource.GetIdentifier() + << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin. Some MainDicomTags might be missing from this answer."; + } + + } + + bool match = true; + + if (lookup_.get() != NULL) + { + DicomMap tags; + resource.GetAllMainDicomTags(tags); + tags.Merge(outRequestedTags); + match = lookup_->IsMatch(tags); + } + + if (match) + { + if (pagingMode_ == PagingMode_FullDatabase) + { + visitor.Apply(resource, outRequestedTags); + } + else + { + if (hasLimitsSince_ && + skipped < limitsSince_) + { + skipped++; + } + else if (hasLimitsCount_ && + countResults >= limitsCount_) + { + // Too many results, don't mark as complete + complete = false; + break; + } + else + { + visitor.Apply(resource, outRequestedTags); + countResults++; + } + } + } + } + + if (complete) + { + visitor.MarkAsComplete(); + } + } + + bool ResourceFinder::IsStorageAccessAllowed() + { + switch (storageAccessMode_) + { + case FindStorageAccessMode_DiskOnAnswer: + case FindStorageAccessMode_DiskOnLookupAndAnswer: + return true; + case FindStorageAccessMode_DatabaseOnly: + return false; + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void ResourceFinder::Execute(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) + { + class Visitor : public IVisitor + { + private: + const ResourceFinder& that_; + ServerIndex& index_; + Json::Value& target_; + DicomToJsonFormat format_; + bool hasRequestedTags_; + bool includeAllMetadata_; + + public: + Visitor(const ResourceFinder& that, + ServerIndex& index, + Json::Value& target, + DicomToJsonFormat format, + bool hasRequestedTags, + bool includeAllMetadata) : + that_(that), + index_(index), + target_(target), + format_(format), + hasRequestedTags_(hasRequestedTags), + includeAllMetadata_(includeAllMetadata) + { + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + if (that_.responseContent_ != ResponseContentFlags_ID) + { + Json::Value item; + that_.Expand(item, resource, index_, format_); + + if (hasRequestedTags_) + { + static const char* const REQUESTED_TAGS = "RequestedTags"; + item[REQUESTED_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, format_); + } + + target_.append(item); + } + else + { + target_.append(resource.GetIdentifier()); + } + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + } + }; + + UpdateRequestLimits(context); + + target = Json::arrayValue; + + Visitor visitor(*this, context.GetIndex(), target, format, HasRequestedTags(), includeAllMetadata); + Execute(visitor, context); + } + + + bool ResourceFinder::ExecuteOneResource(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) + { + Json::Value answer; + Execute(answer, context, format, includeAllMetadata); + + if (answer.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_InternalError); + } + else if (answer.size() > 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else if (answer.empty()) + { + // Inexistent resource (or was deleted between the first and second phases) + return false; + } + else + { + target = answer[0]; + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,197 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "Database/FindRequest.h" +#include "Database/FindResponse.h" + +namespace Orthanc +{ + class DatabaseLookup; + class ServerContext; + class ServerIndex; + + class ResourceFinder : public boost::noncopyable + { + public: + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) = 0; + + virtual void MarkAsComplete() = 0; + }; + + private: + enum PagingMode + { + PagingMode_FullDatabase, + PagingMode_FullManual, + PagingMode_ManualSkip + }; + + FindRequest request_; + uint64_t databaseLimits_; + std::unique_ptr<DatabaseLookup> lookup_; + bool isSimpleLookup_; + PagingMode pagingMode_; + bool hasLimitsSince_; + bool hasLimitsCount_; + uint64_t limitsSince_; + uint64_t limitsCount_; + ResponseContentFlags responseContent_; + FindStorageAccessMode storageAccessMode_; + std::set<DicomTag> requestedTags_; + std::set<DicomTag> requestedComputedTags_; + + bool isWarning002Enabled_; + bool isWarning004Enabled_; + bool isWarning005Enabled_; + + bool IsRequestedComputedTag(const DicomTag& tag) const + { + return requestedComputedTags_.find(tag) != requestedComputedTags_.end(); + } + + void ConfigureChildrenCountComputedTag(DicomTag tag, + ResourceType parentLevel, + ResourceType childLevel); + + void InjectChildrenCountComputedTag(DicomMap& requestedTags, + DicomTag tag, + const FindResponse::Resource& resource, + ResourceType level) const; + + static SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource); + + void InjectComputedTags(DicomMap& requestedTags, + const FindResponse::Resource& resource) const; + + void UpdateRequestLimits(ServerContext& context); + + bool HasRequestedTags() const + { + return requestedTags_.size() > 0; + } + + bool IsStorageAccessAllowed(); + + public: + ResourceFinder(ResourceType level, + ResponseContentFlags responseContent, + FindStorageAccessMode storageAccessMode); + + void SetDatabaseLimits(uint64_t limits); + + void SetOrthancId(ResourceType level, + const std::string& id) + { + request_.SetOrthancId(level, id); + } + + void SetLimitsSince(uint64_t since); + + void SetLimitsCount(uint64_t count); + + void SetDatabaseLookup(const DatabaseLookup& lookup); + + void AddRequestedTag(const DicomTag& tag); + + void AddRequestedTags(const std::set<DicomTag>& tags); + + void AddOrdering(const DicomTag& tag, + FindRequest::OrderingDirection direction) + { + request_.AddOrdering(tag, direction); + } + + void AddOrdering(MetadataType metadataType, + FindRequest::OrderingDirection direction) + { + request_.AddOrdering(metadataType, direction); + } + + void AddMetadataConstraint(DatabaseMetadataConstraint* constraint) + { + request_.AddMetadataConstraint(constraint); + } + + void SetLabels(const std::set<std::string>& labels) + { + request_.SetLabels(labels); + } + + void AddLabel(const std::string& label) + { + request_.AddLabel(label); + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + request_.SetLabelsConstraint(constraint); + } + + void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve) + { + request_.SetRetrieveOneInstanceMetadataAndAttachments(retrieve); + } + + void SetRetrieveMetadata(bool retrieve) + { + request_.SetRetrieveMetadata(retrieve); + } + + void SetRetrieveAttachments(bool retrieve) + { + request_.SetRetrieveAttachments(retrieve); + } + + // NB: "index" is only used in this method to fill the "IsStable" information + void Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index, + DicomToJsonFormat format) const; + + void Execute(IVisitor& visitor, + ServerContext& context); + + void Execute(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata); + + bool ExecuteOneResource(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata); + + uint64_t Count(ServerContext& context) const; + }; +}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,287 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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/>. - **/ - - -#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../PrecompiledHeadersServer.h" -#endif - -#include "DatabaseConstraint.h" - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/OrthancException.h" -#else -# include <OrthancException.h> -#endif - -#include <cassert> - - -namespace Orthanc -{ - namespace Plugins - { -#if ORTHANC_ENABLE_PLUGINS == 1 - OrthancPluginResourceType Convert(ResourceType type) - { - switch (type) - { - case ResourceType_Patient: - return OrthancPluginResourceType_Patient; - - case ResourceType_Study: - return OrthancPluginResourceType_Study; - - case ResourceType_Series: - return OrthancPluginResourceType_Series; - - case ResourceType_Instance: - return OrthancPluginResourceType_Instance; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_ENABLE_PLUGINS == 1 - ResourceType Convert(OrthancPluginResourceType type) - { - switch (type) - { - case OrthancPluginResourceType_Patient: - return ResourceType_Patient; - - case OrthancPluginResourceType_Study: - return ResourceType_Study; - - case OrthancPluginResourceType_Series: - return ResourceType_Series; - - case OrthancPluginResourceType_Instance: - return ResourceType_Instance; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - OrthancPluginConstraintType Convert(ConstraintType constraint) - { - switch (constraint) - { - case ConstraintType_Equal: - return OrthancPluginConstraintType_Equal; - - case ConstraintType_GreaterOrEqual: - return OrthancPluginConstraintType_GreaterOrEqual; - - case ConstraintType_SmallerOrEqual: - return OrthancPluginConstraintType_SmallerOrEqual; - - case ConstraintType_Wildcard: - return OrthancPluginConstraintType_Wildcard; - - case ConstraintType_List: - return OrthancPluginConstraintType_List; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - ConstraintType Convert(OrthancPluginConstraintType constraint) - { - switch (constraint) - { - case OrthancPluginConstraintType_Equal: - return ConstraintType_Equal; - - case OrthancPluginConstraintType_GreaterOrEqual: - return ConstraintType_GreaterOrEqual; - - case OrthancPluginConstraintType_SmallerOrEqual: - return ConstraintType_SmallerOrEqual; - - case OrthancPluginConstraintType_Wildcard: - return ConstraintType_Wildcard; - - case OrthancPluginConstraintType_List: - return ConstraintType_List; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - } - - DatabaseConstraint::DatabaseConstraint(ResourceType level, - const DicomTag& tag, - bool isIdentifier, - ConstraintType type, - const std::vector<std::string>& values, - bool caseSensitive, - bool mandatory) : - level_(level), - tag_(tag), - isIdentifier_(isIdentifier), - constraintType_(type), - values_(values), - caseSensitive_(caseSensitive), - mandatory_(mandatory) - { - if (type != ConstraintType_List && - values_.size() != 1) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) : - level_(Plugins::Convert(constraint.level)), - tag_(constraint.tagGroup, constraint.tagElement), - isIdentifier_(constraint.isIdentifierTag), - constraintType_(Plugins::Convert(constraint.type)), - caseSensitive_(constraint.isCaseSensitive), - mandatory_(constraint.isMandatory) - { - if (constraintType_ != ConstraintType_List && - constraint.valuesCount != 1) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - values_.resize(constraint.valuesCount); - - for (uint32_t i = 0; i < constraint.valuesCount; i++) - { - assert(constraint.values[i] != NULL); - values_[i].assign(constraint.values[i]); - } - } -#endif - - - const std::string& DatabaseConstraint::GetValue(size_t index) const - { - if (index >= values_.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - return values_[index]; - } - } - - - const std::string& DatabaseConstraint::GetSingleValue() const - { - if (values_.size() != 1) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return values_[0]; - } - } - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, - std::vector<const char*>& tmpValues) const - { - memset(&constraint, 0, sizeof(constraint)); - - tmpValues.resize(values_.size()); - - for (size_t i = 0; i < values_.size(); i++) - { - tmpValues[i] = values_[i].c_str(); - } - - constraint.level = Plugins::Convert(level_); - constraint.tagGroup = tag_.GetGroup(); - constraint.tagElement = tag_.GetElement(); - constraint.isIdentifierTag = isIdentifier_; - constraint.isCaseSensitive = caseSensitive_; - constraint.isMandatory = mandatory_; - constraint.type = Plugins::Convert(constraintType_); - constraint.valuesCount = values_.size(); - constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]); - } -#endif - - - void DatabaseConstraints::Clear() - { - for (size_t i = 0; i < constraints_.size(); i++) - { - assert(constraints_[i] != NULL); - delete constraints_[i]; - } - - constraints_.clear(); - } - - - void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint) - { - if (constraint == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - else - { - constraints_.push_back(constraint); - } - } - - - const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const - { - if (index >= constraints_.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - assert(constraints_[index] != NULL); - return *constraints_[index]; - } - } -}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h Mon Nov 25 17:03:47 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. - * - * 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_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" -#else -// This is for the "orthanc-databases" project to reuse this file -# include <DicomFormat/DicomMap.h> -#endif - -#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 - -#if ORTHANC_ENABLE_PLUGINS == 1 -# include <orthanc/OrthancCDatabasePlugin.h> -# if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in 1.3.1 -# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2) -# undef ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT -# define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1 -# endif -# endif -#endif - -#include <deque> - -namespace Orthanc -{ - enum ConstraintType - { - ConstraintType_Equal, - ConstraintType_SmallerOrEqual, - ConstraintType_GreaterOrEqual, - ConstraintType_Wildcard, - ConstraintType_List - }; - - namespace Plugins - { -#if ORTHANC_ENABLE_PLUGINS == 1 - OrthancPluginResourceType Convert(ResourceType type); -#endif - -#if ORTHANC_ENABLE_PLUGINS == 1 - ResourceType Convert(OrthancPluginResourceType type); -#endif - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - OrthancPluginConstraintType Convert(ConstraintType constraint); -#endif - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - ConstraintType Convert(OrthancPluginConstraintType constraint); -#endif - } - - - // This class is also used by the "orthanc-databases" project - class DatabaseConstraint : public boost::noncopyable - { - private: - ResourceType level_; - DicomTag tag_; - bool isIdentifier_; - ConstraintType constraintType_; - std::vector<std::string> values_; - bool caseSensitive_; - bool mandatory_; - - public: - DatabaseConstraint(ResourceType level, - const DicomTag& tag, - bool isIdentifier, - ConstraintType type, - const std::vector<std::string>& values, - bool caseSensitive, - bool mandatory); - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - explicit DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint); -#endif - - ResourceType GetLevel() const - { - return level_; - } - - const DicomTag& GetTag() const - { - return tag_; - } - - bool IsIdentifier() const - { - return isIdentifier_; - } - - ConstraintType GetConstraintType() const - { - return constraintType_; - } - - size_t GetValuesCount() const - { - return values_.size(); - } - - const std::string& GetValue(size_t index) const; - - const std::string& GetSingleValue() const; - - bool IsCaseSensitive() const - { - return caseSensitive_; - } - - bool IsMandatory() const - { - return mandatory_; - } - - bool IsMatch(const DicomMap& dicom) const; - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, - std::vector<const char*>& tmpValues) const; -#endif - }; - - - class DatabaseConstraints : public boost::noncopyable - { - private: - std::deque<DatabaseConstraint*> constraints_; - - public: - ~DatabaseConstraints() - { - Clear(); - } - - void Clear(); - - void AddConstraint(DatabaseConstraint* constraint); // Takes ownership - - bool IsEmpty() const - { - return constraints_.empty(); - } - - size_t GetSize() const - { - return constraints_.size(); - } - - const DatabaseConstraint& GetConstraint(size_t index) const; - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,112 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DatabaseDicomTagConstraint.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#if ORTHANC_ENABLE_PLUGINS == 1 +# include "../../Plugins/Engine/PluginsEnumerations.h" +#endif + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + DatabaseDicomTagConstraint::DatabaseDicomTagConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive, + bool mandatory) : + level_(level), + tag_(tag), + isIdentifier_(isIdentifier), + constraintType_(type), + values_(values), + caseSensitive_(caseSensitive), + mandatory_(mandatory) + { + if (type != ConstraintType_List && + values_.size() != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const std::string& DatabaseDicomTagConstraint::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return values_[index]; + } + } + + + const std::string& DatabaseDicomTagConstraint::GetSingleValue() const + { + if (values_.size() != 1) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return values_[0]; + } + } + + +#if ORTHANC_ENABLE_PLUGINS == 1 + void DatabaseDicomTagConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector<const char*>& tmpValues) const + { + memset(&constraint, 0, sizeof(constraint)); + + tmpValues.resize(values_.size()); + + for (size_t i = 0; i < values_.size(); i++) + { + tmpValues[i] = values_[i].c_str(); + } + + constraint.level = Plugins::Convert(level_); + constraint.tagGroup = tag_.GetGroup(); + constraint.tagElement = tag_.GetElement(); + constraint.isIdentifierTag = isIdentifier_; + constraint.isCaseSensitive = caseSensitive_; + constraint.isMandatory = mandatory_; + constraint.type = Plugins::Convert(constraintType_); + constraint.valuesCount = values_.size(); + constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]); + } +#endif +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,101 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../ServerEnumerations.h" +#include "IDatabaseConstraint.h" + +#if ORTHANC_ENABLE_PLUGINS == 1 +# include "../../Plugins/Include/orthanc/OrthancCDatabasePlugin.h" +#endif + +namespace Orthanc +{ + class DatabaseDicomTagConstraint : public IDatabaseConstraint + { + private: + ResourceType level_; + DicomTag tag_; + bool isIdentifier_; + ConstraintType constraintType_; + std::vector<std::string> values_; + bool caseSensitive_; + bool mandatory_; + + public: + DatabaseDicomTagConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive, + bool mandatory); + + ResourceType GetLevel() const + { + return level_; + } + + const DicomTag& GetTag() const + { + return tag_; + } + + bool IsIdentifier() const + { + return isIdentifier_; + } + + virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE + { + return constraintType_; + } + + virtual size_t GetValuesCount() const ORTHANC_OVERRIDE + { + return values_.size(); + } + + virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE; + + virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE; + + virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE + { + return caseSensitive_; + } + + virtual bool IsMandatory() const ORTHANC_OVERRIDE + { + return mandatory_; + } + + +#if ORTHANC_ENABLE_PLUGINS == 1 + void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector<const char*>& tmpValues) const; +#endif + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,132 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DatabaseDicomTagConstraints.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + void DatabaseDicomTagConstraints::Clear() + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + delete constraints_[i]; + } + + constraints_.clear(); + } + + + void DatabaseDicomTagConstraints::AddConstraint(DatabaseDicomTagConstraint* constraint) + { + if (constraint == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + constraints_.push_back(constraint); + } + } + + + const DatabaseDicomTagConstraint& DatabaseDicomTagConstraints::GetConstraint(size_t index) const + { + if (index >= constraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(constraints_[index] != NULL); + return *constraints_[index]; + } + } + + + std::string DatabaseDicomTagConstraints::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + const DatabaseDicomTagConstraint& constraint = *constraints_[i]; + s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) + + ": " + constraint.GetTag().Format(); + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + s += " == " + constraint.GetSingleValue(); + break; + + case ConstraintType_SmallerOrEqual: + s += " <= " + constraint.GetSingleValue(); + break; + + case ConstraintType_GreaterOrEqual: + s += " >= " + constraint.GetSingleValue(); + break; + + case ConstraintType_Wildcard: + s += " ~~ " + constraint.GetSingleValue(); + break; + + case ConstraintType_List: + { + s += " in [ "; + bool first = true; + for (size_t j = 0; j < constraint.GetValuesCount(); j++) + { + if (first) + { + first = false; + } + else + { + s += ", "; + } + s += constraint.GetValue(j); + } + s += "]"; + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s += "\n"; + } + + return s; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,61 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "DatabaseDicomTagConstraint.h" + +#include <deque> + +namespace Orthanc +{ + class DatabaseDicomTagConstraints : public boost::noncopyable + { + private: + std::deque<DatabaseDicomTagConstraint*> constraints_; + + public: + ~DatabaseDicomTagConstraints() + { + Clear(); + } + + void Clear(); + + void AddConstraint(DatabaseDicomTagConstraint* constraint); // Takes ownership + + bool IsEmpty() const + { + return constraints_.empty(); + } + + size_t GetSize() const + { + return constraints_.size(); + } + + const DatabaseDicomTagConstraint& GetConstraint(size_t index) const; + + std::string Format() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DatabaseMetadataConstraint.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive) : + metadata_(metadata), + constraintType_(type), + values_(values), + caseSensitive_(caseSensitive) + { + if (type != ConstraintType_List && + values_.size() != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata, + ConstraintType type, + const std::string& value, + bool caseSensitive) : + metadata_(metadata), + constraintType_(type), + caseSensitive_(caseSensitive) + { + if (type == ConstraintType_List) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + values_.push_back(value); + } + + const std::string& DatabaseMetadataConstraint::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return values_[index]; + } + } + + + const std::string& DatabaseMetadataConstraint::GetSingleValue() const + { + if (values_.size() != 1) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return values_[0]; + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../ServerEnumerations.h" +#include "IDatabaseConstraint.h" + +namespace Orthanc +{ + class DatabaseMetadataConstraint : public IDatabaseConstraint + { + private: + MetadataType metadata_; + ConstraintType constraintType_; + std::vector<std::string> values_; + bool caseSensitive_; + + public: + DatabaseMetadataConstraint(MetadataType metadata, + ConstraintType type, + const std::string& value, + bool caseSensitive); + + DatabaseMetadataConstraint(MetadataType metadata, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive); + + const MetadataType& GetMetadata() const + { + return metadata_; + } + + virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE + { + return constraintType_; + } + + virtual size_t GetValuesCount() const ORTHANC_OVERRIDE + { + return values_.size(); + } + + virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE; + + virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE; + + virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE + { + return caseSensitive_; + } + + virtual bool IsMandatory() const ORTHANC_OVERRIDE + { + return true; + } + + }; +}
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -30,7 +30,7 @@ #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/Toolbox.h" -#include "DatabaseConstraint.h" +#include "DatabaseDicomTagConstraint.h" #include <boost/regex.hpp> @@ -154,7 +154,7 @@ } - DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) : + DicomTagConstraint::DicomTagConstraint(const DatabaseDicomTagConstraint& constraint) : tag_(constraint.GetTag()), constraintType_(constraint.GetConstraintType()), caseSensitive_(constraint.IsCaseSensitive()), @@ -215,6 +215,24 @@ } + static bool HasIntersection(const std::set<std::string>& expected, + const std::string& values) + { + std::vector<std::string> tokens; + Toolbox::TokenizeString(tokens, values, '\\'); + + for (size_t i = 0; i < tokens.size(); i++) + { + if (expected.find(tokens[i]) != expected.end()) + { + return true; + } + } + + return false; + } + + bool DicomTagConstraint::IsMatch(const std::string& value) const { NormalizedString source(value, caseSensitive_); @@ -224,7 +242,17 @@ case ConstraintType_Equal: { NormalizedString reference(GetValue(), caseSensitive_); - return source.GetValue() == reference.GetValue(); + + if (GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + std::set<std::string> expected; + expected.insert(reference.GetValue()); + return HasIntersection(expected, source.GetValue()); + } + else + { + return source.GetValue() == reference.GetValue(); + } } case ConstraintType_SmallerOrEqual: @@ -251,17 +279,16 @@ case ConstraintType_List: { + std::set<std::string> references; + for (std::set<std::string>::const_iterator it = values_.begin(); it != values_.end(); ++it) { NormalizedString reference(*it, caseSensitive_); - if (source.GetValue() == reference.GetValue()) - { - return true; - } + references.insert(reference.GetValue()); } - return false; + return HasIntersection(references, source.GetValue()); } default: @@ -342,8 +369,9 @@ } - DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level, - DicomTagType tagType) const + DatabaseDicomTagConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical, + ResourceType level, + DicomTagType tagType) const { bool isIdentifier, caseSensitive; @@ -365,13 +393,21 @@ std::vector<std::string> values; values.reserve(values_.size()); - + + isIdentical = true; + for (std::set<std::string>::const_iterator it = values_.begin(); it != values_.end(); ++it) { if (isIdentifier) { - values.push_back(ServerToolbox::NormalizeIdentifier(*it)); + std::string normalized = ServerToolbox::NormalizeIdentifier(*it); + values.push_back(normalized); + + if (normalized != *it) + { + isIdentical = false; + } } else { @@ -379,7 +415,7 @@ } } - return new DatabaseConstraint(level, tag_, isIdentifier, constraintType_, - values, caseSensitive, mandatory_); + return new DatabaseDicomTagConstraint(level, tag_, isIdentifier, constraintType_, + values, caseSensitive, mandatory_); } }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Fri Nov 29 10:54:20 2024 +0100 @@ -25,7 +25,7 @@ #include "../ServerEnumerations.h" #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" -#include "DatabaseConstraint.h" +#include "DatabaseDicomTagConstraint.h" #include <boost/shared_ptr.hpp> @@ -62,7 +62,7 @@ explicit DicomTagConstraint(const DicomTagConstraint& other); - explicit DicomTagConstraint(const DatabaseConstraint& constraint); + explicit DicomTagConstraint(const DatabaseDicomTagConstraint& constraint); const DicomTag& GetTag() const { @@ -109,7 +109,8 @@ std::string Format() const; - DatabaseConstraint* ConvertToDatabaseConstraint(ResourceType level, - DicomTagType tagType) const; + DatabaseDicomTagConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */, + ResourceType level, + DicomTagType tagType) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/IDatabaseConstraint.h Fri Nov 29 10:54:20 2024 +0100 @@ -0,0 +1,50 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 + +#include "../ServerEnumerations.h" +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IDatabaseConstraint : public boost::noncopyable + { + public: + virtual ~IDatabaseConstraint() + { + } + + virtual ConstraintType GetConstraintType() const = 0; + + virtual size_t GetValuesCount() const = 0; + + virtual const std::string& GetValue(size_t index) const = 0; + + virtual const std::string& GetSingleValue() const = 0; + + virtual bool IsCaseSensitive() const = 0; + + virtual bool IsMandatory() const = 0; + }; +}
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -21,25 +21,14 @@ **/ -#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../PrecompiledHeadersServer.h" -#endif - +#include "../PrecompiledHeadersServer.h" #include "ISqlLookupFormatter.h" -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/OrthancException.h" -# include "../../../OrthancFramework/Sources/Toolbox.h" -#else -# include <OrthancException.h> -# include <Toolbox.h> -#endif - -#include "DatabaseConstraint.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/Toolbox.h" +#include "../Database/FindRequest.h" +#include "DatabaseDicomTagConstraint.h" +#include "../Database/MainDicomTagsRegistry.h" #include <cassert> #include <boost/lexical_cast.hpp> @@ -68,11 +57,32 @@ throw OrthancException(ErrorCode_InternalError); } } - + + static std::string FormatLevel(const char* prefix, ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + return std::string(prefix) + "patients"; + + case ResourceType_Study: + return std::string(prefix) + "studies"; + + case ResourceType_Series: + return std::string(prefix) + "series"; + + case ResourceType_Instance: + return std::string(prefix) + "instances"; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + static bool FormatComparison(std::string& target, ISqlLookupFormatter& formatter, - const DatabaseConstraint& constraint, + const IDatabaseConstraint& constraint, size_t index, bool escapeBrackets) { @@ -244,7 +254,7 @@ static void FormatJoin(std::string& target, - const DatabaseConstraint& constraint, + const DatabaseDicomTagConstraint& constraint, size_t index) { std::string tag = "t" + boost::lexical_cast<std::string>(index); @@ -274,6 +284,99 @@ boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); } + static void FormatJoin(std::string& target, + const DatabaseMetadataConstraint& constraint, + ResourceType level, + size_t index) + { + std::string tag = "t" + boost::lexical_cast<std::string>(index); + + if (constraint.IsMandatory()) + { + target = " INNER JOIN "; + } + else + { + target = " LEFT JOIN "; + } + + target += "Metadata "; + + target += tag + " ON " + tag + ".id = " + FormatLevel(level) + + ".internalId AND " + tag + ".type = " + + boost::lexical_cast<std::string>(constraint.GetMetadata()); + } + + + static void FormatJoinForOrdering(std::string& target, + const DicomTag& tag, + size_t index, + ResourceType requestLevel) + { + std::string orderArg = "order" + boost::lexical_cast<std::string>(index); + + target.clear(); + + ResourceType tagLevel; + DicomTagType tagType; + MainDicomTagsRegistry registry; + + registry.LookupTag(tagLevel, tagType, tag); + + if (tagLevel == ResourceType_Patient && requestLevel == ResourceType_Study) + { // Patient tags are copied at study level + tagLevel = ResourceType_Study; + } + + std::string tagTable; + if (tagType == DicomTagType_Identifier) + { + tagTable = "DicomIdentifiers "; + } + else + { + tagTable = "MainDicomTags "; + } + + std::string tagFilter = orderArg + ".tagGroup = " + boost::lexical_cast<std::string>(tag.GetGroup()) + " AND " + orderArg + ".tagElement = " + boost::lexical_cast<std::string>(tag.GetElement()); + + if (tagLevel == requestLevel) + { + target = " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + FormatLevel(requestLevel) + + ".internalId AND " + tagFilter; + } + else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 1) + { + target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId " + " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "parent.internalId AND " + tagFilter; + } + else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 2) + { + target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId " + " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId " + " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandparent.internalId AND " + tagFilter; + } + else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 3) + { + target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId " + " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId " + " INNER JOIN Resources " + orderArg + "grandgrandparent ON " + orderArg + "grandgrandparent.internalId = " + orderArg + "grandparent.parentId " + " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandgrandparent.internalId AND " + tagFilter; + } + } + + static void FormatJoinForOrdering(std::string& target, + const MetadataType& metadata, + size_t index, + ResourceType requestLevel) + { + std::string arg = "order" + boost::lexical_cast<std::string>(index); + + + target = " INNER JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) + + ".internalId AND " + arg + ".type = " + + boost::lexical_cast<std::string>(metadata); + } static std::string Join(const std::list<std::string>& values, const std::string& prefix, @@ -308,7 +411,7 @@ static bool FormatComparison2(std::string& target, ISqlLookupFormatter& formatter, - const DatabaseConstraint& constraint, + const DatabaseDicomTagConstraint& constraint, bool escapeBrackets) { std::string comparison; @@ -478,7 +581,7 @@ void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel, ResourceType& upperLevel, const ResourceType& queryLevel, - const DatabaseConstraints& lookup) + const DatabaseDicomTagConstraints& lookup) { assert(ResourceType_Patient < ResourceType_Study && ResourceType_Study < ResourceType_Series && @@ -506,12 +609,13 @@ void ISqlLookupFormatter::Apply(std::string& sql, ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, size_t limit) { + // get the limit levels of the DICOM Tags lookup ResourceType lowerLevel, upperLevel; GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); @@ -526,7 +630,7 @@ for (size_t i = 0; i < lookup.GetSize(); i++) { - const DatabaseConstraint& constraint = lookup.GetConstraint(i); + const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i); std::string comparison; @@ -617,9 +721,233 @@ } + void ISqlLookupFormatter::Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const FindRequest& request) + { + const bool escapeBrackets = formatter.IsEscapeBrackets(); + ResourceType queryLevel = request.GetLevel(); + const std::string& strQueryLevel = FormatLevel(queryLevel); + + ResourceType lowerLevel, upperLevel; + GetLookupLevels(lowerLevel, upperLevel, queryLevel, request.GetDicomTagConstraints()); + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + std::string ordering; + std::string orderingJoins; + + if (request.GetOrdering().size() > 0) + { + int counter = 0; + std::vector<std::string> orderByFields; + for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it) + { + std::string orderingJoin; + + switch ((*it)->GetKeyType()) + { + case FindRequest::KeyType_DicomTag: + FormatJoinForOrdering(orderingJoin, (*it)->GetDicomTag(), counter, request.GetLevel()); + break; + case FindRequest::KeyType_Metadata: + FormatJoinForOrdering(orderingJoin, (*it)->GetMetadataType(), counter, request.GetLevel()); + break; + default: + throw OrthancException(ErrorCode_InternalError); + } + orderingJoins += orderingJoin; + + std::string orderByField; + +#if ORTHANC_SQLITE_VERSION < 3030001 + // this is a way to push NULL values at the end before "NULLS LAST" was introduced: + // first filter by 0/1 and then by the column value itself + orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value IS NULL, "; +#endif + orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value"; + + if ((*it)->GetDirection() == FindRequest::OrderingDirection_Ascending) + { + orderByField += " ASC"; + } + else + { + orderByField += " DESC"; + } + orderByFields.push_back(orderByField); + ++counter; + } + + std::string orderByFieldsString; + Toolbox::JoinStrings(orderByFieldsString, orderByFields, ", "); + + ordering = "ROW_NUMBER() OVER (ORDER BY " + orderByFieldsString; +#if ORTHANC_SQLITE_VERSION >= 3030001 + ordering += " NULLS LAST"; +#endif + ordering += ") AS rowNumber"; + } + else + { + ordering = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber"; // we need a default ordering in order to make default queries repeatable when using since&limit + } + + sql = ("SELECT " + + strQueryLevel + ".publicId, " + + strQueryLevel + ".internalId, " + + ordering + + " FROM Resources AS " + strQueryLevel); + + + std::string joins, comparisons; + + // handle parent constraints + if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel) + { + ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel(); + + if (topParentLevel == queryLevel) + { + comparisons += " AND " + FormatLevel(topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel)); + } + else + { + comparisons += " AND " + FormatLevel("parent", topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel)); + + for (int level = queryLevel; level > topParentLevel; level--) + { + joins += " INNER JOIN Resources " + + FormatLevel("parent", static_cast<ResourceType>(level - 1)) + " ON " + + FormatLevel("parent", static_cast<ResourceType>(level - 1)) + ".internalId = "; + if (level == queryLevel) + { + joins += FormatLevel(static_cast<ResourceType>(level)) + ".parentId"; + } + else + { + joins += FormatLevel("parent", static_cast<ResourceType>(level)) + ".parentId"; + } + } + } + } + + size_t count = 0; + + const DatabaseDicomTagConstraints& dicomTagsConstraints = request.GetDicomTagConstraints(); + for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++) + { + const DatabaseDicomTagConstraint& constraint = dicomTagsConstraints.GetConstraint(i); + + std::string comparison; + + if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) + { + std::string join; + FormatJoin(join, constraint, count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it) + { + std::string comparison; + + if (FormatComparison(comparison, formatter, *(*it), count, escapeBrackets)) + { + std::string join; + FormatJoin(join, *(*it), request.GetLevel(), count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); + } + + std::list<std::string> where; + where.push_back(strQueryLevel + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + + if (!request.GetLabels().empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + + const std::set<std::string>& labels = request.GetLabels(); + std::list<std::string> formattedLabels; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + formattedLabels.push_back(formatter.GenerateParameter(*it)); + } + + std::string condition; + switch (request.GetLabelsConstraint()) + { + case LabelsConstraint_Any: + condition = "> 0"; + break; + + case LabelsConstraint_All: + condition = "= " + boost::lexical_cast<std::string>(labels.size()); + break; + + case LabelsConstraint_None: + condition = "= 0"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel + + ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); + } + + sql += joins + orderingJoins + Join(where, " WHERE ", " AND "); + + if (request.HasLimits()) + { + sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount()); + } + } + + void ISqlLookupFormatter::ApplySingleLevel(std::string& sql, ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, LabelsConstraint labelsConstraint, @@ -638,7 +966,7 @@ for (size_t i = 0; i < lookup.GetSize(); i++) { - const DatabaseConstraint& constraint = lookup.GetConstraint(i); + const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i); std::string comparison; @@ -730,5 +1058,4 @@ sql += " LIMIT " + boost::lexical_cast<std::string>(limit); } } - }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Fri Nov 29 10:54:20 2024 +0100 @@ -23,19 +23,16 @@ #pragma once -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/Enumerations.h" -#else -# include <Enumerations.h> -#endif +#include "../../../OrthancFramework/Sources/Enumerations.h" #include <boost/noncopyable.hpp> #include <vector> namespace Orthanc { - class DatabaseConstraints; - + class DatabaseDicomTagConstraints; + class FindRequest; + enum LabelsConstraint { LabelsConstraint_All, @@ -43,7 +40,6 @@ LabelsConstraint_None }; - // This class is also used by the "orthanc-databases" project class ISqlLookupFormatter : public boost::noncopyable { public: @@ -57,6 +53,8 @@ virtual std::string FormatWildcardEscape() = 0; + virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0; + /** * Whether to escape '[' and ']', which is only needed for * MSSQL. New in Orthanc 1.10.0, from the following changeset: @@ -67,11 +65,11 @@ static void GetLookupLevels(ResourceType& lowerLevel, ResourceType& upperLevel, const ResourceType& queryLevel, - const DatabaseConstraints& lookup); + const DatabaseDicomTagConstraints& lookup); static void Apply(std::string& sql, ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, // New in Orthanc 1.12.0 LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 @@ -79,10 +77,14 @@ static void ApplySingleLevel(std::string& sql, ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, + const DatabaseDicomTagConstraints& lookup, ResourceType queryLevel, const std::set<std::string>& labels, // New in Orthanc 1.12.0 LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 size_t limit); + + static void Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const FindRequest& request); }; }
--- a/OrthancServer/Sources/ServerContext.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -42,6 +42,7 @@ #include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" +#include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" @@ -68,12 +69,6 @@ namespace Orthanc { - static void ComputeStudyTags(ExpandedResource& resource, - ServerContext& context, - const std::string& studyPublicId, - const std::set<DicomTag>& requestedTags); - - static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax) { return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit || @@ -360,8 +355,9 @@ ServerContext::ServerContext(IDatabaseWrapper& database, IStorageArea& area, bool unitTesting, - size_t maxCompletedJobs) : - index_(*this, database, (unitTesting ? 20 : 500)), + size_t maxCompletedJobs, + bool readOnly) : + index_(*this, database, (unitTesting ? 20 : 500), readOnly), area_(area), compressionEnabled_(false), storeMD5_(true), @@ -387,6 +383,7 @@ ingestTranscodingOfUncompressed_(true), ingestTranscodingOfCompressed_(true), preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit), + readOnly_(readOnly), deidentifyLogs_(false), serverStartTimeUtc_(boost::posix_time::second_clock::universal_time()) { @@ -403,7 +400,14 @@ new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET(); jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); + saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); + if (readOnly_ && saveJobs_) + { + LOG(WARNING) << "READ-ONLY SYSTEM: SaveJobs = true is incompatible with a ReadOnly system, ignoring this configuration"; + saveJobs_ = false; + } + metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); // New configuration options in Orthanc 1.5.1 @@ -958,12 +962,13 @@ void ServerContext::AnswerAttachment(RestApiOutput& output, + ResourceType level, const std::string& resourceId, FileContentType content) { FileInfo attachment; int64_t revision; - if (!index_.LookupAttachment(attachment, revision, resourceId, content)) + if (!index_.LookupAttachment(attachment, revision, level, resourceId, content)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -975,7 +980,8 @@ } - void ServerContext::ChangeAttachmentCompression(const std::string& resourceId, + void ServerContext::ChangeAttachmentCompression(ResourceType level, + const std::string& resourceId, FileContentType attachmentType, CompressionType compression) { @@ -986,7 +992,7 @@ FileInfo attachment; int64_t revision; - if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType)) + if (!index_.LookupAttachment(attachment, revision, level, resourceId, attachmentType)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -1035,11 +1041,91 @@ dicomAsJson["7fe0,0010"] = pixelData; } + + static bool LookupMetadata(std::string& value, + MetadataType key, + const std::map<MetadataType, std::string>& instanceMetadata) + { + std::map<MetadataType, std::string>::const_iterator found = instanceMetadata.find(key); + + if (found == instanceMetadata.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + static bool LookupAttachment(FileInfo& target, + FileContentType type, + const std::map<FileContentType, FileInfo>& attachments) + { + std::map<FileContentType, FileInfo>::const_iterator found = attachments.find(type); + + if (found == attachments.end()) + { + return false; + } + else if (found->second.GetContentType() == type) + { + target = found->second; + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + void ServerContext::ReadDicomAsJson(Json::Value& result, const std::string& instancePublicId, const std::set<DicomTag>& ignoreTagLength) { + // TODO-FIND: This is a compatibility method, should be removed + + std::map<MetadataType, std::string> metadata; + std::map<FileContentType, FileInfo> attachments; + + FileInfo attachment; + int64_t revision; // Ignored + + if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom)) + { + attachments[FileContentType_Dicom] = attachment; + } + + if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomUntilPixelData)) + { + attachments[FileContentType_DicomUntilPixelData] = attachment; + } + + if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomAsJson)) + { + attachments[FileContentType_DicomAsJson] = attachment; + } + + std::string s; + if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance, + MetadataType_Instance_PixelDataOffset)) + { + metadata[MetadataType_Instance_PixelDataOffset] = s; + } + + ReadDicomAsJson(result, instancePublicId, metadata, attachments, ignoreTagLength); + } + + + void ServerContext::ReadDicomAsJson(Json::Value& result, + const std::string& instancePublicId, + const std::map<MetadataType, std::string>& instanceMetadata, + const std::map<FileContentType, FileInfo>& instanceAttachments, + const std::set<DicomTag>& ignoreTagLength) + { /** * CASE 1: The DICOM file, truncated at pixel data, is available * as an attachment (it was created either because the storage @@ -1048,9 +1134,8 @@ **/ FileInfo attachment; - int64_t revision; // Ignored - if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) + if (LookupAttachment(attachment, FileContentType_DicomUntilPixelData, instanceAttachments)) { std::string dicom; @@ -1076,8 +1161,7 @@ { std::string s; - if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, - MetadataType_Instance_PixelDataOffset)) + if (LookupMetadata(s, MetadataType_Instance_PixelDataOffset, instanceMetadata)) { hasPixelDataOffset = false; @@ -1108,7 +1192,7 @@ if (hasPixelDataOffset && area_.HasReadRange() && - index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) && + LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) && attachment.GetCompressionType() == CompressionType_None) { /** @@ -1131,7 +1215,7 @@ InjectEmptyPixelData(result); } else if (ignoreTagLength.empty() && - index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson)) + LookupAttachment(attachment, FileContentType_DicomAsJson, instanceAttachments)) { /** * CASE 3: This instance was created using Orthanc <= @@ -1213,7 +1297,7 @@ const std::string& instancePublicId) { int64_t revision; - ReadAttachment(dicom, revision, attachmentId, instancePublicId, FileContentType_Dicom, true /* uncompress */); + ReadAttachment(dicom, revision, attachmentId, ResourceType_Instance, instancePublicId, FileContentType_Dicom, true /* uncompress */); } @@ -1238,7 +1322,7 @@ { FileInfo attachment; int64_t revision; // Ignored - if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) + if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomUntilPixelData)) { StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); @@ -1253,7 +1337,7 @@ return false; } - if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) + if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError, "Unable to read the DICOM file of instance " + instancePublicId); @@ -1262,7 +1346,7 @@ std::string s; if (attachment.GetCompressionType() == CompressionType_None && - index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, + index_.LookupMetadata(s, instancePublicId, ResourceType_Instance, MetadataType_Instance_PixelDataOffset) && !s.empty()) { @@ -1290,13 +1374,14 @@ void ServerContext::ReadAttachment(std::string& result, int64_t& revision, std::string& attachmentId, + ResourceType level, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded, bool skipCache) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, revision, instancePublicId, content)) + if (!index_.LookupAttachment(attachment, revision, level, instancePublicId, content)) { throw OrthancException(ErrorCode_InternalError, "Unable to read attachment " + EnumerationToString(content) + @@ -1541,191 +1626,6 @@ } - void ServerContext::Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t since, - size_t limit) - { - unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? - limitFindInstances_ : limitFindResults_); - - std::vector<std::string> resources, instances; - const DicomTagConstraint* dicomModalitiesConstraint = NULL; - - bool hasModalitiesInStudyLookup = (queryLevel == ResourceType_Study && - lookup.GetConstraint(dicomModalitiesConstraint, DICOM_TAG_MODALITIES_IN_STUDY) && - ((dicomModalitiesConstraint->GetConstraintType() == ConstraintType_Equal && !dicomModalitiesConstraint->GetValue().empty()) || - (dicomModalitiesConstraint->GetConstraintType() == ConstraintType_List && !dicomModalitiesConstraint->GetValues().empty()))); - - std::unique_ptr<DatabaseLookup> fastLookup(lookup.Clone()); - - if (hasModalitiesInStudyLookup) - { - fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY); - } - - { - const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); - GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit); - } - - bool complete = (databaseLimit == 0 || - resources.size() <= databaseLimit); - - LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); - - /** - * "resources" contains the Orthanc ID of the resource at level - * "queryLevel", "instances" contains one the Orthanc ID of one - * sample instance from this resource. - **/ - assert(resources.size() == instances.size()); - - size_t countResults = 0; - size_t skipped = 0; - - const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); - - for (size_t i = 0; i < instances.size(); i++) - { - // Optimization in Orthanc 1.5.1 - Don't read the full JSON from - // the disk if only "main DICOM tags" are to be returned - - boost::shared_ptr<Json::Value> dicomAsJson; - - bool hasOnlyMainDicomTags; - DicomMap dicom; - DicomMap allMainDicomTagsFromDB; - - if (!IsStorageAccessAllowedForAnswers(findStorageAccessMode_) - || fastLookup->HasOnlyMainDicomTags()) - { - // Case (1): The main DICOM tags, as stored in the database, - // are sufficient to look for match - - if (!GetIndex().GetAllMainDicomTags(allMainDicomTagsFromDB, instances[i])) - { - // The instance has been removed during the execution of the - // lookup, ignore it - continue; - } - - // New in Orthanc 1.6.0: Only keep the main DICOM tags at the - // level of interest for the query - switch (queryLevel) - { - // WARNING: Don't reorder cases below, and don't add "break" - case ResourceType_Instance: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Instance); - - case ResourceType_Series: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Series); - - case ResourceType_Study: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Study); - - case ResourceType_Patient: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Patient); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - hasOnlyMainDicomTags = true; - } - else - { - // Case (2): Need to read the "DICOM-as-JSON" attachment from - // the storage area - dicomAsJson.reset(new Json::Value); - ReadDicomAsJson(*dicomAsJson, instances[i]); - - dicom.FromDicomAsJson(*dicomAsJson); - - // This map contains the entire JSON, i.e. more than the main DICOM tags - hasOnlyMainDicomTags = false; - } - - if (fastLookup->IsMatch(dicom)) - { - bool isMatch = true; - - if (hasModalitiesInStudyLookup) - { - std::set<DicomTag> requestedTags; - requestedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); - ExpandedResource resource; - ComputeStudyTags(resource, *this, resources[i], requestedTags); - - std::vector<std::string> modalities; - Toolbox::TokenizeString(modalities, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\'); - bool hasAtLeastOneModalityMatching = false; - for (size_t m = 0; m < modalities.size(); m++) - { - hasAtLeastOneModalityMatching |= dicomModalitiesConstraint->IsMatch(modalities[m]); - } - - isMatch = isMatch && hasAtLeastOneModalityMatching; - // copy the value of ModalitiesInStudy such that it can be reused to build the answer - allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY)); - } - - if (isMatch) - { - if (skipped < since) - { - skipped++; - } - else if (limit != 0 && - countResults >= limit) - { - // Too many results, don't mark as complete - complete = false; - break; - } - else - { - if (IsStorageAccessAllowedForAnswers(findStorageAccessMode_) && - dicomAsJson.get() == NULL && - isDicomAsJsonNeeded) - { - dicomAsJson.reset(new Json::Value); - ReadDicomAsJson(*dicomAsJson, instances[i]); - } - - if (hasOnlyMainDicomTags) - { - // This is Case (1): The variable "dicom" only contains the main DICOM tags - visitor.Visit(resources[i], instances[i], allMainDicomTagsFromDB, dicomAsJson.get()); - } - else - { - // Remove the non-main DICOM tags from "dicom" if Case (2) - // was used, for consistency with Case (1) - - DicomMap mainDicomTags; - mainDicomTags.ExtractMainDicomTags(dicom); - visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); - } - - countResults ++; - } - } - } - } - - if (complete) - { - visitor.MarkAsComplete(); - } - - LOG(INFO) << "Number of matching resources: " << countResults; - } - bool ServerContext::LookupOrReconstructMetadata(std::string& target, const std::string& publicId, ResourceType level, @@ -1738,8 +1638,7 @@ if (metadata == MetadataType_Instance_SopClassUid || metadata == MetadataType_Instance_TransferSyntax) { - int64_t revision; // Ignored - if (index_.LookupMetadata(target, revision, publicId, level, metadata)) + if (index_.LookupMetadata(target, publicId, level, metadata)) { return true; } @@ -1794,8 +1693,7 @@ else { // No backward - int64_t revision; // Ignored - return index_.LookupMetadata(target, revision, publicId, level, metadata); + return index_.LookupMetadata(target, publicId, level, metadata); } } @@ -2135,570 +2033,6 @@ isUnknownSopClassAccepted_ = accepted; } - - static void SerializeExpandedResource(Json::Value& target, - const ExpandedResource& resource, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags) - { - target = Json::objectValue; - - target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); - target["ID"] = resource.GetPublicId(); - - switch (resource.GetLevel()) - { - case ResourceType_Patient: - break; - - case ResourceType_Study: - target["ParentPatient"] = resource.parentId_; - break; - - case ResourceType_Series: - target["ParentStudy"] = resource.parentId_; - break; - - case ResourceType_Instance: - target["ParentSeries"] = resource.parentId_; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) - { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: - { - Json::Value c = Json::arrayValue; - - for (std::list<std::string>::const_iterator - it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) - { - c.append(*it); - } - - if (resource.GetLevel() == ResourceType_Patient) - { - target["Studies"] = c; - } - else if (resource.GetLevel() == ResourceType_Study) - { - target["Series"] = c; - } - else - { - target["Instances"] = c; - } - break; - } - - case ResourceType_Instance: - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) - { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - if (resource.expectedNumberOfInstances_ < 0) - { - target["ExpectedNumberOfInstances"] = Json::nullValue; - } - else - { - target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; - } - target["Status"] = resource.status_; - break; - - case ResourceType_Instance: - { - target["FileSize"] = static_cast<unsigned int>(resource.fileSize_); - target["FileUuid"] = resource.fileUuid_; - - if (resource.indexInSeries_ < 0) - { - target["IndexInSeries"] = Json::nullValue; - } - else - { - target["IndexInSeries"] = resource.indexInSeries_; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (!resource.anonymizedFrom_.empty()) - { - target["AnonymizedFrom"] = resource.anonymizedFrom_; - } - - if (!resource.modifiedFrom_.empty()) - { - target["ModifiedFrom"] = resource.modifiedFrom_; - } - - if (resource.GetLevel() == ResourceType_Patient || - resource.GetLevel() == ResourceType_Study || - resource.GetLevel() == ResourceType_Series) - { - target["IsStable"] = resource.isStable_; - - if (!resource.lastUpdate_.empty()) - { - target["LastUpdate"] = resource.lastUpdate_; - } - } - - // serialize tags - - static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; - static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; - - DicomMap mainDicomTags; - resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel()); - - target[MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); - - if (resource.GetLevel() == ResourceType_Study) - { - DicomMap patientMainDicomTags; - resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags); - - target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); - } - - if (requestedTags.size() > 0) - { - static const char* const REQUESTED_TAGS = "RequestedTags"; - - DicomMap tags; - resource.GetMainDicomTags().ExtractTags(tags, requestedTags); - - target[REQUESTED_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format); - - } - - { - Json::Value labels = Json::arrayValue; - - for (std::set<std::string>::const_iterator it = resource.labels_.begin(); it != resource.labels_.end(); ++it) - { - labels.append(*it); - } - - target["Labels"] = labels; - } - } - - - static void ComputeInstanceTags(ExpandedResource& resource, - ServerContext& context, - const std::string& instancePublicId, - const std::set<DicomTag>& requestedTags) - { - if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); - resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY); - } - } - - - static void ComputeSeriesTags(ExpandedResource& resource, - ServerContext& context, - const std::string& seriesPublicId, - const std::set<DicomTag>& requestedTags) - { - if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0) - { - ServerIndex& index = context.GetIndex(); - std::list<std::string> instances; - - index.GetChildren(instances, seriesPublicId); - - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - } - } - - static void ComputeStudyTags(ExpandedResource& resource, - ServerContext& context, - const std::string& studyPublicId, - const std::set<DicomTag>& requestedTags) - { - ServerIndex& index = context.GetIndex(); - std::list<std::string> series; - std::list<std::string> instances; - - bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0; - bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0; - bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0; - bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0; - - index.GetChildren(series, studyPublicId); - - if (hasModalitiesInStudy) - { - std::set<std::string> values; - - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - DicomMap tags; - index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series); - - const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); - - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - values.insert(value->GetContent()); - } - } - - std::string modalities; - Toolbox::JoinStrings(modalities, values, "\\"); - - resource.GetMainDicomTags().SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); - resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY); - } - - if (hasNbRelatedSeries) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, - boost::lexical_cast<std::string>(series.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - } - - if (hasNbRelatedInstances || hasSopClassesInStudy) - { - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - std::list<std::string> seriesInstancesIds; - index.GetChildren(seriesInstancesIds, *it); - - instances.splice(instances.end(), seriesInstancesIds); - } - - if (hasNbRelatedInstances) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - } - - if (hasSopClassesInStudy) - { - std::set<std::string> values; - - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string value; - - if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid)) - { - values.insert(value); - } - } - - if (values.size() > 0) - { - std::string sopClassUids; - Toolbox::JoinStrings(sopClassUids, values, "\\"); - resource.GetMainDicomTags().SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); - } - - resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY); - } - } - } - - static void ComputePatientTags(ExpandedResource& resource, - ServerContext& context, - const std::string& patientPublicId, - const std::set<DicomTag>& requestedTags) - { - ServerIndex& index = context.GetIndex(); - - std::list<std::string> studies; - std::list<std::string> series; - std::list<std::string> instances; - - bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0; - bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0; - bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0; - - index.GetChildren(studies, patientPublicId); - - if (hasNbRelatedStudies) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, - boost::lexical_cast<std::string>(studies.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - } - - if (hasNbRelatedSeries || hasNbRelatedInstances) - { - for (std::list<std::string>::const_iterator - it = studies.begin(); it != studies.end(); ++it) - { - std::list<std::string> thisSeriesIds; - index.GetChildren(thisSeriesIds, *it); - series.splice(series.end(), thisSeriesIds); - } - - if (hasNbRelatedSeries) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, - boost::lexical_cast<std::string>(series.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - } - } - - if (hasNbRelatedInstances) - { - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - std::list<std::string> thisInstancesIds; - index.GetChildren(thisInstancesIds, *it); - instances.splice(instances.end(), thisInstancesIds); - } - - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - } - } - - - static void ComputeTags(ExpandedResource& resource, - ServerContext& context, - const std::string& resourceId, - ResourceType level, - const std::set<DicomTag>& requestedTags) - { - if (level == ResourceType_Patient - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient)) - { - ComputePatientTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Study - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study)) - { - ComputeStudyTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Series - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series)) - { - ComputeSeriesTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Instance - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Instance)) - { - ComputeInstanceTags(resource, context, resourceId, requestedTags); - } - } - - bool ServerContext::ExpandResource(Json::Value& target, - const std::string& publicId, - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - std::string unusedInstanceId; - Json::Value* unusedDicomAsJson = NULL; - DicomMap unusedMainDicomTags; - - return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags, allowStorageAccess); - } - - bool ServerContext::ExpandResource(Json::Value& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource (if already available) - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - ExpandedResource resource; - - if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess)) - { - SerializeExpandedResource(target, resource, format, requestedTags); - return true; - } - - return false; - } - - bool ServerContext::ExpandResource(ExpandedResource& resource, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource (if already available) - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags, - bool allowStorageAccess) - { - // first try to get the tags from what is already available - - if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) && - mainDicomTags.GetSize() > 0 && - dicomAsJson != NULL) - { - - resource.GetMainDicomTags().Merge(mainDicomTags); - - if (dicomAsJson->isObject()) - { - resource.GetMainDicomTags().FromDicomAsJson(*dicomAsJson); - } - - std::set<DicomTag> retrievedTags; - std::set<DicomTag> missingTags; - resource.GetMainDicomTags().GetTags(retrievedTags); - - Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags); - - // if all possible tags have been read, no need to get them from DB anymore - if (missingTags.size() > 0 && DicomMap::HasOnlyComputedTags(missingTags)) - { - resource.missingRequestedTags_ = missingTags; - ComputeTags(resource, *this, publicId, level, requestedTags); - return true; - } - else if (missingTags.size() == 0) - { - expandFlags = static_cast<ExpandResourceFlags>(expandFlags & ~ExpandResourceFlags_IncludeMainDicomTags); - } - - if (missingTags.size() == 0 && expandFlags == ExpandResourceFlags_None) // we have already retrieved anything we need - { - return true; - } - } - - if (expandFlags != ExpandResourceFlags_None && - GetIndex().ExpandResource(resource, publicId, level, requestedTags, - static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature - { - // check the main dicom tags list has not changed since the resource was stored - if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.GetLevel())) - { - OrthancConfiguration::ReaderLock lock; - if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb)) - { - LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false) - << " has been stored with another version of Main Dicom Tags list, you should POST to /" - << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false) - << "/" << resource.GetPublicId() - << "/reconstruct to update the list of tags saved in DB. Some MainDicomTags might be missing from this answer."; - } - } - - // possibly merge missing requested tags from dicom-as-json - if (allowStorageAccess && - !resource.missingRequestedTags_.empty() && - !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) - { - OrthancConfiguration::ReaderLock lock; - if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) - { - std::set<DicomTag> missingTags; - Toolbox::AppendSets(missingTags, resource.missingRequestedTags_); - for (std::set<DicomTag>::const_iterator it = resource.missingRequestedTags_.begin(); it != resource.missingRequestedTags_.end(); ++it) - { - if (DicomMap::IsComputedTag(*it)) - { - missingTags.erase(*it); - } - } - - std::string missings; - FromDcmtkBridge::FormatListOfTags(missings, missingTags); - - LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " - << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false) - << " : " << missings; - } - - - std::string instanceId_ = instanceId; - DicomMap tagsFromJson; - - if (dicomAsJson == NULL) - { - if (instanceId_.empty()) - { - if (level == ResourceType_Instance) - { - instanceId_ = publicId; - } - else - { - std::list<std::string> instancesIds; - GetIndex().GetChildInstances(instancesIds, publicId); - if (instancesIds.size() < 1) - { - throw OrthancException(ErrorCode_InternalError, "ExpandResource: no instances found"); - } - instanceId_ = instancesIds.front(); - } - } - - Json::Value tmpDicomAsJson; - ReadDicomAsJson(tmpDicomAsJson, instanceId_, resource.missingRequestedTags_ /* ignoreTagLength */); // read all tags from DICOM and avoid cropping requested tags - tagsFromJson.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); - } - else - { - tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/); - } - - resource.GetMainDicomTags().Merge(tagsFromJson); - } - - // compute the requested tags - ComputeTags(resource, *this, publicId, level, requestedTags); - } - else - { - return false; - } - - return true; - } - int64_t ServerContext::GetServerUpTime() const { boost::posix_time::ptime nowUtc = boost::posix_time::second_clock::universal_time(); @@ -2706,5 +2040,4 @@ return elapsed.total_seconds(); } - }
--- a/OrthancServer/Sources/ServerContext.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.h Fri Nov 29 10:54:20 2024 +0100 @@ -66,25 +66,6 @@ friend class ServerIndex; // To access "RemoveFile()" public: - class ILookupVisitor : public boost::noncopyable - { - public: - virtual ~ILookupVisitor() - { - } - - virtual bool IsDicomAsJsonNeeded() const = 0; - - virtual void MarkAsComplete() = 0; - - // NB: "dicomAsJson" must *not* be deleted, and can be NULL if - // "!IsDicomAsJsonNeeded()" - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) = 0; - }; - struct StoreResult { private: @@ -277,6 +258,7 @@ boost::mutex dynamicOptionsMutex_; bool isUnknownSopClassAccepted_; std::set<DicomTransferSyntax> acceptedTransferSyntaxes_; + bool readOnly_; StoreResult StoreAfterTranscoding(std::string& resultPublicId, DicomInstanceToStore& dicom, @@ -321,7 +303,8 @@ ServerContext(IDatabaseWrapper& database, IStorageArea& area, bool unitTesting, - size_t maxCompletedJobs); + size_t maxCompletedJobs, + bool readOnly); ~ServerContext(); @@ -344,6 +327,15 @@ { return compressionEnabled_; } + bool IsReadOnly() const + { + return readOnly_; + } + + bool IsSaveJobs() const + { + return saveJobs_; + } bool AddAttachment(int64_t& newRevision, const std::string& resourceId, @@ -364,19 +356,27 @@ bool isReconstruct = false); void AnswerAttachment(RestApiOutput& output, + ResourceType level, const std::string& resourceId, FileContentType content); - void ChangeAttachmentCompression(const std::string& resourceId, + void ChangeAttachmentCompression(ResourceType level, + const std::string& resourceId, FileContentType attachmentType, CompressionType compression); void ReadDicomAsJson(Json::Value& result, const std::string& instancePublicId, + const std::map<MetadataType, std::string>& instanceMetadata, + const std::map<FileContentType, FileInfo>& instanceAttachments, const std::set<DicomTag>& ignoreTagLength); void ReadDicomAsJson(Json::Value& result, - const std::string& instancePublicId); + const std::string& instancePublicId, + const std::set<DicomTag>& ignoreTagLength); // TODO-FIND: Can this be removed? + + void ReadDicomAsJson(Json::Value& result, + const std::string& instancePublicId); // TODO-FIND: Can this be removed? void ReadDicom(std::string& dicom, const std::string& instancePublicId); @@ -395,6 +395,7 @@ void ReadAttachment(std::string& result, int64_t& revision, std::string& attachmentId, + ResourceType level, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded, @@ -445,21 +446,9 @@ void Stop(); - void Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t since, - size_t limit); - - void Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) + uint64_t GetDatabaseLimits(ResourceType level) const { - Apply(visitor, lookup, queryLevel, std::set<std::string>(), LabelsConstraint_All, since, limit); + return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); } bool LookupOrReconstructMetadata(std::string& target, @@ -600,33 +589,6 @@ void SetUnknownSopClassAccepted(bool accepted); - bool ExpandResource(Json::Value& target, - const std::string& publicId, - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess); - - bool ExpandResource(Json::Value& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess); - - bool ExpandResource(ExpandedResource& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags, - bool allowStorageAccess); - FindStorageAccessMode GetFindStorageAccessMode() const { return findStorageAccessMode_;
--- a/OrthancServer/Sources/ServerEnumerations.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -431,6 +431,86 @@ } } + ChangeType StringToChangeType(const std::string& value) + { + if (value == "CompletedSeries") + { + return ChangeType_CompletedSeries; + } + else if (value == "NewInstance") + { + return ChangeType_NewInstance; + } + else if (value == "NewPatient") + { + return ChangeType_NewPatient; + } + else if (value == "NewSeries") + { + return ChangeType_NewSeries; + } + else if (value == "NewStudy") + { + return ChangeType_NewStudy; + } + else if (value == "AnonymizedStudy") + { + return ChangeType_AnonymizedStudy; + } + else if (value == "AnonymizedSeries") + { + return ChangeType_AnonymizedSeries; + } + else if (value == "ModifiedStudy") + { + return ChangeType_ModifiedStudy; + } + else if (value == "ModifiedSeries") + { + return ChangeType_ModifiedSeries; + } + else if (value == "AnonymizedPatient") + { + return ChangeType_AnonymizedPatient; + } + else if (value == "ModifiedPatient") + { + return ChangeType_ModifiedPatient; + } + else if (value == "StablePatient") + { + return ChangeType_StablePatient; + } + else if (value == "StableStudy") + { + return ChangeType_StableStudy; + } + else if (value == "StableSeries") + { + return ChangeType_StableSeries; + } + else if (value == "Deleted") + { + return ChangeType_Deleted; + } + else if (value == "NewChildInstance") + { + return ChangeType_NewChildInstance; + } + else if (value == "UpdatedAttachment") + { + return ChangeType_UpdatedAttachment; + } + else if (value == "UpdatedMetadata") + { + return ChangeType_UpdatedMetadata; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Invalid value for a change: " + value); + } + } + const char* EnumerationToString(Verbosity verbosity) { @@ -536,4 +616,49 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } -} + + ResponseContentFlags StringToResponseContent(const std::string& value) + { + if (value == "MainDicomTags") + { + return ResponseContentFlags_MainDicomTags; + } + else if (value == "RequestedTags") + { + return ResponseContentFlags_RequestedTags; + } + else if (value == "Metadata") + { + return ResponseContentFlags_Metadata; + } + else if (value == "Status") + { + return ResponseContentFlags_Status; + } + else if (value == "Parent") + { + return ResponseContentFlags_Parent; + } + else if (value == "Children") + { + return ResponseContentFlags_Children; + } + else if (value == "Labels") + { + return ResponseContentFlags_Labels; + } + else if (value == "Attachments") + { + return ResponseContentFlags_Attachments; + } + else if (value == "IsStable") + { + return ResponseContentFlags_IsStable; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unrecognized value for \"ResponseContent\": " + value); + } + } +} \ No newline at end of file
--- a/OrthancServer/Sources/ServerEnumerations.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerEnumerations.h Fri Nov 29 10:54:20 2024 +0100 @@ -112,6 +112,51 @@ TransactionType_ReadWrite }; + enum ConstraintType + { + ConstraintType_Equal, + ConstraintType_SmallerOrEqual, + ConstraintType_GreaterOrEqual, + ConstraintType_Wildcard, + ConstraintType_List + }; + + enum ResponseContentFlags + { + ResponseContentFlags_ID = (1 << 0), + ResponseContentFlags_Type = (1 << 1), + ResponseContentFlags_RequestedTags = (1 << 2), + ResponseContentFlags_MainDicomTags = (1 << 3), + ResponseContentFlags_MetadataLegacy = (1 << 4), // when "Expand": true -> all metadata are included at root level + ResponseContentFlags_AttachmentsLegacy = (1 << 5), // when "Expand": true -> include attachments info at instance level + ResponseContentFlags_Metadata = (1 << 6), // all metadata are listed in a "Metadata" field + ResponseContentFlags_Attachments = (1 << 7), // all attachments are listed in a "Attachments" field + ResponseContentFlags_Status = (1 << 8), + ResponseContentFlags_Parent = (1 << 9), + ResponseContentFlags_Children = (1 << 10), + ResponseContentFlags_Labels = (1 << 11), + ResponseContentFlags_IsStable = (1 << 12), + + ResponseContentFlags_INTERNAL_CountResources = (1 << 31), + + // Some predefined combinations + ResponseContentFlags_ExpandTrue = (ResponseContentFlags_ID | + ResponseContentFlags_Type | + ResponseContentFlags_RequestedTags | + ResponseContentFlags_MainDicomTags | + ResponseContentFlags_MetadataLegacy | + ResponseContentFlags_AttachmentsLegacy | + ResponseContentFlags_Status | + ResponseContentFlags_Parent | + ResponseContentFlags_Children | + ResponseContentFlags_Labels | + ResponseContentFlags_IsStable), // equivalent to "Expand": true + + ResponseContentFlags_Default = (ResponseContentFlags_ID | + ResponseContentFlags_Type | + ResponseContentFlags_RequestedTags) // minimal content as soon as you have a "ResponseContent" + + }; /** * WARNING: Do not change the explicit values in the enumerations @@ -206,7 +251,11 @@ Warnings_None, Warnings_001_TagsBeingReadFromStorage, Warnings_002_InconsistentDicomTagsInDb, - Warnings_003_DecoderFailure, // new in Orthanc 1.12.5 + Warnings_003_DecoderFailure, // new in Orthanc 1.12.5 + Warnings_004_NoMainDicomTagsSignature, // new in Orthanc 1.12.5 + Warnings_005_RequestingTagFromLowerResourceLevel, // new in Orthanc 1.12.5 + Warnings_006_RequestingTagFromMetaHeader, // new in Orthanc 1.12.5 + Warnings_007_MissingRequestedTagsNotReadFromDisk // new in Orthanc 1.12.5 }; @@ -239,6 +288,8 @@ Verbosity StringToVerbosity(const std::string& str); + ResponseContentFlags StringToResponseContent(const std::string& str); + std::string EnumerationToString(FileContentType type); std::string GetFileContentMime(FileContentType type); @@ -251,6 +302,8 @@ const char* EnumerationToString(StoreStatus status); const char* EnumerationToString(ChangeType type); + + ChangeType StringToChangeType(const std::string& value); const char* EnumerationToString(Verbosity verbosity);
--- a/OrthancServer/Sources/ServerIndex.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -266,19 +266,19 @@ } }; - void ServerIndex::UpdateStatisticsThread(ServerIndex* that, - unsigned int threadSleepGranularityMilliseconds) + void ServerIndex::PerformDbHouskeeping(ServerIndex* that, + unsigned int threadSleepGranularityMilliseconds) { - Logging::SetCurrentThreadName("DB-STATS"); + Logging::SetCurrentThreadName("DB-HK"); - static const unsigned int SLEEP_SECONDS = 60; + static const unsigned int SLEEP_SECONDS = 1; if (threadSleepGranularityMilliseconds > 1000) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - LOG(INFO) << "Starting the update statistics thread (sleep = " << SLEEP_SECONDS << " seconds)"; + LOG(INFO) << "Starting the DB Housekeeping thread (sleep = " << SLEEP_SECONDS << " seconds)"; unsigned int count = 0; unsigned int countThreshold = (1000 * SLEEP_SECONDS) / threadSleepGranularityMilliseconds; @@ -290,14 +290,20 @@ if (count >= countThreshold) { - uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; - that->GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); + try + { + that->PerformDbHousekeeping(); + } + catch (...) + { + LOG(ERROR) << "Error while performing DB Housekeeping"; + } count = 0; } } - LOG(INFO) << "Stopping the update statistics thread"; + LOG(INFO) << "Stopping the DB Housekeeping thread"; } void ServerIndex::FlushThread(ServerIndex* that, @@ -346,35 +352,60 @@ ServerIndex::ServerIndex(ServerContext& context, IDatabaseWrapper& db, - unsigned int threadSleepGranularityMilliseconds) : - StatelessDatabaseOperations(db), + unsigned int threadSleepGranularityMilliseconds, + bool readOnly) : + StatelessDatabaseOperations(db, readOnly), done_(false), maximumStorageMode_(MaxStorageMode_Recycle), maximumStorageSize_(0), - maximumPatients_(0) + maximumPatients_(0), + readOnly_(readOnly) { SetTransactionContextFactory(new TransactionContextFactory(context)); // Initial recycling if the parameters have changed since the last // execution of Orthanc - StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_); + if (!readOnly) + { + StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_); + } // For some DB engines (like SQLite), make sure we flush the DB to disk at regular interval if (GetDatabaseCapabilities().HasFlushToDisk()) { - flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds); + if (readOnly) + { + LOG(WARNING) << "READ-ONLY SYSTEM: not starting the flush disk thread"; + } + else + { + flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds); + } } - // For some DB plugins that implements the UpdateAndGetStatistics function, updating - // the statistics can take quite some time if you have not done it for a long time - // -> make sure they are updated at regular interval - if (GetDatabaseCapabilities().HasUpdateAndGetStatistics()) + // For some DB plugins, some housekeeping might be required at regular interval + // like computing the statistics or update some tables after an upgrade + if (GetDatabaseCapabilities().HasDbHousekeeping()) { - updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds); + if (readOnly) + { + LOG(WARNING) << "READ-ONLY SYSTEM: not starting the DB Housekeeping thread"; + } + else + { + dbHousekeepingThread_ = boost::thread(PerformDbHouskeeping, this, threadSleepGranularityMilliseconds); + } } - unstableResourcesMonitorThread_ = boost::thread - (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds); + if (readOnly) + { + LOG(WARNING) << "READ-ONLY SYSTEM: not starting the unstable resources monitor thread"; + } + else + { + unstableResourcesMonitorThread_ = boost::thread + (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds); + } } @@ -399,9 +430,9 @@ flushThread_.join(); } - if (updateStatisticsThread_.joinable()) + if (dbHousekeepingThread_.joinable()) { - updateStatisticsThread_.join(); + dbHousekeepingThread_.join(); } if (unstableResourcesMonitorThread_.joinable())
--- a/OrthancServer/Sources/ServerIndex.h Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerIndex.h Fri Nov 29 10:54:20 2024 +0100 @@ -42,7 +42,7 @@ bool done_; boost::mutex monitoringMutex_; boost::thread flushThread_; - boost::thread updateStatisticsThread_; + boost::thread dbHousekeepingThread_; boost::thread unstableResourcesMonitorThread_; LeastRecentlyUsedIndex<std::pair<ResourceType, int64_t>, UnstableResourcePayload> unstableResources_; @@ -50,12 +50,13 @@ MaxStorageMode maximumStorageMode_; uint64_t maximumStorageSize_; unsigned int maximumPatients_; + bool readOnly_; static void FlushThread(ServerIndex* that, unsigned int threadSleep); - static void UpdateStatisticsThread(ServerIndex* that, - unsigned int threadSleep); + static void PerformDbHouskeeping(ServerIndex* that, + unsigned int threadSleep); static void UnstableResourcesMonitorThread(ServerIndex* that, unsigned int threadSleep); @@ -64,13 +65,11 @@ int64_t id, const std::string& publicId); - bool IsUnstableResource(ResourceType type, - int64_t id); - public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, - unsigned int threadSleepGranularityMilliseconds); + unsigned int threadSleepGranularityMilliseconds, + bool readOnly); ~ServerIndex(); @@ -103,5 +102,8 @@ bool hasOldRevision, int64_t oldRevision, const std::string& oldMD5); + + bool IsUnstableResource(ResourceType type, + int64_t id); }; }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -541,7 +541,7 @@ { FileInfo tmp; int64_t revision; // ignored - if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom)) + if (index.LookupAttachment(tmp, revision, ResourceType_Instance, id, FileContentType_Dicom)) { instances_.push_back(Instance(id, tmp.GetUncompressedSize())); } @@ -622,7 +622,7 @@ { // This is resource is marked for expansion std::list<std::string> children; - index.GetChildren(children, it->first); + index.GetChildren(children, GetResourceLevel(level_), it->first); std::unique_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -49,7 +49,7 @@ // Add all the instances of the series as to be processed std::list<std::string> instances; - GetContext().GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, ResourceType_Series, series); for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -69,7 +69,7 @@ else { std::list<std::string> series; - GetContext().GetIndex().GetChildren(series, study); + GetContext().GetIndex().GetChildren(series, ResourceType_Study, study); for (std::list<std::string>::const_iterator it = series.begin(); it != series.end(); ++it)
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -744,10 +744,18 @@ } else { - ExpandedResource originalStudy; - if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags)) + FindRequest request(ResourceType_Study); + request.SetOrthancStudyId(*studyId); + request.SetRetrieveMainDicomTags(true); + + FindResponse response; + GetContext().GetIndex().ExecuteFind(response, request); + + if (response.GetSize() == 1) { - targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false); + DicomMap tags; + response.GetResourceByIndex(0).GetMainDicomTags(tags, ResourceType_Study); + targetPatientId = tags.GetStringValue(DICOM_TAG_PATIENT_ID, "", false); } else { @@ -762,22 +770,34 @@ // if the patient exists, check how many child studies it has. if (lookupPatientResult.size() >= 1) { - ExpandedResource targetPatient; - - if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast<ExpandResourceFlags>(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren))) + FindRequest request(ResourceType_Patient); + request.SetOrthancPatientId(lookupPatientResult[0]); + request.SetRetrieveMainDicomTags(true); + request.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true); + + FindResponse response; + GetContext().GetIndex().ExecuteFind(response, request); + + if (response.GetSize() == 1) { - const std::list<std::string> childrenIds = targetPatient.childrenIds_; + const FindResponse::Resource& targetPatient = response.GetResourceByIndex(0); + + const std::set<std::string>& childrenIds = targetPatient.GetChildrenIdentifiers(ResourceType_Study); + bool targetPatientHasOtherStudies = childrenIds.size() > 1; if (childrenIds.size() == 1) { - targetPatientHasOtherStudies = std::find(childrenIds.begin(), childrenIds.end(), *studyId) == childrenIds.end(); // if the patient has one study that is not the one being modified + targetPatientHasOtherStudies = (childrenIds.find(*studyId) == childrenIds.end()); // if the patient has one study that is not the one being modified } if (targetPatientHasOtherStudies) { + DicomMap mainDicomTags; + targetPatient.GetMainDicomTags(mainDicomTags, ResourceType_Patient); + // this is allowed if all patient replacedTags do match the target patient tags DicomMap targetPatientTags; - targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags); + mainDicomTags.ExtractPatientInformation(targetPatientTags); std::set<DicomTag> mainPatientTags; DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient);
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -213,7 +213,7 @@ // Add all the instances of the series as to be processed std::list<std::string> instances; - GetContext().GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, ResourceType_Series, series); for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it)
--- a/OrthancServer/Sources/SliceOrdering.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/SliceOrdering.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -198,8 +198,7 @@ try { - int64_t revision; // Ignored - if (index.LookupMetadata(s, revision, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries)) + if (index.LookupMetadata(s, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries)) { indexInSeries_ = boost::lexical_cast<size_t>(Toolbox::StripSpaces(s)); hasIndexInSeries_ = true; @@ -297,7 +296,7 @@ void SliceOrdering::CreateInstances() { std::list<std::string> instancesId; - index_.GetChildren(instancesId, seriesId_); + index_.GetChildren(instancesId, ResourceType_Series, seriesId_); instances_.reserve(instancesId.size()); for (std::list<std::string>::const_iterator
--- a/OrthancServer/Sources/main.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/main.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -63,7 +63,7 @@ static const char* const KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION = "DicomTlsMinimumProtocolVersion"; static const char* const KEY_DICOM_TLS_ACCEPTED_CIPHERS = "DicomTlsCiphersAccepted"; static const char* const KEY_MAXIMUM_PDU_LENGTH = "MaximumPduLength"; - +static const char* const KEY_READ_ONLY = "ReadOnly"; class OrthancStoreRequestHandler : public IStoreRequestHandler { @@ -825,6 +825,7 @@ PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level"); PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden"); PrintErrorCode(ErrorCode_DuplicateResource, "Duplicate resource"); + PrintErrorCode(ErrorCode_IncompatibleConfigurations, "Your configuration file contains configuration that are mutually incompatible"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); @@ -836,7 +837,7 @@ PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database"); PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement"); PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement"); - PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)"); + PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bind a value while out of range (serious error)"); PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement"); PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice"); PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction"); @@ -1517,6 +1518,7 @@ bool loadJobsFromDatabase) { size_t maxCompletedJobs; + bool readOnly; { OrthancConfiguration::ReaderLock lock; @@ -1544,6 +1546,9 @@ LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended"; } + // New option in Orthanc 1.12.5 + readOnly = lock.GetConfiguration().GetBooleanParameter(KEY_READ_ONLY, false); + // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0) DicomAssociationParameters::SetDefaultOwnCertificatePath( lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""), @@ -1558,46 +1563,54 @@ lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true)); } - ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs); + ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs, readOnly); { OrthancConfiguration::ReaderLock lock; - context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false)); - context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true)); + if (context.IsReadOnly()) + { + LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: StorageCompression, StoreMD5ForAttachments, OverwriteInstances, MaximumPatientCount, MaximumStorageSize, MaximumStorageMode, SaveJobs"; + } + else + { + context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false)); + context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true)); - // New option in Orthanc 1.4.2 - context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); + // New option in Orthanc 1.4.2 + context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); - try - { - context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0)); - } - catch (...) - { - context.GetIndex().SetMaximumPatientCount(0); + try + { + context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0)); + } + catch (...) + { + context.GetIndex().SetMaximumPatientCount(0); + } + + try + { + uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0); + context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); + } + catch (...) + { + context.GetIndex().SetMaximumStorageSize(0); + } + + try + { + std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle"); + context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode)); + } + catch (...) + { + context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle); + } } - try - { - uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0); - context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); - } - catch (...) - { - context.GetIndex().SetMaximumStorageSize(0); - } - - try - { - std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle"); - context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode)); - } - catch (...) - { - context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle); - } - + // note: this config is valid in ReadOnlyMode try { uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128); @@ -1955,7 +1968,7 @@ SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); MemoryStorageArea inMemoryStorage; - ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */); + ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateOpenApiDocumentation(openapi); context.Stop(); @@ -2006,7 +2019,7 @@ SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); MemoryStorageArea inMemoryStorage; - ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */); + ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html"); context.Stop();
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -166,8 +166,9 @@ DicomTagConstraint c(tag, type, value, true, true); - DatabaseConstraints lookup; - lookup.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + DatabaseDicomTagConstraints lookup; + bool isEquivalent; // unused + lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); std::set<std::string> noLabel; transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); @@ -186,9 +187,10 @@ DicomTagConstraint c1(tag, type1, value1, true, true); DicomTagConstraint c2(tag, type2, value2, true, true); - DatabaseConstraints lookup; - lookup.AddConstraint(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - lookup.AddConstraint(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + DatabaseDicomTagConstraints lookup; + bool isEquivalent; // unused + lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); + lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); std::set<std::string> noLabel; transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); @@ -619,7 +621,7 @@ FilesystemStorage storage(path); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10); + ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -701,7 +703,7 @@ FilesystemStorage storage(path); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10); + ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -818,7 +820,7 @@ MemoryStorageArea storage; SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10); + ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); context.SetCompressionEnabled(true); @@ -863,15 +865,15 @@ { FileInfo nope; int64_t revision; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson)); } FileInfo dicom1, pixelData1; - int64_t revision; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom)); + int64_t revision = -1; + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, ResourceType_Instance, id, FileContentType_Dicom)); ASSERT_EQ(0, revision); revision = -1; - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData)); ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, @@ -913,14 +915,14 @@ { FileInfo nope; int64_t revision; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson)); } FileInfo dicom2, pixelData2; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, ResourceType_Instance, id, FileContentType_Dicom)); ASSERT_EQ(0, revision); revision = -1; - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData)); ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, @@ -983,7 +985,7 @@ MemoryStorageArea storage; SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10); + ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); context.SetCompressionEnabled(compression);
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri Nov 29 10:54:20 2024 +0100 @@ -536,7 +536,7 @@ OrthancJobsSerialization() { db_.Open(); - context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10)); + context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */)); context_->SetupJobsEngine(true, false); }