caruby-tissue 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. data/History.txt +4 -0
  2. data/bin/crtdump +11 -3
  3. data/bin/{seed → crtseed} +0 -0
  4. data/conf/annotation/pathology_scg/AdditionalFinding.hbm.xml +19 -0
  5. data/conf/annotation/pathology_scg/BasePathologyAnnotation.hbm.xml +260 -0
  6. data/conf/annotation/pathology_scg/BreastMargin.hbm.xml +21 -0
  7. data/conf/annotation/pathology_scg/BreastMarginInvolved.hbm.xml +15 -0
  8. data/conf/annotation/pathology_scg/BreastMarginUninvolved.hbm.xml +14 -0
  9. data/conf/annotation/pathology_scg/CNSMargin.hbm.xml +19 -0
  10. data/conf/annotation/pathology_scg/CNSMarginLocation.hbm.xml +14 -0
  11. data/conf/annotation/pathology_scg/CarcinomaInSituStatus.hbm.xml +14 -0
  12. data/conf/annotation/pathology_scg/ColorectalLocalExcisionMarginUninvolved.hbm.xml +15 -0
  13. data/conf/annotation/pathology_scg/ColorectalResectedMarginUninvolved.hbm.xml +22 -0
  14. data/conf/annotation/pathology_scg/Cytogenetics.hbm.xml +15 -0
  15. data/conf/annotation/pathology_scg/DeepMelanomaMargin.hbm.xml +16 -0
  16. data/conf/annotation/pathology_scg/Details.hbm.xml +14 -0
  17. data/conf/annotation/pathology_scg/DirectExtensionOfTumor.hbm.xml +14 -0
  18. data/conf/annotation/pathology_scg/DistalMargin.hbm.xml +15 -0
  19. data/conf/annotation/pathology_scg/DistanceFromAnalVerge.hbm.xml +23 -0
  20. data/conf/annotation/pathology_scg/DistantMetastasis.hbm.xml +19 -0
  21. data/conf/annotation/pathology_scg/ExcionalBiopsyMarginUninvolved.hbm.xml +15 -0
  22. data/conf/annotation/pathology_scg/ExcisionalBiopsyColorectalDeepMargin.hbm.xml +15 -0
  23. data/conf/annotation/pathology_scg/ExcisionalBiopsyColorectalLateralOrMucosalMargin.hbm.xml +15 -0
  24. data/conf/annotation/pathology_scg/ExtraprostaticExtension.hbm.xml +20 -0
  25. data/conf/annotation/pathology_scg/ExtraprostaticExtensionTissueSites.hbm.xml +14 -0
  26. data/conf/annotation/pathology_scg/GleasonScore.hbm.xml +16 -0
  27. data/conf/annotation/pathology_scg/HistologicGrade.hbm.xml +16 -0
  28. data/conf/annotation/pathology_scg/HistologicType.hbm.xml +19 -0
  29. data/conf/annotation/pathology_scg/HistologicVariantType.hbm.xml +14 -0
  30. data/conf/annotation/pathology_scg/ImmunoPhenotyping.hbm.xml +16 -0
  31. data/conf/annotation/pathology_scg/Invasion.hbm.xml +16 -0
  32. data/conf/annotation/pathology_scg/KidneyMarginLocation.hbm.xml +15 -0
  33. data/conf/annotation/pathology_scg/KidneyNephrectomyMargin.hbm.xml +19 -0
  34. data/conf/annotation/pathology_scg/LateralMelanomaMargin.hbm.xml +16 -0
  35. data/conf/annotation/pathology_scg/LocalExcisionColorectalDeepMargin.hbm.xml +15 -0
  36. data/conf/annotation/pathology_scg/LocalExcisionColorectalLateralMargin.hbm.xml +20 -0
  37. data/conf/annotation/pathology_scg/LungResectionMargin.hbm.xml +17 -0
  38. data/conf/annotation/pathology_scg/LungResectionMarginsUninvolved.hbm.xml +14 -0
  39. data/conf/annotation/pathology_scg/MacroscopicExtentOfTumor.hbm.xml +14 -0
  40. data/conf/annotation/pathology_scg/MesentricMargin.hbm.xml +15 -0
  41. data/conf/annotation/pathology_scg/MetastasisTissueSite.hbm.xml +15 -0
  42. data/conf/annotation/pathology_scg/Microcalcification.hbm.xml +14 -0
  43. data/conf/annotation/pathology_scg/NottinghamHistologicScore.hbm.xml +17 -0
  44. data/conf/annotation/pathology_scg/OtherResectedOrgans.hbm.xml +15 -0
  45. data/conf/annotation/pathology_scg/PancreasMargin.hbm.xml +20 -0
  46. data/conf/annotation/pathology_scg/PancreasMarginInvolvedByInvasiveCarcinoma.hbm.xml +15 -0
  47. data/conf/annotation/pathology_scg/PancreasMarginUninvolvedByInvasiveCarcinoma.hbm.xml +20 -0
  48. data/conf/annotation/pathology_scg/PathologicalStaging.hbm.xml +16 -0
  49. data/conf/annotation/pathology_scg/PolypConfiguration.hbm.xml +15 -0
  50. data/conf/annotation/pathology_scg/PrimaryTumorStage.hbm.xml +14 -0
  51. data/conf/annotation/pathology_scg/ProstateMarginLocation.hbm.xml +15 -0
  52. data/conf/annotation/pathology_scg/ProximalMargin.hbm.xml +15 -0
  53. data/conf/annotation/pathology_scg/RadialMargin.hbm.xml +15 -0
  54. data/conf/annotation/pathology_scg/RadicalProstatectomyMargin.hbm.xml +20 -0
  55. data/conf/annotation/pathology_scg/RegionalLymphNode.hbm.xml +19 -0
  56. data/conf/annotation/pathology_scg/SatelliteNodule.hbm.xml +14 -0
  57. data/conf/annotation/pathology_scg/SpecimenCollectionGroup.hbm.xml +87 -0
  58. data/conf/annotation/pathology_scg/SpecimenIntegrity.hbm.xml +15 -0
  59. data/conf/annotation/pathology_scg/SpecimenSize.hbm.xml +17 -0
  60. data/conf/annotation/pathology_scg/TissueSide.hbm.xml +14 -0
  61. data/conf/annotation/pathology_scg/TumorSize.hbm.xml +29 -0
  62. data/conf/annotation/pathology_scg/TumorTissueSite.hbm.xml +20 -0
  63. data/conf/annotation/pathology_scg/UninvolvedMelanomaMargin.hbm.xml +15 -0
  64. data/conf/annotation/pathology_specimen/AdditionalFinding.hbm.xml +19 -0
  65. data/conf/annotation/pathology_specimen/AdditionalPathologicFinding.hbm.xml +18 -0
  66. data/conf/annotation/pathology_specimen/Details.hbm.xml +15 -0
  67. data/conf/annotation/pathology_specimen/GleasonScore.hbm.xml +16 -0
  68. data/conf/annotation/pathology_specimen/HistologicGrade.hbm.xml +16 -0
  69. data/conf/annotation/pathology_specimen/HistologicType.hbm.xml +19 -0
  70. data/conf/annotation/pathology_specimen/HistologicVariantType.hbm.xml +14 -0
  71. data/conf/annotation/pathology_specimen/Invasion.hbm.xml +16 -0
  72. data/conf/annotation/pathology_specimen/NottinghamHistologicScore.hbm.xml +17 -0
  73. data/conf/annotation/pathology_specimen/Specimen.hbm.xml +52 -0
  74. data/conf/annotation/pathology_specimen/SpecimenBaseSolidTissuePathologyAnnotation.hbm.xml +73 -0
  75. data/examples/galena/lib/galena/cli/seed.rb +0 -21
  76. data/examples/galena/lib/galena/migration/frozen_shims.rb +6 -5
  77. data/examples/galena/lib/galena/seed/defaults.rb +0 -5
  78. data/{lib → examples/galena/lib}/galena.rb +0 -0
  79. data/lib/catissue/annotation/annotatable.rb +37 -0
  80. data/lib/catissue/annotation/annotatable_class.rb +255 -0
  81. data/lib/catissue/annotation/annotation.rb +49 -0
  82. data/lib/catissue/annotation/annotation_class.rb +277 -0
  83. data/lib/catissue/annotation/annotation_module.rb +77 -0
  84. data/lib/catissue/annotation/hibernate_mapping.rb +46 -0
  85. data/lib/catissue/annotation/proxy.rb +28 -0
  86. data/lib/catissue/annotation/proxy_class.rb +68 -0
  87. data/lib/catissue/cli/migrate.rb +2 -2
  88. data/lib/catissue/cli/smoke.rb +6 -4
  89. data/lib/catissue/database/annotation/annotation_service.rb +75 -61
  90. data/lib/catissue/database/annotation/annotator.rb +17 -76
  91. data/lib/catissue/database/annotation/entity_facade.rb +265 -0
  92. data/lib/catissue/database/annotation/id_generator.rb +62 -0
  93. data/lib/catissue/database/annotation/integration_service.rb +105 -59
  94. data/lib/catissue/database/annotation/reference_writer.rb +150 -0
  95. data/lib/catissue/database/controlled_values.rb +12 -12
  96. data/lib/catissue/database.rb +148 -58
  97. data/lib/catissue/domain/abstract_specimen.rb +40 -14
  98. data/lib/catissue/domain/abstract_specimen_collection_group.rb +1 -3
  99. data/lib/catissue/domain/collection_protocol.rb +13 -5
  100. data/lib/catissue/domain/collection_protocol_event.rb +1 -14
  101. data/lib/catissue/domain/consent_tier_response.rb +2 -0
  102. data/lib/catissue/domain/consent_tier_status.rb +5 -3
  103. data/lib/catissue/domain/container.rb +14 -10
  104. data/lib/catissue/domain/container_position.rb +8 -0
  105. data/lib/catissue/domain/container_type.rb +13 -6
  106. data/lib/catissue/domain/participant.rb +15 -10
  107. data/lib/catissue/domain/site.rb +9 -3
  108. data/lib/catissue/domain/specimen.rb +79 -40
  109. data/lib/catissue/domain/specimen_array.rb +11 -1
  110. data/lib/catissue/domain/specimen_collection_group.rb +79 -41
  111. data/lib/catissue/domain/specimen_event_parameters.rb +5 -8
  112. data/lib/catissue/domain/specimen_position.rb +0 -2
  113. data/lib/catissue/domain/specimen_requirement.rb +1 -1
  114. data/lib/catissue/domain/storage_container.rb +109 -48
  115. data/lib/catissue/domain/storage_type.rb +1 -1
  116. data/lib/catissue/migration/migrator.rb +6 -14
  117. data/lib/catissue/resource.rb +18 -8
  118. data/lib/catissue/util/position.rb +11 -1
  119. data/lib/catissue/util/storable.rb +18 -11
  120. data/lib/catissue/util/storage_type_holder.rb +44 -6
  121. data/lib/catissue/version.rb +1 -1
  122. metadata +86 -35
  123. data/bin/migrate.rb +0 -42
  124. data/bin/seed.rb +0 -43
  125. data/examples/galena/doc/CaTissue/Participant.html +0 -241
  126. data/examples/galena/doc/CaTissue/SpecimenCollectionGroup.html +0 -190
  127. data/examples/galena/doc/CaTissue/StorageContainer.html +0 -179
  128. data/examples/galena/doc/CaTissue/TissueSpecimen.html +0 -320
  129. data/examples/galena/doc/CaTissue.html +0 -93
  130. data/examples/galena/doc/Galena/Seed/Defaults.html +0 -650
  131. data/examples/galena/doc/Galena/Seed.html +0 -203
  132. data/examples/galena/doc/Galena.html +0 -172
  133. data/examples/galena/doc/_index.html +0 -181
  134. data/examples/galena/doc/class_list.html +0 -36
  135. data/examples/galena/doc/css/common.css +0 -1
  136. data/examples/galena/doc/css/full_list.css +0 -53
  137. data/examples/galena/doc/css/style.css +0 -307
  138. data/examples/galena/doc/file.README.html +0 -153
  139. data/examples/galena/doc/file_list.html +0 -38
  140. data/examples/galena/doc/frames.html +0 -13
  141. data/examples/galena/doc/index.html +0 -153
  142. data/examples/galena/doc/js/app.js +0 -202
  143. data/examples/galena/doc/js/full_list.js +0 -149
  144. data/examples/galena/doc/js/jquery.js +0 -154
  145. data/examples/galena/doc/method_list.html +0 -163
  146. data/examples/galena/doc/top-level-namespace.html +0 -112
  147. data/lib/README.html +0 -33
  148. data/lib/catissue/database/annotation/annotatable_service.rb +0 -25
  149. data/lib/catissue/database/annotation/entity_manager.rb +0 -10
  150. data/lib/galena/cli/seed.rb +0 -43
  151. data/lib/galena/migration/filter_shims.rb +0 -43
  152. data/lib/galena/migration/frozen_shims.rb +0 -53
  153. data/lib/galena/seed/defaults.rb +0 -109
