dicom 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/dicom/link.rb CHANGED
@@ -3,6 +3,7 @@ module DICOM
3
3
  # This class handles the construction and interpretation of network packages as well as network communication.
4
4
  #
5
5
  class Link
6
+ include Logging
6
7
 
7
8
  # A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
8
9
  attr_accessor :file_handler
@@ -10,12 +11,6 @@ module DICOM
10
11
  attr_accessor :max_package_size
11
12
  # A hash which keeps track of the relationship between context ID and chosen transfer syntax.
12
13
  attr_accessor :presentation_contexts
13
- # A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
14
- attr_accessor :verbose
15
- # An array containing any error messages recorded.
16
- attr_reader :errors
17
- # An array containing any status messages recorded.
18
- attr_reader :notices
19
14
  # A TCP network session where the DICOM communication is done with a remote host or client.
20
15
  attr_reader :session
21
16
 
@@ -32,7 +27,6 @@ module DICOM
32
27
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
33
28
  # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
34
29
  # * <tt>:timeout</tt> -- Fixnum. The maximum period to wait for an answer before aborting the communication.
35
- # * <tt>:verbose</tt> -- Boolean. If set to false, the DLink instance will run silently and not output warnings and error messages to the screen. Defaults to true.
36
30
  #
37
31
  def initialize(options={})
38
32
  require 'socket'
@@ -44,11 +38,6 @@ module DICOM
44
38
  @max_receive_size = @max_package_size
45
39
  @timeout = options[:timeout] || 10 # seconds
46
40
  @min_length = 10 # minimum number of bytes to expect in an incoming transmission
47
- @verbose = options[:verbose]
48
- @verbose = true if @verbose == nil # Default verbosity is 'on'.
49
- # Other instance variables:
50
- @errors = Array.new # errors and warnings are put in this array
51
- @notices = Array.new # information on successful transmissions are put in this array
52
41
  # Variables used for monitoring state of transmission:
53
42
  @session = nil # TCP connection
54
43
  @association = nil # DICOM Association status
@@ -70,10 +59,10 @@ module DICOM
70
59
  if info[:pdu] != PDU_RELEASE_REQUEST
71
60
  # For some reason we didnt get our expected release request. Determine why:
72
61
  if info[:valid]
73
- add_error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.")
62
+ logger.error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.")
74
63
  handle_abort(false)
75
64
  else
76
- add_error("Timed out while waiting for a release request. Closing the connection.")
65
+ logger.error("Timed out while waiting for a release request. Closing the connection.")
77
66
  end
78
67
  stop_session
79
68
  else
@@ -368,7 +357,7 @@ module DICOM
368
357
  info = interpret_abort(message)
369
358
  else
370
359
  info = {:valid => false}
371
- add_error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})")
360
+ logger.error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})")
372
361
  end
373
362
  return info
374
363
  end
@@ -380,7 +369,7 @@ module DICOM
380
369
  # * <tt>default_message</tt> -- A boolean which unless set as nil/false will make the method print the default status message.
381
370
  #
382
371
  def handle_abort(default_message=true)
383
- add_notice("An unregonizable (non-DICOM) message was received.") if default_message
372
+ logger.warn("An unregonizable (non-DICOM) message was received.") if default_message
384
373
  build_association_abort
385
374
  transmit
386
375
  end
@@ -453,7 +442,7 @@ module DICOM
453
442
  # Handles the rejection message (The response used to an association request when its formalities are not correct).
454
443
  #
455
444
  def handle_rejection
456
- add_notice("An incoming association request was rejected. Error code: #{association_error}")
445
+ logger.warn("An incoming association request was rejected. Error code: #{association_error}")
457
446
  # Insert the error code in the info hash:
458
447
  info[:reason] = association_error
459
448
  # Send an association rejection:
@@ -465,7 +454,7 @@ module DICOM
465
454
  #
466
455
  def handle_release
467
456
  stop_receiving
468
- add_notice("Received a release request. Releasing association.")
457
+ logger.info("Received a release request. Releasing association.")
469
458
  build_release_response
470
459
  transmit
471
460
  stop_session
@@ -557,7 +546,7 @@ module DICOM
557
546
  end
558
547
  else
559
548
  # Length of the message is less than what is specified in the message. Need to listen for more. This is hopefully handled properly now.
560
- #add_error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
549
+ #logger.error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
561
550
  @first_part = msg.string
562
551
  end
563
552
  else
