caruby-tissue 1.5.6 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (255) hide show
  1. data/Gemfile +17 -0
  2. data/History.md +5 -1
  3. data/README.md +2 -2
  4. data/bin/crtdump +2 -8
  5. data/bin/crtexample +2 -5
  6. data/bin/crtmigrate +3 -6
  7. data/bin/crtsmoke +3 -8
  8. data/conf/wustl/{log4j.properties → linux/log4j.properties} +3 -3
  9. data/conf/wustl/windows/log4j.properties +40 -0
  10. data/examples/galena/Gemfile +16 -0
  11. data/examples/galena/Gemfile.lock +88 -0
  12. data/examples/galena/README.md +16 -16
  13. data/examples/galena/Rakefile +30 -0
  14. data/examples/galena/bin/seed +5 -11
  15. data/examples/galena/conf/annotation/defaults.yaml +2 -0
  16. data/examples/galena/conf/{migration/annotation_fields.yaml → annotation/fields.yaml} +2 -4
  17. data/examples/galena/conf/defaults.yaml +9 -0
  18. data/examples/galena/conf/{migration/filter_fields.yaml → filter/fields.yaml} +0 -1
  19. data/examples/galena/conf/filter/values.yaml +8 -0
  20. data/examples/galena/conf/{migration/frozen_defaults.yaml → frozen/defaults.yaml} +0 -0
  21. data/examples/galena/conf/{migration/frozen_fields.yaml → frozen/fields.yaml} +0 -2
  22. data/examples/galena/conf/{migration/general_fields.yaml → general/fields.yaml} +0 -24
  23. data/examples/galena/conf/registration/fields.yaml +6 -0
  24. data/examples/galena/conf/{migration/simple_fields.yaml → simple/fields.yaml} +1 -6
  25. data/examples/galena/data/annotation.csv +1 -1
  26. data/examples/galena/data/filter.csv +1 -1
  27. data/examples/galena/data/frozen.csv +1 -1
  28. data/examples/galena/data/general.csv +1 -1
  29. data/examples/galena/data/registration.csv +1 -1
  30. data/examples/galena/data/simple.csv +1 -1
  31. data/examples/galena/galena.gemspec +24 -0
  32. data/examples/galena/lib/galena/filter.rb +25 -0
  33. data/examples/galena/lib/galena/{tissue/migration/frozen_shims.rb → frozen.rb} +6 -10
  34. data/examples/galena/lib/galena/seed.rb +126 -0
  35. data/examples/galena/lib/galena/version.rb +3 -0
  36. data/examples/galena/lib/galena.rb +18 -7
  37. data/examples/galena/log/galena.log +37351 -0
  38. data/examples/galena/log/galena.log.0 +147830 -0
  39. data/examples/galena/spec/annotation_spec.rb +46 -0
  40. data/examples/galena/spec/filter_spec.rb +94 -0
  41. data/examples/galena/spec/frozen_spec.rb +39 -0
  42. data/examples/galena/spec/general_spec.rb +62 -0
  43. data/examples/galena/spec/registration_spec.rb +37 -0
  44. data/examples/galena/spec/seed.rb +107 -0
  45. data/examples/galena/spec/simple_spec.rb +58 -0
  46. data/examples/galena/spec/spec_helper.rb +11 -0
  47. data/examples/galena/spec/support/migration.rb +70 -0
  48. data/lib/catissue/annotation/annotatable.rb +10 -8
  49. data/lib/catissue/annotation/annotation.rb +7 -7
  50. data/lib/catissue/annotation/de_integration.rb +9 -20
  51. data/lib/catissue/annotation/importer.rb +148 -0
  52. data/lib/catissue/annotation/introspector.rb +32 -0
  53. data/lib/catissue/annotation/metadata.rb +422 -0
  54. data/lib/catissue/annotation/proxy.rb +2 -2
  55. data/lib/catissue/annotation/proxy_class.rb +45 -30
  56. data/lib/catissue/annotation/record_entry_proxy.rb +2 -2
  57. data/lib/catissue/cli/command.rb +14 -24
  58. data/lib/catissue/cli/example.rb +5 -3
  59. data/lib/catissue/cli/migrate.rb +45 -37
  60. data/lib/catissue/cli/smoke.rb +2 -3
  61. data/lib/catissue/database/annotation/annotation_service.rb +8 -17
  62. data/lib/catissue/database/annotation/entity_facade.rb +33 -30
  63. data/lib/catissue/database/annotation/id_generator.rb +1 -1
  64. data/lib/catissue/database/annotation/integration_service.rb +11 -4
  65. data/lib/catissue/database/annotation/reference_writer.rb +38 -38
  66. data/lib/catissue/database/controlled_value_finder.rb +13 -28
  67. data/lib/catissue/database/controlled_values.rb +73 -45
  68. data/lib/catissue/database.rb +637 -277
  69. data/lib/catissue/domain/abstract_domain_object.rb +5 -0
  70. data/lib/catissue/domain/abstract_position.rb +3 -5
  71. data/lib/catissue/domain/abstract_specimen.rb +79 -65
  72. data/lib/catissue/domain/abstract_specimen_collection_group.rb +3 -6
  73. data/lib/catissue/domain/address.rb +0 -2
  74. data/lib/catissue/domain/cancer_research_group.rb +0 -3
  75. data/lib/catissue/domain/capacity.rb +2 -4
  76. data/lib/catissue/domain/check_in_check_out_event_parameter.rb +0 -3
  77. data/lib/catissue/domain/collection_event_parameters.rb +2 -7
  78. data/lib/catissue/domain/collection_protocol.rb +11 -16
  79. data/lib/catissue/domain/collection_protocol_event.rb +19 -12
  80. data/lib/catissue/domain/collection_protocol_registration.rb +8 -12
  81. data/lib/catissue/domain/consent_tier_response.rb +0 -4
  82. data/lib/catissue/domain/consent_tier_status.rb +1 -4
  83. data/lib/catissue/domain/container.rb +10 -10
  84. data/lib/catissue/domain/container_position.rb +4 -7
  85. data/lib/catissue/domain/container_type.rb +4 -7
  86. data/lib/catissue/domain/department.rb +0 -3
  87. data/lib/catissue/domain/disposal_event_parameters.rb +5 -5
  88. data/lib/catissue/domain/embedded_event_parameters.rb +1 -4
  89. data/lib/catissue/domain/external_identifier.rb +0 -12
  90. data/lib/catissue/domain/frozen_event_parameters.rb +1 -4
  91. data/lib/catissue/domain/institution.rb +0 -3
  92. data/lib/catissue/domain/new_specimen_array_order_item.rb +0 -5
  93. data/lib/catissue/domain/order_details.rb +0 -2
  94. data/lib/catissue/domain/participant/clinical/chemotherapy.rb +1 -3
  95. data/lib/catissue/domain/participant/clinical/duration.rb +2 -4
  96. data/lib/catissue/domain/participant/clinical/radiation_therapy.rb +2 -4
  97. data/lib/catissue/domain/participant.rb +22 -24
  98. data/lib/catissue/domain/participant_medical_identifier.rb +0 -4
  99. data/lib/catissue/domain/password.rb +0 -4
  100. data/lib/catissue/domain/race.rb +0 -3
  101. data/lib/catissue/domain/received_event_parameters.rb +3 -6
  102. data/lib/catissue/domain/site.rb +1 -4
  103. data/lib/catissue/domain/specimen/pathology/additional_finding.rb +12 -0
  104. data/lib/catissue/domain/specimen/pathology/details.rb +12 -0
  105. data/lib/catissue/domain/specimen/pathology/gleason_score.rb +12 -0
  106. data/lib/catissue/domain/specimen/pathology/histologic_grade.rb +12 -0
  107. data/lib/catissue/domain/specimen/pathology/histologic_type.rb +19 -0
  108. data/lib/catissue/domain/specimen/pathology/histologic_variant_type.rb +12 -0
  109. data/lib/catissue/domain/specimen/pathology/invasion.rb +12 -0
  110. data/lib/catissue/domain/specimen/pathology/prostate_specimen_gleason_score.rb +5 -11
  111. data/lib/catissue/domain/specimen/pathology/prostate_specimen_pathology_annotation.rb +12 -12
  112. data/lib/catissue/domain/specimen/pathology/specimen_additional_finding.rb +5 -14
  113. data/lib/catissue/domain/specimen/pathology/specimen_base_solid_tissue_pathology_annotation.rb +6 -21
  114. data/lib/catissue/domain/specimen/pathology/specimen_details.rb +4 -10
  115. data/lib/catissue/domain/specimen/pathology/specimen_histologic_grade.rb +4 -10
  116. data/lib/catissue/domain/specimen/pathology/specimen_histologic_type.rb +6 -14
  117. data/lib/catissue/domain/specimen/pathology/specimen_histologic_variant_type.rb +5 -11
  118. data/lib/catissue/domain/specimen/pathology/specimen_invasion.rb +5 -11
  119. data/lib/catissue/domain/specimen.rb +113 -76
  120. data/lib/catissue/domain/specimen_array.rb +0 -3
  121. data/lib/catissue/domain/specimen_array_content.rb +1 -4
  122. data/lib/catissue/domain/specimen_array_type.rb +1 -4
  123. data/lib/catissue/domain/specimen_characteristics.rb +0 -3
  124. data/lib/catissue/domain/specimen_collection_group/pathology/base_pathology_annotation.rb +2 -4
  125. data/lib/catissue/domain/specimen_collection_group/pathology/base_solid_tissue_pathology_annotation.rb +2 -4
  126. data/lib/catissue/domain/specimen_collection_group.rb +43 -53
  127. data/lib/catissue/domain/specimen_event_parameters.rb +24 -32
  128. data/lib/catissue/domain/specimen_position.rb +8 -5
  129. data/lib/catissue/domain/specimen_protocol.rb +3 -6
  130. data/lib/catissue/domain/specimen_requirement.rb +22 -20
  131. data/lib/catissue/domain/storage_container.rb +9 -12
  132. data/lib/catissue/domain/storage_type.rb +6 -10
  133. data/lib/catissue/domain/transfer_event_parameters.rb +3 -6
  134. data/lib/catissue/domain/user.rb +22 -29
  135. data/lib/catissue/{util → helpers}/collectible.rb +23 -18
  136. data/lib/catissue/helpers/collectible_event_parameters.rb +68 -0
  137. data/lib/catissue/helpers/controlled_value.rb +35 -0
  138. data/lib/catissue/{domain → helpers}/hash_code.rb +0 -0
  139. data/lib/catissue/{util → helpers}/location.rb +6 -5
  140. data/lib/catissue/helpers/log.rb +4 -0
  141. data/lib/catissue/{util → helpers}/person.rb +1 -1
  142. data/lib/catissue/{util → helpers}/position.rb +10 -8
  143. data/lib/catissue/helpers/properties_loader.rb +143 -0
  144. data/lib/catissue/{util → helpers}/storable.rb +2 -1
  145. data/lib/catissue/{util → helpers}/storage_type_holder.rb +9 -3
  146. data/lib/catissue/{annotation/annotatable_class.rb → metadata.rb} +73 -95
  147. data/lib/catissue/migration/migratable.rb +93 -44
  148. data/lib/catissue/migration/migrator.rb +26 -42
  149. data/lib/catissue/migration/shims.rb +1 -1
  150. data/lib/catissue/migration/unique.rb +76 -0
  151. data/lib/catissue/resource.rb +16 -20
  152. data/lib/catissue/version.rb +1 -1
  153. data/lib/catissue/wustl/logger.rb +52 -32
  154. data/lib/catissue.rb +38 -20
  155. data/test/lib/catissue/database/controlled_values_test.rb +22 -27
  156. data/test/lib/catissue/database/database_test.rb +18 -0
  157. data/test/lib/catissue/domain/address_test.rb +9 -11
  158. data/test/lib/catissue/domain/ca_tissue_test_defaults_test.rb +5 -16
  159. data/test/lib/catissue/domain/capacity_test.rb +2 -2
  160. data/test/lib/catissue/domain/collection_event_parameters_test.rb +16 -8
  161. data/test/lib/catissue/domain/collection_protocol_event_test.rb +1 -1
  162. data/test/lib/catissue/domain/collection_protocol_registration_test.rb +26 -16
  163. data/test/lib/catissue/domain/collection_protocol_test.rb +2 -2
  164. data/test/lib/catissue/domain/container_position_test.rb +7 -4
  165. data/test/lib/catissue/domain/department_test.rb +3 -3
  166. data/test/lib/catissue/domain/disposal_event_parameters_test.rb +1 -1
  167. data/test/lib/catissue/domain/external_identifier_test.rb +5 -1
  168. data/test/lib/catissue/domain/location_test.rb +4 -4
  169. data/test/lib/catissue/domain/participant_medical_identifier_test.rb +3 -3
  170. data/test/lib/catissue/domain/participant_test.rb +33 -20
  171. data/test/lib/catissue/domain/received_event_parameters_test.rb +19 -0
  172. data/test/lib/catissue/domain/site_test.rb +2 -2
  173. data/test/lib/catissue/domain/specimen_array_test.rb +3 -3
  174. data/test/lib/catissue/domain/specimen_array_type_test.rb +6 -6
  175. data/test/lib/catissue/domain/specimen_characteristics_test.rb +1 -1
  176. data/test/lib/catissue/domain/specimen_collection_group_test.rb +49 -13
  177. data/test/lib/catissue/domain/specimen_event_parameters_test.rb +4 -4
  178. data/test/lib/catissue/domain/specimen_position_test.rb +1 -1
  179. data/test/lib/catissue/domain/specimen_requirement_test.rb +2 -2
  180. data/test/lib/catissue/domain/specimen_test.rb +58 -24
  181. data/test/lib/catissue/domain/storage_container_test.rb +3 -16
  182. data/test/lib/catissue/domain/storage_type_test.rb +3 -3
  183. data/test/lib/catissue/domain/transfer_event_parameters_test.rb +17 -17
  184. data/test/lib/catissue/domain/user_test.rb +32 -34
  185. data/test/lib/catissue/helpers/properties_loader_test.rb +19 -0
  186. data/test/lib/catissue/migration/{test_case.rb → helpers/test_case.rb} +30 -20
  187. data/test/lib/examples/galena/tissue/domain/examples_test.rb +28 -38
  188. data/test/lib/examples/galena/tissue/helpers/test_case.rb +24 -0
  189. metadata +175 -99
  190. data/bin/crtextract +0 -47
  191. data/examples/galena/conf/extract/simple_fields.yaml +0 -4
  192. data/examples/galena/conf/migration/annotation_defaults.yaml +0 -2
  193. data/examples/galena/conf/migration/filter_defaults.yaml +0 -1
  194. data/examples/galena/conf/migration/filter_values.yaml +0 -13
  195. data/examples/galena/conf/migration/participant_fields.yaml +0 -4
  196. data/examples/galena/conf/migration/registration_fields.yaml +0 -5
  197. data/examples/galena/data/participant.csv +0 -1
  198. data/examples/galena/lib/galena/tissue/migration/filter_shims.rb +0 -41
  199. data/examples/galena/lib/galena/tissue/seed/defaults.rb +0 -127
  200. data/examples/psbin/README.md +0 -45
  201. data/examples/psbin/conf/adjuvant_hormone_defaults.yaml +0 -2
  202. data/examples/psbin/conf/adjuvant_radiation_defaults.yaml +0 -3
  203. data/examples/psbin/conf/biopsy_defaults.yaml +0 -3
  204. data/examples/psbin/conf/biopsy_fields.yaml +0 -9
  205. data/examples/psbin/conf/neoadjuvant_hormone_defaults.yaml +0 -2
  206. data/examples/psbin/conf/neoadjuvant_radiation_defaults.yaml +0 -3
  207. data/examples/psbin/conf/patient_defaults.yaml +0 -3
  208. data/examples/psbin/conf/patient_fields.yaml +0 -5
  209. data/examples/psbin/conf/surgery_defaults.yaml +0 -4
  210. data/examples/psbin/conf/surgery_fields.yaml +0 -15
  211. data/examples/psbin/conf/t_stage_defaults.yaml +0 -1
  212. data/examples/psbin/conf/t_stage_fields.yaml +0 -4
  213. data/examples/psbin/conf/therapy_fields.yaml +0 -5
  214. data/examples/psbin/data/adjuvant_hormone.csv +0 -1
  215. data/examples/psbin/data/adjuvant_radiation.csv +0 -1
  216. data/examples/psbin/data/biopsy.csv +0 -1
  217. data/examples/psbin/data/neoadjuvant_hormone.csv +0 -1
  218. data/examples/psbin/data/neoadjuvant_radiation.csv +0 -1
  219. data/examples/psbin/data/patient.csv +0 -1
  220. data/examples/psbin/data/surgery.csv +0 -1
  221. data/examples/psbin/data/t_stage.csv +0 -1
  222. data/examples/psbin/lib/psbin/biopsy_shims.rb +0 -15
  223. data/examples/psbin/lib/psbin/surgery_shims.rb +0 -15
  224. data/lib/catissue/annotation/annotation_class.rb +0 -406
  225. data/lib/catissue/annotation/annotation_module.rb +0 -106
  226. data/lib/catissue/domain.rb +0 -26
  227. data/lib/catissue/extract/command.rb +0 -31
  228. data/lib/catissue/extract/delta.rb +0 -58
  229. data/lib/catissue/extract/extractor.rb +0 -99
  230. data/lib/catissue/migration/uniquify.rb +0 -2
  231. data/lib/catissue/util/collectible_event_parameters.rb +0 -71
  232. data/lib/catissue/util/controlled_value.rb +0 -29
  233. data/lib/catissue/util/uniquify.rb +0 -86
  234. data/test/fixtures/catissue/domain/conf/catissue_override.yaml +0 -9
  235. data/test/fixtures/catissue/extract/conf/scg_extract.yaml +0 -3
  236. data/test/fixtures/catissue/extract/conf/scg_fields.yaml +0 -3
  237. data/test/fixtures/catissue/extract/conf/spc_extract.yaml +0 -3
  238. data/test/fixtures/catissue/extract/conf/spc_fields.yaml +0 -4
  239. data/test/fixtures/lib/catissue/defaults_test_fixture.rb +0 -206
  240. data/test/fixtures/lib/examples/galena/migration/alt_key_shims.rb +0 -7
  241. data/test/lib/catissue/domain/base_haemotology_pathology_test.rb +0 -24
  242. data/test/lib/catissue/extract/delta_test.rb +0 -25
  243. data/test/lib/catissue/extract/extractor_test.rb +0 -43
  244. data/test/lib/catissue/import/importable_module_test.rb +0 -14
  245. data/test/lib/catissue/test_case.rb +0 -247
  246. data/test/lib/examples/galena/tissue/migration/annotation_test.rb +0 -29
  247. data/test/lib/examples/galena/tissue/migration/filter_test.rb +0 -29
  248. data/test/lib/examples/galena/tissue/migration/frozen_test.rb +0 -36
  249. data/test/lib/examples/galena/tissue/migration/general_test.rb +0 -56
  250. data/test/lib/examples/galena/tissue/migration/participant_test.rb +0 -61
  251. data/test/lib/examples/galena/tissue/migration/registration_test.rb +0 -17
  252. data/test/lib/examples/galena/tissue/migration/seedify.rb +0 -119
  253. data/test/lib/examples/galena/tissue/migration/simple_test.rb +0 -30
  254. data/test/lib/examples/galena/tissue/migration/test_case.rb +0 -72
  255. data/test/lib/examples/psbin/migration_test.rb +0 -153
