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.
- data/Gemfile +9 -0
- data/History.md +5 -1
- data/lib/caruby.rb +3 -5
- data/lib/caruby/caruby-src.tar.gz +0 -0
- data/lib/caruby/database.rb +53 -69
- data/lib/caruby/database/application_service.rb +25 -0
- data/lib/caruby/database/cache.rb +60 -0
- data/lib/caruby/database/fetched_matcher.rb +52 -38
- data/lib/caruby/database/lazy_loader.rb +4 -4
- data/lib/caruby/database/operation.rb +34 -0
- data/lib/caruby/database/persistable.rb +171 -86
- data/lib/caruby/database/persistence_service.rb +32 -34
- data/lib/caruby/database/persistifier.rb +100 -43
- data/lib/caruby/database/reader.rb +107 -85
- data/lib/caruby/database/reader_template_builder.rb +60 -0
- data/lib/caruby/database/saved_matcher.rb +3 -3
- data/lib/caruby/database/sql_executor.rb +88 -17
- data/lib/caruby/database/writer.rb +213 -177
- data/lib/caruby/database/writer_template_builder.rb +334 -0
- data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
- data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
- data/lib/caruby/{util → helpers}/person.rb +3 -3
- data/lib/caruby/{util → helpers}/properties.rb +7 -9
- data/lib/caruby/{util → helpers}/roman.rb +2 -2
- data/lib/caruby/{util → helpers}/version.rb +1 -1
- data/lib/caruby/json/deserializer.rb +2 -2
- data/lib/caruby/json/serializer.rb +49 -7
- data/lib/caruby/metadata.rb +30 -0
- data/lib/caruby/metadata/java_property.rb +21 -0
- data/lib/caruby/metadata/propertied.rb +191 -0
- data/lib/caruby/metadata/property.rb +22 -0
- data/lib/caruby/metadata/property_characteristics.rb +201 -0
- data/lib/caruby/migration/migratable.rb +11 -182
- data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
- data/lib/caruby/resource.rb +20 -823
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/cache_test.rb +54 -0
- data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
- data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
- data/test/lib/caruby/helpers/properties_test.rb +34 -0
- data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
- data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
- data/test/lib/helper.rb +7 -0
- metadata +161 -214
- data/lib/caruby/cli/application.rb +0 -36
- data/lib/caruby/cli/command.rb +0 -202
- data/lib/caruby/csv/csv_mapper.rb +0 -159
- data/lib/caruby/csv/csvio.rb +0 -203
- data/lib/caruby/database/search_template_builder.rb +0 -56
- data/lib/caruby/database/store_template_builder.rb +0 -278
- data/lib/caruby/domain.rb +0 -193
- data/lib/caruby/domain/attribute.rb +0 -584
- data/lib/caruby/domain/attributes.rb +0 -628
- data/lib/caruby/domain/dependency.rb +0 -225
- data/lib/caruby/domain/id_alias.rb +0 -22
- data/lib/caruby/domain/importer.rb +0 -183
- data/lib/caruby/domain/introspection.rb +0 -176
- data/lib/caruby/domain/inverse.rb +0 -172
- data/lib/caruby/domain/inversible.rb +0 -90
- data/lib/caruby/domain/java_attribute.rb +0 -173
- data/lib/caruby/domain/merge.rb +0 -185
- data/lib/caruby/domain/metadata.rb +0 -142
- data/lib/caruby/domain/mixin.rb +0 -35
- data/lib/caruby/domain/properties.rb +0 -95
- data/lib/caruby/domain/reference_visitor.rb +0 -428
- data/lib/caruby/domain/uniquify.rb +0 -50
- data/lib/caruby/import/java.rb +0 -387
- data/lib/caruby/migration/migrator.rb +0 -918
- data/lib/caruby/migration/resource_module.rb +0 -9
- data/lib/caruby/migration/uniquify.rb +0 -17
- data/lib/caruby/util/attribute_path.rb +0 -44
- data/lib/caruby/util/cache.rb +0 -56
- data/lib/caruby/util/class.rb +0 -149
- data/lib/caruby/util/collection.rb +0 -1152
- data/lib/caruby/util/domain_extent.rb +0 -46
- data/lib/caruby/util/file_separator.rb +0 -65
- data/lib/caruby/util/inflector.rb +0 -27
- data/lib/caruby/util/log.rb +0 -95
- data/lib/caruby/util/math.rb +0 -12
- data/lib/caruby/util/merge.rb +0 -59
- data/lib/caruby/util/module.rb +0 -18
- data/lib/caruby/util/options.rb +0 -97
- data/lib/caruby/util/partial_order.rb +0 -35
- data/lib/caruby/util/pretty_print.rb +0 -204
- data/lib/caruby/util/stopwatch.rb +0 -74
- data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
- data/lib/caruby/util/transitive_closure.rb +0 -55
- data/lib/caruby/util/tree.rb +0 -48
- data/lib/caruby/util/trie.rb +0 -37
- data/lib/caruby/util/uniquifier.rb +0 -30
- data/lib/caruby/util/validation.rb +0 -20
- data/lib/caruby/util/visitor.rb +0 -365
- data/lib/caruby/util/weak_hash.rb +0 -36
- data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
- data/test/lib/caruby/csv/csvio_test.rb +0 -69
- data/test/lib/caruby/database/persistable_test.rb +0 -92
- data/test/lib/caruby/domain/domain_test.rb +0 -112
- data/test/lib/caruby/domain/inversible_test.rb +0 -99
- data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
- data/test/lib/caruby/import/java_test.rb +0 -80
- data/test/lib/caruby/import/mixed_case_test.rb +0 -14
- data/test/lib/caruby/migration/test_case.rb +0 -102
- data/test/lib/caruby/test_case.rb +0 -230
- data/test/lib/caruby/util/cache_test.rb +0 -23
- data/test/lib/caruby/util/class_test.rb +0 -61
- data/test/lib/caruby/util/collection_test.rb +0 -398
- data/test/lib/caruby/util/command_test.rb +0 -55
- data/test/lib/caruby/util/domain_extent_test.rb +0 -60
- data/test/lib/caruby/util/file_separator_test.rb +0 -30
- data/test/lib/caruby/util/inflector_test.rb +0 -12
- data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
- data/test/lib/caruby/util/merge_test.rb +0 -83
- data/test/lib/caruby/util/module_test.rb +0 -25
- data/test/lib/caruby/util/options_test.rb +0 -59
- data/test/lib/caruby/util/partial_order_test.rb +0 -42
- data/test/lib/caruby/util/pretty_print_test.rb +0 -85
- data/test/lib/caruby/util/properties_test.rb +0 -50
- data/test/lib/caruby/util/stopwatch_test.rb +0 -18
- data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
- data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
- data/test/lib/caruby/util/tree_test.rb +0 -23
- data/test/lib/caruby/util/trie_test.rb +0 -14
- data/test/lib/caruby/util/visitor_test.rb +0 -278
- data/test/lib/caruby/util/weak_hash_test.rb +0 -45
- data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
- data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -0,0 +1,334 @@
|
|
1
|
+
require 'jinx/resource/reference_visitor'
|
2
|
+
require 'jinx/resource/copy_visitor'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
class Database
|
6
|
+
module Writer
|
7
|
+
# TemplateBuilder creates a template suitable for a database save operation.
|
8
|
+
class TemplateBuilder
|
9
|
+
# Creates a new TemplateBuilder for the given database. The attributes to merge into
|
10
|
+
# the template are determined by the block given to this initializer, filtered as follows:
|
11
|
+
# * If the save operation is a create, then exclude the auto-generated attributes.
|
12
|
+
# * If the visited object has an identifier, then include only those attributes
|
13
|
+
# which {Domain::Property#cascade_update_to_create?} or have an identifier.
|
14
|
+
#
|
15
|
+
# @param [Database] database the target database
|
16
|
+
# @yield [ref] the required selector block which determines which attributes are copied into the template
|
17
|
+
# @yieldparam [Jinx::Resource] ref the domain object to copy
|
18
|
+
def initialize(database)
|
19
|
+
@database = database
|
20
|
+
unless block_given? then
|
21
|
+
Jinx.fail(ArgumentError, "@{qp} is missing the required template copy attribute selector block")
|
22
|
+
end
|
23
|
+
|
24
|
+
# the mergeable attributes filter the given block with exclusions
|
25
|
+
@mergeable = Proc.new { |ref| mergeable_attributes(ref, yield(ref)) }
|
26
|
+
# the savable prerequisite reference visitor
|
27
|
+
@prereq_vstr = Jinx::ReferenceVisitor.new(:prune_cycle) { |ref| savable_template_attributes(ref) }
|
28
|
+
|
29
|
+
# the savable attributes filter the given block with exclusions
|
30
|
+
savable = Proc.new { |ref| savable_attributes(ref, yield(ref)) }
|
31
|
+
# the domain attributes to copy is determined by the constructor caller
|
32
|
+
# @quirk caTissue must copy all of the non-domain attributes rather than just the identifier,
|
33
|
+
# since caTissue auto-generated Specimen update requires the parent collection status. This
|
34
|
+
# is the only known occurrence of a referenced object required non-identifier attribute.
|
35
|
+
# The copy attributes are parameterized by the top-level save target.
|
36
|
+
copier = Proc.new do |src|
|
37
|
+
tgt = src.copy
|
38
|
+
logger.debug { "Store template builder copied #{src.qp} into #{tgt}." }
|
39
|
+
copy_proxied_save_references(src, tgt)
|
40
|
+
tgt
|
41
|
+
end
|
42
|
+
# the template copier
|
43
|
+
@copy_vstr = Jinx::CopyVisitor.new(:mergeable => savable, :copier => copier) do |ref|
|
44
|
+
savable_template_attributes(ref)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns a new domain object which serves as the argument for obj create or update.
|
49
|
+
#
|
50
|
+
# This method copies a portion of the obj object graph to a template object.
|
51
|
+
# The template object graph consists of copies of obj object graph which are necessary
|
52
|
+
# to store obj. The template object graph contains only those references which are
|
53
|
+
# essential to the store operation.
|
54
|
+
#
|
55
|
+
# @quirk caCORE +caCORE+ expects the store argument to be carefully prepared prior to
|
56
|
+
# the create or update. build_savable_template culls the target object with a template
|
57
|
+
# which includes only those references which are necessary for the store to succeed.
|
58
|
+
# This template builder ensures that mandatory independent references exist. Cascaded
|
59
|
+
# dependent references are included in the template but are not created before submission
|
60
|
+
# to +caCORE+. These reference attribute distinctions are implicit application rules which
|
61
|
+
# are explicated in the +caRuby+ application domain class definition using Metadata
|
62
|
+
# methods.
|
63
|
+
#
|
64
|
+
# @quirk caCORE +caCORE+ create issues an error if a create argument directly or
|
65
|
+
# indirectly references a non-cascaded domain object without an identifier, even if the
|
66
|
+
# reference is not relevant to the create. The template returned by this method elides
|
67
|
+
# all non-essential references.
|
68
|
+
#
|
69
|
+
# @quirk caCORE application business logic performs unnecessary verification
|
70
|
+
# of uncascaded references as if they were a cascaded create. This can result in
|
71
|
+
# an obscure ApplicationException. The server.log stack trace indicates the
|
72
|
+
# extraneous verification code. For example, +caTissue+ +NewSpecimenBizLogic.validateStorageContainer+
|
73
|
+
# is unnecessarily called on a SpecimenCollectionGroup (SCG) update. SCG does not
|
74
|
+
# cascade to Specimen, but caTissue considers the SCG update a Specimen create
|
75
|
+
# anyway if the SCG references a Specimen without an identifier. The Specimen
|
76
|
+
# business logic then raises an exception when it finds a StorageContainer
|
77
|
+
# without an identifier in the Specimen object graph. Therefore, an update must
|
78
|
+
# build a savable template which prunes the update object graph to exclude uncascaded
|
79
|
+
# objects. These uncascaded objects should be ignored by the application but aren't.
|
80
|
+
#
|
81
|
+
# @param [Jinx::Resource] obj the domain object to save
|
82
|
+
# @return [Jinx::Resource] the template to use as the caCORE argument
|
83
|
+
def build_template(obj, autogenerated=false)
|
84
|
+
# set the database operation subject
|
85
|
+
@subject = obj
|
86
|
+
# prepare the object for a store operation
|
87
|
+
ensure_savable(obj)
|
88
|
+
# copy the cascade hierarchy
|
89
|
+
logger.debug { "Building savable template for #{obj.qp}..." }
|
90
|
+
tmpl = @copy_vstr.visit(obj)
|
91
|
+
logger.debug { "Built #{obj.qp} template #{tmpl.qp} by mapping references #{@copy_vstr.matches.qp}" }
|
92
|
+
tmpl
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Ensure that the given domain object can be created or updated by setting the identifier for
|
98
|
+
# each independent reference in the create template object graph.
|
99
|
+
#
|
100
|
+
# @quirk caCORE +caCORE+ raises an +ApplicationException+ if an independent reference in the
|
101
|
+
# save argument does not have an identifier. The +caCORE+ server log error is as follows:
|
102
|
+
# java.lang.IllegalArgumentException: id to load is required for loading
|
103
|
+
# The server log stack trace indicates a bizlogic line that offers a clue to the offending
|
104
|
+
# reference. caRuby determines which references are independent based on the introspected
|
105
|
+
# metadata and creates them if necessary.
|
106
|
+
#
|
107
|
+
# @param [Jinx::Resource] obj the object to save
|
108
|
+
# @raise [ValidationError] if the object is invalid
|
109
|
+
def ensure_savable(obj)
|
110
|
+
# Ensure that an update is fetched to complete the object state with any missing references.
|
111
|
+
if obj.identifier and not obj.fetched? then
|
112
|
+
logger.debug { "Fetching #{obj} prior to building a save template..." }
|
113
|
+
@database.find(obj)
|
114
|
+
end
|
115
|
+
# Fill in the savable references for an update.
|
116
|
+
fetch_savable_references(obj) if obj.identifier
|
117
|
+
# Add defaults, which might introduce prerequisites.
|
118
|
+
obj.add_defaults
|
119
|
+
# Fetch or create if necessary the prerequisite references.
|
120
|
+
prereqs = collect_prerequisites(obj)
|
121
|
+
unless prereqs.empty? then
|
122
|
+
logger.debug { "Ensuring references for #{obj.qp} exist: #{prereqs.map { |ref| ref.qp }.to_series}..." }
|
123
|
+
@database.ensure_exists(prereqs)
|
124
|
+
logger.debug { "Prerequisite references for #{obj.qp} exist: #{prereqs.map { |ref| ref }.to_series}." }
|
125
|
+
end
|
126
|
+
# Verify that the object is complete
|
127
|
+
obj.validate
|
128
|
+
end
|
129
|
+
|
130
|
+
# Fetches the given update object savable references on demand.
|
131
|
+
#
|
132
|
+
# @param [Resource] the object to update
|
133
|
+
def fetch_savable_references(obj)
|
134
|
+
logger.debug { "Fetching #{obj} savable references on demand..." }
|
135
|
+
@database.lazy_loader.enable do
|
136
|
+
obj.class.savable_template_attributes.each { |pa| obj.send(pa) }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the attributes to visit in building the template for the given
|
141
|
+
# domain object. The visitable attributes consist of the following:
|
142
|
+
# * The {Propertied#unproxied_savable_template_attributes} filtered as follows:
|
143
|
+
# * If the database operation is a create, then exclude the cascaded attributes.
|
144
|
+
# * If the given object has an identifier, then exclude the attributes which
|
145
|
+
# have the the :no_cascade_update_to_create flag set.
|
146
|
+
# * The {Propertied#proxied_savable_template_attributes} are included if and
|
147
|
+
# only if every referenced object has an identifier, and therefore does not
|
148
|
+
# need to be proxied.
|
149
|
+
#
|
150
|
+
# @quirk caTissue caTissue ignores some references, e.g. Participant CPR, and auto-generates
|
151
|
+
# the values instead. Therefore, the create template builder excludes these auto-generated
|
152
|
+
# attributes. After the create, the auto-generated references are merged into the created
|
153
|
+
# object graph and the references are updated if necessary.
|
154
|
+
#
|
155
|
+
# @param [Jinx::Resource] obj the domain object copied to the update template
|
156
|
+
# @return [<Symbol>] the reference attributes to include in the update template
|
157
|
+
def savable_template_attributes(obj)
|
158
|
+
# The starting set of candidate attributes is the unproxied cascaded references.
|
159
|
+
unproxied = savable_attributes(obj, obj.class.unproxied_savable_template_attributes)
|
160
|
+
# The proxied attributes to save.
|
161
|
+
proxied = savable_proxied_attributes(obj)
|
162
|
+
# The combined set of savable attributes
|
163
|
+
if proxied.empty? then
|
164
|
+
# This is almost always the code path.
|
165
|
+
unproxied
|
166
|
+
else
|
167
|
+
sas = unproxied.to_set.merge(proxied)
|
168
|
+
obj.class.domain_attributes.compose { |prop| sas.include?(prop.attribute) }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Filters the given attributes, if necessary, to exclude attributes as follows:
|
173
|
+
# * If the save operation is a create, then exclude the
|
174
|
+
# {Domain::Property#autogenerated?} attributes.
|
175
|
+
#
|
176
|
+
# @param [Jinx::Resource] obj the visited domain object
|
177
|
+
# @param [Propertied::Filter] the savable attribute filter
|
178
|
+
# @return [Propertied::Filter] the composed attribute filter
|
179
|
+
def mergeable_attributes(obj, attributes)
|
180
|
+
# If this is an update, then there is no filter on the given attributes.
|
181
|
+
return attributes if @subject.identifier
|
182
|
+
# This is a create: ignore the optional auto-generated attributes.
|
183
|
+
mas = obj.mandatory_attributes.to_set
|
184
|
+
attributes.compose { |pa| mas.include?(pa.to_sym) or not pa.autogenerated? }
|
185
|
+
end
|
186
|
+
|
187
|
+
# Composes the given attributes, if necessary, to exclude attributes as follows:
|
188
|
+
# * If the save operation is a create, then exclude the auto-generated attributes
|
189
|
+
# and independent attributes with unsaved references.
|
190
|
+
# * If the visited object has an identifier, then include only those attributes
|
191
|
+
# which {Domain::Property#cascade_update_to_create?} or have an identifier.
|
192
|
+
#
|
193
|
+
# @param (see #mergeable_attributes)
|
194
|
+
# @return (see #mergeable_attributes)
|
195
|
+
def savable_attributes(obj, attributes)
|
196
|
+
mgbl = mergeable_attributes(obj, attributes)
|
197
|
+
if obj.identifier.nil? then
|
198
|
+
mgbl.compose do |prop|
|
199
|
+
not bidirectional_unsaved_independent_collection?(obj, prop)
|
200
|
+
end
|
201
|
+
else
|
202
|
+
# The currently visited object is an update: include attributes which
|
203
|
+
# either cascade update to create or have saved references.
|
204
|
+
mgbl.compose do |prop|
|
205
|
+
prop.cascade_update_to_create? or Persistable.saved?(obj.send(prop.attribute))
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def bidirectional_unsaved_independent_collection?(obj, property)
|
211
|
+
property.collection? and
|
212
|
+
property.independent? and
|
213
|
+
property.bidirectional? and
|
214
|
+
Persistable.unsaved?(obj.send(property.attribute))
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns the proxied attributes to save. A proxied attribute is included only if the proxied
|
218
|
+
# dependents have an identifier, since those without an identifer are created separately via
|
219
|
+
# the proxy.
|
220
|
+
#
|
221
|
+
# @param [Jinx::Resource] obj the visited domain object
|
222
|
+
# @return [Jinx::AttributeEnumerator] the proxied cascaded attributes with an unsaved reference
|
223
|
+
def savable_proxied_attributes(obj)
|
224
|
+
# Include a proxied reference only if the proxied dependents have an identifier,
|
225
|
+
# since those without an identifer are created separately via the proxy.
|
226
|
+
obj.class.proxied_savable_template_attributes.filter do |pa|
|
227
|
+
ref = obj.send(pa)
|
228
|
+
case ref
|
229
|
+
when Enumerable then ref.all? { |dep| dep.identifier }
|
230
|
+
when Jinx::Resource then ref.identifier
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Copies proxied references as needed.
|
236
|
+
#
|
237
|
+
# @quirk caTissue even though Specimen save cascades to SpecimenPosition,
|
238
|
+
# SpecimenPosition cannot be updated directly. Rather than simply not
|
239
|
+
# cascading to the SpecimenPosition, caTissue checks a Specimen save argument
|
240
|
+
# to ensure that the SpecimenPosition reflects the current database state
|
241
|
+
# rather than the desired cascaded state. Play along with this bizarre
|
242
|
+
# mechanism by adding our own bizarre work-around mechanism to copy a
|
243
|
+
# proxied reference only if it has an identifier. This works only because
|
244
|
+
# another work-around in the #{Writer} updates proxied
|
245
|
+
# references via the proxy create before building the update template.
|
246
|
+
def copy_proxied_save_references(obj, template)
|
247
|
+
return unless obj.identifier
|
248
|
+
obj.class.proxied_savable_template_attributes.each do |pa|
|
249
|
+
# the proxy source
|
250
|
+
ref = obj.send(pa)
|
251
|
+
case ref
|
252
|
+
when Enumerable then
|
253
|
+
# recurse on the source collection
|
254
|
+
coll = template.send(pa)
|
255
|
+
ref.each do |dep|
|
256
|
+
copy = copy_proxied_save_reference(obj, pa, template, dep)
|
257
|
+
coll << copy if copy
|
258
|
+
end
|
259
|
+
when Jinx::Resource then
|
260
|
+
# copy the source
|
261
|
+
copy = copy_proxied_save_reference(obj, pa, template, ref)
|
262
|
+
# set the attribute to the copy
|
263
|
+
template.set_property_value(pa, copy) if copy
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Copies a proxied reference.
|
269
|
+
#
|
270
|
+
# @return [Jinx::Resource, nil] the copy, or nil if no copy is made
|
271
|
+
def copy_proxied_save_reference(obj, attribute, template, proxied)
|
272
|
+
# only copy an existing proxied
|
273
|
+
return unless proxied.identifier
|
274
|
+
# the proxied attribute => value hash
|
275
|
+
vh = proxied.value_hash
|
276
|
+
# map references to either the copied owner or a new copy of the reference
|
277
|
+
tvh = vh.transform_value { |v| Jinx::Resource === v ? (v == obj ? template : v.copy) : v }
|
278
|
+
# the copy with the adjusted values
|
279
|
+
copy = proxied.class.new.merge_attributes(tvh)
|
280
|
+
logger.debug { "Created #{obj.qp} proxied #{attribute} save template copy #{proxied.pp_s}." }
|
281
|
+
copy
|
282
|
+
end
|
283
|
+
|
284
|
+
# A directly or indirectly referenced object is a target object save prerequisite if
|
285
|
+
# none of the following is true:
|
286
|
+
# * it is cascaded and will be created when the target is saved
|
287
|
+
# * it is a bidirectional unsaved independent collection reference
|
288
|
+
# * it is the target object
|
289
|
+
# * it has an identifier
|
290
|
+
# * it is in an immediate or recursive dependent of the target object
|
291
|
+
# * the current save operation is in the context of creating the referenced object
|
292
|
+
#
|
293
|
+
# @param [Jinx::Resource] obj the domain object to save
|
294
|
+
# @return [<Jinx::Resource>] the references which must be created in order to store the object
|
295
|
+
def collect_prerequisites(obj)
|
296
|
+
prereqs = Set.new
|
297
|
+
# visit the cascaded attributes
|
298
|
+
@prereq_vstr.visit(obj) do |pref|
|
299
|
+
# Check each mergeable attribute for prerequisites. The mergeable attributes includes
|
300
|
+
# both cascaded and independent attributes. The selection block filters for independent
|
301
|
+
# domain objects which don't have an identifier.
|
302
|
+
@mergeable.call(pref).each_pair do |ma, mp|
|
303
|
+
# Cascaded attributes are not prerequisite, since they are created when the owner is
|
304
|
+
# created. The exception is if the owner will be updated but the dependent will not
|
305
|
+
# be created upon owner update and the dependent does not reference the owner.
|
306
|
+
# In that case, create the dependent before saving the owner. This ensures that the
|
307
|
+
# owner saves the reference to the new dependent.
|
308
|
+
#
|
309
|
+
# Note that each non-prerequisite cascaded reference is still visited in order to ensure
|
310
|
+
# that each independent object referenced by a cascaded reference is recognized as a
|
311
|
+
# candidate prerequisite.
|
312
|
+
if mp.cascaded? then
|
313
|
+
unless pref.identifier.nil? or mp.inverse_property or mp.cascade_update_to_create? then
|
314
|
+
pref.send(ma).enumerate do |mref|
|
315
|
+
prereqs << mref if mref != obj and mref.identifier.nil?
|
316
|
+
end
|
317
|
+
end
|
318
|
+
elsif not bidirectional_unsaved_independent_collection?(pref, mp) then
|
319
|
+
# add qualified prerequisite attribute references
|
320
|
+
pref.send(ma).enumerate do |mref|
|
321
|
+
# Add each unsaved reference that is not a direct or indirect dependent.
|
322
|
+
unless mref == obj or mref.identifier or mref.owner_ancestor?(obj) then
|
323
|
+
prereqs << mref
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
prereqs
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
File without changes
|
@@ -83,13 +83,13 @@ class Coordinate < Array
|
|
83
83
|
# @raise [TypeError] if other is not a Coordinate
|
84
84
|
def <=>(other)
|
85
85
|
return 0 if equal?(other)
|
86
|
-
|
87
|
-
|
86
|
+
Jinx.fail(TypeError, "Can't compare #{self} with #{other} since it is not a Coordinate") unless Coordinate === other
|
87
|
+
Jinx.fail(ArgumentError, "Can't compare #{self} with #{other} since it has a different dimension count") unless size == other.size
|
88
88
|
REXML::SyncEnumerator.new(self.reverse, other.reverse).each_with_index do |pair, index|
|
89
89
|
dim = pair.first
|
90
90
|
odim = pair.last
|
91
|
-
|
92
|
-
|
91
|
+
Jinx.fail(ArgumentError, "Can't compare #{self} with missing dimension #{index} to #{other}") unless dim
|
92
|
+
Jinx.fail(ArgumentError, "Can't compare #{self} to #{other} with missing dimension #{index}") unless odim
|
93
93
|
cmp = dim <=> odim
|
94
94
|
return cmp unless cmp.zero?
|
95
95
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'jinx/helpers/validation'
|
2
2
|
|
3
3
|
# Mix-in for standard Person attributes.
|
4
4
|
module CaRuby
|
@@ -119,10 +119,10 @@ module CaRuby
|
|
119
119
|
# or if there is a middle name but no first name
|
120
120
|
def validate
|
121
121
|
if last.nil? and first.nil? then
|
122
|
-
|
122
|
+
Jinx.fail(Jinx::ValidationError, "Name is missing both the first and last fields")
|
123
123
|
end
|
124
124
|
if !middle.nil? and first.nil? then
|
125
|
-
|
125
|
+
Jinx.fail(Jinx::ValidationError, "Name with middle field #{middle} is missing the first field")
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
@@ -1,9 +1,8 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'set'
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
require 'caruby/util/merge'
|
3
|
+
require 'jinx/helpers/pretty_print'
|
4
|
+
require 'jinx/helpers/collection'
|
5
|
+
require 'jinx/helpers/merge'
|
7
6
|
|
8
7
|
module CaRuby
|
9
8
|
# Exception raised if a configuration property is missing or invalid.
|
@@ -67,15 +66,14 @@ module CaRuby
|
|
67
66
|
#
|
68
67
|
# Raises ConfigurationError if file doesn't exist or couldn't be parsed.
|
69
68
|
def load_properties(file)
|
70
|
-
|
71
|
-
logger.debug { "Loading properties file #{file}..." }
|
69
|
+
Jinx.fail(ConfigurationError, "Properties file not found: #{File.expand_path(file)}") unless File.exists?(file)
|
72
70
|
properties = {}
|
73
71
|
begin
|
74
72
|
YAML::load_file(file).each { |key, value| properties[key.to_sym] = value }
|
75
73
|
rescue
|
76
|
-
|
74
|
+
Jinx.fail(ConfigurationError, "Could not read properties file #{file}: " + $!)
|
77
75
|
end
|
78
|
-
|
76
|
+
# Uncomment the following line to print detail properties.
|
79
77
|
#logger.debug { "#{file} properties:\n#{properties.pp_s}" }
|
80
78
|
# parse comma-delimited string values of array properties into arrays
|
81
79
|
@array_properties.each do |key|
|
@@ -105,7 +103,7 @@ module CaRuby
|
|
105
103
|
# Validates that the required properties exist.
|
106
104
|
def validate_properties
|
107
105
|
@required_properties.each do |key|
|
108
|
-
|
106
|
+
Jinx.fail(ConfigurationError, "A required #{@application} property was not found: #{key}") unless has_property?(key)
|
109
107
|
end
|
110
108
|
end
|
111
109
|
end
|
@@ -6,7 +6,7 @@ class String
|
|
6
6
|
when /^(I{0,3})$/ then $1.size
|
7
7
|
when /^(I{0,3})(V|X)$/ then ROMAN_UNITS[$2] - $1.size
|
8
8
|
when /^(V)(I{0,3})$/ then ROMAN_UNITS[$1] + $2.size
|
9
|
-
else
|
9
|
+
else Jinx.fail(ArgumentError, "#{self} is not a roman numeral in the range I-X")
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
@@ -18,7 +18,7 @@ end
|
|
18
18
|
class Integer
|
19
19
|
# @return [String] the roman numeral equivalent of this integer
|
20
20
|
def to_roman
|
21
|
-
if self < 1 or self > 10 then
|
21
|
+
if self < 1 or self > 10 then Jinx.fail(ArgumentError, "#{self} cannot be converted to a roman numeral in the range I-X")
|
22
22
|
elsif self < 4 then 'I' * self
|
23
23
|
elsif self < 6 then ('I' * (5 - self)) + 'V'
|
24
24
|
elsif self < 9 then 'V' + ('I' * (self - 5))
|
@@ -26,7 +26,7 @@ class Version < Array
|
|
26
26
|
# Version.new(1, 1, beta) > beta #=> true
|
27
27
|
def <=>(other)
|
28
28
|
return 0 if equal?(other)
|
29
|
-
|
29
|
+
Jinx.fail(ArgumentError, "Comparand is not a #{self.class}: #{other}") unless self.class === other
|
30
30
|
return -1 if other.predecessor == self
|
31
31
|
return 1 unless predecessor.nil? or predecessor < other
|
32
32
|
each_with_index do |component, index|
|
@@ -2,10 +2,10 @@ require 'json'
|
|
2
2
|
|
3
3
|
module CaRuby
|
4
4
|
module JSON
|
5
|
-
# JSON => {Resource} deserializer.
|
5
|
+
# JSON => {Jinx::Resource} deserializer.
|
6
6
|
module Deserializer
|
7
7
|
# @param [String] json the JSON to deserialize
|
8
|
-
# @return [Resource] the deserialized object
|
8
|
+
# @return [Jinx::Resource] the deserialized object
|
9
9
|
def json_create(json)
|
10
10
|
# Make the new object from the json data attribute => value hash.
|
11
11
|
new(json['data'])
|