@@ -0,0 +1,265 @@
1
+ require 'singleton'
2
+ require 'caruby/import/java'
3
+ require 'caruby/database/sql_executor'
4
+ require 'catissue/database/annotation/id_generator'
5
+
6
+ module CaTissue
7
+ module Annotation
8
+ # Import this EntityManager dependency before EntityManager.
9
+ java_import('edu.wustl.common.security.exceptions.UserNotAuthorizedException')
10
+
11
+ # Import the caTissue Java EntityManager.
12
+ java_import('edu.common.dynamicextensions.entitymanager.EntityManager')
13
+
14
+ # EntityFacade is the caRuby interface to the caTissue EntityManager. EntityManager is
15
+ # the caTissue singleton Winnebago object for doing lots of things with dynamic extensions.
16
+ class EntityFacade
17
+ include Singleton
18
+
19
+ private
20
+
21
+ # Initializes the caTissue EntityManager, an id generator and a SQL executor. The id
22
+ # generator and executor are used for the caTissue bug work-arounds described in the
23
+ # method docs and {IdGenerator}.
24
+ def initialize
25
+ # the encapsulated caTissue singleton
26
+ @emgr = EntityManager.instance
27
+ # the work-around id generator
28
+ @idgen = IdGenerator.new
29
+ # a general-purpose SQL executor for calling the work-arounds
30
+ @executor = CaRuby::SQLExecutor.new(CaTissue.access_properties)
31
+ # the primary entity class => entity id hash
32
+ @pr_eid_hash = {}
33
+ end
34
+
35
+ public
36
+
37
+ # @param [Annotation] the annotation object
38
+ # @return [Integer] a new identifier for the given annotation object
39
+ def next_identifier(annotation)
40
+ # Commented line is broken - see IdGenerator doc.
41
+ # EntityManager.instance.getNextIdentifierForEntity(annotation.class.name.demodulize)
42
+
43
+ # The entity table name, which will be a cryptic value like DE_E_1283.
44
+ eid = primary_entity_id(annotation.class)
45
+ aeid = common_ancestor_entity_id(eid)
46
+ tbl = annotation_table_for_entity_id(aeid)
47
+ next_identifier_for_table(tbl)
48
+ end
49
+
50
+ # @param [String] the table name
51
+ # @return [Integer] the next identifier to use when creating a table record
52
+ def next_identifier_for_table(table)
53
+ # delegate to id generator
54
+ @idgen.next_identifier(table)
55
+ end
56
+
57
+ # caTissue alert - unlike the hook entity id lookup, the annotation entity id lookup strips the leading
58
+ # package prefix from the annotation class name. caTissue DE API requires this undocumented inconsistency.
59
+ #
60
+ # caTissue alert - call into caTissue to get entity id doesn't work. caRuby uses direct SQL instead.
61
+ #
62
+ # @param [Class] klass the {Annotation} primary class
63
+ # @param [Boolean] validate flag indicating whether to raise an exception if the class is not primary
64
+ # @return [Integer] the caTissue entity id for the class
65
+ # @raise [AnnotationError] if the validate flag is set and the class is not primary
66
+ def primary_entity_id(klass, validate=true)
67
+ eid = @pr_eid_hash[klass] ||= recursive_primary_entity_id(klass)
68
+ if eid.nil? and validate then raise AnnotationError.new("Entity not found for annotation #{klass}") end
69
+ eid
70
+ end
71
+
72
+ # @param [Class] klass the {Annotatable} class
73
+ # @return [Integer] the class entity id
74
+ def hook_entity_id(klass)
75
+ entity_id_for_class_designator(klass.java_class.name)
76
+ end
77
+
78
+ # caTissue alert - call into caTissue to get entity id doesn't work for non-primary object.
79
+ # Furthermore, the SQL used for the #{#primary_entity_id} doesn't work for associated annotation
80
+ # classes. Use alternative SQL instead.
81
+ #
82
+ # @param [Integer] eid the referencing entity id
83
+ # @param [String] eid the association property name
84
+ # @return [Integer] the referenced {Annotation} class entity id
85
+ def associated_entity_id(eid, name)
86
+ # The caTissue role is capitalized.
87
+ role = name.capitalize_first
88
+ ref_eid = recursive_associated_entity_id(eid, role)
89
+ if ref_eid then
90
+ logger.debug { "Entity id #{eid} is associated with property #{name} via entity id #{ref_eid}." }
91
+ else
92
+ logger.debug { "Entity id #{eid} is not associated with property #{name}." }
93
+ end
94
+ ref_eid
95
+ end
96
+
97
+ # caTissue alert - Annotation classes are incorrectly mapped to entity ids, which in turn are
98
+ # incorrectly mapped to a table name. A candidate work-around is to bypass the caTissue DE
99
+ # mechanism and hit the DE Hibernate config files directly. However, the DE Hibernate mappings
100
+ # are incorrect and possibly no longer used. Therefore, the table must be obtained by SQL
101
+ # work-arounds.
102
+ #
103
+ # @param [Annotation] obj the annotation object
104
+ # @return [String] the entity table name
105
+ # @param [Integer] the annotation entity identifier
106
+ # @return [String] the entity table name
107
+ def annotation_table_for_entity_id(eid)
108
+ result = @executor.execute { |dbh| dbh.select_one(TABLE_NAME_SQL, eid) }
109
+ if result.nil? then raise AnnotationError.new("Table not found for annotation entity id #{eid}") end
110
+ tbl = result[0]
111
+ logger.debug { "Annotation entity with id #{eid} has table #{tbl}." }
112
+ tbl
113
+ end
114
+
115
+ # @param (see #associated_entity_id)
116
+ # @return [Integer, nil] the parent entity id, if any
117
+ def parent_entity_id(eid)
118
+ result = @executor.execute { |dbh| dbh.select_one(PARENT_ENTITY_ID_SQL, eid) }
119
+ result[0] if result
120
+ end
121
+
122
+ # Obtains the undocumented caTisue container id for the given primary entity id.
123
+ #
124
+ # caTissue alert - EntityManager.getContainerIdForEntitycontainer uses incorrect table
125
+ # (cf. https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=421&sid=5252d951301e598eebf3e90036da43cb).
126
+ # The standard DE API call submits the query:
127
+ # SELECT IDENTIFIER FROM dyextn_container WHERE ENTITY_ID = ?
128
+ # This results in the error:
129
+ # Unknown column 'ENTITY_ID' in 'where clause'
130
+ # The correct SQL is as follows:
131
+ # SELECT IDENTIFIER FROM dyextn_container WHERE ABSTRACT_ENTITY_ID = ?
132
+ # The work-around is to call this SQL directly.
133
+ #
134
+ # @return [Integer] eid the primary entity id
135
+ # @raise [AnnotationError] if no container id is found
136
+ def container_id(eid)
137
+ # The following call is broken (see method doc).
138
+ # EntityManager.instance.get_container_id_for_entity(eid)
139
+ # Work-around caTissue bug with direct query.
140
+ result = @executor.execute { |dbh| dbh.select_one(CTR_ID_SQL, eid) }
141
+ cid = result[0].to_i if result
142
+ if cid.nil? then
143
+ raise AnnotationError.new("Dynamic extension container id not found for annotation #{annotation} with entity id #{eid}")
144
+ end
145
+ logger.debug { "Annotation with entity id #{eid} has container id #{cid}." }
146
+ cid
147
+ end
148
+
149
+ private
150
+
151
+ # @param (see #primary_entity_id)
152
+ # @return (see #primary_entity_id)
153
+ def recursive_primary_entity_id(klass)
154
+ eid = nonrecursive_primary_entity_id(klass) || parent_primary_entity_id(klass)
155
+ if eid then logger.debug { "#{klass.qp} has entity id #{eid}." } end
156
+ eid
157
+ end
158
+
159
+ # @param (see #primary_entity_id)
160
+ # @return (see #primary_entity_id)
161
+ def nonrecursive_primary_entity_id(klass)
162
+ # The Java class package is the entity group, the Java class unqualified name is the caption.
163
+ pkg, cls_nm = klass.java_class.name.split('.')
164
+ # Dive into some obscure SQL
165
+ result = @executor.execute { |dbh| dbh.select_one(CTR_ENTITY_ID_SQL, pkg, cls_nm) }
166
+ result[0] if result
167
+ end
168
+
169
+ # @param (see #primary_entity_id)
170
+ # @return (see #primary_entity_id)
171
+ def parent_primary_entity_id(klass)
172
+ nonrecursive_primary_entity_id(klass.superclass) if klass.superclass < Annotation
173
+ end
174
+
175
+ # @param [Integer] the starting entity id
176
+ # @return [Integer] the top-most ancestor entity id
177
+ def common_ancestor_entity_id(eid)
178
+ peid = parent_entity_id(eid)
179
+ peid ? common_ancestor_entity_id(peid) : eid
180
+ end
181
+
182
+ # @param eid (see #associated_entity_id)
183
+ # @param role the property role name
184
+ # @return [Integer, nil] the associated entity id, if any
185
+ def recursive_associated_entity_id(eid, role)
186
+ nonrecursive_associated_entity_id(eid, role) or parent_associated_entity_id(eid, role)
187
+ end
188
+
189
+ # @param (see #recursive_associated_entity_id)
190
+ # @return [Integer, nil] the associated entity id in the context of the parent, if any
191
+ def parent_associated_entity_id(eid, role)
192
+ peid = parent_entity_id(eid) || return
193
+ logger.debug { "Finding entity id #{eid} #{role} associated entity id using parent entity id #{peid}..." }
194
+ recursive_associated_entity_id(peid, role)
195
+ end
196
+
197
+ # @param (see #recursive_associated_entity_id)
198
+ # @return @return [Integer, nil] the directly associated entity id, if any
199
+ def nonrecursive_associated_entity_id(eid, role)
200
+ logger.debug { "Finding entity id #{eid} #{role} associated entity id..." }
201
+ result = @executor.execute { |dbh| dbh.select_one(ASSN_ENTITY_ID_SQL, eid, role) }
202
+ # The role role can be a mutation of the property name with spaces inserted in the
203
+ # camel-case components, e.g. 'Additional Finding' instead of 'AdditionalFinding'.
204
+ # TODO - fix this kludge by finding out how the role relates to the property in the
205
+ # database.
206
+ if result.nil? and role =~ /.+[A-Z]/ then
207
+ alt = role.gsub(/(.)([A-Z])/, '\1 \2')
208
+ logger.debug { "Attempting to find entity id #{eid} #{role} associated entity id using variant #{alt}..." }
209
+ result = @executor.execute { |dbh| dbh.select_one(ASSN_ENTITY_ID_SQL, eid, alt) }
210
+ end
211
+ if result.nil? and role =~ /[pP]athologic[^a]/ then
212
+ alt = role.sub(/([pP])athologic/, '\1athological')
213
+ logger.debug { "Attempting to find entity id #{eid} #{role} associated entity id using variant #{alt}..." }
214
+ result = @executor.execute { |dbh| dbh.select_one(ASSN_ENTITY_ID_SQL, eid, alt) }
215
+ end
216
+ if result.nil? then
217
+ logger.debug { "Entity id #{eid} is not directly associated with #{role}." }
218
+ end
219
+ result[0] if result
220
+ end
221
+
222
+ # @param [String] designator the class name, demodulized in the case of an annotation entity
223
+ # @return [Integer] the caTissue entity id for the given class name
224
+ # @raise [CaRuby::DatabaseError] if the DE entity id is not found for the given designator
225
+ def entity_id_for_class_designator(designator)
226
+ @emgr.getEntityId(designator) or
227
+ raise CaRuby::DatabaseError.new("Dynamic extension entity id not found for #{designator}")
228
+ end
229
+
230
+ # The SQL to find an entity id for a primary entity.
231
+ CTR_ENTITY_ID_SQL = <<EOS
232
+ select ctr.ABSTRACT_ENTITY_ID
233
+ from DYEXTN_CONTAINER ctr, DYEXTN_ENTITY_GROUP grp
234
+ where ctr.ENTITY_GROUP_ID = grp.IDENTIFIER
235
+ and grp.SHORT_NAME = ?
236
+ and ctr.CAPTION = ?
237
+ EOS
238
+
239
+ # The SQL to find an entity id for a secondary annotation referenced by a primary annotation.
240
+ ASSN_ENTITY_ID_SQL = <<EOS
241
+ select assn.TARGET_ENTITY_ID
242
+ from DYEXTN_ATTRIBUTE attr, DYEXTN_ABSTRACT_ENTITY ae, DYEXTN_ASSOCIATION assn, DYEXTN_ROLE role
243
+ where assn.IDENTIFIER = attr.IDENTIFIER
244
+ and attr.ENTIY_ID = ae.id
245
+ and assn.TARGET_ROLE_ID = role.IDENTIFIER
246
+ and ae.id = ?
247
+ and role.name = ?
248
+ EOS
249
+
250
+ # The SQL to find a parent entity id for a given entity id.
251
+ PARENT_ENTITY_ID_SQL = 'select e.parent_entity_id from DYEXTN_ENTITY e where e.identifier = ?'
252
+
253
+ # The SQL to find the database table for an entity id.
254
+ TABLE_NAME_SQL = <<EOS
255
+ select dp.name
256
+ from dyextn_database_properties dp, dyextn_table_properties tp
257
+ where dp.identifier = tp.identifier
258
+ and tp.abstract_entity_id = ?
259
+ EOS
260
+
261
+ # The caTissue DE API container id bug work-around query
262
+ CTR_ID_SQL = "select identifier from dyextn_container where abstract_entity_id = ?"
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,62 @@
1
+ require 'caruby/database/sql_executor'
2
+
3
+ module CaTissue
4
+ module Annotation
5
+ # The IdGenerator delegates to the caTissue entity manager to create a new identifier for an annotation.
6
+ class IdGenerator
7
+ def initialize
8
+ @executor = CaRuby::SQLExecutor.new(CaTissue.access_properties)
9
+ end
10
+
11
+ # caTissue alert - DE API subquery search fails.
12
+ # EntityManager.getNextIdentifierForEntity(EntityManager.java:2689) returns zero for some, but not all, DEs.
13
+ # This is a cascading error that is difficult to trace. The EntityManager.getNextIdentifierForEntity error
14
+ # is printed to the console rather than propagated up the call stack. A subsequent create then fails because
15
+ # the identifier is not set.
16
+ #
17
+ # A candidate work-around is to resolve the DE table name by issuing a direct SQL call and pass this
18
+ # to DE API EntityManagerUtil.getNextIdentifier. However, EntityManagerUtil is broken as well, for a
19
+ # different reason.
20
+ #
21
+ # Another candidate work-around is to get the next id manually from the caTissue DYEXTN_ID_GENERATOR table.
22
+ # However, this table is suspect since it is not defined as a database sequence generator and is not
23
+ # referenced in the caTissue source code. It is not used by the caTissue GUI when creating an annotation.
24
+ #
25
+ # The work-around to the work-around to the work-around is to call the following SQL directly:
26
+ # select max(identifier) from <table>
27
+ # where \<table\> is the result of the EntityManager work-around query. The EntityManager work-around query
28
+ # is described in
29
+ # https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=404&p=2785&sid=febe0a1271b3d00020927741a94e9bff#p2785.
30
+ #
31
+ # Unfortunately, the +select max+ work-around is hampered by the obvious concurrent access race condition.
32
+ # For an unknown reason, caTissue DE does not use database sequence generators to make DE identifiers.
33
+ # Even if the caTissue DE API worked, it might suffer from the same race condition. The DE API
34
+ # EntityManagerUtil caches identifiers. The API call is synchronized, but it is unclear whether each DE
35
+ # client accesses the same EntityManagerUtil instance as the GUI DE action processor. If not, then the
36
+ # caTissue DE API, assuming that it were functional, would be open to an even more serious concurrency
37
+ # conflict than the work-around race condition.
38
+ #
39
+ # @param [String] the entity table
40
+ # @return [Integer] a new identifier for the given annotation object
41
+ def next_identifier(table)
42
+ # Commented line is broken - see method doc.
43
+ # The caTissue EntityManagerUtil hands out table record ids in the work-around.
44
+ #identifier = EntityManagerUtil.getNextIdentifier(table)
45
+
46
+ logger.debug { "Work around caTissue DE EntityManagerUtil bug by fetching the maximum #{table} identifier directly from the database..." }
47
+ sql = TABLE_MAX_ID_SQL_TMPL % table
48
+ result = @executor.execute { |dbh| dbh.select_one(sql) }
49
+ max = result ? result[0].to_i : 0
50
+ identifier = max + 1
51
+ # End of work-around
52
+
53
+ logger.debug { "Next #{table} identifier is #{identifier}." }
54
+ identifier
55
+ end
56
+
57
+ private
58
+
59
+ TABLE_MAX_ID_SQL_TMPL = "select max(identifier) from %s"
60
+ end
61
+ end
62
+ end
@@ -2,85 +2,131 @@ require 'caruby/database/persistence_service'
2
2
 