@@ -1,30 +1,35 @@
1
1
  require 'singleton'
2
- require 'caruby/util/uniquifier'
3
- require 'caruby/util/validation'
4
- require 'caruby/util/topological_sync_enumerator'
2
+ require 'jinx/helpers/validation'
3
+ require 'jinx/helpers/uniquifier'
5
4
  require 'caruby/database'
6
5
  require 'catissue/database/annotation/annotator'
7
- require 'catissue/util/collectible_event_parameters'
6
+ require 'catissue/helpers/collectible_event_parameters'
7
+ require 'catissue/helpers/collectible'
8
8
 
9
9
  module CaTissue
10
- # Database mediates access to the caTissue application server.
11
- # Superclass +CaRuby::Database+ functionality base class methods are overridden as necessary
12
- # to enable caTissue-specific work-arounds and alternate search strategies.
13
10
  # A CaTissue::Database mediates access to the caTissue database.
14
- # The CaRuby::Database functionality is preserved and not expanded, but this CaTissue::Database overrides
15
- # several base class private methods to enable alternate CaTissue-specific search strategies and work
16
- # around caTissue and caCORE bugs.
11
+ # The CaRuby::Database functionality is preserved, but this CaTissue::Database overrides
12
+ # several base class private methods to enable alternate CaTissue-specific search strategies
13
+ # and work around caTissue and caCORE bugs.
14
+ #
15
+ # This class contains a grab-bag of long and convoluted method implementations for the
16
+ # many caTissue API quirks that necessitate special attention.
17
17
  class Database < CaRuby::Database
18
18
  include Singleton
19
19
 
20
- # return [CaRuby::SQLExecutor] a utility SQL executor
21
- attr_reader :executor, :access_properties
22
-
23
- # Creates a new Database with the +catissuecore+ service and {CaTissue.access_properties}.
20
+ # Creates a new Database with the +catissuecore+ service and {#access_properties}.
24
21
  def initialize
