caruby-tissue 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +44 -0
  5. data/bin/crtdump +31 -0
  6. data/bin/crtexample +18 -0
  7. data/bin/crtextract +47 -0
  8. data/bin/crtmigrate +17 -0
  9. data/bin/crtsmoke +27 -0
  10. data/examples/galena/README.md +53 -0
  11. data/examples/galena/bin/migrate.rb +42 -0
  12. data/examples/galena/bin/seed.rb +43 -0
  13. data/examples/galena/conf/extract/simple_fields.yaml +4 -0
  14. data/examples/galena/conf/migration/filter_fields.yaml +7 -0
  15. data/examples/galena/conf/migration/filter_migration.yaml +9 -0
  16. data/examples/galena/conf/migration/frozen_fields.yaml +11 -0
  17. data/examples/galena/conf/migration/frozen_migration.yaml +9 -0
  18. data/examples/galena/conf/migration/general_fields.yaml +42 -0
  19. data/examples/galena/conf/migration/general_migration.yaml +9 -0
  20. data/examples/galena/conf/migration/simple_fields.yaml +30 -0
  21. data/examples/galena/conf/migration/simple_migration.yaml +7 -0
  22. data/examples/galena/conf/migration/small_fields.yaml +24 -0
  23. data/examples/galena/conf/migration/small_migration.yaml +9 -0
  24. data/examples/galena/data/filter.csv +1 -0
  25. data/examples/galena/data/frozen.csv +1 -0
  26. data/examples/galena/data/general.csv +1 -0
  27. data/examples/galena/data/minimal.csv +1 -0
  28. data/examples/galena/data/simple.csv +1 -0
  29. data/examples/galena/data/small.csv +1 -0
  30. data/examples/galena/doc/CaTissue.html +93 -0
  31. data/examples/galena/doc/CaTissue/CollectionProtocolRegistration.html +181 -0
  32. data/examples/galena/doc/CaTissue/Participant.html +241 -0
  33. data/examples/galena/doc/CaTissue/SpecimenCollectionGroup.html +190 -0
  34. data/examples/galena/doc/CaTissue/StorageContainer.html +179 -0
  35. data/examples/galena/doc/CaTissue/TissueSpecimen.html +320 -0
  36. data/examples/galena/doc/Galena.html +290 -0
  37. data/examples/galena/doc/Galena/Seed.html +203 -0
  38. data/examples/galena/doc/Galena/Seed/Defaults.html +646 -0
  39. data/examples/galena/doc/_index.html +188 -0
  40. data/examples/galena/doc/class_list.html +36 -0
  41. data/examples/galena/doc/css/common.css +1 -0
  42. data/examples/galena/doc/css/full_list.css +53 -0
  43. data/examples/galena/doc/css/style.css +307 -0
  44. data/examples/galena/doc/file.README.html +108 -0
  45. data/examples/galena/doc/file_list.html +38 -0
  46. data/examples/galena/doc/frames.html +13 -0
  47. data/examples/galena/doc/index.html +108 -0
  48. data/examples/galena/doc/js/app.js +202 -0
  49. data/examples/galena/doc/js/full_list.js +149 -0
  50. data/examples/galena/doc/js/jquery.js +154 -0
  51. data/examples/galena/doc/method_list.html +179 -0
  52. data/examples/galena/doc/top-level-namespace.html +112 -0
  53. data/examples/galena/lib/README.html +33 -0
  54. data/examples/galena/lib/galena.rb +8 -0
  55. data/examples/galena/lib/galena/cli/seed.rb +43 -0
  56. data/examples/galena/lib/galena/migration/filter_shims.rb +43 -0
  57. data/examples/galena/lib/galena/migration/frozen_shims.rb +54 -0
  58. data/examples/galena/lib/galena/seed/defaults.rb +97 -0
  59. data/lib/catissue.rb +26 -0
  60. data/lib/catissue/cli/command.rb +51 -0
  61. data/lib/catissue/cli/example.rb +31 -0
  62. data/lib/catissue/cli/migrate.rb +60 -0
  63. data/lib/catissue/cli/smoke.rb +45 -0
  64. data/lib/catissue/database.rb +451 -0
  65. data/lib/catissue/database/annotation/annotatable_service.rb +25 -0
  66. data/lib/catissue/database/annotation/annotation_service.rb +79 -0
  67. data/lib/catissue/database/annotation/annotator.rb +84 -0
  68. data/lib/catissue/database/annotation/entity_manager.rb +10 -0
  69. data/lib/catissue/database/annotation/integration_service.rb +87 -0
  70. data/lib/catissue/database/controlled_value_finder.rb +43 -0
  71. data/lib/catissue/database/controlled_values.rb +162 -0
  72. data/lib/catissue/domain/abstract_domain_object.rb +8 -0
  73. data/lib/catissue/domain/abstract_position.rb +22 -0
  74. data/lib/catissue/domain/abstract_specimen.rb +288 -0
  75. data/lib/catissue/domain/abstract_specimen_collection_group.rb +25 -0
  76. data/lib/catissue/domain/address.rb +13 -0
  77. data/lib/catissue/domain/cancer_research_group.rb +11 -0
  78. data/lib/catissue/domain/capacity.rb +34 -0
  79. data/lib/catissue/domain/check_in_check_out_event_parameter.rb +19 -0
  80. data/lib/catissue/domain/collection_event_parameters.rb +13 -0
  81. data/lib/catissue/domain/collection_protocol.rb +177 -0
  82. data/lib/catissue/domain/collection_protocol_event.rb +108 -0
  83. data/lib/catissue/domain/collection_protocol_registration.rb +108 -0
  84. data/lib/catissue/domain/consent_tier_response.rb +13 -0
  85. data/lib/catissue/domain/consent_tier_status.rb +29 -0
  86. data/lib/catissue/domain/container.rb +234 -0
  87. data/lib/catissue/domain/container_position.rb +21 -0
  88. data/lib/catissue/domain/container_type.rb +131 -0
  89. data/lib/catissue/domain/department.rb +13 -0
  90. data/lib/catissue/domain/disposal_event_parameters.rb +13 -0
  91. data/lib/catissue/domain/embedded_event_parameters.rb +10 -0
  92. data/lib/catissue/domain/external_identifier.rb +22 -0
  93. data/lib/catissue/domain/frozen_event_parameters.rb +10 -0
  94. data/lib/catissue/domain/institution.rb +13 -0
  95. data/lib/catissue/domain/new_specimen_array_order_item.rb +35 -0
  96. data/lib/catissue/domain/order_details.rb +25 -0
  97. data/lib/catissue/domain/participant.rb +138 -0
  98. data/lib/catissue/domain/participant_medical_identifier.rb +38 -0
  99. data/lib/catissue/domain/password.rb +11 -0
  100. data/lib/catissue/domain/race.rb +11 -0
  101. data/lib/catissue/domain/received_event_parameters.rb +25 -0
  102. data/lib/catissue/domain/scg_event_parameters.rb +11 -0
  103. data/lib/catissue/domain/site.rb +30 -0
  104. data/lib/catissue/domain/specimen.rb +456 -0
  105. data/lib/catissue/domain/specimen_array.rb +47 -0
  106. data/lib/catissue/domain/specimen_array_content.rb +19 -0
  107. data/lib/catissue/domain/specimen_array_type.rb +20 -0
  108. data/lib/catissue/domain/specimen_characteristics.rb +20 -0
  109. data/lib/catissue/domain/specimen_collection_group.rb +412 -0
  110. data/lib/catissue/domain/specimen_event_parameters.rb +111 -0
  111. data/lib/catissue/domain/specimen_position.rb +38 -0
  112. data/lib/catissue/domain/specimen_protocol.rb +34 -0
  113. data/lib/catissue/domain/specimen_requirement.rb +143 -0
  114. data/lib/catissue/domain/storage_container.rb +204 -0
  115. data/lib/catissue/domain/storage_type.rb +82 -0
  116. data/lib/catissue/domain/transfer_event_parameters.rb +53 -0
  117. data/lib/catissue/domain/user.rb +100 -0
  118. data/lib/catissue/extract/command.rb +31 -0
  119. data/lib/catissue/extract/delta.rb +62 -0
  120. data/lib/catissue/extract/extractor.rb +99 -0
  121. data/lib/catissue/migration/migrator.rb +101 -0
  122. data/lib/catissue/migration/shims.rb +108 -0
  123. data/lib/catissue/migration/uniquify.rb +111 -0
  124. data/lib/catissue/resource.rb +84 -0
  125. data/lib/catissue/util/controlled_value.rb +29 -0
  126. data/lib/catissue/util/location.rb +116 -0
  127. data/lib/catissue/util/log.rb +30 -0
  128. data/lib/catissue/util/person.rb +31 -0
  129. data/lib/catissue/util/position.rb +54 -0
  130. data/lib/catissue/util/storable.rb +34 -0
  131. data/lib/catissue/util/storage_type_holder.rb +30 -0
  132. data/lib/catissue/version.rb +7 -0
  133. metadata +212 -0
