comparison UnitTestsSources/FromDcmtkTests.cpp @ 3820:f89eac983c9b transcoding

refactoring DicomUserConnection as DicomAssociation
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 09 Apr 2020 17:45:25 +0200
parents 1237bd0bbdb2
children f2488b645f5f
comparison
equal deleted inserted replaced
3819:1237bd0bbdb2 3820:f89eac983c9b
2402 { 2402 {
2403 TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); 2403 TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm");
2404 } 2404 }
2405 } 2405 }
2406 2406
2407
2408
2409 #ifdef _WIN32
2410 /**
2411 * "The maximum length, in bytes, of the string returned in the buffer
2412 * pointed to by the name parameter is dependent on the namespace provider,
2413 * but this string must be 256 bytes or less.
2414 * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
2415 **/
2416 # define HOST_NAME_MAX 256
2417 # include <winsock.h>
2418 #endif
2419
2420
2421 #if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
2422 /**
2423 * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
2424 * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
2425 * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
2426 * that the result will fit."
2427 * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
2428 **/
2429 #define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
2407 #endif 2430 #endif
2431
2432
2433 #include "../Core/DicomNetworking/RemoteModalityParameters.h"
2434
2435
2436 #include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout()
2437
2438
2439
2440 namespace Orthanc
2441 {
2442 // By default, the timeout for client DICOM connections is set to 10 seconds
2443 static boost::mutex defaultTimeoutMutex_;
2444 static uint32_t defaultTimeout_ = 10;
2445
2446
2447 class DicomAssociationParameters
2448 {
2449 private:
2450 std::string localAet_;
2451 std::string remoteAet_;
2452 std::string remoteHost_;
2453 uint16_t remotePort_;
2454 ModalityManufacturer manufacturer_;
2455 DicomAssociationRole role_;
2456 uint32_t timeout_;
2457
2458 void ReadDefaultTimeout()
2459 {
2460 boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
2461 timeout_ = defaultTimeout_;
2462 }
2463
2464 public:
2465 DicomAssociationParameters() :
2466 localAet_("STORESCU"),
2467 remoteAet_("ANY-SCP"),
2468 remoteHost_("127.0.0.1"),
2469 remotePort_(104),
2470 manufacturer_(ModalityManufacturer_Generic),
2471 role_(DicomAssociationRole_Default)
2472 {
2473 ReadDefaultTimeout();
2474 }
2475
2476 DicomAssociationParameters(const std::string& localAet,
2477 const RemoteModalityParameters& remote) :
2478 localAet_(localAet),
2479 remoteAet_(remote.GetApplicationEntityTitle()),
2480 remoteHost_(remote.GetHost()),
2481 remotePort_(remote.GetPortNumber()),
2482 manufacturer_(remote.GetManufacturer()),
2483 role_(DicomAssociationRole_Default),
2484 timeout_(defaultTimeout_)
2485 {
2486 ReadDefaultTimeout();
2487 }
2488
2489 const std::string& GetLocalApplicationEntityTitle() const
2490 {
2491 return localAet_;
2492 }
2493
2494 const std::string& GetRemoteApplicationEntityTitle() const
2495 {
2496 return remoteAet_;
2497 }
2498
2499 const std::string& GetRemoteHost() const
2500 {
2501 return remoteHost_;
2502 }
2503
2504 uint16_t GetRemotePort() const
2505 {
2506 return remotePort_;
2507 }
2508
2509 ModalityManufacturer GetRemoteManufacturer() const
2510 {
2511 return manufacturer_;
2512 }
2513
2514 DicomAssociationRole GetRole() const
2515 {
2516 return role_;
2517 }
2518
2519 void SetLocalApplicationEntityTitle(const std::string& aet)
2520 {
2521 localAet_ = aet;
2522 }
2523
2524 void SetRemoteApplicationEntityTitle(const std::string& aet)
2525 {
2526 remoteAet_ = aet;
2527 }
2528
2529 void SetRemoteHost(const std::string& host)
2530 {
2531 if (host.size() > HOST_NAME_MAX - 10)
2532 {
2533 throw OrthancException(ErrorCode_ParameterOutOfRange,
2534 "Invalid host name (too long): " + host);
2535 }
2536
2537 remoteHost_ = host;
2538 }
2539
2540 void SetRemotePort(uint16_t port)
2541 {
2542 remotePort_ = port;
2543 }
2544
2545 void SetRemoteManufacturer(ModalityManufacturer manufacturer)
2546 {
2547 manufacturer_ = manufacturer;
2548 }
2549
2550 void SetRole(DicomAssociationRole role)
2551 {
2552 role_ = role;
2553 }
2554
2555 void SetRemoteModality(const RemoteModalityParameters& parameters)
2556 {
2557 SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
2558 SetRemoteHost(parameters.GetHost());
2559 SetRemotePort(parameters.GetPortNumber());
2560 SetRemoteManufacturer(parameters.GetManufacturer());
2561 }
2562
2563 bool IsEqual(const DicomAssociationParameters& other) const
2564 {
2565 return (localAet_ == other.localAet_ &&
2566 remoteAet_ == other.remoteAet_ &&
2567 remoteHost_ == other.remoteHost_ &&
2568 remotePort_ == other.remotePort_ &&
2569 manufacturer_ == other.manufacturer_ &&
2570 role_ == other.role_);
2571 }
2572
2573 void SetTimeout(uint32_t seconds)
2574 {
2575 timeout_ = seconds;
2576 }
2577
2578 uint32_t GetTimeout() const
2579 {
2580 return timeout_;
2581 }
2582
2583 bool HasTimeout() const
2584 {
2585 return timeout_ != 0;
2586 }
2587
2588 static void SetDefaultTimeout(uint32_t seconds)
2589 {
2590 LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): "
2591 << seconds << " seconds (0 = no timeout)";
2592
2593 {
2594 boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
2595 defaultTimeout_ = seconds;
2596 }
2597 }
2598
2599 void CheckCondition(const OFCondition& cond,
2600 const std::string& command) const
2601 {
2602 if (cond.bad())
2603 {
2604 // Reformat the error message from DCMTK by turning multiline
2605 // errors into a single line
2606
2607 std::string s(cond.text());
2608 std::string info;
2609 info.reserve(s.size());
2610
2611 bool isMultiline = false;
2612 for (size_t i = 0; i < s.size(); i++)
2613 {
2614 if (s[i] == '\r')
2615 {
2616 // Ignore
2617 }
2618 else if (s[i] == '\n')
2619 {
2620 if (isMultiline)
2621 {
2622 info += "; ";
2623 }
2624 else
2625 {
2626 info += " (";
2627 isMultiline = true;
2628 }
2629 }
2630 else
2631 {
2632 info.push_back(s[i]);
2633 }
2634 }
2635
2636 if (isMultiline)
2637 {
2638 info += ")";
2639 }
2640
2641 throw OrthancException(ErrorCode_NetworkProtocol,
2642 "DicomUserConnection - " + command + " to AET \"" +
2643 GetRemoteApplicationEntityTitle() + "\": " + info);
2644 }
2645 }
2646 };
2647
2648
2649 class DicomAssociation : public boost::noncopyable
2650 {
2651 private:
2652 // This is the maximum number of presentation context IDs (the
2653 // number of odd integers between 1 and 255)
2654 // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
2655 static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
2656
2657 struct ProposedPresentationContext
2658 {
2659 std::string sopClassUid_;
2660 std::set<DicomTransferSyntax> transferSyntaxes_;
2661 };
2662
2663 typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> > AcceptedPresentationContexts;
2664
2665 bool isOpen_;
2666 std::vector<ProposedPresentationContext> proposed_;
2667 AcceptedPresentationContexts accepted_;
2668 T_ASC_Network* net_;
2669 T_ASC_Parameters* params_;
2670 T_ASC_Association* assoc_;
2671
2672 void Initialize()
2673 {
2674 isOpen_ = false;
2675 net_ = NULL;
2676 params_ = NULL;
2677 assoc_ = NULL;
2678
2679 // Must be after "isOpen_ = false"
2680 ClearPresentationContexts();
2681 }
2682
2683 void CheckConnecting(const DicomAssociationParameters& parameters,
2684 const OFCondition& cond)
2685 {
2686 try
2687 {
2688 parameters.CheckCondition(cond, "connecting");
2689 }
2690 catch (OrthancException&)
2691 {
2692 CloseInternal();
2693 throw;
2694 }
2695 }
2696
2697 void CloseInternal()
2698 {
2699 if (assoc_ != NULL)
2700 {
2701 ASC_releaseAssociation(assoc_);
2702 ASC_destroyAssociation(&assoc_);
2703 assoc_ = NULL;
2704 params_ = NULL;
2705 }
2706 else
2707 {
2708 if (params_ != NULL)
2709 {
2710 ASC_destroyAssociationParameters(&params_);
2711 params_ = NULL;
2712 }
2713 }
2714
2715 if (net_ != NULL)
2716 {
2717 ASC_dropNetwork(&net_);
2718 net_ = NULL;
2719 }
2720
2721 accepted_.clear();
2722 isOpen_ = false;
2723 }
2724
2725 void AddAccepted(const std::string& sopClassUid,
2726 DicomTransferSyntax syntax,
2727 uint8_t presentationContextId)
2728 {
2729 AcceptedPresentationContexts::iterator found = accepted_.find(sopClassUid);
2730
2731 if (found == accepted_.end())
2732 {
2733 std::map<DicomTransferSyntax, uint8_t> syntaxes;
2734 syntaxes[syntax] = presentationContextId;
2735 accepted_[sopClassUid] = syntaxes;
2736 }
2737 else
2738 {
2739 if (found->second.find(syntax) != found->second.end())
2740 {
2741 LOG(WARNING) << "The same transfer syntax ("
2742 << GetTransferSyntaxUid(syntax)
2743 << ") was accepted twice for the same SOP class UID ("
2744 << sopClassUid << ")";
2745 }
2746 else
2747 {
2748 found->second[syntax] = presentationContextId;
2749 }
2750 }
2751 }
2752
2753 public:
2754 DicomAssociation()
2755 {
2756 Initialize();
2757 }
2758
2759 ~DicomAssociation()
2760 {
2761 try
2762 {
2763 Close();
2764 }
2765 catch (OrthancException&)
2766 {
2767 // Don't throw exception in destructors
2768 }
2769 }
2770
2771 bool IsOpen() const
2772 {
2773 return isOpen_;
2774 }
2775
2776 void ClearPresentationContexts()
2777 {
2778 Close();
2779 proposed_.clear();
2780 proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
2781 }
2782
2783 void Open(const DicomAssociationParameters& parameters)
2784 {
2785 if (isOpen_)
2786 {
2787 return; // Already open
2788 }
2789
2790 // Timeout used during association negociation and ASC_releaseAssociation()
2791 uint32_t acseTimeout = parameters.GetTimeout();
2792 if (acseTimeout == 0)
2793 {
2794 /**
2795 * Timeout is disabled. Global timeout (seconds) for
2796 * connecting to remote hosts. Default value is -1 which
2797 * selects infinite timeout, i.e. blocking connect().
2798 **/
2799 dcmConnectionTimeout.set(-1);
2800 acseTimeout = 10;
2801 }
2802 else
2803 {
2804 dcmConnectionTimeout.set(acseTimeout);
2805 }
2806
2807 T_ASC_SC_ROLE dcmtkRole;
2808 switch (parameters.GetRole())
2809 {
2810 case DicomAssociationRole_Default:
2811 dcmtkRole = ASC_SC_ROLE_DEFAULT;
2812 break;
2813
2814 case DicomAssociationRole_Scu:
2815 dcmtkRole = ASC_SC_ROLE_SCU;
2816 break;
2817
2818 case DicomAssociationRole_Scp:
2819 dcmtkRole = ASC_SC_ROLE_SCP;
2820 break;
2821
2822 default:
2823 throw OrthancException(ErrorCode_ParameterOutOfRange);
2824 }
2825
2826 assert(net_ == NULL &&
2827 params_ == NULL &&
2828 assoc_ == NULL);
2829
2830 if (proposed_.empty())
2831 {
2832 throw OrthancException(ErrorCode_BadSequenceOfCalls,
2833 "No presentation context was proposed");
2834 }
2835
2836 LOG(INFO) << "Opening a DICOM SCU connection from AET \""
2837 << parameters.GetLocalApplicationEntityTitle()
2838 << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle()
2839 << "\" on host " << parameters.GetRemoteHost()
2840 << ":" << parameters.GetRemotePort()
2841 << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")";
2842
2843 CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
2844 CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
2845
2846 // Set this application's title and the called application's title in the params
2847 CheckConnecting(parameters, ASC_setAPTitles(
2848 params_, parameters.GetLocalApplicationEntityTitle().c_str(),
2849 parameters.GetRemoteApplicationEntityTitle().c_str(), NULL));
2850
2851 // Set the network addresses of the local and remote entities
2852 char localHost[HOST_NAME_MAX];
2853 gethostname(localHost, HOST_NAME_MAX - 1);
2854
2855 char remoteHostAndPort[HOST_NAME_MAX];
2856
2857 #ifdef _MSC_VER
2858 _snprintf
2859 #else
2860 snprintf
2861 #endif
2862 (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d",
2863 parameters.GetRemoteHost().c_str(), parameters.GetRemotePort());
2864
2865 CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
2866
2867 // Set various options
2868 CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false));
2869
2870 // Setup the list of proposed presentation contexts
2871 unsigned int presentationContextId = 1;
2872 for (size_t i = 0; i < proposed_.size(); i++)
2873 {
2874 assert(presentationContextId <= 255);
2875 const char* sopClassUid = proposed_[i].sopClassUid_.c_str();
2876
2877 const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
2878
2879 std::vector<const char*> transferSyntaxes;
2880 transferSyntaxes.reserve(source.size());
2881
2882 for (std::set<DicomTransferSyntax>::const_iterator
2883 it = source.begin(); it != source.end(); ++it)
2884 {
2885 transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
2886 }
2887
2888 assert(!transferSyntaxes.empty());
2889 CheckConnecting(parameters, ASC_addPresentationContext(
2890 params_, presentationContextId, sopClassUid,
2891 &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
2892
2893 presentationContextId += 2;
2894 }
2895
2896 // Do the association
2897 CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
2898 isOpen_ = true;
2899
2900 // Inspect the accepted transfer syntaxes
2901 LST_HEAD **l = &params_->DULparams.acceptedPresentationContext;
2902 if (*l != NULL)
2903 {
2904 DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
2905 LST_Position(l, (LST_NODE*)pc);
2906 while (pc)
2907 {
2908 if (pc->result == ASC_P_ACCEPTANCE)
2909 {
2910 DicomTransferSyntax transferSyntax;
2911 if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
2912 {
2913 AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
2914 }
2915 else
2916 {
2917 LOG(WARNING) << "Unknown transfer syntax received from AET \""
2918 << parameters.GetRemoteApplicationEntityTitle()
2919 << "\": " << pc->acceptedTransferSyntax;
2920 }
2921 }
2922
2923 pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
2924 }
2925 }
2926
2927 if (accepted_.empty())
2928 {
2929 throw OrthancException(ErrorCode_NoPresentationContext,
2930 "Unable to negotiate a presentation context with AET \"" +
2931 parameters.GetRemoteApplicationEntityTitle() + "\"");
2932 }
2933 }
2934
2935 void Close()
2936 {
2937 if (isOpen_)
2938 {
2939 CloseInternal();
2940 }
2941 }
2942
2943 bool LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target,
2944 const std::string& sopClassUid) const
2945 {
2946 if (!IsOpen())
2947 {
2948 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
2949 }
2950
2951 AcceptedPresentationContexts::const_iterator found = accepted_.find(sopClassUid);
2952
2953 if (found == accepted_.end())
2954 {
2955 return false;
2956 }
2957 else
2958 {
2959 target = found->second;
2960 return true;
2961 }
2962 }
2963
2964 void ProposeGenericPresentationContext(const std::string& sopClassUid)
2965 {
2966 std::set<DicomTransferSyntax> ts;
2967 ts.insert(DicomTransferSyntax_LittleEndianImplicit);
2968 ts.insert(DicomTransferSyntax_LittleEndianExplicit);
2969 ts.insert(DicomTransferSyntax_BigEndianExplicit);
2970 ProposePresentationContext(sopClassUid, ts);
2971 }
2972
2973 void ProposePresentationContext(const std::string& sopClassUid,
2974 DicomTransferSyntax transferSyntax)
2975 {
2976 std::set<DicomTransferSyntax> ts;
2977 ts.insert(transferSyntax);
2978 ProposePresentationContext(sopClassUid, ts);
2979 }
2980
2981 void ProposePresentationContext(const std::string& sopClassUid,
2982 const std::set<DicomTransferSyntax>& transferSyntaxes)
2983 {
2984 if (transferSyntaxes.empty())
2985 {
2986 throw OrthancException(ErrorCode_ParameterOutOfRange,
2987 "No transfer syntax provided");
2988 }
2989
2990 if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
2991 {
2992 throw OrthancException(ErrorCode_ParameterOutOfRange,
2993 "Too many proposed presentation contexts");
2994 }
2995
2996 if (IsOpen())
2997 {
2998 Close();
2999 }
3000
3001 ProposedPresentationContext context;
3002 context.sopClassUid_ = sopClassUid;
3003 context.transferSyntaxes_ = transferSyntaxes;
3004
3005 proposed_.push_back(context);
3006 }
3007
3008 T_ASC_Association& GetDcmtkAssociation() const
3009 {
3010 if (isOpen_)
3011 {
3012 assert(assoc_ != NULL);
3013 return *assoc_;
3014 }
3015 else
3016 {
3017 throw OrthancException(ErrorCode_BadSequenceOfCalls,
3018 "The connection is not open");
3019 }
3020 }
3021
3022 T_ASC_Network& GetDcmtkNetwork() const
3023 {
3024 if (isOpen_)
3025 {
3026 assert(net_ != NULL);
3027 return *net_;
3028 }
3029 else
3030 {
3031 throw OrthancException(ErrorCode_BadSequenceOfCalls,
3032 "The connection is not open");
3033 }
3034 }
3035 };
3036
3037
3038
3039 static void TestAndCopyTag(DicomMap& result,
3040 const DicomMap& source,
3041 const DicomTag& tag)
3042 {
3043 if (!source.HasTag(tag))
3044 {
3045 throw OrthancException(ErrorCode_BadRequest);
3046 }
3047 else
3048 {
3049 result.SetValue(tag, source.GetValue(tag));
3050 }
3051 }
3052
3053
3054 namespace
3055 {
3056 struct FindPayload
3057 {
3058 DicomFindAnswers* answers;
3059 const char* level;
3060 bool isWorklist;
3061 };
3062 }
3063
3064
3065 static void FindCallback(
3066 /* in */
3067 void *callbackData,
3068 T_DIMSE_C_FindRQ *request, /* original find request */
3069 int responseCount,
3070 T_DIMSE_C_FindRSP *response, /* pending response received */
3071 DcmDataset *responseIdentifiers /* pending response identifiers */
3072 )
3073 {
3074 FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
3075
3076 if (responseIdentifiers != NULL)
3077 {
3078 if (payload.isWorklist)
3079 {
3080 ParsedDicomFile answer(*responseIdentifiers);
3081 payload.answers->Add(answer);
3082 }
3083 else
3084 {
3085 DicomMap m;
3086 FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
3087
3088 if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
3089 {
3090 m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
3091 }
3092
3093 payload.answers->Add(m);
3094 }
3095 }
3096 }
3097
3098
3099 static void NormalizeFindQuery(DicomMap& fixedQuery,
3100 ResourceType level,
3101 const DicomMap& fields)
3102 {
3103 std::set<DicomTag> allowedTags;
3104
3105 // WARNING: Do not add "break" or reorder items in this switch-case!
3106 switch (level)
3107 {
3108 case ResourceType_Instance:
3109 DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
3110
3111 case ResourceType_Series:
3112 DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
3113
3114 case ResourceType_Study:
3115 DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
3116
3117 case ResourceType_Patient:
3118 DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
3119 break;
3120
3121 default:
3122 throw OrthancException(ErrorCode_InternalError);
3123 }
3124
3125 switch (level)
3126 {
3127 case ResourceType_Patient:
3128 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
3129 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
3130 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
3131 break;
3132
3133 case ResourceType_Study:
3134 allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
3135 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
3136 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
3137 allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
3138 break;
3139
3140 case ResourceType_Series:
3141 allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
3142 break;
3143
3144 default:
3145 break;
3146 }
3147
3148 allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
3149
3150 DicomArray query(fields);
3151 for (size_t i = 0; i < query.GetSize(); i++)
3152 {
3153 const DicomTag& tag = query.GetElement(i).GetTag();
3154 if (allowedTags.find(tag) == allowedTags.end())
3155 {
3156 LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
3157 }
3158 else
3159 {
3160 fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
3161 }
3162 }
3163 }
3164
3165
3166
3167 static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
3168 ModalityManufacturer manufacturer)
3169 {
3170 // Fix outgoing C-Find requests issue for Syngo.Via and its
3171 // solution was reported by Emsy Chan by private mail on
3172 // 2015-06-17. According to Robert van Ommen (2015-11-30), the
3173 // same fix is required for Agfa Impax. This was generalized for
3174 // generic manufacturer since it seems to affect PhilipsADW,
3175 // GEWAServer as well:
3176 // https://bitbucket.org/sjodogne/orthanc/issues/31/
3177
3178 switch (manufacturer)
3179 {
3180 case ModalityManufacturer_GenericNoWildcardInDates:
3181 case ModalityManufacturer_GenericNoUniversalWildcard:
3182 {
3183 std::unique_ptr<DicomMap> fix(fields.Clone());
3184
3185 std::set<DicomTag> tags;
3186 fix->GetTags(tags);
3187
3188 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
3189 {
3190 // Replace a "*" wildcard query by an empty query ("") for
3191 // "date" or "all" value representations depending on the
3192 // type of manufacturer.
3193 if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
3194 (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
3195 FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
3196 {
3197 const DicomValue* value = fix->TestAndGetValue(*it);
3198
3199 if (value != NULL &&
3200 !value->IsNull() &&
3201 value->GetContent() == "*")
3202 {
3203 fix->SetValue(*it, "", false);
3204 }
3205 }
3206 }
3207
3208 return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */);
3209 }
3210
3211 default:
3212 return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */);
3213 }
3214 }
3215
3216
3217
3218 class DicomControlUserConnection : public boost::noncopyable
3219 {
3220 private:
3221 DicomAssociationParameters parameters_;
3222 DicomAssociation association_;
3223
3224 void SetupPresentationContexts()
3225 {
3226 association_.ProposeGenericPresentationContext(UID_VerificationSOPClass);
3227 association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
3228 association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
3229 association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
3230 association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
3231 }
3232
3233 void FindInternal(DicomFindAnswers& answers,
3234 DcmDataset* dataset,
3235 const char* sopClass,
3236 bool isWorklist,
3237 const char* level)
3238 {
3239 assert(isWorklist ^ (level != NULL));
3240
3241 association_.Open(parameters_);
3242
3243 FindPayload payload;
3244 payload.answers = &answers;
3245 payload.level = level;
3246 payload.isWorklist = isWorklist;
3247
3248 // Figure out which of the accepted presentation contexts should be used
3249 int presID = ASC_findAcceptedPresentationContextID(
3250 &association_.GetDcmtkAssociation(), sopClass);
3251 if (presID == 0)
3252 {
3253 throw OrthancException(ErrorCode_DicomFindUnavailable,
3254 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
3255 }
3256
3257 T_DIMSE_C_FindRQ request;
3258 memset(&request, 0, sizeof(request));
3259 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
3260 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
3261 request.Priority = DIMSE_PRIORITY_MEDIUM;
3262 request.DataSetType = DIMSE_DATASET_PRESENT;
3263
3264 T_DIMSE_C_FindRSP response;
3265 DcmDataset* statusDetail = NULL;
3266
3267 #if DCMTK_VERSION_NUMBER >= 364
3268 int responseCount;
3269 #endif
3270
3271 OFCondition cond = DIMSE_findUser(
3272 &association_.GetDcmtkAssociation(), presID, &request, dataset,
3273 #if DCMTK_VERSION_NUMBER >= 364
3274 responseCount,
3275 #endif
3276 FindCallback, &payload,
3277 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3278 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3279 &response, &statusDetail);
3280
3281 if (statusDetail)
3282 {
3283 delete statusDetail;
3284 }
3285
3286 parameters_.CheckCondition(cond, "C-FIND");
3287
3288
3289 /**
3290 * New in Orthanc 1.6.0: Deal with failures during C-FIND.
3291 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
3292 **/
3293
3294 if (response.DimseStatus != 0x0000 && // Success
3295 response.DimseStatus != 0xFF00 && // Pending - Matches are continuing
3296 response.DimseStatus != 0xFF01) // Pending - Matches are continuing
3297 {
3298 char buf[16];
3299 sprintf(buf, "%04X", response.DimseStatus);
3300
3301 if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
3302 {
3303 throw OrthancException(ErrorCode_NetworkProtocol,
3304 HttpStatus_422_UnprocessableEntity,
3305 "C-FIND SCU to AET \"" +
3306 parameters_.GetRemoteApplicationEntityTitle() +
3307 "\" has failed with DIMSE status 0x" + buf +
3308 " (unable to process - invalid query ?)");
3309 }
3310 else
3311 {
3312 throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
3313 parameters_.GetRemoteApplicationEntityTitle() +
3314 "\" has failed with DIMSE status 0x" + buf);
3315 }
3316 }
3317 }
3318
3319 void MoveInternal(const std::string& targetAet,
3320 ResourceType level,
3321 const DicomMap& fields)
3322 {
3323 association_.Open(parameters_);
3324
3325 std::unique_ptr<ParsedDicomFile> query(
3326 ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
3327 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
3328
3329 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
3330 switch (level)
3331 {
3332 case ResourceType_Patient:
3333 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
3334 break;
3335
3336 case ResourceType_Study:
3337 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
3338 break;
3339
3340 case ResourceType_Series:
3341 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
3342 break;
3343
3344 case ResourceType_Instance:
3345 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
3346 break;
3347
3348 default:
3349 throw OrthancException(ErrorCode_ParameterOutOfRange);
3350 }
3351
3352 // Figure out which of the accepted presentation contexts should be used
3353 int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass);
3354 if (presID == 0)
3355 {
3356 throw OrthancException(ErrorCode_DicomMoveUnavailable,
3357 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
3358 }
3359
3360 T_DIMSE_C_MoveRQ request;
3361 memset(&request, 0, sizeof(request));
3362 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
3363 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
3364 request.Priority = DIMSE_PRIORITY_MEDIUM;
3365 request.DataSetType = DIMSE_DATASET_PRESENT;
3366 strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
3367
3368 T_DIMSE_C_MoveRSP response;
3369 DcmDataset* statusDetail = NULL;
3370 DcmDataset* responseIdentifiers = NULL;
3371 OFCondition cond = DIMSE_moveUser(
3372 &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
3373 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3374 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3375 &association_.GetDcmtkNetwork(), NULL, NULL,
3376 &response, &statusDetail, &responseIdentifiers);
3377
3378 if (statusDetail)
3379 {
3380 delete statusDetail;
3381 }
3382
3383 if (responseIdentifiers)
3384 {
3385 delete responseIdentifiers;
3386 }
3387
3388 parameters_.CheckCondition(cond, "C-MOVE");
3389
3390
3391 /**
3392 * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
3393 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
3394 **/
3395
3396 if (response.DimseStatus != 0x0000 && // Success
3397 response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing
3398 {
3399 char buf[16];
3400 sprintf(buf, "%04X", response.DimseStatus);
3401
3402 if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
3403 {
3404 throw OrthancException(ErrorCode_NetworkProtocol,
3405 HttpStatus_422_UnprocessableEntity,
3406 "C-MOVE SCU to AET \"" +
3407 parameters_.GetRemoteApplicationEntityTitle() +
3408 "\" has failed with DIMSE status 0x" + buf +
3409 " (unable to process - resource not found ?)");
3410 }
3411 else
3412 {
3413 throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
3414 parameters_.GetRemoteApplicationEntityTitle() +
3415 "\" has failed with DIMSE status 0x" + buf);
3416 }
3417 }
3418 }
3419
3420 public:
3421 DicomControlUserConnection()
3422 {
3423 SetupPresentationContexts();
3424 }
3425
3426 DicomControlUserConnection(const DicomAssociationParameters& params) :
3427 parameters_(params)
3428 {
3429 SetupPresentationContexts();
3430 }
3431
3432 void SetParameters(const DicomAssociationParameters& params)
3433 {
3434 if (!parameters_.IsEqual(params))
3435 {
3436 association_.Close();
3437 parameters_ = params;
3438 }
3439 }
3440
3441 const DicomAssociationParameters& GetParameters() const
3442 {
3443 return parameters_;
3444 }
3445
3446 bool Echo()
3447 {
3448 association_.Open(parameters_);
3449
3450 DIC_US status;
3451 parameters_.CheckCondition(
3452 DIMSE_echoUser(&association_.GetDcmtkAssociation(),
3453 association_.GetDcmtkAssociation().nextMsgID++,
3454 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3455 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3456 &status, NULL),
3457 "C-ECHO");
3458
3459 return status == STATUS_Success;
3460 }
3461
3462
3463 void Find(DicomFindAnswers& result,
3464 ResourceType level,
3465 const DicomMap& originalFields,
3466 bool normalize)
3467 {
3468 std::unique_ptr<ParsedDicomFile> query;
3469
3470 if (normalize)
3471 {
3472 DicomMap fields;
3473 NormalizeFindQuery(fields, level, originalFields);
3474 query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
3475 }
3476 else
3477 {
3478 query.reset(new ParsedDicomFile(originalFields,
3479 GetDefaultDicomEncoding(),
3480 false /* be strict */));
3481 }
3482
3483 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
3484
3485 const char* clevel = NULL;
3486 const char* sopClass = NULL;
3487
3488 switch (level)
3489 {
3490 case ResourceType_Patient:
3491 clevel = "PATIENT";
3492 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
3493 sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
3494 break;
3495
3496 case ResourceType_Study:
3497 clevel = "STUDY";
3498 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
3499 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3500 break;
3501
3502 case ResourceType_Series:
3503 clevel = "SERIES";
3504 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
3505 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3506 break;
3507
3508 case ResourceType_Instance:
3509 clevel = "IMAGE";
3510 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
3511 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3512 break;
3513
3514 default:
3515 throw OrthancException(ErrorCode_ParameterOutOfRange);
3516 }
3517
3518
3519 const char* universal;
3520 if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
3521 {
3522 universal = "*";
3523 }
3524 else
3525 {
3526 universal = "";
3527 }
3528
3529
3530 // Add the expected tags for this query level.
3531 // WARNING: Do not reorder or add "break" in this switch-case!
3532 switch (level)
3533 {
3534 case ResourceType_Instance:
3535 if (!dataset->tagExists(DCM_SOPInstanceUID))
3536 {
3537 DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
3538 }
3539
3540 case ResourceType_Series:
3541 if (!dataset->tagExists(DCM_SeriesInstanceUID))
3542 {
3543 DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
3544 }
3545
3546 case ResourceType_Study:
3547 if (!dataset->tagExists(DCM_AccessionNumber))
3548 {
3549 DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
3550 }
3551
3552 if (!dataset->tagExists(DCM_StudyInstanceUID))
3553 {
3554 DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
3555 }
3556
3557 case ResourceType_Patient:
3558 if (!dataset->tagExists(DCM_PatientID))
3559 {
3560 DU_putStringDOElement(dataset, DCM_PatientID, universal);
3561 }
3562
3563 break;
3564
3565 default:
3566 throw OrthancException(ErrorCode_ParameterOutOfRange);
3567 }
3568
3569 assert(clevel != NULL && sopClass != NULL);
3570 FindInternal(result, dataset, sopClass, false, clevel);
3571 }
3572
3573
3574 void Move(const std::string& targetAet,
3575 ResourceType level,
3576 const DicomMap& findResult)
3577 {
3578 DicomMap move;
3579 switch (level)
3580 {
3581 case ResourceType_Patient:
3582 TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
3583 break;
3584
3585 case ResourceType_Study:
3586 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3587 break;
3588
3589 case ResourceType_Series:
3590 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3591 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
3592 break;
3593
3594 case ResourceType_Instance:
3595 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3596 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
3597 TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
3598 break;
3599
3600 default:
3601 throw OrthancException(ErrorCode_InternalError);
3602 }
3603
3604 MoveInternal(targetAet, level, move);
3605 }
3606
3607
3608 void Move(const std::string& targetAet,
3609 const DicomMap& findResult)
3610 {
3611 if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
3612 {
3613 throw OrthancException(ErrorCode_InternalError);
3614 }
3615
3616 const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
3617 ResourceType level = StringToResourceType(tmp.c_str());
3618
3619 Move(targetAet, level, findResult);
3620 }
3621
3622
3623 void MovePatient(const std::string& targetAet,
3624 const std::string& patientId)
3625 {
3626 DicomMap query;
3627 query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
3628 MoveInternal(targetAet, ResourceType_Patient, query);
3629 }
3630
3631 void MoveStudy(const std::string& targetAet,
3632 const std::string& studyUid)
3633 {
3634 DicomMap query;
3635 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
3636 MoveInternal(targetAet, ResourceType_Study, query);
3637 }
3638
3639 void MoveSeries(const std::string& targetAet,
3640 const std::string& studyUid,
3641 const std::string& seriesUid)
3642 {
3643 DicomMap query;
3644 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
3645 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
3646 MoveInternal(targetAet, ResourceType_Series, query);
3647 }
3648
3649 void MoveInstance(const std::string& targetAet,
3650 const std::string& studyUid,
3651 const std::string& seriesUid,
3652 const std::string& instanceUid)
3653 {
3654 DicomMap query;
3655 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
3656 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
3657 query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
3658 MoveInternal(targetAet, ResourceType_Instance, query);
3659 }
3660
3661
3662 void FindWorklist(DicomFindAnswers& result,
3663 ParsedDicomFile& query)
3664 {
3665 DcmDataset* dataset = query.GetDcmtkObject().getDataset();
3666 const char* sopClass = UID_FINDModalityWorklistInformationModel;
3667
3668 FindInternal(result, dataset, sopClass, true, NULL);
3669 }
3670 };
3671
3672 }
3673
3674
3675 TEST(Toto, DISABLED_DicomAssociation)
3676 {
3677 DicomAssociationParameters params;
3678 params.SetLocalApplicationEntityTitle("ORTHANC");
3679 params.SetRemoteApplicationEntityTitle("PACS");
3680 params.SetRemotePort(2001);
3681
3682 #if 0
3683 DicomAssociation assoc;
3684 assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass);
3685 assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass);
3686 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
3687 DicomTransferSyntax_JPEGProcess1);
3688 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
3689 DicomTransferSyntax_JPEGProcess2_4);
3690 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
3691 DicomTransferSyntax_JPEG2000);
3692
3693 assoc.Open(params);
3694
3695 int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage);
3696 printf(">> %d\n", presID);
3697
3698 std::map<DicomTransferSyntax, uint8_t> pc;
3699 printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage));
3700
3701 for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
3702 it = pc.begin(); it != pc.end(); ++it)
3703 {
3704 printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second);
3705 }
3706 #else
3707 DicomControlUserConnection assoc(params);
3708
3709 try
3710 {
3711 printf(">> %d\n", assoc.Echo());
3712 }
3713 catch (OrthancException&)
3714 {
3715 }
3716
3717 params.SetRemoteApplicationEntityTitle("PACS");
3718 params.SetRemotePort(2000);
3719 assoc.SetParameters(params);
3720 printf(">> %d\n", assoc.Echo());
3721
3722 #endif
3723 }
3724
3725
3726 #endif