25
- @access_properties = CaTissue.access_properties
26
- super(SVC_NAME, @access_properties)
27
- @executor = CaRuby::SQLExecutor.new(@access_properties)
22
+ super(SVC_NAME) { access_properties }
23
+ end
24
+
25
+ # @return (see CaRuby::Domain.properties)
26
+ def access_properties
27
+ CaTissue.properties
28
+ end
29
+
30
+ # return [CaRuby::SQLExecutor] a utility SQL executor
31
+ def executor
32
+ @executor ||= create_executor
28
33
  end
29
34
 
30
35
  # @return [Annotator] the annotator utility
@@ -32,7 +37,7 @@ module CaTissue
32
37
  @annotator ||= Annotator.new(self)
33
38
  end
34
39
 
35
- # If the given domain object is an {Annotation}, then this method returns the {AnnotationService}
40
+ # If the given domain object is an {Annotation}, then this method returns the +CaRuby::AnnotationService+
36
41
  # for the object {AnnotationModule}, otherwise this method returns the standard {CaTissue::Database}
37
42
  # service.
38
43
  #
@@ -42,7 +47,22 @@ module CaTissue
42
47
  klass < Annotation ? klass.annotation_module.persistence_service : super
43
48
  end
44
49
 
45
- # Augments {CaRuby::Database#ensure_exists} to ensure that an {Annotation::Proxy} reference identifier
50
+ # @quirk caTissue caTissue Address create succeeds but does not set the result identifier, rendering
51
+ # the result useless. This leads to obscure cascading errors if the address is saved before
52
+ # creating and then updating the address owner. Throw an exception if the create argument is an
53
+ # address. An address can be created but only in the context of its owner, and special case logic
54
+ # is required to fetch the address by an inverted query on the owner address reference.
55
+ #
56
+ # @param [Resource] obj the dependent domain object to save
57
+ # @raise [CaRuby::DatabaseError] if the object is a {CaTissue::Address}
58
+ def create(obj)
59
+ if CaTissue::Address === obj then
60
+ Jinx.fail(CaRuby::DatabaseError, "Address creation is not supported, since a caTissue bug does not set the create result identifier property.")
61
+ end
62
+ super
63
+ end
64
+
65
+ # Augments +CaRuby::Database.ensure_exists} to ensure that an {Annotation::Proxy+ reference identifier
46
66
  # reflects the hook identifier.
47
67
  #
48
68
  # @param (see CaRuby::Database::Writer#ensure_exists)
@@ -61,8 +81,26 @@ module CaTissue
61
81
  UPD_EID_SQL = 'update catissue_external_identifier set name = ?, value = ?, specimen_id = ? where identifier = ?'
62
82
 
63
83
  UPD_CTR_SQL = 'update catissue_consent_tier_response set response = ? where identifier = ?'
84
+
85
+ MAX_ADDR_ID_SQL = "select max(identifier) from catissue_address"
86
+
87
+ # return (see #executor)
88
+ def create_executor
89
+ # Augment the user-defined application properties with Hibernate properties.
90
+ props = access_properties
91
+ hprops = Java::edu.wustl.common.hibernate.HibernateUtil.configuration.properties.to_hash rescue nil
92
+ if hprops then
93
+ props[:database_user] ||= hprops['connection.username']
94
+ props[:database_password] ||= hprops['connection.password']
95
+ if not props.has_key?(:database_name) and not props.has_key?(:database_port) and hprops.has_key?('connection.url') then
96
+ props[:database_url] ||= hprops['connection.url']
97
+ end
98
+ props[:database_driver_class] ||= hprops['connection.driver_class']
99
+ end
100
+ CaRuby::SQLExecutor.new(props)
101
+ end
64
102
 
65
- # Overrides #{CaRuby::Database::Writer#recursive_save?} to support the update work-around
103
+ # Overrides #+CaRuby::Database::Writer.recursive_save?+ to support the update work-around
66
104
  # described in {#update_object}. A recursive SCG update is allowed if the nested
67
105
  # transaction sequence is:
68
106
  # * Update SCG
@@ -72,7 +110,7 @@ module CaTissue
72
110
  # @param (see CaRuby::Database::Writer#recursive_save?)
73
111
  # @return (see CaRuby::Database::Writer#recursive_save?)
74
112
  def recursive_save?(obj, operation)
75
- super and not scg_event_update_workaround?(obj, operation)
113
+ super and not collectible_event_update_workaround?(obj, operation)
76
114
  end
77
115
 
78
116
  # Returns whether the given specimens are compatible. The target is compatible with the source
@@ -120,36 +158,86 @@ module CaTissue
120
158
  parent = ref.send(attribute) || next
121
159
  resolved = id_ref_hash[parent.identifier] || next
122
160
  logger.debug { "Resetting #{ref.qp} #{attribute} from #{parent} to #{resolved} in order to fix a caCORE inconsistency..." }
123
- ref.set_attribute(attribute, resolved)
161
+ ref.set_property_value(attribute, resolved)
124
162
  end
125
163
  end
126
164
 
127
165
  # @quirk caTissue Bug #135: Update SCG SpecimenEventParameters raises AuditException.
128
166
  # Work around is to update the SCG instead.
129
- # @quirk caTissue CPR consent tier response update results in Access denied error.
167
+ #
168
+ # @quirk caTissue CPR consent tier response update results in 'Access denied' error.
130
169
  # Work-around is to update the response using a direct SQL call.
131
170
  #
171
+ # @quirk caTissue The label is auto-generated by caTissue when the specimen is created.
172
+ # Label creation depends on an auto-generator. Therefore, the specimen must be refetched
173
+ # when collected in order to reflect the database label value.
174
+ #
175
+ # @quirk caTissue The Specimen label is auto-generated when the collection status is changed
176
+ # from Pending to Collected. However, the update argument label is not updated to reflect
177
+ # the auto-generated value. Note that the barcode is auto-generated on create, not update.
178
+ #
179
+ # @quirk caTissue Updating a User does not cascade update to the address.
180
+ # address is cascade none in Hibernate, but is created in caTissue, perhaps by biz logic.
181
+ # An address change is ignored by the User update. Since Address cannot be updated
182
+ # separately due to a different caTissue bug, the address update must be performed
183
+ # by a work-around here.
184
+ #
185
+ # @quirk caTissue Specimen update does not cascade to a referenced SpecimenEventParameters.
186
+ # Even though the reference property is marked cascade in Hibernate, the update does
187
+ # not propagate to the SpecimenEventParameters. Specimen create does cascade. SCG update
188
+ # cascades. Certain SpecimenEventParameters subclasses have variant Specimen cascade
189
+ # behavior, as described in other quirk documentation. The work-around is to update
190
+ # the Specimen SpecimenEventParameters directly rather than delegating the update
191
+ # to the Specimen owner.
192
+ #
193
+ # @quirk caTissue caTissue Specimen update ignores the available quantity. Unlike the
194
+ # auto-generated Specimen update, there is no known work-around
195
+ #
132
196
  # @param (see CaRuby::Database#update_object)
133
- # @return (see CaRuby::Database#update_object)
134
197
  def update_object(obj)
135
- if collectible_event_parameters?(obj) then
136
- save_collectible_event_parameters(obj)
198
+ # prep work
199
+ case obj
200
+ when CaTissue::SpecimenCollectionGroup then
201
+ if obj.collection_protocol_registration.nil? then
202
+ # add the extraneous SCG template CPR
203
+ logger.debug { "Work around caTissue bug by fetching extraneous #{obj} CPR..." }
204
+ obj.collection_protocol_registration = fetch_association(obj, :collection_protocol_registration)
205
+ end
206
+ if obj.collection_status.nil? and obj.collection_event_parameters then
207
+ obj.collection_status = 'Complete'
208
+ logger.debug { "Set #{obj} status to Complete since it has collection event parameters." }
209
+ end
210
+ when CaTissue::Specimen then
211
+ prepare_specimen_for_update(obj)
212
+ when CaTissue::StorageContainer then
213
+ if obj.storage_type.nil? then
214
+ logger.debug { "Fetching #{obj} storage type prior to update..." }
215
+ lazy_loader.enable { obj.storage_type }
216
+ end
217
+ end
218
+
219
+ # Delegate update special cases.
220
+ if CaTissue::User === obj then
221
+ if obj.address.identifier.nil? or obj.address.changed? then
222
+ return update_user_address(obj, obj.address)
223
+ end
224
+ end
225
+ if CollectibleEventParameters === obj then
226
+ if obj.specimen_collection_group then
227
+ return save_collectible_scg_event_parameters(obj)
228
+ elsif obj.specimen then
229
+ return update_from_template(obj)
230
+ end
231
+ elsif SpecimenEventParameters === obj then
232
+ return update_from_template(obj)
137
233
  elsif CaTissue::ConsentTierResponse === obj then
138
- update_consent_tier_response(obj)
234
+ return update_consent_tier_response(obj)
139
235
  elsif Annotation === obj then
140
- raise CaRuby::DatabaseError.new("Annotation update is not supported on #{obj}")
141
- else
142
- case obj
143
- when CaTissue::Specimen then
144
- # Specimen activity status is not always set to default; don't know why.
145
- # TODO - isolate and fix at source
146
- obj.activity_status ||= 'Active'
147
- when CaTissue::StorageContainer then
148
- logger.debug { "Fetching #{obj} storage type prior to update..." }
149
- if obj.storage_type.nil? then lazy_loader.enable { obj.storage_type } end
150
- end
151
- super
236
+ Jinx.fail(CaRuby::DatabaseError, "Annotation update is not supported on #{obj}")
152
237
  end
238
+
239
+ # Finally, the standard update.
240
+ super
153
241
  end
154
242
 
155
243
  # Updates the given dependent.
@@ -157,92 +245,112 @@ module CaTissue
157
245
  # @quirk caTissue 1.2 user address update results in authorization error. Work-around is to
158
246
  # create a new address record.
159
247
  #
248
+ # @quirk caTissue Specimen update cascades to child update according to Hibernate, but
249
+ # caTissue somehow circumvents the child update. The child database content is not changed
250
+ # to reflect the update argument. Work-around is to update the child independently after
251
+ # the parent update.
252
+ #
253
+ # @quirk caTissue The aforementioned {#save_with_template} caTissue collectible event parameters
254
+ # dependent bug implies that the dependent must be saved directly rather than via a cascade
255
+ # from the Specimen or SCG owner to the referenced event parameters. The direct save avoids
256
+ # a tangled nest of obscure caTissue bugs described in the {#save_with_template} rubydoc.
257
+ #
160
258
  # @param (see CaRuby::Writer#update_changed_dependent)
161
- def update_changed_dependent(owner, attribute, dependent, autogenerated)
162
- if CaTissue::User === owner and attribute == :address then
259
+ def update_changed_dependent(owner, property, dependent, autogenerated)
260
+ # Save the changed collectible event parameters directly rather than via a cascade.
261
+ if CollectibleEventParameters === dependent then
262
+ logger.debug { "Work around a caTissue bug by resaving the collected #{owner} #{dependent} directly rather than via a cascade..." }
263
+ return update_from_template(dependent)
264
+ end
265
+ if CaTissue::User === owner and property.attribute == :address then
163
266
  update_user_address(owner, dependent)