@@ -0,0 +1,25 @@
1
+ require 'delegate'
2
+ require 'caruby/database/persistence_service'
3
+
4
+ module CaTissue
5
+ module Annotation
6
+ # An AnnotatableService queries and saves domain classes which hold annotation attributes.
7
+ class AnnotatableService < DelegateClass(CaRuby::PersistenceService)
8
+ def initialize(database, app_service, integration_service)
9
+ super(app_service)
10
+ @database = database
11
+ @integration_service = integration_service
12
+ end
13
+ #
14
+ # def create(obj)
15
+ # super
16
+ # # TODO - refactor CaRuby::Database to remove Annotation cases; iterate over each annotation attribute here
17
+ # end
18
+ #
19
+ # def update(obj)
20
+ # super
21
+ # # TODO - refactor CaRuby::Database to remove Annotation cases; iterate over each annotation attribute here
22
+ # end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ require 'caruby/util/inflector'
2
+ require 'caruby/database/persistence_service'
3
+ require 'caruby/util/uniquifier'
4
+
5
+ module CaTissue
6
+ module Annotation
7
+ # An AnnotationService queries and saves CaTissue annotations.
8
+ class AnnotationService < CaRuby::PersistenceService
9
+ # The IdGenerator calls the caTissue entity manager to create a new identifier for
10
+ # an annotation.
11
+ class IdGenerator
12
+ # Creates a new IdGenerator with the given caTissue entity_manager.
13
+ def initialize(entity_manager)
14
+ super()
15
+ @entity_manager = entity_manager
16
+ end
17
+
18
+ # Returns a new identifier for the given annotation object.
19
+ def next_identifier(annotation)
20
+ logger.debug { "Generating identifier for annotation #{annotation}..." }
21
+ id = @entity_manager.get_next_identifier_for_entity(annotation.class.qp).to_i
22
+ # work-around caTissue bug - use a high number that won't conflict with the autogenerated range
23
+ # cf. https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=404&sid=786325c9c46503529a2b64f0b66fed0a
24
+ if id.nil? or id.zero? then
25
+ id = Uniquifier.qualifier
26
+ logger.warn("Entity manager did not create annotation identifier; using id #{id}.")
27
+ end
28
+ id
29
+ end
30
+ end
31
+
32
+ # Creates an AnnotationService for the given CaTissue::Database, service name and options.
33
+ #
34
+ # Supported options include the following:
35
+ # * :anchor - the required anchor class
36
+ # * :integration_service - the required IntegrationService
37
+ # * :entity_manager - the required EntityManager
38
+ # * +version+ - the optional caTissue version identifier
39
+ def initialize(database, service_name, options)
40
+ super(service_name, Options.get(:version, options))
41
+ @database = database
42
+ @anchor_class = Options.get(:anchor, options)
43
+ @integration_service = Options.get(:integration_service, options)
44
+ entity_manager = Options.get(:entity_manager, options)
45
+ @id_generator = IdGenerator.new(entity_manager)
46
+ end
47
+
48
+ # Query the given domain object obj with the given attribute path. The query result is
49
+ # an array of CaRuby::Annotation objects. Overrides {CaRuby::PersistenceService#query} to
50
+ # delegate to the caTissue integration service when the penultimate search class is this
51
+ # AnnotationService anchor class, e.g.:
52
+ # query(CaTissue::SpecimenCollectionGroup.new(:name => 'Biobank_1_5'), :base_pathology_annotation)
53
+ # or
54
+ # query(CaTissue::ReceivedEventParameters.new(:user => user), :specimen_collection_group, :base_pathology_annotation)
55
+ def query(obj, *path)
56
+ # delegate path[0...-1] to database unless path.length < 2
57
+ return @database.query(obj, *path[0...-1]).map { |fetched| query(fetched, path[-1]) }.flatten unless path.length < 2
58
+ return super unless @anchor_class === obj.class
59
+ # fetch the anchor objects if necessary
60
+ return @database.query(obj).map { |fetched| query(fetched, *path) }.flatten if obj.identifier.nil?
61
+ # obj is an anchor with an identifier; make a template
62
+ template = obj.copy(:identifier)
63
+ # delegate to the integration service to find the anchor annotations
64
+ @integration_service.query(template, *path)
65
+ end
66
+
67
+ # Augments the CaRuby::PersistenceService create method to handle caTissue annotation service peculiarities,
68
+ # e.g. assigning an identifier since assignment is not done automatically as is the case with the default
69
+ # application service.
70
+ def create(annotation)
71
+ annotation.identifier ||= @id_generator.next_identifier(annotation)
72
+ super
73
+ owner = annotation.owner
74
+ @integration_service.associate(owner, annotation) if CaRuby::Annotatable === owner
75
+ annotation
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,84 @@
1
+ require 'caruby/util/properties'
2
+ require 'catissue/database/annotation/integration_service'
3
+ require 'catissue/database/annotation/annotatable_service'
4
+ require 'catissue/database/annotation/annotation_service'
5
+ require 'catissue/database/annotation/entity_manager'
6
+
7
+ module CaTissue
8
+ module Annotation
9
+ # An Annotator creates annotation services for annotatable and annotation classes.
10
+ class Annotator
11
+ def initialize(database)
12
+ @database = database
13
+ #the sole DE integration service, used by the annotation services
14
+ @entity_manager = Annotation::EntityManager.instance
15
+ @integration_service = Annotation::IntegrationService.new(@entity_manager)
16
+ @anchor_svc_hash = {}
17
+ @ann_mod_svc_hash = {}
18
+ end
19
+
20
+ # Returns the CaRuby::PersistenceService for the given klass, or nil if klass is neither
21
+ # annotatable nor an annotation.
22
+ def service(klass)
23
+ annotatable_service(klass) or annotation_service(klass)
24
+ end
25
+
26
+ private
27
+
28
+ # Returns the Annotation::AnnotatableService for the given klass, or nil if klass is not annotatable.
29
+ def annotatable_service(klass)
30
+ return @anchor_svc_hash[klass] ||= create_annotatable_service(klass) if JavaImport::AnnotatableClass === klass
31
+ annotatable_service(klass.superclass) if klass.superclass
32
+ end
33
+
34
+ # Returns the Annotation::AnnotatorService for the given klass, or nil if klass is not an annotation.
35
+ def annotation_service(klass)
36
+ return @ann_mod_svc_hash[klass.annotation_module] ||= create_annotation_service(klass.annotation_module) if JavaImport::AnnotationClass === klass
37
+ annotation_service(klass.superclass) if klass.superclass
38
+ end
39
+
40
+ def create_annotatable_service(klass)
41
+ Annotation::AnnotatableService.new(@database, @database.persistence_service, @integration_service)
42
+ end
43
+
44
+ def create_annotation_service(mod)
45
+ Annotation::AnnotationService.new(@database, mod.service, :anchor => mod.anchor_class,
46
+ :integration_service => @integration_service, :entity_manager => @entity_manager)
47
+ end
48
+
49
+ # TODO - delete if obsolete
50
+ # Returns the class => annotator hash from the given database's properties.
51
+ def create_service_hashes(database)
52
+ @anchor_svc_hash = {}
53
+ @pkg_svc_hash = {}
54
+ #the sole DE integration service, used by the annotation services
55
+ integration_service = Annotation::IntegrationService.new
56
+ entity_manager = Annotation::EntityManager.instance
57
+ # the anchor class => { package => { attribute => type } } hash
58
+ anchor_pkg_attrs_hash = CaTissue.access_properties[CaRuby::Domain::Properties::ANNOTATIONS_PROP]
59
+ return {} if anchor_pkg_attrs_hash.nil?
60
+ # the package => service name hash
61
+ pkg_svc_nm_hash = CaTissue.access_properties[CaRuby::Domain::Properties::ANN_SVCS_PROP]
62
+ if pkg_svc_nm_hash.nil? then
63
+ raise CaRuby::ConfigurationError.new("Annotation service property missing: #{CaRuby::Domain::Properties::ANN_SVCS_PROP}")
64
+ end
65
+ # build the anchor => service and package => service hashes
66
+ anchor_service = Annotation::AnnotatableService.new(database, database.persistence_service, integration_service)
67
+ # make an annotatable service for each anchor and an annotator service for each package
68
+ anchor_pkg_attrs_hash.each do |anchor_cls_nm, pkg_signatures_hash|
69
+ anchor_class = CaTissue.const_get(anchor_cls_nm)
70
+ @anchor_svc_hash[anchor_class] = anchor_service
71
+ pkg_signatures_hash.each do |pkg, signatures|
72
+ svc_nm = pkg_svc_nm_hash[pkg]
73
+ if svc_nm.nil? then
74
+ raise CaRuby::DatabaseError.new("Annotation service property value missing for package #{pkg} in property #{CaRuby::Domain::Properties::ANN_SVCS_PROP}")
75
+ end
76
+ @pkg_svc_hash[pkg] = Annotation::AnnotationService.new(database, svc_nm, :anchor => anchor_class,
77
+ :integration_service => integration_service, :entity_manager => entity_manager)
78
+ logger.debug { "Annotator service #{anchor_class.qp} #{svc_nm} created for attributes #{signatures.pp_s(:single_line)}" }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ require 'caruby/import/java'
2
+
3
+ module CaTissue
4
+ module Annotation
5
+ # EntityManager dependencies
6
+ java_import('edu.wustl.common.security.exceptions.UserNotAuthorizedException')
7
+ # EntityManager is the caTissue singleton Winnebago object for persisting annotations.
8
+ java_import('edu.common.dynamicextensions.entitymanager.EntityManager')
9
+ end
10
+ end
@@ -0,0 +1,87 @@
1
+ require 'caruby/database/persistence_service'
2
+
3
+ module CaTissue
4
+ module Annotation
5
+ # An IntegrationService fetches and saves CaTissue anchor-annotation associations.
6
+ class IntegrationService < CaRuby::PersistenceService
7
+ SERVICE_NAME = 'deintegration'
8
+
9
+ java_import('deintegration.EntityMap')
10
+ java_import('deintegration.EntityMapRecord')
11
+
12
+ def initialize(entity_manager)
13
+ super(SERVICE_NAME)
14
+ @entity_manager = entity_manager
15
+ end
16
+
17
+ # Associates the given anchor domain object to annotation.
18
+ def associate(anchor, annotation)
19
+ logger.debug { "Associating annotation #{annotation} to owner #{anchor}..." }
20
+ association = create_entity_map_record(anchor, annotation)
21
+ create(association)
22
+ end
23
+
24
+ # Removes the existing association between the given anchor domain object to annotation.
25
+ def dissociate(anchor, annotation)
26
+ association = create_entity_map_record(anchor, annotation)
27
+ delete(association)
28
+ end
29
+
30
+ private
31
+
32
+ # Flag to work around caTissue and JRuby bugs
33
+ ENTITY_MAP_BUG = true
34
+
35
+ ## The cruft below is adapted from caTissue ClientDemo_SCG.java and cleaned up (but still obscure). ##
36
+
37
+ def create_entity_map_record(anchor, annotation)
38
+ record = EntityMapRecord.new
39
+ raise CaRuby::DatabaseError.new("Annotation entity map static entity does not have an identifier: #{anchor}") if anchor.identifier.nil?
40
+ record.static_entity_record_id = anchor.identifier
41
+ raise CaRuby::DatabaseError.new("Annotation entity map dynamic entity does not have an identifier: #{annotation}") if annotation.identifier.nil?
42
+ record.dynamic_entity_record_id = annotation.identifier
43
+ record.form_context = form_context(anchor, annotation)
44
+ record.form_context_id = record.form_context.identifier if record.form_context
45
+ record
46
+ end
47
+
48
+ # Returns the undocumented caTissue FormContext, which might be another bit of caTissue presentation
49
+ # flotsam polluting the data layer.
50
+ def form_context(anchor, annotation)
51
+ map = EntityMap.new
52
+ map.static_entity_id = anchor_entity_id(anchor)
53
+ return if ENTITY_MAP_BUG
54
+ # 2 bugs:
55
+ # * caTissue bug - bad container id query, cf. https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=421&sid=5252d951301e598eebf3e90036da43cb
56
+ # * jRuby bug? - query on map call to map.pp_s wipes out the map java_class! Can't step into pp_s with debugger so can't isolate cause without more work
57
+ # TODO - isolate and file both bugs
58
+ #map.container_id = entity_container_id(annotation)
59
+ map.form_context_collection.first if query(map)
60
+ end
61
+
62
+ def entity_container_id(annotation)
63
+ entity_id = annotation_entity_id(annotation)
64
+ container_id = @entity_manager.get_container_id_for_entity(entity_id)
65
+ raise CaRuby::DatabaseError.new("Dynamic extension container id not found for annotation #{annotation}") if container_id.nil?
66
+ logger.debug { "Dynamic extension container id for #{annotation}: #{container_id}" } and container_id
67
+ end
68
+
69
+ # Returns the undocumented caTissue entity id for the given anchor entity's Java class name.
70
+ def anchor_entity_id(anchor)
71
+ entity_id_for_class_designator(anchor.class.java_class.name)
72
+ end
73
+
74
+ # Returns the undocumented caTissue entity id for the given annotation entity's demodulized Java class name.
75
+ #
76
+ # caTissue alert - unlike #anchor_entity_id, {#annotation_entity_id} strips the leading package prefix from the annotation
77
+ # class name. caTissue DE API requires this undocumented inconsistency.
78
+ def annotation_entity_id(annotation)
79
+ entity_id_for_class_designator(annotation.class.java_class.name[/[^.]+$/])
80
+ end
81
+
82
+ def entity_id_for_class_designator(designator)
83
+ @entity_manager.get_entity_id(designator) or raise CaRuby::DatabaseError.new("Dynamic extension entity id not found for #{designator}")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,43 @@
1
+ require 'caruby/util/log'
2
+ require 'catissue/database/controlled_values'
3
+
4
+ module CaTissue
5
+ # Finds attribute controlled values.
6
+ class ControlledValueFinder
7
+ # Creates a new ControlledValueFinder for the given attribute.
8
+ # The optional YAML properties file name maps input values to controlled values.
9
+ def initialize(attribute, file=nil)
10
+ @attribute = attribute
11
+ @remap_hash = load_controlled_value_hash(file)
12
+ end
13
+
14
+ # Returns the CV value for the given source value.
15
+ # If the value is remapped, then that value is returned.
16
+ # Otherwise, if the value is a standard CV, then the CV value is returned.
17
+ # Otherwise, a warning message is printed to the log and this method returns nil.
18
+ def controlled_value(value)
19
+ return if value.blank?
20
+ remapped = remapped_controlled_value(value)
21
+ return remapped if remapped
22
+ cv = supported_controlled_value(value)
23
+ logger.warn("#{@attribute} value '#{value}' ignored since it is not a recognized controlled value.") if cv.nil?
24
+ cv.value if cv
25
+ end
26
+
27
+ private
28
+
29
+ def remapped_controlled_value(value)
30
+ @remap_hash[value]
31
+ end
32
+
33
+ def supported_controlled_value(value)
34
+ ControlledValues.instance.find(@attribute, value)
35
+ end
36
+
37
+ def load_controlled_value_hash(file)
38
+ return {} unless file and File.exists?(file)
39
+ logger.debug { "Loading controlled value map for #{@attribute} from #{file}..." }
40
+ YAML::load_file(file)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,162 @@
1
+ require 'singleton'
2
+ require 'caruby/util/log'
3
+ require 'caruby/util/collection'
4
+ require 'caruby/util/options'
5
+ require 'caruby/util/visitor'
6
+ require 'catissue/resource'
7
+ require 'caruby/database/sql_executor'
8
+ require 'catissue/util/controlled_value'
9
+ require 'caruby/util/transitive_closure'
10
+ require 'caruby/domain/properties'
11
+
12
+ module CaTissue
13
+ # This ControlledValues class loads caTissue permissible values from the database.
14
+ # Use of this class requires the +dbi+ gem and the following caTissue database access
15
+ # properties are defined in the
16
+ # home directory .catissue.yaml file:
17
+ # * :database_host - the database host
18
+ # * :database - the database name
19
+ # * :database_user - the database username (not the caTissue login name)
20
+ # * :database_password - the database password (not the caTissue login password)
21
+ #
22
+ # The default :database_host is the application :host property value, which in turn
23
+ # defaults to +localhost+.
24
+ #
25
+ # The optional :database_port property overrides the default MySQL port.
26
+ #
27
+ # ControlledValues is an auxiliary utility class and is not used by the CaTissue Ruby API.
28
+ class ControlledValues
29
+ include Singleton
30
+
31
+ PUBLIC_ID_ROOTS_STMT = "select identifier, value from catissue_permissible_value where public_id = ? and parent_identifier is null or parent_identifier = 0"
32
+
33
+ CHILDREN_STMT = "select identifier, value from catissue_permissible_value where parent_identifier = ?"
34
+
35
+ INSERT_STMT = "insert into catissue_permissible_value (identifier, parent_identifier, public_id, value) values (?, ?, ?, ?)"
36
+
37
+ DELETE_STMT = "delete from catissue_permissible_value where identifier = ?"
38
+
39
+ MAX_ID_STMT = "select max(identifier) from catissue_permissible_value"
40
+
41
+ SEARCH_STMT = "select identifier from catissue_permissible_value where value collate latin1_bin = ?"
42
+
43
+ def initialize
44
+ # the default database name, used for direct database access
45
+ CaTissue.access_properties[:database] ||= CaTissue::Database::DEF_DATABASE_NAME
46
+ @executor = CaRuby::SQLExecutor.new(CaTissue.access_properties)
47
+ @pid_loaded_hash = LazyHash.new { |pid| load_pid_cvs(pid) }
48
+ @pid_value_cv_hash = LazyHash.new do |pid|
49
+ CaseInsensitiveHash.new { |hash, value| hash[value] = load_cv(pid, value) unless value.nil? }
50
+ end
51
+ end
52
+
53
+ # Returns the transitive closure of each CV with the given public id and its children.
54
+ # The CVs are loaded from the database if necessary.
55
+ #
56
+ # The following public id aliases are supported:
57
+ # * :tissue_site
58
+ # * :clinical_diagnosis
59
+ #
60
+ #@param [String,Symbol] public_id_or_alias the caTissue public id or an alias defined above
61
+ # @return [<ControlledValue>] instances for the given public_id_or_alias
62
+ def for_public_id(public_id_or_alias)
63
+ pid = ControlledValue.standard_public_id(public_id_or_alias)
64
+ @pid_loaded_hash[pid].values
65
+ end
66
+
67
+ # Returns the ControlledValue with the given public_id_or_alias and value.
68
+ # Loads the CV if necessary from the database. The loaded CV does not have a parent or children.
69
+ #
70
+ #@param [String,Symbol] public_id_or_alias the supported for_public_id alias
71
+ # @see #for_public_id load the CV hierarchy with supported aliases
72
+ def find(public_id_or_alias, value)
73
+ pid = ControlledValue.standard_public_id(public_id_or_alias)
74
+ @pid_value_cv_hash[pid][value]
75
+ end
76
+
77
+ # Creates a new controlled value record in the database from the given ControlledValue cv.
78
+ # The default identifier is the next identifier in the permissible values table.
79
+ #
80
+ # @param [ControlledValue] cv the controlled value to create
81
+ # @return cv
82
+ def create(cv)
83
+ cv.identifier ||= next_id
84
+ raise ArgumentError.new("Controlled value create is missing a public id") if cv.public_id.nil?
85
+ raise ArgumentError.new("Controlled value create is missing a value") if cv.value.nil?
86
+ logger.debug { "Creating controlled value #{cv} in the database..." }
87
+ @executor.execute { |dbh| dbh.prepare(INSERT_STMT).execute(cv.identifier, cv.parent_identifier, cv.public_id, cv.value) }
88
+ logger.debug { "Controlled value #{cv.public_id} #{cv.value} created with identifier #{cv.identifier}" }
89
+ @pid_value_cv_hash[cv.public_id][cv.value] = cv
90
+ end
91
+
92
+ # Deletes the given ControlledValue record in the database. Recursively deletes the
93
+ # transitive closure of children as well.
94
+ #
95
+ # @param [ControlledValue] cv the controlled value to delete
96
+ def delete(cv)
97
+ @executor.execute { |dbh| delete_recursive(cv, dbh.prepare(DELETE_STMT)) }
98
+ end
99
+
100
+ private
101
+
102
+ def load_pid_cvs(pid)
103
+ fetch_cvs_with_public_id(pid, @pid_value_cv_hash[pid])
104
+ end
105
+
106
+ def load_cv(public_id, value)
107
+ logger.debug { "Loading controlled value #{public_id} #{value} from the database..." }
108
+ row = @executor.execute { |dbh| dbh.select_one(SEARCH_STMT, value) }
109
+ logger.debug { "Controlled value #{public_id} #{value} not found." } and return if row.nil?
110
+ identifier = row[0]
111
+ logger.debug { "Controlled value #{public_id} #{value} loaded with identifier #{identifier}." }
112
+ make_controlled_value(:identifier => identifier, :public_id => public_id, :value => value)
113
+ end
114
+
115
+ def next_id
116
+ @executor.execute { |dbh| dbh.select_one(MAX_ID_STMT)[0].to_i } + 1
117
+ end
118
+
119
+ def delete_recursive(cv, sth)
120
+ raise ArgumentError.new("Controlled value to delete is missing an identifier") if cv.identifier.nil?
121
+ logger.debug { "Deleting controlled value #{cv} from the database..." }
122
+ logger.debug { "Deleting controlled value #{cv} children: #{cv.children.pp_s}..." } unless cv.children.empty?
123
+ cv.children.each { |child| delete_recursive(child, sth) }
124
+ sth.execute(cv.identifier)
125
+ @pid_value_cv_hash[cv.public_id].delete(cv.value)
126
+ logger.debug { "Controlled value #{cv} deleted." }
127
+ end
128
+
129
+ def fetch_cvs_with_public_id(pid, value_cv_hash)
130
+ id_cv_hash = {}
131
+ logger.debug { "Loading #{pid} controlled values from the database..." }
132
+ cvs = []
133
+ @executor.execute do |dbh|
134
+ dbh.select_all(PUBLIC_ID_ROOTS_STMT, pid) do |row|
135
+ identifier, value = row
136
+ cvs << value_cv_hash[value] ||= make_controlled_value(:identifier => identifier, :public_id => pid, :value => value)
137
+ end
138
+ # load the root CVs children
139
+ cvs.each { |cv| fetch_descendants(cv, dbh, value_cv_hash) }
140
+ end
141
+ value_cv_hash
142
+ end
143
+
144
+ def fetch_descendants(cv, dbh, value_cv_hash)
145
+ children = []
146
+ dbh.select_all(CHILDREN_STMT, cv.identifier) do |row|
147
+ identifier, value = row
148
+ children << value_cv_hash[value] = make_controlled_value(:identifier => identifier, :public_id => pid, :parent => cv, :value => value)
149
+ end
150
+ # recurse to chidren
151
+ children.each { |cv| fetch_descendants(cv, dbh, value_cv_hash) }
152
+ end
153
+
154
+ # Returns a new ControlledValue with attributes set by the given attribute => value hash.
155
+ def make_controlled_value(value_hash)
156
+ cv = ControlledValue.new(value_hash[:value], value_hash[:parent])
157
+ cv.identifier = value_hash[:identifier]
158
+ cv.public_id = value_hash[:public_id]
159
+ cv
160
+ end
161
+ end
162
+ end