3
3
  module CaTissue
4
4
  module Annotation
5
- # An IntegrationService fetches and saves CaTissue anchor-annotation associations.
5
+ # An IntegrationService fetches and saves CaTissue hook-annotation associations.
6
6
  class IntegrationService < CaRuby::PersistenceService
7
7
  SERVICE_NAME = 'deintegration'
8
8
 
9
9
  java_import('deintegration.EntityMap')
10
+
10
11
  java_import('deintegration.EntityMapRecord')
12
+
13
+ java_import('deintegration.FormContext')
11
14
 
12
- def initialize(entity_manager)
15
+ # @param [EntityFacade] the global entity manager
16
+ def initialize
13
17
  super(SERVICE_NAME)
14
- @entity_manager = entity_manager
18
+ # SQL executor to handle caTissue DE API bug work-around
19
+ @executor = CaRuby::SQLExecutor.new(CaTissue.access_properties)
15
20
  end
16
21
 
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
+ # Associates the given hook domain object to the annotation.
23
+ #
24
+ # @param [Annotatable] hook the hook entity
25
+ # @param [Annotation] annotation the annotation entity
26
+ def associate(hook, annotation)
27
+ logger.debug { "Associating annotation #{annotation} to owner #{hook}..." }
28
+ emr = create_entity_map_record(hook, annotation)
29
+ create(emr)
22
30
  end
