caruby-core 1.5.5 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/Gemfile +9 -0
  2. data/History.md +5 -1
  3. data/lib/caruby.rb +3 -5
  4. data/lib/caruby/caruby-src.tar.gz +0 -0
  5. data/lib/caruby/database.rb +53 -69
  6. data/lib/caruby/database/application_service.rb +25 -0
  7. data/lib/caruby/database/cache.rb +60 -0
  8. data/lib/caruby/database/fetched_matcher.rb +52 -38
  9. data/lib/caruby/database/lazy_loader.rb +4 -4
  10. data/lib/caruby/database/operation.rb +34 -0
  11. data/lib/caruby/database/persistable.rb +171 -86
  12. data/lib/caruby/database/persistence_service.rb +32 -34
  13. data/lib/caruby/database/persistifier.rb +100 -43
  14. data/lib/caruby/database/reader.rb +107 -85
  15. data/lib/caruby/database/reader_template_builder.rb +60 -0
  16. data/lib/caruby/database/saved_matcher.rb +3 -3
  17. data/lib/caruby/database/sql_executor.rb +88 -17
  18. data/lib/caruby/database/writer.rb +213 -177
  19. data/lib/caruby/database/writer_template_builder.rb +334 -0
  20. data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
  21. data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
  22. data/lib/caruby/{util → helpers}/person.rb +3 -3
  23. data/lib/caruby/{util → helpers}/properties.rb +7 -9
  24. data/lib/caruby/{util → helpers}/roman.rb +2 -2
  25. data/lib/caruby/{util → helpers}/version.rb +1 -1
  26. data/lib/caruby/json/deserializer.rb +2 -2
  27. data/lib/caruby/json/serializer.rb +49 -7
  28. data/lib/caruby/metadata.rb +30 -0
  29. data/lib/caruby/metadata/java_property.rb +21 -0
  30. data/lib/caruby/metadata/propertied.rb +191 -0
  31. data/lib/caruby/metadata/property.rb +22 -0
  32. data/lib/caruby/metadata/property_characteristics.rb +201 -0
  33. data/lib/caruby/migration/migratable.rb +11 -182
  34. data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
  35. data/lib/caruby/resource.rb +20 -823
  36. data/lib/caruby/version.rb +1 -1
  37. data/test/lib/caruby/database/cache_test.rb +54 -0
  38. data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
  39. data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
  40. data/test/lib/caruby/helpers/properties_test.rb +34 -0
  41. data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
  42. data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
  43. data/test/lib/helper.rb +7 -0
  44. metadata +161 -214
  45. data/lib/caruby/cli/application.rb +0 -36
  46. data/lib/caruby/cli/command.rb +0 -202
  47. data/lib/caruby/csv/csv_mapper.rb +0 -159
  48. data/lib/caruby/csv/csvio.rb +0 -203
  49. data/lib/caruby/database/search_template_builder.rb +0 -56
  50. data/lib/caruby/database/store_template_builder.rb +0 -278
  51. data/lib/caruby/domain.rb +0 -193
  52. data/lib/caruby/domain/attribute.rb +0 -584
  53. data/lib/caruby/domain/attributes.rb +0 -628
  54. data/lib/caruby/domain/dependency.rb +0 -225
  55. data/lib/caruby/domain/id_alias.rb +0 -22
  56. data/lib/caruby/domain/importer.rb +0 -183
  57. data/lib/caruby/domain/introspection.rb +0 -176
  58. data/lib/caruby/domain/inverse.rb +0 -172
  59. data/lib/caruby/domain/inversible.rb +0 -90
  60. data/lib/caruby/domain/java_attribute.rb +0 -173
  61. data/lib/caruby/domain/merge.rb +0 -185
  62. data/lib/caruby/domain/metadata.rb +0 -142
  63. data/lib/caruby/domain/mixin.rb +0 -35
  64. data/lib/caruby/domain/properties.rb +0 -95
  65. data/lib/caruby/domain/reference_visitor.rb +0 -428
  66. data/lib/caruby/domain/uniquify.rb +0 -50
  67. data/lib/caruby/import/java.rb +0 -387
  68. data/lib/caruby/migration/migrator.rb +0 -918
  69. data/lib/caruby/migration/resource_module.rb +0 -9
  70. data/lib/caruby/migration/uniquify.rb +0 -17
  71. data/lib/caruby/util/attribute_path.rb +0 -44
  72. data/lib/caruby/util/cache.rb +0 -56
  73. data/lib/caruby/util/class.rb +0 -149
  74. data/lib/caruby/util/collection.rb +0 -1152
  75. data/lib/caruby/util/domain_extent.rb +0 -46
  76. data/lib/caruby/util/file_separator.rb +0 -65
  77. data/lib/caruby/util/inflector.rb +0 -27
  78. data/lib/caruby/util/log.rb +0 -95
  79. data/lib/caruby/util/math.rb +0 -12
  80. data/lib/caruby/util/merge.rb +0 -59
  81. data/lib/caruby/util/module.rb +0 -18
  82. data/lib/caruby/util/options.rb +0 -97
  83. data/lib/caruby/util/partial_order.rb +0 -35
  84. data/lib/caruby/util/pretty_print.rb +0 -204
  85. data/lib/caruby/util/stopwatch.rb +0 -74
  86. data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
  87. data/lib/caruby/util/transitive_closure.rb +0 -55
  88. data/lib/caruby/util/tree.rb +0 -48
  89. data/lib/caruby/util/trie.rb +0 -37
  90. data/lib/caruby/util/uniquifier.rb +0 -30
  91. data/lib/caruby/util/validation.rb +0 -20
  92. data/lib/caruby/util/visitor.rb +0 -365
  93. data/lib/caruby/util/weak_hash.rb +0 -36
  94. data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
  95. data/test/lib/caruby/csv/csvio_test.rb +0 -69
  96. data/test/lib/caruby/database/persistable_test.rb +0 -92
  97. data/test/lib/caruby/domain/domain_test.rb +0 -112
  98. data/test/lib/caruby/domain/inversible_test.rb +0 -99
  99. data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
  100. data/test/lib/caruby/import/java_test.rb +0 -80
  101. data/test/lib/caruby/import/mixed_case_test.rb +0 -14
  102. data/test/lib/caruby/migration/test_case.rb +0 -102
  103. data/test/lib/caruby/test_case.rb +0 -230
  104. data/test/lib/caruby/util/cache_test.rb +0 -23
  105. data/test/lib/caruby/util/class_test.rb +0 -61
  106. data/test/lib/caruby/util/collection_test.rb +0 -398
  107. data/test/lib/caruby/util/command_test.rb +0 -55
  108. data/test/lib/caruby/util/domain_extent_test.rb +0 -60
  109. data/test/lib/caruby/util/file_separator_test.rb +0 -30
  110. data/test/lib/caruby/util/inflector_test.rb +0 -12
  111. data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
  112. data/test/lib/caruby/util/merge_test.rb +0 -83
  113. data/test/lib/caruby/util/module_test.rb +0 -25
  114. data/test/lib/caruby/util/options_test.rb +0 -59
  115. data/test/lib/caruby/util/partial_order_test.rb +0 -42
  116. data/test/lib/caruby/util/pretty_print_test.rb +0 -85
  117. data/test/lib/caruby/util/properties_test.rb +0 -50
  118. data/test/lib/caruby/util/stopwatch_test.rb +0 -18
  119. data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
  120. data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
  121. data/test/lib/caruby/util/tree_test.rb +0 -23
  122. data/test/lib/caruby/util/trie_test.rb +0 -14
  123. data/test/lib/caruby/util/visitor_test.rb +0 -278
  124. data/test/lib/caruby/util/weak_hash_test.rb +0 -45
  125. data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
  126. data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -1,142 +0,0 @@
