dicom 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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