267
+ elsif CaTissue::Specimen === owner and CaTissue::Specimen === dependent then
268
+ logger.debug { "Work around caTissue bug to update #{dependent} separately after the parent #{owner} update..." }
269
+ prepare_specimen_for_update(dependent)
270
+ update_from_template(dependent)
271
+ logger.debug { "Updated the #{owner} child #{dependent}." }
164
272
  else
165
273
  super
166
274
  end
167
275
  end
276
+
277
+ def prepare_specimen_for_update(obj)
278
+ if obj.collection_status.nil? and obj.collection_event_parameters then
279
+ obj.collection_status = 'Collected'
280
+ logger.debug { "Set #{obj} status to Collected since it has collection event parameters." }
281
+ end
282
+ if obj.characteristics.nil? then
283
+ logger.debug { "Fetching #{obj} characteristics for update..." }
284
+ fetched = fetch_association(obj, :specimen_characteristics)
285
+ Jinx.fail(CaRuby::DatabaseError, "#{obj} is missing characteristics") if fetched.nil?
286
+ logger.debug { "Set #{obj} characteristics to #{fetched}." }
287
+ obj.characteristics = fetched
288
+ elsif obj.characteristics.identifier.nil? then
289
+ logger.debug { "Fetching #{obj} characteristics identifier for update..." }
290
+ fetched = fetch_association(obj, :specimen_characteristics)
291
+ raise Jinx.fail(CaRuby::DatabaseError, "#{obj} is missing characteristics") if fetched.nil?
292
+ obj.characteristics.identifier = fetched.identifier
293
+ logger.debug { "Set #{obj} characteristics #{obj.characteristics} identifier." }
294
+ end
295
+ cep = obj.collection_event_parameters
296
+ rep = obj.received_event_parameters
297
+ if (cep and cep.identifier.nil?) or (rep and rep.identifier.nil?) then
298
+ logger.debug { "Fetching #{obj} collectible event parameters identifiers for update..." }
299
+ eps = fetch_association(obj, :specimen_event_parameters)
300
+ fcep = eps.detect { |ep| CaTissue::CollectionEventParameters === ep }
301
+ if fcep then
302
+ cep.merge(fcep)
303
+ logger.debug { "Set #{obj} #{cep} identifier." }
304
+ end
305
+ frep = eps.detect { |ep| CaTissue::ReceivedEventParameters === ep }
306
+ if frep then
307
+ rep.merge(frep)
308
+ logger.debug { "Set #{obj} #{rep} identifier." }
309
+ end
310
+ end
311
+ # A collected specimen requires a label.
312
+ if obj.collection_status == 'Collected' and obj.label.nil? then
313
+ obj.label = Jinx::Uniquifier.qualifier
314
+ logger.debug { "Worked around caTissue bug by setting a collected specimen label to #{obj.label}." }
315
+ end
316
+ end
168
317
 
169
318
  # Updates the given user address.
170
319
  #
171
- # @param [CaTissue::User] the user
320
+ # @param [CaTissue::User] the user owner
172
321
  # @param [CaTissue::Address] the address to update
322
+ # @return [CaTissue::User] the updated user
173
323
  def update_user_address(user, address)
174
- logger.debug { "Work around caTissue prohibition of #{user} address #{address} update by creating a new address record..." }
324
+ logger.debug { "Work around caTissue prohibition of #{user} address #{address} update by creating a new address record for a dummy user..." }
175
325
  address.identifier = nil
176
- create(address)
326
+ perform(:create, address) { create_object(address) }
327
+ logger.debug { "Worked around caTissue address update bug by swizzling the #{user} address #{address} identifier." }
177
328
  perform(:update, user) { update_object(user) }
178
- fetched = query(user, :address).first
179
- address.identifier = fetched.identifier
180
- logger.debug { "#{user} address identifier reset to #{address.identifier}." }
329
+ user
181
330
  end
182
331
 
183
- # Returns whether operation is the second SCG Update described in {#recursive_save?}.
184
- def scg_event_update_workaround?(obj, operation)
185
- # Is this an SCG update?
186
- return false unless CaTissue::SpecimenCollectionGroup === obj and operation == :update
332
+ # Returns whether operation is the second Update described in {#recursive_save?}.
333
+ def collectible_event_update_workaround?(obj, operation)
334
+ # Is this an update?
335
+ return false unless Collectible === obj and operation == :update
187
336
  last = @operations.last
188
- # Is the nesting operation on an SEP?
189
- return false unless last and collectible_event_parameters?(last.subject)
190
- ev = last.subject
191
- # Is the SEP SCG the same as the target SCG?
192
- return false unless ev.specimen_collection_group == obj
193
- penultimate = @operations[-2]
194
- # Is the operation which nests the SEP operation a SCG update on the same SCG?
195
- penultimate and penultimate.subject == obj
337
+ # Is there a nesting operation?
338
+ return false unless last
339
+ # Is the nesting operation subject a CEP?
340
+ return false unless CaTissue::CollectibleEventParameters === last.subject
341
+ # Is the nesting operation subject owned by the current object?
342
+ return false unless last.subject.owner == obj
343
+ prev = penultimate_save_operation
344
+ # Is the outer save operation subject the current object?
345
+ prev and prev.subject == obj
196
346
  end
197
-
198
- # Augments {CaRuby::Database#save_with_template} to work around the following caTissue anomalies:
199
- #
200
- # @quirk caTissue Bug #149: API update TissueSpecimen position validation incorrect.
201
- # The Specimen update argument must reference the old position, even though the position is not
202
- # updatable, unless old status is Pending. The validation defect described in Bug #149 requires
203
- # a work-around that is also used for a different reason described in the following paragraph.
204
- #
205
- # @quirk caTissue Update of a {CaTissue::Specimen} which references a position must include the former
206
- # position in the caTissue service update argument. A Specimen position is altered as a side-effect
207
- # by creating a proxy save {CaTissue::TransferEventParameters}. The changed position is not reflected
208
- # in the Specimen position, which must be refetched to reflect the database state. This fetch is
209
- # done automatically by {CaRuby::Database} as part of the save proxy mechanism. The Specimen update
210
- # template must include a reference to the former position but not the changed position.
211
- #
212
- # However, the Specimen {CaRuby::Writer#update} argument will include the changed position, not the
213
- # former position. The template built {CaRuby::Writer#update} for submission to the caTissue app
214
- # does not include a position reference, since the position has a save proxy which handles position
215
- # change as part of the {CaRuby::Writer} update dependent propagation.
216
- #
217
- # Thus, updating a Specimen which includes a position change is performed as follows:
218
- # * reconstitute the former position from the Position snapshot taken as part of the
219
- # {CaRuby::Persistable} change tracker.
220
- # * add the former position to the template (which will now differ from the {CaRuby::Writer#update}
221
- # argument).
222
- # * submit the adjusted Specimen template to the caTissue app updateObject.
223
- # * {CaRuby::Writer#update} will propagate the Specimen update to the changed position dependent,
224
- # which in turn saves via the {CaTissue::TransferEventParameters} proxy.
225
- # * The proxy save will in turn refetch the proxied Specimen position to obtain the identifier
226
- # and merge this into the Specimen position.
227
- # * The Specimen update template is used solely to satisfy the often arcane caTissue interaction
228
- # requirements like this work-around, and is thrown away along with its aberrant state.
229
- #
230
- # This work-around is the only case of a save template modification to handle a caTissue special
231
- # case. Note that the {CaTissue::SpecimenPosition} logic does not apply to a
232
- # {CaTissue::ContainerPosition}, which can be updated directly.
233
- #
234
- # The additional complexity of this work-around is necessitated by the caTissue policy of update
235
- # by indirect server-side side-effects that are not reflected back to the client. The caRuby
236
- # policy of a declarative API that persists the save argument as given and reflects the
237
- # changed database state requires this work-around.
238
- #
239
- # @param obj (see #store)
240
- # @param [Resource] template the obj template to submit to caCORE
241
- def save_with_template(obj, template)
242
- if CaTissue::Specimen === obj and obj.position and obj.position.identifier then
243
- add_position_to_specimen_template(obj, template)
347
+
348
+ def penultimate_save_operation
349
+ 2.upto(@operations.size) do |index|
350
+ op = @operations[-index]
351
+ return op if op.save?
244
352
  end
245
- super
353
+ nil
246
354
  end
247
355
 
248
356
  # Adds the specimen position to its save template.
@@ -253,170 +361,256 @@ module CaTissue
253
361
  def add_position_to_specimen_template(specimen, template)
254
362
  pos = specimen.position
255
363
  # the non-domain position attributes
256
- attrs = pos.class.nondomain_attributes
364
+ pas = pos.class.nondomain_attributes
257
365
  # the template position reflects the old values, if available
258
366
  ss = pos.snapshot
259
367
  # the attribute => value hash
260
- vh = ss ? attrs.to_compact_hash { |attr| ss[attr] } : pos.value_hash(attrs)
368
+ vh = ss ? pas.to_compact_hash { |pas| ss[pas] } : pos.value_hash(pas)
261
369
  vh[:specimen] = template
262
370
  vh[:storage_container] = pos.storage_container.copy
263
371
  # the template position reflects the old values
264
372
  template.position = pos.class.new(vh)
265
373
  logger.debug { "Work around #{specimen} update anomaly by copying position #{template.position.qp} to update template #{template.qp} as #{template.position.qp} with values #{vh.qp}..." }
266
374
  end
267
-
268
- # @return [Boolean] whether obj is a CollectibleEventParameters with a SCG owner
269
- def collectible_event_parameters?(obj)
270
- CollectibleEventParameters === obj and obj.specimen_collection_group
271
- end
272
375
 
273
376
  # @param [CollectibleEventParameters] ep the SCG event parameters to save
274
- # @return (see CaRuby::Database#update_object)
377
+ # @return (see #update_object)
275
378
  # @see #create_object
276
379
  # @see #update_object
277
- def save_collectible_event_parameters(ep)
380
+ def save_collectible_scg_event_parameters(ep)
278
381
  scg = ep.specimen_collection_group
279
- logger.debug { "Work around #{ep.qp} caTissue SCG SpecimenEventParameters update bug by updating the owner #{scg.qp} instead..." }
280
- ensure_exists(scg)
281
- # update the CollectibleEventParameters by updating the SCG
282
- update(scg)
283
- raise CaRuby::DatabaseError.new("Update SCG did not cascade to dependent #{ep}") unless ep.identifier
382
+ logger.debug { "Work around #{ep.qp} caTissue SCG event parameters save bug by updating the owner #{scg.qp} instead..." }
383
+ if ep.identifier.nil? then
384
+ ensure_exists(scg)
385
+ return ep if ep.identifier
386
+ end
387
+ update_from_template(scg)
388
+ # Last resort: straight create; probably will fail due to caTissue bug which
389
+ # expects a CEP create to reference a specimen rather than a SCG.
390
+ create_from_template(ep) if ep.identifier.nil?
284
391
  ep