1
- require 'caruby/util/collection'
2
- require 'caruby/import/java'
3
- require 'caruby/domain/java_attribute'
4
- require 'caruby/domain/introspection'
5
- require 'caruby/domain/inverse'
6
- require 'caruby/domain/dependency'
7
- require 'caruby/domain/attributes'
8
- require 'caruby/json/deserializer'
9
-
10
- module CaRuby
11
- module Domain
12
- # Exception raised if a meta-data setting is missing or invalid.
13
- class MetadataError < RuntimeError; end
14
-
15
- # Adds introspected metadata to a Class.
16
- module Metadata
17
- include Introspection, Inverse, Dependency, Attributes, JSON::Deserializer
18
-
19
- # @return [Module] the {Domain} module context
20
- attr_accessor :domain_module
21
-
22
- def self.extended(klass)
23
- super
24
- klass.class_eval do
25
- # Add this class's metadata.
26
- introspect
27
- # Add the {attribute=>value} argument constructor.
28
- class << self
29
- def new(opts=nil)
30
- obj = super()
31
- obj.merge_attributes(opts) if opts
32
- obj
33
- end
34
- end
35
- end
36
- end
37
-
38
- # @return the domain type for attribute, or nil if attribute is not a domain attribute
39
- def domain_type(attribute)
40
- attr_md = attribute_metadata(attribute)
41
- attr_md.type if attr_md.domain?
42
- end
43
-
44
- # Returns an empty value for the given attribute.
45
- # * If this class is not abstract, then the empty value is the initialized value.
46
- # * Otherwise, if the attribute is a Java primitive number then zero.
47
- # * Otherwise, if the attribute is a Java primitive boolean then +false+.
48
- # * Otherwise, the empty value is nil.
49
- #
50
- # @param [Symbol] attribute the target attribute
51
- # @return [Numeric, Boolean, Enumerable, nil] the empty attribute value
52
- def empty_value(attribute)
53
- if abstract? then
54
- attr_md = attribute_metadata(attribute)
55
- # the Java property type
56
- jtype = attr_md.property_descriptor.property_type if JavaAttribute === attr_md
57
- # A primitive is either a boolean or a number (String is not primitive).
58
- if jtype and jtype.primitive? then
59
- type.name == 'boolean' ? false : 0
60
- end
61
- else
62
- # Since this class is not abstract, create a prototype instance on demand and make
63
- # a copy of the initialized collection value from that instance.
64
- @prototype ||= new
65
- value = @prototype.send(attribute) || return
66
- value.class.new
67
- end
68
- end
69
-
70
- # Prints this classifier's content to the log.
71
- def pretty_print(q)
72
- # the Java property descriptors
73
- property_descriptors = java_attributes.wrap { |attr| attribute_metadata(attr).property_descriptor }
74
- # build a map of relevant display label => attributes
75
- prop_printer = property_descriptors.wrap { |pd| PROP_DESC_PRINTER.wrap(pd) }
76
- prop_syms = property_descriptors.map { |pd| pd.name.to_sym }.to_set
77
- aliases = @alias_std_attr_map.keys - attributes.to_a - prop_syms
78
- alias_attr_hash = aliases.to_compact_hash { |aliaz| @alias_std_attr_map[aliaz] }
79
- dependents_printer = dependent_attributes.wrap { |attr| DEPENDENT_ATTR_PRINTER.wrap(attribute_metadata(attr)) }
80
- owner_printer = owners.wrap { |type| TYPE_PRINTER.wrap(type) }
81
- inverses = @attributes.to_compact_hash do |attr|
82
- attr_md = attribute_metadata(attr)
83
- "#{attr_md.type.qp}.#{attr_md.inverse}" if attr_md.inverse
84
- end
85
- domain_attr_printer = domain_attributes.to_compact_hash { |attr| domain_type(attr).qp }
86
- map = {
87
- "Java properties" => prop_printer,
88
- "standard attributes" => attributes,
89
- "aliases to standard attributes" => alias_attr_hash,
90
- "secondary key" => secondary_key_attributes,
91
- "mandatory attributes" => mandatory_attributes,
92
- "domain attributes" => domain_attr_printer,
93
- "creatable domain attributes" => creatable_domain_attributes,
94
- "updatable domain attributes" => updatable_domain_attributes,
95
- "fetched domain attributes" => fetched_domain_attributes,
96
- "cascaded domain attributes" => cascaded_attributes,
97
- "owners" => owner_printer,
98
- "owner attributes" => owner_attributes,
99
- "inverse attributes" => inverses,
100
- "dependent attributes" => dependents_printer,
101
- "default values" => defaults
102
- }.delete_if { |key, value| value.nil_or_empty? }
103
-
104
- # one indented line per entry, all but the last line ending in a comma
105
- content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
106
- # print the content to the log
107
- q.text("#{qp} structure:\n#{content}")
108
- end
109
-
110
- protected
111
-
112
- def self.extend_class(klass, mod)
113
- klass.extend(self)
114
- klass.add_metadata(mod)
115
- end
116
-
117
- private
118
-
119
- # A proc to print the unqualified class name.
120
- TYPE_PRINTER = PrintWrapper.new { |type| type.qp }
121
-
122
- DEPENDENT_ATTR_PRINTER = PrintWrapper.new do |attr_md|
123
- flags = []
124
- flags << :logical if attr_md.logical?
125
- flags << :autogenerated if attr_md.autogenerated?
126
- flags << :disjoint if attr_md.disjoint?
127
- flags.empty? ? "#{attr_md}" : "#{attr_md}(#{flags.join(',')})"
128
- end
129
-
130
- # A proc to print the property descriptor name.
131
- PROP_DESC_PRINTER = PrintWrapper.new { |pd| pd.name }
132
-
133
- def format_print_value(value)
134
- case value
135
- when String then value
136
- when Class then value.qp
137
- else value.pp_s(:single_line)
138
- end
139
- end
140
- end
141
- end
142
- end
@@ -1,35 +0,0 @@
1
- require 'caruby/domain/metadata'
2
-
3
- module CaRuby
4
- module Domain
5
- # Mixin extends a module to add meta-data to included classes.
6
- module Mixin
7
- # Adds {Metadata} to an included class.
8
- #
9
- # @example
10
- # module CaRuby
11
- # module Resource
12
- # def self.included(mod)
13
- # mod.extend(Domain::Mixin)
14
- # end
15
- # end
16
- # end
17
- # module ClinicalTrials
18
- # module Resource
19
- # include CaRuby::Resource
20
- # end
21
- # class Subject
22
- # include Resource #=> introspects the Subject meta-data
23
- # end
24
- # end
25
- #
26
- # @param [Module] class_or_module the included module, usually a class
27
- def included(class_or_module)
28
- super
29
- if Class === class_or_module then
30
- Metadata.ensure_metadata_introspected(class_or_module)
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,95 +0,0 @@
1
- require 'caruby/util/properties'
2
- require 'caruby/util/collection'
3
- require 'caruby/util/options'
4
-
5
- module CaRuby
6
- module Domain
7
- # CaRuby::Domain::Properties specializes the generic CaRuby::Properties class for domain properties.
8
- class Properties < CaRuby::Properties
9
-
10
- # The application login userid environment variable.
11
- USER_ENV_VAR_SUFFIX = 'USER'
12
-
13
- # The application login password environment variable.
14
- PASSWORD_ENV_VAR_SUFFIX = 'PASSWORD'
15
-
16
- # The application service user property name.
17
- USER_PROP = :user
18
-
19
- # The application service password property name.
20
- PASSWORD_PROP = :password
21
-
22
- # The application Java jar location.
23
- PATH_PROP = :path
24
-
25
- attr_reader :application
26
-
27
- # Creates a new Properties.
28
- #
29
- # Supported options include the CaRuby::Properties options as well as the following:
30
- # * :application - the application name
31
- #
32
- # The application name is used as a prefix for application-specific upper-case environment variables
33
- # and lower-case file names, e.g. +CATISSUE_USER+ for the +caTissue+ application login username
34
- # environment variable. The default application name is +caBIG+.
35
- #
36
- # The user properties file is always loaded if it exists. This file's name is a period followed by the
37
- # lower-case application name, located in the home directory, e.g. +~/.catissue.yaml+ for application
38
- # +caTissue+.
39
- def initialize(file=nil, options=nil)
40
- @application = Options.get(:application, options, "caBIG")
41
- super(file, options)
42
- end
43
-
44
- # Loads the properties in the following low-to-high precedence order:
45
- # * the home file +.+_application_+.yaml+, where _application_ is the application name
46
- # * the given property file
47
- # * the environment variables
48
- def load_properties(file)
49
- # canonicalize the file path
50
- file = File.expand_path(file)
51
- # load the home properties file, if it exists
52
- user_file = File.expand_path("~/.#{@application.downcase}.yaml")
53
- super(user_file) if user_file != file and File.exists?(user_file)
54
- # load the given file
55
- super(file)
56
- # the environment variables take precedence
57
- load_environment_properties
58
- # validate the required properties
59
- validate_properties
60
- end
61
-
62
- private
63
-
64
- def load_environment_properties
65
- user = ENV[user_env_var]
66
- if user then
67
- self[USER_PROP] = user
68
- logger.info("#{@application} login user obtained from environment property #{user_env_var} value '#{user}'.")
69
- end
70
- password = ENV[password_env_var]
71
- if password then
72
- self[PASSWORD_PROP] = password
73
- logger.info("#{@application} login password obtained from environment property #{password_env_var} value.")
74
- end
75
- path = ENV[path_env_var]
76
- if path then
77
- self[PATH_PROP] = path
78
- logger.info("#{@application} Java library path obtained from environment property #{path_env_var} value '#{path}'.")
79
- end
80
- end
81
-
82
- def user_env_var
83
- "#{@application}_#{USER_PROP}".upcase
84
- end
85
-
86
- def password_env_var
87
- "#{@application}_#{PASSWORD_PROP}".upcase
88
- end
89
-
90
- def path_env_var
91
- "#{@application}_#{PATH_PROP}".upcase
92
- end
93
- end
94
- end
95
- end
@@ -1,428 +0,0 @@
1
- require 'enumerator'
2
- require 'generator'
3
- require 'caruby/util/options'
4
- require 'caruby/util/collection'
5
- require 'caruby/util/validation'
6
- require 'caruby/util/visitor'
7
- require 'caruby/util/math'
8
-
9
- module CaRuby
10
- # A ReferenceVisitor traverses reference attributes.
11
- class ReferenceVisitor < Visitor
12
- private
13
-
14
- # Flag to print a detailed debugging visit message
15
- DETAIL_DEBUG = false
16
-
17
- public
18
-
19
- attr_reader :ref_attr_hash
20
-
21
- # Creates a new ReferenceVisitor on domain reference attributes.
22
- #
23
- # If a selector block is given to this initializer, then the reference attributes to visit
24
- # are determined by calling the block. Otherwise, the {Domain::Attributes#saved_domain_attributes}
25
- # are visited.
26
- #
27
- # @param options (see Visitor#initialize)
28
- # @yield [ref] selects which attributes to visit next
29
- # @yieldparam [Resource] ref the currently visited domain object
30
- def initialize(options=nil, &selector)
31
- raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
32
- @selector = selector
33
- # delegate to Visitor with the visit selector block
34
- super { |parent| references_to_visit(parent) }
35
- # the visited reference => parent attribute hash
36
- @ref_attr_hash = {}
37
- # TODO - reconcile @excludes here with Visitor exclude.
38
- # refactor usage and interaction with prune_cycles.
39
- # eliminate if possible.
40
- @excludes = []
41
- end
42
-
43
- # @return [Symbol, nil] the parent attribute which was visited to get to the current visited domain object
44
- def attribute
45
- @ref_attr_hash[current]
46
- end
47
-
48
- # Excludes obj from the next visit.
49
- # Exclusions are cleared after visit is completed.
50
- def exclude(obj)
51
- @excludes << obj
52
- end
53
-
54
- # Performs Visitor::visit and clears exclusions.
55
- def visit(obj)
56
- if DETAIL_DEBUG then
57
- logger.debug { "Visiting #{obj.qp} from navigation path #{lineage.qp}..." }
58
- end
59
-
60
- # TODO - current, attribute and parent are nil when a value is expected.
61
- # Uncomment below, build test cases, analyze and fix.
62
- #
63
- # puts "Visit #{obj.qp} current: #{self.current.qp} parent: #{self.parent.qp} attribute: #{self.attribute}"
64
- # puts " lineage:#{lineage.qp}n attributes:#{@ref_attr_hash.qp}"
65
- # puts " reference => attribute hash: #{@ref_attr_hash.qp}"
66
-
67
- result = super
68
- @excludes.clear
69
- result
70
- end
71
-
72
- # Adds a default matcher block if necessary and delegates to {Visitor#sync}. The default matcher block
73
- # calls {Resource#match_in} to match the candidate domain objects to visit.
74
- #
75
- # @yield [ref, others] matches ref in others (optional)
76
- # @yieldparam [Resource] ref the domain object to match
77
- # @yieldparam [<Resource>] the candidates for matching ref
78
- def sync(&matcher)
79
- MatchVisitor.new(:matcher => matcher, &@selector)
80
- end
81
-
82
- protected
83
-
84
- def clear
85
- super
86
- @ref_attr_hash.clear
87
- end
88
-
89
- private
90
-
91
- # @param [Resource] parent the referencing domain object
92
- # @return [<Resource>] the domain attributes to visit next
93
- def attributes_to_visit(parent)
94
- @selector.call(parent)
95
- end
96
-
97
- # @param [Resource] parent the referencing domain object
98
- # @return [<Resource>] the referenced domain objects to visit next for the given parent
99
- def references_to_visit(parent)
100
- attrs = attributes_to_visit(parent)
101
- if attrs.nil? then return Array::EMPTY_ARRAY end
102
- refs = []
103
- attrs.each do | attr|
104
- # the reference(s) to visit
105
- value = parent.send(attr)
106
- # associate each reference to visit with the current visited attribute
107
- value.enumerate do |ref|
108
- @ref_attr_hash[ref] = attr
109
- refs << ref
110
- end
111
- end
112
- if DETAIL_DEBUG then
113
- logger.debug { "Visiting #{parent.qp} references: #{refs.qp}" }
114
- logger.debug { " lineage: #{lineage.qp}" }
115
- logger.debug { " attributes: #{attrs.qp}..." }
116
- end
117
-
118
- refs
119
- end
120
- end
121
-
122
- # A MatchVisitor visits two domain objects' visitable attributes transitive closure in lock-step.
123
- class MatchVisitor < ReferenceVisitor
124
-
125
- attr_reader :matches
126
-
127
- # Creates a new visitor which matches source and target domain object references.
128
- # The domain attributes to visit are determined by calling the selector block given to
129
- # this initializer. The selector arguments consist of the match source and target.
130
- #
131
- # @param (see ReferenceVisitor#initialize)
132
- # @option opts [Proc] :mergeable the block which determines which attributes are merged
133
- # @option opts [Proc] :matchable the block which determines which attributes to match
134
- # (default is the visit selector)
135
- # @option opts [Proc] :matcher the block which matches sources to targets
136
- # @option opts [Proc] :copier the block which copies an unmatched source
137
- # @yield (see ReferenceVisitor#initialize)
138
- # @yieldparam [Resource] source the matched source object
139
- def initialize(opts=nil)
140
- raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
141
- opts = Options.to_hash(opts)
142
- @matcher = opts.delete(:matcher) || Resource.method(:match_all)
143
- @matchable = opts.delete(:matchable)
144
- @copier = opts.delete(:copier)
145
- # the source => target matches
146
- @matches = {}
147
- # the class => {id => target} hash
148
- @id_mtchs = LazyHash.new { Hash.new }
149
- super { |src| yield(src) if @matches[src] }
150
- end
151
-
152
- # Visits the source and target.
153
- #
154
- # If a block is given to this method, then this method returns the evaluation of the block on the visited
155
- # source reference and its matching copy, if any. The default return value is the target which matches
156
- # source.
157
- #
158
- # caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
159
- # with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
160
- # where _a.identifier_ == _a'.identifier_. This visit method remedies this caCORE defect by matching
161
- # source references on a previously matched identifier where possible.
162
- #
163
- # @param [Resource] source the match visit source
164
- # @param [Resource] target the match visit target
165
- # @yield [target, source] the optional block to call on the matched source and target
166
- # @yieldparam [Resource] source the visited source domain object
167
- # @yieldparam [Resource] target the domain object which matches the visited source
168
- def visit(source, target, &block)
169
- # clear the match hashes
170
- @matches.clear
171
- @id_mtchs.clear
172
- # seed the matches with the top-level source => target
173
- add_match(source, target)
174
- # visit the source reference. the visit block merges each source reference into
175
- # the matching target reference.
176
- super(source) { |src| visit_matched(src, &block) }
177
- end
178
-
179
- private
180
-
181
- # Visits the given source domain object.
182
- #
183
- # @param [Resource] source the match visit source
184
- # @yield [target, source] the optional block to call on the matched source and target
185
- # @yieldparam [Resource] source the visited source domain object
186
- # @yieldparam [Resource] target the domain object which matches the visited source
187
- def visit_matched(source)
188
- tgt = match_for_visited(source)
189
- # match the matchable references, if any
190
- if @matchable then
191
- attrs = @matchable.call(source) - attributes_to_visit(source)
192
- attrs.each { |attr| match_reference(source, tgt, attr) }
193
- end
194
- block_given? ? yield(source, tgt) : tgt
195
- end
196
-
197
- # @param source (see #match_visited)
198
- # @return [<Resource>] the domain objects referenced by the source to visit next
199
- def references_to_visit(source)
200
- # the source match
201
- target = match_for_visited(source)
202
- # the attributes to visit
203
- attrs = attributes_to_visit(source)
204
- # the matched source references
205
- match_references(source, target, attrs).keys
206
- end
207
-
208
- # @param source (see #match_visited)
209
- # @return [<Resource>] the source match
210
- # @raise [ValidationError] if there is no match
211
- def match_for_visited(source)
212
- target = @matches[source]
213
- if target.nil? then raise ValidationError.new("Match visitor target not found for #{source}") end
214
- target
215
- end
216
-
217
- # @param [Resource] source (see #match_visited)
218
- # @param [Resource] target the source match
219
- # @param [<Symbol>] attributes the attributes to match on
220
- # @return [{Resource => Resource}] the referenced attribute matches
221
- def match_references(source, target, attributes)
222
- # collect the references to visit
223
- matches = {}
224
- attributes.each do |attr|
225
- matches.merge!(match_reference(source, target, attr))
226
- end
227
- matches
228
- end
229
-
230
- # Matches the given source and target attribute references.
231
- # The match is performed by this visitor's matcher Proc.
232
- #
233
- # @param source (see #visit)
234
- # @param target (see #visit)
235
- # @return [{Resource => Resource}] the referenced source => target matches
236
- def match_reference(source, target, attribute)
237
- srcs = source.send(attribute).to_enum
238
- tgts = target.send(attribute).to_enum
239
-
240
- # the match targets
241
- mtchd_tgts = Set.new
242
- # capture the matched targets and the the unmatched sources
243
- unmtchd_srcs = srcs.reject do |src|
244
- # the prior match, if any
245
- tgt = match_for(src)
246
- mtchd_tgts << tgt if tgt
247
- end
248
-
249
- # the unmatched targets
250
- unmtchd_tgts = tgts.difference(mtchd_tgts)
251
- # match the residual targets and sources
252
- rsd_mtchs = @matcher.call(unmtchd_srcs, unmtchd_tgts)
253
- # add residual matches
254
- rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
255
-
256
- # The source => target match hash.
257
- # If there is a copier, then copy each unmatched source.
258
- matches = srcs.to_compact_hash { |src| match_for(src) or copy_unmatched(src) }
259
- logger.debug { "Match visitor matched #{matches.qp}." } unless matches.empty?
260
-
261
- matches
262
- end
263
-
264
- # @return the target matching the given source
265
- def match_for(source)
266
- @matches[source] or identifier_match(source)
267
- end
268
-
269
- def add_match(source, target)
270
- @matches[source] = target
271
- @id_mtchs[source.class][source.identifier] = target if source.identifier
272
- target
273
- end
274
-
275
- # @return the target matching the given source on the identifier, if any
276
- def identifier_match(source)
277
- tgt = @id_mtchs[source.class][source.identifier] if source.identifier
278
- @matches[source] = tgt if tgt
279
- end
280
-
281
- # @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier,
282
- # nil otherwise
283
- def copy_unmatched(source)
284
- return unless @copier
285
- copy = @copier.call(source)
286
- add_match(source, copy)
287
- end
288
- end
289
-
290
- # A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
291
- class MergeVisitor < MatchVisitor
292
- # Creates a new MergeVisitor on domain attributes.
293
- # The domain attributes to visit are determined by calling the selector block given to
294
- # this initializer as described in {ReferenceVisitor#initialize}.
295
- #
296
- # @param (see MatchVisitor#initialize)
297
- # @option opts [Proc] :mergeable the block which determines which attributes are merged
298
- # @option opts [Proc] :matcher the block which matches sources to targets
299
- # @option opts [Proc] :copier the block which copies an unmatched source
300
- # @yield (see MatchVisitor#initialize)
301
- # @yieldparam (see MatchVisitor#initialize)
302
- def initialize(opts=nil, &selector)
303
- opts = Options.to_hash(opts)
304
- # Merge is depth-first, since the source references must be matched, and created if necessary,
305
- # before they can be merged into the target.
306
- opts[:depth_first] = true
307
- @mergeable = opts.delete(:mergeable) || selector
308
- # each mergeable attribute is matchable
309
- unless @mergeable == selector then
310
- opts[:matchable] = @mergeable
311
- end
312
- super
313
- end
314
-
315
- # Visits the source and target and returns a recursive copy of obj and each of its visitable references.
316
- #
317
- # If a block is given to this method, then this method returns the evaluation of the block on the visited
318
- # source reference and its matching copy, if any. The default return value is the target which matches
319
- # source.
320
- #
321
- # caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
322
- # with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
323
- # where _a.identifier_ == _a'.identifier_. This visit method remedies the caCORE defect by matching source
324
- # references on a previously matched identifier where possible.
325
- #
326
- # @param [Resource] source the domain object to merge from
327
- # @param [Resource] target the domain object to merge into
328
- # @yield [target, source] the optional block to call on the visited source domain object and its matching target
329
- # @yieldparam [Resource] target the domain object which matches the visited source
330
- # @yieldparam [Resource] source the visited source domain object
331
- def visit(source, target)
332
- # visit the source reference. the visit block merges each source reference into
333
- # the matching target reference.
334
- super(source, target) do |src, tgt|
335
- merge(src, tgt)
336
- block_given? ? yield(src, tgt) : tgt
337
- end
338
- end
339
-
340
- private
341
-
342
- # Merges the given source object into the target object.
343
- #
344
- # @param [Resource] source the domain object to merge from
345
- # @param [Resource] target the domain object to merge into
346
- # @return [Resource] the merged target
347
- def merge(source, target)
348
- # trivial case
349
- return target if source.equal?(target)
350
- # the domain attributes to merge
351
- attrs = @mergeable.call(source)
352
- logger.debug { format_merge_log_message(source, target, attrs) }
353
- # merge the non-domain attributes
354
- target.merge_attributes(source)
355
- # merge the source domain attributes into the target
356
- target.merge(source, attrs, @matches)
357
- end
358
-
359
- # @param source (see #merge)
360
- # @param target (see #merge)
361
- # @param attributes (see Mergeable#merge)
362
- # @return [String] the log message
363
- def format_merge_log_message(source, target, attributes)
364
- attr_clause = " including domain attributes #{attributes.to_series}" unless attributes.empty?
365
- "Merging #{source.qp} into #{target.qp}#{attr_clause}..."
366
- end
367
- end
368
-
369
- # A CopyVisitor copies a domain object's visitable attributes transitive closure.
370
- class CopyVisitor < MergeVisitor
371
- # Creates a new CopyVisitor with the options described in {MergeVisitor#initialize}.
372
- # The default :copier option is {Resource#copy}.
373
- #
374
- # @param (see MergeVisitor#initialize)
375
- # @option opts [Proc] :mergeable the mergeable domain attribute selector
376
- # @option opts [Proc] :matcher the match block
377
- # @option opts [Proc] :copier the unmatched source copy block
378
- # @yield (see MergeVisitor#initialize)
379
- # @yieldparam (see MergeVisitor#initialize)
380
- def initialize(opts=nil)
381
- opts = Options.to_hash(opts)
382
- opts[:copier] ||= Proc.new { |src| src.copy }
383
- # no match forces a copy
384
- opts[:matcher] = Proc.new { Hash::EMPTY_HASH }
385
- super
386
- end
387
-
388
- # Visits obj and returns a recursive copy of obj and each of its visitable references.
389
- #
390
- # If a block is given to this method, then the block is called with the visited
391
- # source reference and its matching copy target.
392
- #
393
- # @param (see MergeVisitor#visit)
394
- # @yield (see MergeVisitor#visit)
395
- # @yieldparam (see MergeVisitor#visit)
396
- def visit(source)
397
- target = @copier.call(source)
398
- super(source, target)
399
- end
400
- end
401
-
402
- # A ReferencePathVisitorFactory creates a ReferenceVisitor that traverses an attributes path.
403
- #
404
- # For example, given the attributes:
405
- # treatment: BioMaterial -> Treatment
406
- # measurement: Treatment -> BioMaterial
407
- # then a path visitor given by:
408
- # ReferencePathVisitorFactory.create(BioMaterial, [:treatment, :measurement])
409
- # visits all biomaterial, treatments and measurements derived directly or indirectly from a starting BioMaterial instance.
410
- class ReferencePathVisitorFactory
411
- # @return a new ReferenceVisitor that visits the given path attributes starting at an instance of type
412
- def self.create(type, attributes, options=nil)
413
- # augment the attributes path as a [class, attribute] path
414
- path = []
415
- attributes.each do |attr|
416
- path << [type, attr]
417
- type = type.domain_type(attr)
418
- end
419
-
420
- # make the visitor
421
- visitor = ReferenceVisitor.new(options) do |ref|
422
- # collect the path reference attributes whose source match the ref type up to the next position in the path
423
- max = visitor.lineage.size.min(path.size)
424
- (0...max).map { |i| path[i].last if ref.class == path[i].first }.compact
425
- end
426
- end
427
- end
428
- end