23
31
 
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
32
+ # # Removes the existing association between the given hook domain object to annotation.
33
+ # #
34
+ # # @param (see #associate)
35
+ # def dissociate(hook, annotation)
36
+ # assn = find_entity_map_record(hook, annotation) # TODO - implement
37
+ # delete(assn)
38
+ # end
29
39
 
30
40
  private
31
41
 
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
42
+ #### The cruft below is adapted from caTissue ClientDemo_SCG.java and cleaned up (but still obscure). ####
61
43
 
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
44
+ # Creates an entity map record with content (annotation entity id, annotation id, form context id).
45
+ # This record associates the static hook record to the annotation record qualified by the context.
46
+ #
47
+ # @param (see #associate)
48
+ # @return [EntityMapRecord] the new entity map record
49
+ def create_entity_map_record(hook, annotation)
50
+ # the entity map record with content (annotation entity id, annotation id, context id)
51
+ emr = EntityMapRecord.new
52
+
53
+ # the hook id
54
+ if hook.identifier.nil? then
55
+ raise CaRuby::DatabaseError.new("Annotation entity map static entity does not have an identifier: #{hook}")
56
+ end
57
+ emr.static_entity_record_id = hook.identifier
58
+
59
+ # the annotation id
60
+ if annotation.identifier.nil? then
61
+ raise CaRuby::DatabaseError.new("Annotation entity map dynamic entity does not have an identifier: #{annotation}")
62
+ end
63
+ emr.dynamic_entity_record_id = annotation.identifier
64
+
65
+ # the form context
66
+ ctxt = form_context(hook, annotation)
67
+ if ctxt then
68
+ emr.form_context = ctxt
69
+ emr.form_context_id = ctxt.id
70
+ end
71
+
72
+ emr
67
73
  end