@@ -707,7 +696,7 @@ module DICOM
707
696
  else
708
697
  # Value (variable length)
709
698
  value = msg.decode(item_length, "STR")
710
- add_error("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})")
699
+ logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})")
711
700
  end
712
701
  end
713
702
  stop_receiving
@@ -733,7 +722,7 @@ module DICOM
733
722
  info[:source] = msg.decode(1, "BY")
734
723
  # Reason (1 byte)
735
724
  info[:reason] = msg.decode(1, "BY")
736
- add_error("Warning: ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)")
725
+ logger.warn("ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)")
737
726
  stop_receiving
738
727
  info[:valid] = true
739
728
  return info
@@ -877,7 +866,7 @@ module DICOM
877
866
  # Unknown item type:
878
867
  # Value (variable length)
879
868
  value = msg.decode(item_length, "STR")
880
- add_error("Notice: Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
869
+ logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
881
870
  end
882
871
  end
883
872
  stop_receiving
@@ -923,7 +912,7 @@ module DICOM
923
912
  # Length (2 bytes)
924
913
  length = msg.decode(2, "US")
925
914
  if length > msg.rest_length
926
- add_error("Error: Specified length of command element value exceeds remaining length of the received message! Something is wrong.")
915
+ logger.error("Specified length of command element value exceeds remaining length of the received message! Something is wrong.")
927
916
  end
928
917
  # Reserved (2 bytes)
929
918
  msg.skip(2)
@@ -948,7 +937,7 @@ module DICOM
948
937
  end
949
938
  # Special case: Handle a possible C-ECHO-RQ:
950
939
  if info[:results]["0000,0100"] == C_ECHO_RQ
951
- add_notice("Received an Echo request. Returning an Echo response.")
940
+ logger.info("Received an Echo request. Returning an Echo response.")
952
941
  handle_response
953
942
  end
954
943
  elsif info[:presentation_context_flag] == DATA_MORE_FRAGMENTS or info[:presentation_context_flag] == DATA_LAST_FRAGMENT
@@ -977,7 +966,7 @@ module DICOM
977
966
  length = msg.decode(4, "UL")
978
967
  end
979
968
  if length > msg.rest_length
980
- add_error("Error: Specified length of data element value exceeds remaining length of the received message! Something is wrong.")
969
+ logger.error("The specified length of the data element value exceeds the remaining length of the received message!")
981
970
  end
982
971
  # Fetch the name (& type if not defined already) for this data element:
983
972
  result = LIBRARY.get_name_vr(tag)
@@ -993,7 +982,7 @@ module DICOM
993
982
  end
994
983
  else
995
984
  # Unknown.
996
- add_error("Error: Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
985
+ logger.error("Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
997
986
  stop_receiving
998
987
  end
999
988
  # If only parts of the string was read, return the rest:
@@ -1108,30 +1097,6 @@ module DICOM
1108
1097
  private
1109
1098
 
1110
1099
 
1111
- # Adds a warning or error message to the instance array holding messages,
1112
- # and prints the information to the screen if verbose is set.
1113
- #
1114
- # === Parameters
1115
- #
1116
- # * <tt>error</tt> -- A single error message or an array of error messages.
1117
- #
1118
- def add_error(error)
1119
- puts error if @verbose
1120
- @errors << error
1121
- end
1122
-
1123
- # Adds a notice (information regarding progress or successful communications) to the instance array,
1124
- # and prints the information to the screen if verbose is set.
1125
- #
1126
- # === Parameters
1127
- #
1128
- # * <tt>notice</tt> -- A single status message or an array of status messages.
1129
- #
1130
- def add_notice(notice)
1131
- puts notice if @verbose
1132
- @notices << notice
1133
- end
1134
-
1135
1100
  # Builds the application context (which is part of the association request/response).
1136
1101
  #
1137
1102
  def append_application_context
@@ -1299,7 +1264,7 @@ module DICOM
1299
1264
  when C_ECHO_RQ
1300
1265
  return C_ECHO_RSP
1301
1266
  else
1302
- add_error("Unknown or unsupported request (#{request}) encountered.")
1267
+ logger.error("Unknown or unsupported request (#{request}) encountered.")
1303
1268
  return C_CANCEL_RQ
1304
1269
  end
1305
1270
  end
@@ -1313,19 +1278,19 @@ module DICOM
1313
1278
  def process_reason(reason)
1314
1279
  case reason
1315
1280
  when "00"
1316
- add_error("Reason specified for abort: Reason not specified")
1281
+ logger.error("Reason specified for abort: Reason not specified")
1317
1282
  when "01"
1318
- add_error("Reason specified for abort: Unrecognized PDU")
1283
+ logger.error("Reason specified for abort: Unrecognized PDU")
1319
1284
  when "02"
1320
- add_error("Reason specified for abort: Unexpected PDU")
1285
+ logger.error("Reason specified for abort: Unexpected PDU")
1321
1286
  when "04"
1322
- add_error("Reason specified for abort: Unrecognized PDU parameter")
1287
+ logger.error("Reason specified for abort: Unrecognized PDU parameter")
1323
1288
  when "05"
1324
- add_error("Reason specified for abort: Unexpected PDU parameter")
1289
+ logger.error("Reason specified for abort: Unexpected PDU parameter")
1325
1290
  when "06"
1326
- add_error("Reason specified for abort: Invalid PDU parameter value")
1291
+ logger.error("Reason specified for abort: Invalid PDU parameter value")
1327
1292
  else
1328
- add_error("Reason specified for abort: Unknown reason (Error code: #{reason})")
1293
+ logger.error("Reason specified for abort: Unknown reason (Error code: #{reason})")
1329
1294
  end
1330
1295
  end
1331
1296
 
@@ -1345,15 +1310,15 @@ module DICOM
1345
1310
  # Analyse the result and report what is wrong:
1346
1311
  case result
1347
1312
  when 1
1348
- add_error("Warning: DICOM Request was rejected by the host, reason: 'User-rejection'")
1313
+ logger.warn("DICOM Request was rejected by the host, reason: 'User-rejection'")
1349
1314
  when 2
1350
- add_error("Warning: DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
1315
+ logger.warn("DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
1351
1316
  when 3
1352
- add_error("Warning: DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
1317
+ logger.warn("DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
1353
1318
  when 4
1354
- add_error("Warning: DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
1319
+ logger.warn("DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
1355
1320
  else
1356
- add_error("Warning: DICOM Request was rejected by the host, reason: 'UNKNOWN (#{result})' (Illegal reason provided)")
1321
+ logger.warn("DICOM Request was rejected by the host, reason: 'UNKNOWN (#{result})' (Illegal reason provided)")
1357
1322
  end
1358
1323
  end
1359
1324
  end
@@ -1366,11 +1331,11 @@ module DICOM
1366
1331
  #
1367
1332
  def process_source(source)
1368
1333
  if source == "00"
1369
- add_error("Warning: Connection has been aborted by the service provider because of an error by the service user (client side).")
1334
+ logger.warn("Connection has been aborted by the service provider because of an error by the service user (client side).")
1370
1335
  elsif source == "02"
1371
- add_error("Warning: Connection has been aborted by the service provider because of an error by the service provider (server side).")
1336
+ logger.warn("Connection has been aborted by the service provider because of an error by the service provider (server side).")
1372
1337
  else
1373
- add_error("Warning: Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{source})")
1338
+ logger.warn("Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{source})")
1374
1339
  end
1375
1340
  end
1376
1341
 
@@ -1391,26 +1356,26 @@ module DICOM
1391
1356
  case status
1392
1357
  when 0 # "0000"
1393
1358
  # Last fragment (Break the while loop that listens continuously for incoming packets):
1394
- add_notice("Receipt for successful execution of the desired operation has been received.")
1359
+ logger.info("Receipt for successful execution of the desired operation has been received.")
1395
1360
  stop_receiving
1396
1361
  when 42752 # "a700"
1397
1362
  # Failure: Out of resources. Related fields: 0000,0902
1398
- add_error("Failure! SCP has given the following reason: 'Out of Resources'.")
1363
+ logger.error("Failure! SCP has given the following reason: 'Out of Resources'.")
1399
1364
  when 43264 # "a900"
1400
1365
  # Failure: Identifier Does Not Match SOP Class. Related fields: 0000,0901, 0000,0902
1401
- add_error("Failure! SCP has given the following reason: 'Identifier Does Not Match SOP Class'.")
1366
+ logger.error("Failure! SCP has given the following reason: 'Identifier Does Not Match SOP Class'.")
1402
1367
  when 49152 # "c000"
1403
1368
  # Failure: Unable to process. Related fields: 0000,0901, 0000,0902
1404
- add_error("Failure! SCP has given the following reason: 'Unable to process'.")
1369
+ logger.error("Failure! SCP has given the following reason: 'Unable to process'.")
1405
1370
  when 49408 # "c100"
1406
1371
  # Failure: More than one match found. Related fields: 0000,0901, 0000,0902
1407
- add_error("Failure! SCP has given the following reason: 'More than one match found'.")
1372
+ logger.error("Failure! SCP has given the following reason: 'More than one match found'.")
1408
1373
  when 49664 # "c200"
1409
1374
  # Failure: Unable to support requested template. Related fields: 0000,0901, 0000,0902
1410
- add_error("Failure! SCP has given the following reason: 'Unable to support requested template'.")
1375
+ logger.error("Failure! SCP has given the following reason: 'Unable to support requested template'.")
1411
1376
  when 65024 # "fe00"
1412
1377
  # Cancel: Matching terminated due to Cancel request.
1413
- add_notice("Cancel! SCP has given the following reason: 'Matching terminated due to Cancel request'.")
1378
+ logger.info("Cancel! SCP has given the following reason: 'Matching terminated due to Cancel request'.")
1414
1379
  when 65280 # "ff00"
1415
1380
  # Sub-operations are continuing.
1416
1381
  # (No particular action taken, the program will listen for and receive the coming fragments)
@@ -1418,7 +1383,7 @@ module DICOM
1418
1383
  # More command/data fragments to follow.
1419
1384
  # (No particular action taken, the program will listen for and receive the coming fragments)
1420
1385
  else
1421
- add_error("Error! Something was NOT successful regarding the desired operation. SCP responded with error code: #{status} (tag: 0000,0900). See DICOM PS3.7, Annex C for details.")
1386
+ logger.error("Something was NOT successful regarding the desired operation. SCP responded with error code: #{status} (tag: 0000,0900). See DICOM PS3.7, Annex C for details.")
1422
1387
  end
1423
1388
  end
1424
1389
 
@@ -1465,7 +1430,7 @@ module DICOM
1465
1430
  while @receive
1466
1431
  if (Time.now.to_f - t1) > @timeout
1467
1432
  Thread.kill(thr)
1468
- add_error("No answer was received within the specified timeout period. Aborting.")
1433
+ logger.error("No answer was received within the specified timeout period. Aborting.")
1469
1434
  stop_receiving
1470
1435
  end
1471
1436
  end
@@ -1497,7 +1462,7 @@ module DICOM
1497
1462
  # Query the library with our particular transfer syntax string:
1498
1463
  valid_syntax, @explicit, @data_endian = LIBRARY.process_transfer_syntax(syntax)
1499
1464
  unless valid_syntax
1500
- add_error("Warning: Invalid/unknown transfer syntax encountered! Will try to continue, but errors may occur.")
1465
+ logger.warn("Invalid (unknown) transfer syntax encountered! Will try to continue, but errors may occur.")
1501
1466
  end
1502
1467
  end
1503
1468
 
@@ -1564,4 +1529,4 @@ module DICOM
1564
1529
  end
1565
1530
 
1566
1531
  end
1567
- end
1532
+ end
@@ -0,0 +1,158 @@
1
+ module DICOM
2
+
3
+ # This module handles logging functionality.
4
+ #
5
+ # Logging functionality uses the Standard library's Logger class.
6
+ # To properly handle progname, which inside the DICOM module is simply
7
+ # "DICOM", in all cases, we use an implementation with a proxy class.
8
+ #
9
+ # === Examples
10
+ #
11
+ # require 'dicom'
12
+ # include DICOM
13
+ #
14
+ # # Logging to STDOUT with DEBUG level:
15
+ # DICOM.logger = Logger.new(STDOUT)
16
+ # DICOM.logger.level = Logger::DEBUG
17
+ #
18
+ # # Logging to a file:
19
+ # DICOM.logger = Logger.new('my_logfile.log')
20
+ #
21
+ # # Combine an external logger with DICOM:
22
+ # logger = Logger.new(STDOUT)
23
+ # logger.progname = "MY_APP"
24
+ # DICOM.logger = logger
25
+ # # Now you can call the logger in the following ways:
26
+ # DICOM.logger.info "Message" # => "DICOM: Message"
27
+ # DICOM.logger.info("MY_MODULE) {"Message"} # => "MY_MODULE: Message"
28
+ # logger.info "Message" # => "MY_APP: Message"
29
+ #
30
+ # For more information, please read the Standard library Logger documentation.
31
+ #
32
+ module Logging
33
+
34
+ require 'logger'
35
+
36
+ # Inclusion hook to make the ClassMethods available to whatever
37
+ # includes the Logging module, i.e. the DICOM module.
38
+ #
39
+ def self.included(base)
40
+ base.extend(ClassMethods)
41
+ end
42
+
43
+ module ClassMethods
44
+
45
+ # We use our own ProxyLogger to achieve the features wanted for DICOM logging,
46
+ # e.g. using DICOM as progname for messages logged within the DICOM module
47
+ # (for both the Standard logger as well as the Rails logger), while still allowing
48
+ # a custom progname to be used when the logger is called outside the DICOM module.
49
+ #
50
+ class ProxyLogger
51
+
52
+ # Creating the ProxyLogger instance.
53
+ #
54
+ # === Parameters
55
+ #
56
+ # * <tt>target</tt> -- A Logger instance (e.g. Standard Logger or ActiveSupport::BufferedLogger).
57
+ #
58
+ def initialize(target)
59
+ @target = target
60
+ end
61
+
62
+ # Catches missing methods.
63
+ # In our case, the methods of interest are the typical logger methods,
64
+ # i.e. log, info, fatal, error, debug, where the arguments/block are
65
+ # redirected to the logger in a specific way so that our stated logger
66
+ # features are achieved (this behaviour depends on the logger
67
+ # (Rails vs Standard) and in the case of Standard logger,
68
+ # whether or not a block is given).
69
+ #
70
+ # === Examples
71
+ #
72
+ # # Inside the DICOM module or an external class with 'include DICOM::Logging':
73
+ # logger.info "message"
74
+ #
75
+ # # Calling from outside the DICOM module:
76
+ # DICOM.logger.info "message"
77
+ #
78
+ def method_missing(method_name, *args, &block)
79
+ if method_name.to_s =~ /(log|debug|info|warn|error|fatal)/
80
+ # Rails uses it's own buffered logger which does not
81
+ # work with progname + block as the standard logger does:
82
+ if defined?(Rails)
83
+ @target.send(method_name, "DICOM: #{args.first}")
84
+ elsif block_given?
85
+ @target.send(method_name, *args) { yield }
86
+ else
87
+ @target.send(method_name, "DICOM") { args.first }
88
+ end
89
+ else
90
+ @target.send(method_name, *args, &block)
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ # The logger class variable (must be initialized
97
+ # before it is referenced by the object setter).
98
+ #
99
+ @@logger = nil
100
+
101
+ # The logger object setter.
102
+ # This method is used to replace the default logger instance with
103
+ # a custom logger of your own.
104
+ #
105
+ # === Parameters
106
+ #
107
+ # * <tt>l</tt> -- A Logger instance (e.g. a custom standard Logger).
108
+ #
109
+ # === Examples
110
+ #
111
+ # # Create a logger which ages logfile once it reaches a certain size,
112
+ # # leaves 10 "old log files" with each file being about 1,024,000 bytes:
113
+ # DICOM.logger = Logger.new('foo.log', 10, 1024000)
114
+ #
115
+ def logger=(l)
116
+ @@logger = ProxyLogger.new(l)
117
+ end
118
+
119
+ # The logger object getter.
120
+ # Returns the logger class variable, if defined.
121
+ # If not defined, sets up the Rails logger (if in a Rails environment),
122
+ # or a Standard logger if not.
123
+ #
124
+ # === Examples
125
+ #
126
+ # # Inside the DICOM module (or a class with 'include DICOM::Logging'):
127
+ # logger # => Logger instance
128
+ #
129
+ # # Accessing from outside the DICOM module:
130
+ # DICOM.logger # => Logger instance
131
+ #
132
+ def logger
133
+ @@logger ||= lambda {
134
+ if defined?(Rails)
135
+ ProxyLogger.new(Rails.logger)
136
+ else
137
+ l = Logger.new(STDOUT)
138
+ l.level = Logger::INFO
139
+ ProxyLogger.new(l)
140
+ end
141
+ }.call
142
+ end
143
+
144
+ end
145
+
146
+ # A logger object getter.
147
+ # Forwards the call to the logger class method of the Logging module.
148
+ #
149
+ def logger
150
+ self.class.logger
151
+ end
152
+
153
+ end
154
+
155
+ # Include the Logging module so we can use DICOM.logger.
156
+ include Logging
157
+
158
+ end