285
392
  end
286
393
 
287
394
 
288
- # @param [CaTissue::Consent_TierResponse] ctr the response to update
395
+ # @param [CaTissue::ConsentTierResponse] ctr the response to update
289
396
  # @see #update_object
290
397
  def update_consent_tier_response(ctr)
291
398
  # Call the SQL
292
399
  logger.debug { "Work around caTissue #{ctr} update bug by submitting direct SQL call..." }
293
- @executor.execute { |dbh| dbh.do(UPD_CTR_SQL, ctr.response, ctr.identifier) }
400
+ executor.transact(UPD_CTR_SQL, ctr.response, ctr.identifier)
294
401
  logger.debug { "caTissue #{ctr} update work-around completed." }
295
402
  end
296
403
 
297
- # Overrides {CaRuby::Database::Writer#save_changed_dependents} to handle the following anomalies:
298
- # * create Specimen disposal event last, as described in {#save_changed_specimen_dependents}
404
+ # Overrides +CaRuby::Database::Writer.save_changed_dependents+ to handle the following anomaly:
405
+ #
406
+ # @quirk caTissue DisposalEventParameters must be created after all other Specimen SEPs. This
407
+ # use case arises when migrating a source biorepository discarded specimen for archival.
408
+ #
409
+ # The process for creating a discarded Specimen is as follows:
410
+ # * Create the Specimen with status Active.
411
+ # * Create the non-disposal events.
412
+ # * Create the DisposalEventParameters.
413
+ #
414
+ # {#save_changed_dependents} delegates to this method to handle the latter two steps.
415
+ #
416
+ # A DisposalEventParameters cannot be created for a closed Specimen. Conversely, caTissue closes
417
+ # the Specimen as a side-effect of creating a DisposalEventParameters. Therefore, even if the
418
+ # client submits a closed Specimen for create, this CaTissue::Database must first create the
419
+ # Specimen with status Active, then submit the DisposalEventParameters.
420
+ #
421
+ # This is a work-around on top of the {#create_unavailable_specimen} work-around. See that method
422
+ # for the subtle interaction required between these two work-arounds.
299
423
  #
300
424
  # @param (see CaRuby::Writer#save_dependents)
301
425
  def save_changed_dependents(obj)
302
- case obj
303
- when Specimen then save_changed_specimen_dependents(obj) { super }
304
- else super
305
- end
306
- end
307
-
308
- # Overrides {CaRuby::Database::Writer#save_changed_dependents} on a Specimen to correct the
309
- # following problem:
310
- #
311
- # @quirk caTissue DisposalEventParameters must be created after all other Specimen SEPs.
312
- #
313
- # The process for migrating a discarded Specimen is as follows:
314
- # * Create the Specimen with status Active.
315
- # * Create the non-disposal events.
316
- # * Create the DisposalEventParameters.
317
- #
318
- # A DisposalEventParameters cannot be created for a closed Specimen. Conversely, caTissue closes
319
- # the Specimen as a side-effect of creating a DisposalEventParameters. Therefore, even if the
320
- # client submits a closed Specimen for create, this CaTissue::Database must first create the
321
- # Specimen with status Active, then submit the DisposalEventParameters.
322
- #
323
- # This is a work-around on top of the {#create_unavailable_specimen} work-around. See that method
324
- # for the subtle interaction required between these two work-arounds.
325
- #
326
- # @param [CaTissue::Specimen] the specimen whose dependents are to be saved
327
- # @yield [dependent] calls the base {CaRuby::Writer#save_changed_dependents}
328
- # @yieldparam [Resource] dependent the dependent to save
329
- def save_changed_specimen_dependents(specimen)
330
- dsp = specimen.specimen_events.detect { |ep| CaTissue::DisposalEventParameters === ep }
426
+ if CaTissue::Specimen === obj then
427
+ dsp = obj.specimen_events.detect { |ep| CaTissue::DisposalEventParameters === ep }
428
+ end
331
429
  if dsp then
332
- logger.debug { "Work around caTissue #{specimen.qp} event parameters save order dependency by deferring #{dsp.qp} save..." }
333
- specimen.specimen_events.delete(dsp)
430
+ obj.specimen_events.delete(dsp)
431
+ logger.debug { "Work around a caTissue #{obj.qp} event parameters save order dependency by deferring the #{dsp.qp} save..." }
432
+ obj.specimen_events.delete(dsp)
334
433
  end
335
434
 
435
+ # Delegate to the standard save_changed_dependents.
336
436
  begin
337
- yield specimen
437
+ super
338
438
  ensure
339
- specimen.specimen_events << dsp if dsp
439
+ obj.specimen_events << dsp if dsp
340
440
  end
341
441
 
342
- # save the deferred disposal if any
442
+ # Save the deferred disposal, if any.
343
443
  if dsp then
344
- logger.debug { "Creating deferred #{specimen.qp} dependent #{dsp.qp}..." }
345
- save_dependent(dsp)
444
+ logger.debug { "Creating deferred #{obj.qp} dependent #{dsp.qp}..." }
445
+ save_dependent_if_changed(obj, :specimen_events, dsp)
446
+ if obj.activity_status != 'Closed' then
447
+ logger.debug { "Refetching the disposed #{obj.qp} to reflect the modified activity status..." }
448
+ obj.activity_status = nil
449
+ obj.find
450
+ end
346
451
  end
347
452
  end
348
453
 
349
- # Overrides {CaRuby::Database#build_save_template} to return obj itself if
454
+ # Overrides +CaRuby::Database.build_save_template+ to return obj itself if
350
455
  # obj is an {Annotation}, since annotations do not employ a separate template.
351
456
  #
352
457
  # @param (see CaRuby::Database#build_save_template)
353
458
  # @return (see CaRuby::Database#build_save_template)
354
459
  def build_save_template(obj, builder)
355
- Annotation === obj ? prepare_annotation_for_save(obj) : super
460
+ Annotation === obj ? prepare_annotation_as_save_template(obj) : super
356
461
  end
357
462
 
358
- # Ensures that a primary annotation hook exists.
463
+ # Validates and completes the given annotation object prior to save.
464
+ # The annotation is submitted directly to the caTissue save rather
465
+ # than building a save template, since an annotation save does not
466
+ # recurse to references, unlike a standard save.
359
467
  #
360
468
  # @param [Annotation] annotation the object to create
361
469
  # @return [Annotation] the annotation object
362
- # @raise [DatabaseError] if the annotation does not reference a hook entity
363
- def prepare_annotation_for_save(annotation)
470
+ # @raise (see #ensure_primary_annotation_has_hook)
471
+ def prepare_annotation_as_save_template(annotation)
472
+ ensure_primary_annotation_has_hook(annotation) if annotation.class.primary?
473
+ annotation
474
+ end
475
+
476
+ # Ensures that a primary annotation hook exists.
477
+ #
478
+ # @param (see #prepare_annotation_for_save)
479
+ # @raise [CaRuby::DatabaseError] if the annotation does not reference a hook entity
480
+ def ensure_primary_annotation_has_hook(annotation)
364
481
  hook = annotation.hook
365
482
  if hook.nil? then
366
- raise CaRuby::DatabaseError.new("Cannot save annotation #{annotation} since it does not reference a hook entity")
483
+ Jinx.fail(CaRuby::DatabaseError, "Cannot save annotation #{annotation} since it does not reference a hook entity")
367
484
  end
368
485
  if hook.identifier.nil? then
369
486
  logger.debug { "Ensuring that the annotation #{annotation.qp} hook entity #{hook.qp} exists in the database..." }
370
487
  ensure_exists(hook)
371
488
  end
372
- annotation
373
489
  end
374
490
 
375
- # Overrides {CaRuby::Database::Writer#save_with_template} to work around caTissue bugs.
376
- # @quirk caTissue Bug #63: a SpecimenCollectionGroup update requires the referenced CollectionProtocolRegistration
377
- # with an identifier to hold extraneous CollectionProtocolRegistration content, including the CPR
378
- # collection protocol and PPI.
379
- # @quirk caTissue Bug: CollectionProtocolRegistration must cascade throughCP, but the CP events
380
- # cannot cascade to SpecimenRequirement without raising an Exception. Work-around is to clear the template CP events.
491
+ # Augments +CaRuby::Database.save_with_template+ to work around the following caTissue anomalies:
492
+ #
493
+ # @quirk caTissue Bug #149: API update TissueSpecimen position validation incorrect.
494
+ # The Specimen update argument must reference the old position, even though the position is not
495
+ # updatable, unless old status is Pending. The validation defect described in Bug #149 requires
496
+ # a work-around that is also used for a different reason described in the following paragraph.
497
+ #
498
+ # @quirk caTissue Update of a {CaTissue::Specimen} which references a position must include the former
499
+ # position in the caTissue service update argument. A Specimen position is altered as a side-effect
500
+ # by creating a proxy save {CaTissue::TransferEventParameters}. The changed position is not reflected
501
+ # in the Specimen position, which must be refetched to reflect the database state. This fetch is
502
+ # done automatically by +CaRuby::Database+ as part of the save proxy mechanism. The Specimen update
503
+ # template must include a reference to the former position but not the changed position.
504
+ #
505
+ # However, the Specimen +CaRuby::Writer.update+ argument will include the changed position, not the
506
+ # former position. The template built by +CaRuby::Writer.update+ for submission to the caTissue app
507
+ # does not include a position reference, since the position has a save proxy which handles position
508
+ # change as part of the +CaRuby::Writer+ update dependent propagation.
509
+ #
510
+ # Thus, updating a Specimen which includes a position change is performed as follows:
511
+ # * reconstitute the former position from the Position snapshot taken as part of the
512
+ # +CaRuby::Persistable+ change tracker.
513
+ # * add the former position to the template (which will now differ from the +CaRuby::Writer.update+
514
+ # argument).
515
+ # * submit the adjusted Specimen template to the caTissue app updateObject.
516
+ # * +CaRuby::Writer.update+ will propagate the Specimen update to the changed position dependent,
517
+ # which in turn saves via the {CaTissue::TransferEventParameters} proxy.
518
+ # * The proxy save will in turn refetch the proxied Specimen position to obtain the identifier
519
+ # and merge this into the Specimen position.
520
+ # * The Specimen update template is used solely to satisfy the often arcane caTissue interaction
521
+ # requirements like this work-around, and is thrown away along with its aberrant state.
522
+ #
523
+ # This work-around is the only case of a save template modification to handle a position special
524
+ # case. Note that the {CaTissue::SpecimenPosition} logic does not apply to a
525
+ # {CaTissue::ContainerPosition}, which can be updated directly.
526
+ #
527
+ # The additional complexity of this work-around is necessitated by the caTissue policy of update
528
+ # by indirect server-side side-effects that are not reflected back to the client. The caRuby
529
+ # declarative API policy persists the save argument as given and reflects the changed database
530
+ # state. That policy requires this work-around.
531
+ #
532
+ # @quirk caTissue Bug #63: A SpecimenCollectionGroup update requires that the referenced
533
+ # CollectionProtocolRegistration hold extraneous content, including the CPR collection
534
+ # protocol and PPI.
535
+ #
536
+ # @quirk caTissue Bug: CollectionProtocolRegistration must cascade through the
537
+ # CollectionProtocol, but the CP events cannot cascade to SpecimenRequirement without
538
+ # raising an Exception. The work-around is to clear the template CP events.
539
+ #
540
+ # @quirk caTissue Update SpecimenCollectionGroup requires a collection and received event parameter,
541
+ # even if the collection status is pending. Work-around is to add default parameters.
542
+ #
381
543
  # @quirk caTissue Create Specimen with nil label does not auto-generate the label.