68
74
 
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)
75
+ # @param (see #associate)
76
+ # @return [FormContent] an undocumented bit of caTissue presentation flotsam polluting the data layer
77
+ def form_context(hook, annotation)
78
+ map = entity_map(hook, annotation)
79
+
80
+ # the fetched form context
81
+ ctxts = map.form_context_collection
82
+ if ctxts.empty? then
83
+ logger.debug { "#{hook} entity map #{map.qp} does not have a form context." }
84
+ elsif ctxts.size > 1 then
85
+ ctxt_ids = ctxts.map { |ctxt| ctxt.id }
86
+ raise CaRuby::DatabaseError.new("More than one form context for #{hook} - form context ids: #{ctxt_ids.qp}")
87
+ else
88
+ ctxt = ctxts.first
89
+ logger.debug { "#{hook} has form context id #{ctxt.id}." }
90
+ end
91
+
92
+ ctxt
72
93
  end
73
-
74
- # Returns the undocumented caTissue entity id for the given annotation entity's demodulized Java class name.
94
+
95
+ # caTissue alert - the entity map is associated with a domain class in the hook class hierarchy.
96
+ # The generic approach to determining the entity map for a given hook object and annotation object
97
+ # is to iterate over the hook class hierarchy until a matching ENTITY_MAP record is found for the
98
+ # hook class ancestor and the annotation container id.
75
99
  #
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[/[^.]+$/])
100
+ # @param (see #associate)
101
+ # @return [EntityMap] the entity map
102
+ def entity_map(hook, annotation)
103
+ klass = hook.class
104
+ while klass < CaTissue::AbstractDomainObject
105
+ map = entity_map_for_class(klass, annotation)
106
+ return map if map
107
+ klass = klass.superclass
108
+ end
109
+ nil
80
110
  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}")
111
+
112
+ # @param [Class] klass the hook class
113
+ # @param annotation (see #entity_map)
114
+ # @return (see #entity_map)
115
+ def entity_map_for_class(klass, annotation)
116
+ # A query template
117
+ tmpl = EntityMap.new
118
+ tmpl.static_entity_id = klass.effective_entity_id
119
+ # the container id
120
+ tmpl.container_id = annotation.class.container_id
121
+ # the database record matching the template
122
+ logger.debug { "Fetching the ENTITY_MAP record for #{klass.qp} entity id #{tmpl.static_entity_id} and container id #{tmpl.container_id}..." }
123
+ map = query(tmpl).first
124
+ if map then
125
+ logger.debug { "Entity map found for #{klass.qp} entity id #{tmpl.static_entity_id}, container id #{tmpl.container_id}: #{map.qp}." }
126
+ else
127
+ logger.debug { "ENTITY_MAP record not found for #{klass.qp} entity id #{tmpl.static_entity_id}, container id #{tmpl.container_id}." }
128
+ end
129
+ map
84
130
  end
85
131
  end
86
132
  end
@@ -0,0 +1,150 @@
1
+ module CaTissue
2
+ module Annotation
3
+ # A ReferenceWriter saves annotations to the database. This is a helper class to work around
4
+ # caTissue DE API defects. This class infers a direct data mapping by navigating the caTissue
5
+ # DYEXT tables of the introspected Java annotation class properties.
6
+ class ReferenceWriter
7
+ # @param [Integer] eid the referencing annotation entity id
8
+ # @param [CaRuby::AttributeMetadata] attr_md the annotation attribute metadata of the attribute to save
9
+ # @param [Integer, nil] assn_eid the referenced annotation entity id
10
+ def initialize(eid, attr_md, assn_eid=nil)
11
+ logger.debug { "Mapping annotation #{attr_md.declarer.qp}.#{attr_md} role attributes to database columns..." }
12
+ efcd = EntityFacade.instance
13
+ # the referenced annotation entity id
14
+ assn_eid ||= associated_entity_id(eid, attr_md)
15
+ # the referenced entity database table
16
+ @table = efcd.annotation_table_for_entity_id(assn_eid)
17
+ # map the attribute => column
18
+ attr_col_hash = map_attributes(attr_md.type, assn_eid)
19
+ logger.debug { "Annotation #{attr_md.declarer.qp} #{attr_md} maps to #{@table} as #{attr_col_hash.qp}" }
20
+ # the mapped attributes and columns
21
+ @attrs, cols = attr_col_hash.to_a.transpose
22
+ # the SQL parameters clause
23
+ params = Array.new(cols.size, '?').join(', ')
24
+ # the create SQL
25
+ @cr_sql = CREATE_SQL % [@table, cols.join(', '), params]
26
+ # the update SQL
27
+ @upd_sql = UPDATE_SQL % [@table, cols.map { |col| "#{col} = ?" }.join(', ')]
28
+ # the superclass writer for annotations with superclass DE forms
29
+ @parent = obtain_parent_writer(eid, attr_md)
30
+ end
31
+
32
+ # @param [Annotation] annotation the referenced annotation value
33
+ def save(annotation)
34
+ # select the SQL based on whether this is an update or a create
35
+ sql = annotation.identifier ? @upd_sql : @cr_sql
36
+ # allocate a new database identifier
37
+ annotation.identifier ||= next_identifier
38
+ # the values to bind to the SQL parameters
39
+ values = database_parameters(annotation)
40
+ logger.debug { "Saving #{annotation} to #{@table}..." }
41
+ # dispatch the SQL update or create statement
42
+ CaTissue::Database.instance.executor.execute do |dbh|
43
+ dbh.prepare(sql) { |sth| sth.execute(*values) }
44
+ end
45
+ if @parent then
46
+ logger.debug { "Saving #{annotation} parent entity attributes..." }
47
+ @parent.save(annotation)
48
+ end
49
+ end
50
+
51
+ protected
52
+
53
+ # @return [Integer] the identifier to use when creating a new annotation instance
54
+ def next_identifier
55
+ @parent ? @parent.next_identifier : EntityFacade.instance.next_identifier_for_table(@table)
56
+ end
57
+
58
+ private
59
+
60
+ # @param (see #initialize)
61
+ # @return [Integer] the entity id for the given attribute role
62
+ # @raise [AnnotationError] if the associated entity was not found
63
+ def associated_entity_id(eid, attr_md)
64
+ EntityFacade.instance.associated_entity_id(eid, attr_md.property_descriptor.name) or
65
+ raise AnnotationError.new("Associated entity not found for entity #{eid} attribute #{attr_md}")
66
+ end
67
+
68
+ # @param (see #initialize)
69
+ # @return [Integer, nil] the superclass associated entity id for the given attribute role, or nil if none
70
+ def obtain_parent_writer(eid, attr_md)
71
+ # the superclass entity id for annotations with superclass DE forms
72
+ peid = EntityFacade.instance.parent_entity_id(eid) || return
73
+ # the associated entity id
74
+ aeid = EntityFacade.instance.associated_entity_id(peid, attr_md.property_descriptor.name)
75
+ ReferenceWriter.new(peid, attr_md, aeid) if aeid
76
+ end
77
+
78
+ # @param annotation (see #save)
79
+ # @return [Array] the save SQL call parameters
80
+ def database_parameters(annotation)
81
+ @attrs.map do |attr|
82
+ value = annotation.send(attr)
83
+ Annotation === value ? value.identifier : value
84
+ end
85
+ end
86
+
87
+ def map_attributes(klass, eid)
88
+ # the non-domain columns
89
+ hash = klass.nondomain_attributes.to_compact_hash do |attr|
90
+ nondomain_attribute_column(klass, attr, eid)
91
+ end
92
+ # the owner attribute column
93
+ ownr_attr = klass.owner_attribute
94
+ if ownr_attr then hash[ownr_attr] = owner_attribute_column(klass, ownr_attr, eid) end
95
+ hash
96
+ end
97
+
98
+ def nondomain_attribute_column(klass, attribute, eid)
99
+ if attribute == :identifier then return IDENTIFIER_COL end
100
+ attr_md = klass.attribute_metadata(attribute)
101
+ # skip an attribute declared by the superclass
102
+ return unless attr_md.declarer == klass
103
+ xctr = CaTissue::Database.instance.executor
104
+ prop = attr_md.property_descriptor.name
105
+ logger.debug { "Finding #{klass.qp} #{attribute} column for entity id #{eid} and property #{prop}..." }
106
+ result = xctr.execute { |dbh| dbh.select_one(NONDOMAIN_COLUMN_SQL, prop, eid) }
107
+ col = result[0] if result
108
+ if col.nil? then raise AnnotationError.new("Column not found for #{klass.qp} #{attribute}") end
109
+ col
110
+ end
111
+
112
+ def owner_attribute_column(klass, attribute, eid)
113
+ logger.debug { "Finding #{klass.qp} #{attribute} column in the context of entity id #{eid}..." }
114
+ result = CaTissue::Database.instance.executor.execute { |dbh| dbh.select_one(OWNER_COLUMN_SQL, eid) }
115
+ col = result[0] if result
116
+ if col.nil? then raise AnnotationError.new("Column not found for #{klass.qp} owner attribute #{attribute}") end
117
+ col
118
+ end
119
+
120
+ IDENTIFIER_COL = 'IDENTIFIER'
121
+
122
+ # Generic update template.
123
+ UPDATE_SQL = "update %s set ACTIVITY_STATUS = 'Active', %s"
124
+
125
+ # Generic create template.
126
+ CREATE_SQL = "insert into %s(ACTIVITY_STATUS, %s)\nvalues ('Active', %s)"
127
+
128
+ # SQL to get the primitive column name for a given annotation class entity id and Java property name
129
+ NONDOMAIN_COLUMN_SQL = <<EOS
130
+ select dbp.name
131
+ from DYEXTN_ABSTRACT_METADATA amd, DYEXTN_CONTROL ctl, DYEXTN_CONTAINER ctr, DYEXTN_DATABASE_PROPERTIES dbp, DYEXTN_COLUMN_PROPERTIES cp
132
+ where amd.NAME = ?
133
+ and ctl.CONTAINER_ID = ctr.IDENTIFIER
134
+ and amd.IDENTIFIER = ctl.BASE_ABST_ATR_ID
135
+ and ctl.BASE_ABST_ATR_ID = cp.PRIMITIVE_ATTRIBUTE_ID
136
+ and dbp.IDENTIFIER = cp.IDENTIFIER
137
+ and ctr.ABSTRACT_ENTITY_ID = ?
138
+ EOS
139
+
140
+ # SQL to get the annotation reference column name for a given annotation target entity id.
141
+ # The target entity id is obtained by calling {EntityFacade#associated_entity_id}.
142
+ OWNER_COLUMN_SQL = <<EOS
143
+ select cst.TARGET_ENTITY_KEY
144
+ from DYEXTN_CONSTRAINT_PROPERTIES cst, DYEXTN_ASSOCIATION assn
145
+ where cst.ASSOCIATION_ID = assn.IDENTIFIER
146
+ and assn.TARGET_ENTITY_ID = ?
147
+ EOS
148
+ end
149
+ end
150
+ end