382
544
  # Work-around is to set the label to a unique value.
383
545
  #
546
+ # @quirk caTissue When caTissue updates a Specimen referencing a child Specimen with an identifier which
547
+ # is setting the collection status to +Collected+ and has a received or collection event parameters
548
+ # without an identifier, then caTissue creates the referenced event parameters as well as spurious
549
+ # auto-generated received and collection event parameters. This behavior differs from the top-level
550
+ # Specimen, where the event parameters in the argument are simply ignored. The work-around is to
551
+ # recursively strip the derived received and collection event parameters, then fetch, match and resave
552
+ # the stripped event parameters. This behavior is not entirely confirmed, because the various forms
553
+ # of caTissue event parameters corruption are hard to isolate and catalog. The best recourse is to
554
+ # assume that caTissue will ignore or corrupt any received and collection event parameters references
555
+ # and strip, fetch, match and resave these event parameters separately.
556
+ #
557
+ # @quirk caTissue When caTissue updates a pending SCG to status Complete then a collection and
558
+ # received event parameters is added to each referenced top-level specimen, even though the
559
+ # specimen status is not updated from Pending to Collected. Event parameters are not added to
560
+ # child specimens.
561
+ #
562
+ # @param obj [Resource] obj the object to save
563
+ # @param [Resource] template the template to submit to caCORE
384
564
  # @raise DatabaseError if the object to save is an {Annotation::Proxy}, which is not supported
385
565
  def save_with_template(obj, template)
386
566
  # special cases to work around caTissue bugs
387
567
  if CaTissue::CollectionProtocolRegistration === obj and template.collection_protocol then
388
568
  template.collection_protocol.collection_protocol_events.clear
389
569
  elsif CaTissue::Specimen === obj then
390
- if template.label.nil? then
391
- logger.debug { "Work around caTissue label bug by setting the #{obj.qp} update template #{template.qp} label to a unique value." }
392
- template.label = Uniquifier.qualifier
393
- end
394
- elsif obj.identifier.nil? and CaTissue::ExternalIdentifier === obj then
395
- # application service save
396
- result = submit_save_template(obj, template)
397
- # if app service is not broken, then sync the result and return
398
- if obj.identifier then
399
- sync_saved(obj, result)
400
- return
570
+ # if template.identifier.nil? and template.label.nil? then
571
+ # logger.debug { "Work around caTissue label bug by setting the #{obj.qp} create template #{template.qp} label to a unique value." }
572
+ # template.label = Jinx::Uniquifier.qualifier
573
+ # end
574
+ if obj.position and obj.position.identifier then
575
+ add_position_to_specimen_template(obj, template)
401
576
  end
402
- logger.debug { "Work around caTissue ExternalIdentifier create bug by updating the bogus caTissue auto-generated empty #{obj.specimen} EID directly with SQL..." }
403
- # app service is broken; fetch the identifier and set directly via SQL
404
- tmpl = obj.class.new
405
- tmpl.setSpecimen(obj.specimen)
406
- eids = query(tmpl).select { |eid| eid.name.nil? }
407
- if eids.size > 1 then
408
- raise DatabaseError.new("#{spc} has more than external identifier without a name: #{eids}")
577
+ # Anticipate the caTissue disposed Specimen update side-effect by removing
578
+ # the consent tier statuses.
579
+ if obj.disposed? then
580
+ unless obj.consent_tier_statuses.empty? then
581
+ obj.consent_tier_statuses.clear
582
+ template.consent_tier_statuses.clear
583
+ logger.debug { "Anticipated a caTissue side-effect by clearing the disposed #{obj.qp} consent tier statuses prior to save." }
584
+ end
409
585
  end
410
- # Set the identifier.
411
- obj.identifier = eids.first.identifier
412
- # Call the SQL
413
- @executor.execute { |dbh| dbh.do(UPD_EID_SQL, obj.name, obj.value, obj.specimen.identifier, obj.identifier) }
414
- logger.debug { "caTissue #{obj} create work-around completed." }
415
- return
586
+ # TODO - is there a test case for this? Isn't EID create delegated to
587
+ # specimen create, which cascades to the EID?
588
+ # elsif obj.identifier.nil? and CaTissue::ExternalIdentifier === obj then
589
+ # # application service save
590
+ # result = submit_save_template(obj, template)
591
+ # # if app service is not broken, then sync the result and return
592
+ # if obj.identifier then
593
+ # sync_saved(obj, result)
594
+ # return
595
+ # end
596
+ # logger.debug { "Work around caTissue ExternalIdentifier create bug by updating the phantom caTissue auto-generated empty #{obj.specimen} EID directly with SQL..." }
597
+ # # app service is broken; fetch the identifier and set directly via SQL
598
+ # tmpl = obj.class.new
599
+ # tmpl.setSpecimen(obj.specimen)
600
+ # eids = query(tmpl).select { |eid| eid.name.nil? }
601
+ # if eids.size > 1 then
602
+ # Jinx.fail(DatabaseError, "#{spc} has more than external identifier without a name: #{eids}")
603
+ # end
604
+ # # Set the identifier.
605
+ # obj.identifier = eids.first.identifier
606
+ # # Call the SQL
607
+ # @executor.transact(UPD_EID_SQL, obj.name, obj.value, obj.specimen.identifier, obj.identifier)
608
+ # logger.debug { "caTissue #{obj} create work-around completed." }
609
+ # return
416
610
  elsif obj.identifier and CaTissue::SpecimenEventParameters === obj then
417
- # TODO - this case occurs in the simple_test migration; fix it there and remove this check
611
+ # TODO - this case occurs in the simple_test migration; fix it there and remove this check.
418
612
  # TODO - KLUDGE!!!! FIX AT SOURCE AND REMOVE SEP KLUDGE AS WELL!!!!
419
- # fix this with an auto-gen add_defaults
613
+ # Fix this with an auto-gen add_defaults?
420
614
  if template.user.nil? then template.user = query(template, :user).first end
421
615
  if template.specimen.nil? and template.specimen_collection_group.nil? then
422
616
  template.specimen = query(template, :specimen).first
@@ -425,45 +619,142 @@ module CaTissue
425
619
  elsif obj.identifier and CaTissue::SpecimenCollectionGroup === obj then
426
620
  # add the extraneous SCG template CPR protocol and PPI, if necessary
427
621
  cpr = obj.collection_protocol_registration
428
- if cpr.nil? then raise ValidationError.new("#{obj} cannot be updated since it is missing a CPR") end
622
+ if cpr.nil? then Jinx.fail(Jinx::ValidationError, "#{obj} cannot be updated since it is missing a CPR") end
429
623
  tcpr = template.collection_protocol_registration
430
- if tcpr.nil? then raise ValidationError.new("#{obj} CPR #{cpr} was not copied to the update template #{tcpr}") end
624
+ if tcpr.nil? then Jinx.fail(Jinx::ValidationError, "#{obj} CPR #{cpr} was not copied to the update template #{tcpr}") end
431
625
  if tcpr.collection_protocol.nil? then
432
626
  pcl = lazy_loader.enable { cpr.collection_protocol }
433
- if pcl.nil? then raise ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} protocol") end
627
+ if pcl.nil? then Jinx.fail(Jinx::ValidationError, "#{obj} cannot be updated since it is missing a referenced CPR #{cpr} protocol") end
434
628
  tpcl = pcl.copy(:identifier)
435
629
  logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} protocol #{tpcl}..." }
436
630
  tmpl.collection_protocol = tpcl
437
631
  end
438
632
  if tcpr.protocol_participant_identifier.nil? then
439
633
  ppi = lazy_loader.enable { cpr.protocol_participant_identifier }
440
- if ppi.nil? then raise ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} PPI") end
634
+ if ppi.nil? then
635
+ Jinx.fail(Jinx::ValidationError, "#{obj} cannot be updated since it is missing a referenced CPR #{cpr} PPI required to work around a caTissue SCG update bug")
636
+ end
441
637
  tppi = ppi.copy(:identifier)
442
638
  logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} PPI #{tppi}..." }
443
639
  tmpl.protocol_participant_identifier = tppi
444
640
  end
445
- #
446
- # TODO - why aren't obj SEPs below added with add_defaults_autogenerated in writer.rb?
447
- #
448
- unless template.received? then
449
- if obj.received? then raise DatabaseError.new("#{obj} is received, but the generated template #{template} is not.") end
641
+ unless obj.received? then
450
642
  rep = obj.instance_eval { create_default_received_event_parameters }
451
- if rep.nil? then raise CaRuby::DatabaseError.new("Default received event parameters were not added to #{obj}.") end
643
+ if rep.nil? then Jinx.fail(CaRuby::DatabaseError, "Default received event parameters were not added to #{obj}.") end
452
644
  rep.copy.merge_attributes(:user => rep.user, :specimen_collection_group => template)
453
645
  end
454
- unless template.collected? then
646
+ unless obj.collected? then
455
647
  cep = obj.instance_eval { create_default_collection_event_parameters }
456
- if cep.nil? then raise CaRuby::DatabaseError.new("Default collection event parameters were not added to #{obj}.") end
648
+ if cep.nil? then Jinx.fail(CaRuby::DatabaseError, "Default collection event parameters were not added to #{obj}.") end
457
649
  cep.copy.merge_attributes(:user => cep.user, :specimen_collection_group => template)
458
650
  end
459
651
  elsif Annotation::Proxy === obj then
460
- raise CaRuby::DatabaseError.new("Annotation proxy direct database save is not supported: #{obj}")
652
+ Jinx.fail(CaRuby::DatabaseError, "Annotation proxy direct database save is not supported: #{obj}")
461
653
  elsif Annotation === obj and obj.class.primary? then
462
654
  copy_annotation_proxy_owner_to_template(obj, template)
463
655
  end
464
656
 
465
- # delegate to standard save
657
+ # Work around a caTissue bug by removing CollectibleEventParameters.
658
+ ceps = strip_collectible_event_parameters(obj, template) if Collectible === obj
659
+
660
+ # delegate to the standard save
466
661
  super
662
+
663
+ # post-process the deferred CEPs
664
+ if ceps and not ceps.empty? then
665
+ # the owner => target CEP hash
666
+ hash = LazyHash.new { Array.new }
667
+ ceps.each { |cep| hash[cep.owner] << cep }
668
+ hash.each do |owner, teps|
669
+ logger.debug { "Refetch the #{owner} event parameters to work around a caTissue bug..." }
670
+ fetched = fetch_association(owner, :specimen_event_parameters)
671
+ teps.each do |tep|
672
+ match = fetched.detect { |fep| tep.class === fep }
673
+ if match then
674
+ logger.debug { "Matched the #{owner} event parameter #{tep} to the fetched #{fep}." }
675
+ tep.merge(fep)
676
+ else
677
+ logger.debug { "#{owner} event parameter #{tep} does not match a fetched event parameters object." }
678
+ end
679
+ end
680
+ end
681
+ end
682
+ end
683
+
684
+ # Removes the unsaved {CollectibleEventParameters} from the given template to work around the
685
+ # caTissue bug described in {#save_with_template}.
686
+ #
687
+ # The CollectibleEventParameters are required if and only if one of the following is true:
688
+ # * the operation is a SCG save and the collected status is not pending
689
+ # * the operation is an update to a previously collected Specimen
690
+ # In all other cases, the CollectibleEventParameters are removed.
691
+ #
692
+ # This method is applied recursively to Specimen children.
693
+ #
694
+ # @param [Collectible] the Specimen or SCG template
695
+ # @return [<CollectibleEventParameters>] the removed event parameters
696
+ def strip_collectible_event_parameters(obj, template)
697
+ if obj.collected? then
698
+ return if CaTissue::SpecimenCollectionGroup === obj
699
+ if obj.identifier then
700
+ if obj.changed?(:collection_status) then
701
+ fseps = fetch_association(obj, :specimen_event_parameters)
702
+ obj.collectible_event_parameters.each do |cep|
703
+ fcep = fseps.detect { |fsep| cep.class === fsep }
704
+ cep.merge(fcep) if fcep
705
+ end
706
+ template.collectible_event_parameters.each do |cep|
707
+ fcep = fseps.detect { |fsep| cep.class === fsep }
708
+ cep.merge(fcep) if fcep
709
+ end
710
+ end
711
+ return
712
+ end
713
+ end
714
+ ceps = template.specimen_event_parameters.select { |ep| CollectibleEventParameters === ep }
715
+ unless ceps.empty? then
716
+ ceps.each { |cep| template.specimen_event_parameters.delete(cep) }
717
+ logger.debug { "Worked around caTissue bug by stripping the following collectible event parameters from the #{template} template: #{ceps.pp_s}." }
718
+ end
719
+ if CaTissue::Specimen === template then
720
+ obj.children.each do |spc|
721
+ tmpl = spc.match_in(template.children)
722
+ ceps.concat(strip_collectible_event_parameters(spc, tmpl)) if tmpl
723
+ end
724
+ end
725
+ ceps
726
+ end
727
+
728
+ #
729
+ # @quirk caTissue When caTissue updates a pending Specimen to status Complete, an auto-generated
730
+ # collection and received event parameters is added to the Specimen even if the Specimen already
731
+ # has a collection and received event parameters. The redundant event parameters corrupts the
732
+ # database content and sporadically results in a GUI Severe Server Error with no trace-back.
733
+ # Work around this bug by deleting the extraneous CollectibleEventParameters.
734
+ # Hammer the database directly to back out this insidious caTissue-generated corruption.
735
+ def submit_save_template(obj, template)
736
+ result = super
737
+
738
+ if CaTissue::Specimen === obj and obj.identifier and obj.collected? and obj.changed?(:collection_status) then
739
+ fseps = query(obj.copy(:identifier), :specimen_event_parameters)
740
+ obj.collectible_event_parameters.each do |cep|
741
+ next if cep.identifier.nil?
742
+ bogus = fseps.detect { |fsep| cep.class === fsep and cep.identifier != fsep.identifier } || next
743
+ logger.debug { "Work around caTissue event parameters auto-corruption bug by deleting the bogus #{bogus}..." }
744
+ table = case bogus.class.name.demodulize
745
+ when 'CollectionEventParameters' then 'catissue_coll_event_param'
746
+ when 'ReceivedEventParameters' then 'catissue_received_event_param'
747
+ else Jinx.fail(CaRuby::DatabaseError, "Collectible event parameter class not recognized: #{bogus.class}")
748
+ end
749
+ sql = "delete from #{table} where identifier = ?"
750
+ executor.transact(sql, bogus.identifier)
751
+ sql = "delete from catissue_specimen_event_param where identifier = ?"
752
+ executor.transact(sql, bogus.identifier)
753
+ logger.debug { "Worked around caTissue event parameters auto-corruption bug by deleting the bogus #{bogus}." }
754
+ end
755
+ end
756
+
757
+ result
467
758
  end
468
759
 
469
760
  # The annotation proxy is not copied because the attribute redirects to the hook rather
@@ -473,51 +764,116 @@ module CaTissue
473
764
  # @param [Annotation] obj the copy source
474
765
  # @param [Annotation] template the copy target
475
766
  def copy_annotation_proxy_owner_to_template(obj, template)
476
- attr_md = obj.class.proxy_attribute_metadata
767
+ prop = obj.class.proxy_property
477
768
  # Ignore the proxy attribute if it is defined by caRuby rather than caTissue.
478
- return unless attr_md.java_property?
479
- rdr, wtr = attr_md.property_accessors
769
+ return unless prop and prop.java_property?
770
+ rdr, wtr = prop.property_accessors
480
771
  pxy = obj.send(rdr)
481
772
  logger.debug { "Setting #{obj.qp} template #{template.qp} proxy owner to #{pxy}..." }
482
773
  template.send(wtr, pxy)
483
774
  end
484
775
 
485
- # Augment {CaRuby::Database::Writer#create_object} to work around caTissue bugs.
776
+ # Augment +CaRuby::Database::Writer.create_object+ to work around caTissue bugs and pass through
777
+ # an {Annotation::Proxy} to the referenced annotations.
778
+ #
486
779
  # @quirk caTissue Bug #124: SCG SpecimenEventParameters save fails validation.
487
780
  # Work-around is to create the SEP by updating the SCG.
488
- # @quirk If obj is a CaTissue::Specimen with the is_available flag set to false, then work around the bug
489
- # described in {#create_unavailable_specimen}.
490
- # @quirk caTissue Bug #161: Specimen API disposal not reflected in result activity status.
781
+ #
782
+ # @quirk caTissue If the save argument domain object is a CaTissue::Specimen with the +is_available+
783
+ # flag set to false, then work around the bug described in {#create_unavailable_specimen}.
784
+ #
785
+ # @quirk caTissue Bug #161: Specimen API disposal is not reflected in the saved result activity status.
491
786
  # DisposalEventParameters create sets the owner Specimen activity_status to +Closed+ as a side-effect.
492
787
  # Reflect this side-effect in the submitted DisposalEventParameters owner Specimen object.
493
- # Pass through an {Annotation::Proxy} to the referenced annotations.
788
+ #
789
+ # @quirk caTissue 1.2 An undocumented caTissue "feature" is that Specimen API disposal clears the
790
+ # Specimen consent tier statuses as a side-effect. Reflect this side-effect in the submitted
791
+ # DisposalEventParameters owner Specimen object.
792
+ #
793
+ # @quirk caTissue 1.2 Creating a specimen aliquot without a label or barcode resuts in a server
794
+ # uniqueness constraint failure SQL error. Work-around is to generate the aliquot and barcode
795
+ # on the fly.
796
+ #
797
+ # @quirk caTissue caTissue API create with an Address argument completes without an exception but
798
+ # silently ignores the argument and does not create the record. The create call has a result
799
+ # that is a copy of the argument, including the missing identifier.
800
+ # Unlike the {Database} {#create} method, this private {#create_object} method must allow an Address
801
+ # object in order to support the user address update caTissue bug work-around. However, the address
802
+ # update bug work-around encounters the Address create missing id caTissue bug. The
803
+ # work-around for this caTissue bug work-around bug is to bypass the caTissue API, hit the database
804
+ # directly with SQL, find the matching database record, and set the identifier to the matching record
805
+ # identifier. The match is complicated by the possibility that a different client might create an
806
+ # address after the SQL transaction but before the max id query. The work-around for this potential
807
+ # caTissue bug work-around bug work-around bug is to fetch addresses until one matches, then set the
808
+ # created address identifier to that fetched record identifier.
494
809
  #
495
810
  # @param [Resource] obj the dependent domain object to save
496
811
  def create_object(obj)
497
- if collectible_event_parameters?(obj) then
498
- save_collectible_event_parameters(obj)
812
+ if CaTissue::Address === obj then
813
+ return create_address(obj)
814
+ elsif CollectibleEventParameters === obj and obj.specimen_collection_group then
815
+ return save_collectible_scg_event_parameters(obj)
499
816
  elsif CaTissue::Specimen === obj then
500
817
  obj.add_defaults
501
- if obj.is_available == false or obj.available_quantity.zero? then
502
- # Note that the obj.is_available == false test is required as opposed to obj.is_available?,
503
- # since a nil is_available flag does not imply an unavailable specimen.
504
- return create_unavailable_specimen(obj) { super }
818
+ # Work around aliquot bug
819
+ if obj.parent and obj.characteristics == obj.parent.characteristics then
820
+ if obj.label.nil? then
821
+ obj.label = obj.barcode = Jinx::Uniquifier.qualifier
822
+ logger.debug { "Worked around caTissue 1.2 bug by creating #{obj} aliquot label and barcode." }
823
+ end
824
+ obj.barcode = obj.label if obj.barcode.nil?
825
+ end
826
+ # Special case for an unavailable specimen.
827
+ # The obj.is_available == false test is required as opposed to obj.is_available?,
828
+ # since a nil is_available flag does not imply an unavailable specimen.
829
+ if obj.is_available == false or obj.available_quantity.zero? or obj.disposed? then
830
+ return create_unavailable_specimen(obj)
505
831
  end
506
832
  end
507
833
 
508
834
  # standard create
509
835
  super
510
-
836
+
511
837
  # replicate caTissue create side-effects in the submitted object
512
838
  if CaTissue::DisposalEventParameters === obj then
513
839
  obj.specimen.activity_status = 'Closed'
514
840
  logger.debug { "Set the created DisposalEventParameters #{obj.qp} owner #{obj.specimen.qp} activity status to Closed." }
841
+ unless obj.specimen.consent_tier_statuses.empty? then
842
+ obj.specimen.consent_tier_statuses.clear
843
+ logger.debug { "Cleared the created DisposalEventParameters #{obj.qp} owner #{obj.specimen.qp} consent tier statuses." }
844
+ end
515
845
  end
516
846
 
517
847
  obj
518
848
  end
519
849
 
520
- # Overrides {CaRuby::Database#create_from_template} as follows:
850
+ # The Address attribute => column map for those attributes whose column differs from the attribute name.
851
+ ADDRESS_ATTR_COL_HASH = { :zip_code => :zipcode }
852
+
853
+ # @param [CaTissue::Address] address the address to create
854
+ # @return [CaTissue::Address] the created address
855
+ # @raise [CaRuby::DatabaseError] if the address could not be created
856
+ # @see #create_object
857
+ def create_address(address)
858
+ vh = address.value_hash
859
+ cols = vh.keys.map { |pa| ADDRESS_ATTR_COL_HASH[pa] or pa }
860
+ sql = "insert into catissue_address ( #{cols.join(', ')} )\nvalues ( #{Array.new(vh.size).fill('?').join(', ')} )"
861
+ logger.debug { "Bypass the broken caTissue API address create and hit the database directly." }
862
+ next_addr_id = executor.query(MAX_ADDR_ID_SQL).first.first
863
+ executor.transact(sql, *vh.values)
864
+ while fetched = CaTissue::Address.new(:identifier => next_addr_id).find do
865
+ if vh.all? { |pa, v| fetched.send(pa) == v } then
866
+ address.identifier = fetched.identifier
867
+ address.take_snapshot
868
+ logger.debug { "Created #{address.qp}." }
869
+ return address
870
+ end
871
+ next_addr_id += 1
872
+ end
873
+ Jinx.fail(CaRuby::DatabaseError, "No address found which matches the created #{address}")
874
+ end
875
+
876
+ # Overrides +CaRuby::Database.create_from_template+ as follows:
521
877
  # * Surrogate {Annotation::Proxy} is "created" by setting the identifier to its hook owner.
522
878
  # The create operation then creates referenced uncreated dependents.
523
879
  #
@@ -526,7 +882,7 @@ module CaTissue
526
882
  if Annotation::Proxy === obj then
527
883
  hook = obj.hook
528
884
  if hook.identifier.nil? then
529
- raise CaRuby::DatabaseError.new("Annotation proxy #{obj.qp} hook owner #{hook.qp} does not have an identifier")
885
+ Jinx.fail(CaRuby::DatabaseError, "Annotation proxy #{obj.qp} hook owner #{hook.qp} does not have an identifier")
530
886
  end
531
887
  obj.identifier = hook.identifier
532
888
  obj.take_snapshot
@@ -543,51 +899,54 @@ module CaTissue
543
899
  # Creates the given specimen by working around the following bug:
544
900
  #
545
901
  # @quirk caTissue Bug #160: Missing Is Available? validation.
546
- # Cannot create a Specimen with any of the following conditions:
547
- # * zero available_quantity
548
- # * is_available flag set to false
549
- # * activity_status is +Closed+
550
- #
551
- # The work-around is to set the flags to true and +Active+, resp., set the quantities
552
- # to a non-zero value, create the Specimen and then update the created Specimen with
553
- # the original values.
554
- #
555
- # If spc has a disposal event, then this work-around interacts with the {#save_changed_dependents}
556
- # work-around as follows:
557
- # * delete that event from the Specimen.
558
- # * Create the Specimen as described above.
559
- # * Update the Specimen as described above, but do no set the activity_status to +Closed+.
560
- # * Create the pending disposal event.
902
+ # Cannot create a Specimen with any of the following conditions:
903
+ # * zero available_quantity
904
+ # * is_available flag set to false
905
+ # * activity_status is +Closed+
906
+ #
907
+ # The work-around is to set the flags to true and +Active+, resp., set the quantities
908
+ # to a non-zero value, create the Specimen and then update the created Specimen with
909
+ # the original values.
910
+ #
911
+ # If the specimen has a disposal event, then this work-around interacts with the
912
+ # {#save_changed_dependents} work-around as follows:
913
+ # * Delete that event from the Specimen.
914
+ # * Create the Specimen as described above.
915
+ # * Update the Specimen as described above, but do not set the activity_status to +Closed+.
916
+ # * Create the pending disposal event.
561
917
  #
562
918
  # @param [CaTissue::Specimen] specimen the specimen to create
563
919
  def create_unavailable_specimen(specimen)
564
920
  logger.debug { "Resetting #{specimen} quantities and available flag temporarily to work around caTissue Bug #160..." }
565
921
  specimen.is_available = true
922
+ # Capture the intended initial quantity and status.
566
923
  oiqty = specimen.initial_quantity
567
- oaqty = specimen.available_quantity
568
924
  ostatus = specimen.activity_status
925
+ # Reset the quantities and status to values which caTissue will accept.
569
926
  specimen.initial_quantity = 1.0
570
927
  specimen.available_quantity = 1.0
571
928
  specimen.activity_status = 'Active'
572
929
  # Cannot reset a disposed Specimen quantity, so postpone disposal until
573
930
  # quantities are reset.
574
931
  dsp = specimen.specimen_events.detect { |sep| CaTissue::DisposalEventParameters === sep }
575
- if dsp then
576
- specimen.specimen_events.delete(dsp)
577
- end
932
+ if dsp then specimen.specimen_events.delete(dsp) end
578
933
 
579
- # delegate to standard create
580
- yield
934
+ # Delegate to the standard create.
935
+ self.class.superclass.instance_method(:create_object).bind(self).call(specimen)
581
936
 
582
- logger.debug { "Reupdating created #{specimen} with initial quantity and available flag set back to original values to complete caTissue Bug #160 work-around..." }
937
+ logger.debug { "Complete the caTissue Bug #160 work-around by reupdating the created #{specimen} with the initial quantity set back to the original value..." }
938
+ # Restore the available flag and initial quantity.
583
939
  specimen.is_available = false
584
940
  specimen.initial_quantity = oiqty
585
- # the available quantity is always zero, since the available flag is set to false
941
+ # The available quantity is always zero, since the available flag is set to false.
586
942
  specimen.available_quantity = 0.0
587
943
  # Leave status Active if there is a disposal event, since quantities cannot be reset
588
944
  # on a closed Specimen and creating the disposal event below will close the Specimen.
589
945
  specimen.activity_status = ostatus unless dsp
590
- update(specimen)
946
+ # Update directly without a cyclic operation check, since update(specimen) of a
947
+ # derived specimen delegates to the parent, which in turn might be the outer
948
+ # save context.
949
+ update_from_template(specimen)
591
950
 
592
951
  # Finally, create the disposal event if one is pending.
593
952
  if dsp then
@@ -598,8 +957,23 @@ module CaTissue
598
957
  logger.debug { "#{specimen} caTissue Bug #160 work-around completed." }
599
958
  specimen
600
959
  end
960
+
961
+ # Overrides +CaRuby::Database::Persistifier.detoxify+ to work around the
962
+ # caTissue bugs described in {CaTissue::Specimen.remove_phantom_external_identifier}
963
+ # and {CaTissue::Participant.remove_phantom_medical_identifier}.
964
+ def detoxify(toxic)
965
+ if toxic.collection? then
966
+ case toxic.first
967
+ when CaTissue::ExternalIdentifier then
968
+ CaTissue::Specimen.remove_phantom_external_identifier(toxic)
969
+ when CaTissue::ParticipantMedicalIdentifier then
970
+ CaTissue::Participant.remove_phantom_medical_identifier(toxic)
971
+ end
972
+ end
973
+ super
974
+ end
601
975
 
602
- # Overrides {CaRuby::Database::Reader#fetch_object} to circumvent {Annotation} fetch, since an annotation
976
+ # Overrides +CaRuby::Database::Reader.fetch_object} to circumvent {Annotation+ fetch, since an annotation
603
977
  # does not have a key.
604
978
  def fetch_object(obj)
605
979
  super or fetch_alternative(obj)
@@ -635,24 +1009,10 @@ module CaTissue
635
1009
  # Annotations are created following the owner create.
636
1010
  #
637
1011
  # @param obj (see #create_object)
638
- # @param [Attribute] attr_md the candidate attribute metadata
1012
+ # @param [CaRuby::Property] prop the candidate attribute metadata
639
1013
  # @return [Boolean] whether the attribute should not be included in the create template
640
- def exclude_pending_create_attribute?(obj, attr_md)
641
- attr_md.type < Annotation or super
642
- end
643
-
644
- # Override {CaRuby::Database#query_safe} to work around the following +caTissue+ bugs:
645
- # * @quirk caTissue Specimen auto-generates blank ExternalIdentifier.
646
- # cf. https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=436&sid=ef98f502fc0ab242781b7759a0eaff36
647
- # * @quirk caTissue Specimen auto-generates blank PMI.
648
- def query_safe(obj_or_hql, *path)
649
- if path.last == :external_identifiers then
650
- CaTissue::Specimen.remove_empty_external_identifier(super)
651
- elsif path.last == :participant_medical_identifiers then
652
- CaTissue::Specimen.remove_empty_medical_identifier(super)
653
- else
654
- super
655
- end
1014
+ def exclude_pending_create_attribute?(obj, prop)
1015
+ prop.type < Annotation or super
656
1016
  end
657
1017
 
658
1018
  # @quirk caTissue Bug #147: SpecimenRequirement query ignores CPE.
@@ -687,8 +1047,8 @@ module CaTissue
687
1047
  # @return [Boolean] whether the given attribute is a reference from an {Annotatable} to a #{Annotation::Proxy}
688
1048
  def hook_proxy_attribute?(obj, attribute)
689
1049
  return false if attribute.nil?
690
- attr_md = obj.class.attribute_metadata(attribute)
691
- attr_md.declarer < Annotatable and attr_md.type < Annotation::Proxy
1050
+ prop = obj.class.property(attribute)
1051
+ prop.declarer < Annotatable and prop.type < Annotation::Proxy
692
1052
  end
693
1053
 
694
1054
  # Queries on the given object attribute using the {Annotation::IntegrationService}.
@@ -722,7 +1082,7 @@ module CaTissue
722
1082
  annotator.integrator.find(proxy)
723
1083
  end
724
1084
 
725
- # @quirk caCORE Override {CaRuby::Database::Reader#invertible_query?} to enable the Bug #147 work
1085
+ # @quirk caCORE Override +CaRuby::Database::Reader.invertible_query?+ to enable the Bug #147 work
726
1086
  # around in {#query_object}. Invertible queries are performed to work around Bug #79. However, this
727
1087
  # work-around induces Bug #147, so we disable the Bug #79 work-around here for the special case of
728
1088
  # a CPE in order to enable the Bug #147 work-around. And so it goes....
@@ -788,4 +1148,4 @@ module CaTissue
788
1148
  attribute ? query(cpe, :specimen_requirements, attribute) : query(cpe, :specimen_requirements)
789
1149
  end
790
1150
  end
791
- end
1151
+ end