jinx 2.1.2 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.md CHANGED
@@ -1,6 +1,10 @@
1
1
  This history lists major release themes. See the GitHub commits (https://github.com/jinx/core)
2
2
  for change details.
3
3
 
4
+ 2.1.3 / 2012-08-18
5
+ ------------------
6
+ * Pull UID out of uniquifier.
7
+
4
8
  2.1.2 / 2012-06-12
5
9
  ------------------
6
10
  * Fine-tune introspection.
data/LEGAL CHANGED
@@ -1,5 +1,4 @@
1
1
  LEGAL NOTICE INFORMATION
2
2
  ------------------------
3
-
4
3
  All the files in this distribution are covered under either the MIT
5
4
  license (see the file LICENSE).
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  JINX: JRuby Java introspector
2
2
  =============================
3
-
4
3
  **Home**: [http://github.com/jinx/core](http://github.com/jinx/core)
5
4
  **Git**: [http://github.com/jinx/core](http://github.com/jinx/core)
6
5
  **Author**: OHSU Knight Cancer Institute
@@ -9,12 +8,10 @@ JINX: JRuby Java introspector
9
8
 
10
9
  Synopsis
11
10
  --------
12
-
13
11
  Jinx introspects a Java application domain package.
14
12
 
15
13
  Feature List
16
14
  ------------
17
-
18
15
  1. Introspects Java properties.
19
16
 
20
17
  2. Annotates the introspected object model with additional meta-data.
@@ -39,6 +36,5 @@ See the [example](http://github.com/jinx/core/tree/master/examples/family) for a
39
36
 
40
37
  Copyright
41
38
  ---------
42
-
43
39
  Jinx © 2012 by [Oregon Health & Science University](http://www.ohsu.edu/xd/health/services/cancer/index.cfm).
44
40
  Jinx is licensed under the MIT license. Please see the LICENSE and LEGAL files for more information.
@@ -66,7 +66,9 @@ module Jinx
66
66
  # @option opts [Boolean] :debug whether to include debug messages in the log file
67
67
  # @return [MultilineLogger] the global logger
68
68
  def open(dev=nil, opts=nil)
69
- raise RuntimeError.new("Log already open") if open?
69
+ if open? then
70
+ raise RuntimeError.new("The logger has already opened the log#{' file ' + @dev if String === @dev}")
71
+ end
70
72
  dev, opts = nil, dev if Hash === dev
71
73
  dev ||= default_log_file(Options.get(:app, opts))
72
74
  FileUtils.mkdir_p(File.dirname(dev)) if String === dev
@@ -0,0 +1,39 @@
1
+ require 'jinx/helpers/uid'
2
+
3
+ module Jinx
4
+ # A mix-in to convert a String to a unique value.
5
+ module StringUniquifier
6
+ # Returns a relatively unique String for the given base String. Each call returns
7
+ # a distinct value.
8
+ #
9
+ # This method is useful to transform a String object key to a unique value for
10
+ # testing purposes.
11
+ #
12
+ # The unique value is comprised of a prefix and suffix. The prefix is the base value
13
+ # with spaces replaced by an underscore. The suffix is given by a {Jinx::UID.generate}
14
+ # qualifier converted to digits and lower-case letters, excluding the digits 0, 1 and
15
+ # the characters l, o to avoid confusion.
16
+ #
17
+ # @example
18
+ # Jinx::StringUniquifier.uniquify('Groucho') #=> Groucho_wiafye6e
19
+ #
20
+ # @param value [String] the value to make unique
21
+ # @return [String, nil] the new unique value
22
+ # @raise [ArgumentError] if value is not a String
23
+ def self.uniquify(value)
24
+ raise ArgumentError.new("#{value.qp} is not a String") unless String === value
25
+ s = ''
26
+ n = UID.generate
27
+ while n > 0 do
28
+ n, m = n.divmod(32)
29
+ s << CHARS[m]
30
+ end
31
+ [value.gsub(' ', '_'), s].join('_')
32
+ end
33
+
34
+ private
35
+
36
+ # The qualifier character range.
37
+ CHARS = 'abcdefghijkmnpqrstuvwxyz23456789'
38
+ end
39
+ end
@@ -22,8 +22,6 @@ class Object
22
22
  # @children = []
23
23
  # parent.children << self if parent
24
24
  # end
25
- #
26
- # def to_s;
27
25
  # end
28
26
  # a = Node.new('a'); b = Node.new('b', a), c = Node.new('c', a); d = Node.new('d', c)
29
27
  # a.transitive_closure { |node| node.children }.to_a.join(", ") #=> a, b, c, d
@@ -0,0 +1,19 @@
1
+ module Jinx
2
+ # A unique identifier generator.
3
+ module UID
4
+ # Returns a relatively unique integer. Successive calls to this method
5
+ # within the same time zone spaced more than a millisecond apart return different
6
+ # integers. Each generated qualifier is greater than the previous by an unspecified
7
+ # amount.
8
+ def self.generate
9
+ # the first date that this method could be called
10
+ @first ||= Date.new(2011, 12, 01)
11
+ # days as integer + milliseconds as fraction since the first date
12
+ diff = DateTime.now - @first
13
+ # shift a tenth of a milli up into the integer portion
14
+ decimillis = diff * 24 * 60 * 60 * 10000
15
+ # truncate the fraction
16
+ decimillis.truncate
17
+ end
18
+ end
19
+ end
@@ -193,7 +193,7 @@ module Jinx
193
193
  # @return [Visitor] the filter visitor
194
194
  # @yield [parent, children] the filter to select which of the children to visit next
195
195
  # @yieldparam parent the currently visited node
196
- # @yieldparam children the nodes slated by this Visitor to visit next
196
+ # @yieldparam [Array] children the nodes slated by this visitor to visit next
197
197
  # @raise [ArgumentError] if a block is not given to this method
198
198
  def filter
199
199
  raise ArgumentError.new("A filter block is not given to the visitor filter method") unless block_given?
@@ -229,20 +229,28 @@ module Jinx
229
229
  # Visits the root node and all descendants.
230
230
  def visit_root(node, &operator)
231
231
  clear
232
- prune_cycle_nodes(node) if @prune_cycle_flag
233
- # visit the root node
234
- visit_recursive(node, &operator)
232
+ # Exclude cycles if the prune cycles flag is set.
233
+ @exclude.merge!(cyclic_nodes(node)) if @prune_cycle_flag
234
+ # Visit the root node.
235
+ result = visit_recursive(node, &operator)
236
+ # Reset the exclusions if the prune cycles flag is set.
237
+ @exclude.clear if @prune_cycle_flag
238
+ result
235
239
  end
236
-
237
- # Excludes the internal nodes in cycles starting and ending at the given root.
238
- def prune_cycle_nodes(root)
239
- @exclude.clear
240
- # visit the root, which will detect cycles, and remove the visited nodes afterwords
241
- @prune_cycle_flag = false
242
- to_enum(root).collect.each { |node| @visited.delete(node) }
243
- @prune_cycle_flag = true
244
- # add each cyclic internal node to the exclude list
245
- @cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
240
+
241
+ # @example
242
+ # visitor.visit(a)
243
+ # visit.to_enum #=> [a, b, c, u, v, x, y, z]
244
+ # visit.cycles #=> [[a, b, u, x, a], [c, z, c]]
245
+ # visit.cyclic_nodes #=> [b, u, x, c, z]
246
+ # @return [Array] the non-root nodes in visit cycles
247
+ def cyclic_nodes(root)
248
+ copts = @options.reject { |k, v| k == :prune_cycle }
249
+ cycler = Visitor.new(copts, &@navigator)
250
+ cycler.visit(root)
251
+ cyclic = cycler.cycles.flatten.uniq
252
+ cyclic.delete(root)
253
+ cyclic
246
254
  end
247
255
 
248
256
  def visit_recursive(node, &operator)
@@ -298,7 +306,9 @@ module Jinx
298
306
  @visitor = visitor
299
307
  @root = node
300
308
  end
301
-
309
+
310
+ # @yield [node] operates on the visited node
311
+ # @yieldparam node the visited node
302
312
  def each
303
313
  @visitor.visit(@root) { |node| yield(node) }
304
314
  end
@@ -314,8 +324,8 @@ module Jinx
314
324
 
315
325
  # Visits the given pair of nodes.
316
326
  #
317
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
318
- # argument.
327
+ # @param [(Object, Object), <(Object, Object)>] nodes the node pair
328
+ # @raise [ArgumentError] if the arguments do not consist of either two nodes or one two-item array
319
329
  def visit(*nodes)
320
330
  if nodes.size == 1 then
321
331
  nodes = nodes.first
@@ -324,10 +334,10 @@ module Jinx
324
334
  super(nodes)
325
335
  end
326
336
 
327
- # Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
328
- #
329
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
330
- # argument.
337
+ # @param (see #visit)
338
+ # @param [(Object, Object), <(Object, Object)>] nodes
339
+ # @return [Enumerable] the result of applying the given block to each matched node starting at the given root nodes
340
+ # @raise [ArgumentError] if the arguments do not consist of either two nodes or one two-item array
331
341
  def to_enum(*nodes)
332
342
  if nodes.size == 1 then
333
343
  nodes = nodes.first
@@ -17,9 +17,9 @@ module Jinx
17
17
  # Adds an optional {attribute=>value} constructor parameter to this class.
18
18
  def add_attribute_value_initializer
19
19
  class << self
20
- def new(opts=nil)
20
+ def new(params=nil)
21
21
  obj = super()
22
- obj.merge_attributes(opts) if opts
22
+ obj.merge_attributes(params) if params
23
23
  obj
24
24
  end
25
25
  end
@@ -246,7 +246,7 @@ module Jinx
246
246
  @local_prop_hash = {}
247
247
  @prop_hash = append_ancestor_enum(@local_prop_hash) { |par| par.property_hash }
248
248
  @attributes = Enumerable::Enumerator.new(@prop_hash, :each_key)
249
- @local_mndty_flt = Set.new
249
+ @local_mndty_attrs = Set.new
250
250
  @local_defaults = {}
251
251
  @defaults = append_ancestor_enum(@local_defaults) { |par| par.defaults }
252
252
  end
@@ -393,7 +393,7 @@ module Jinx
393
393
 
394
394
  # @param [Symbol] attribute the mandatory attribute
395
395
  def add_mandatory_attribute(attribute)
396
- @local_mndty_flt << standard_attribute(attribute)
396
+ @local_mndty_attrs << standard_attribute(attribute)
397
397
  end
398
398
 
399
399
  # Marks the given attribute with flags supported by {Property#qualify}.
@@ -414,16 +414,16 @@ module Jinx
414
414
  # An attribute declared in a superclass Resource is hidden from this Resource but retained in
415
415
  # the declaring Resource.
416
416
  def remove_attribute(attribute)
417
- std_prop = standard_attribute(attribute)
417
+ sa = standard_attribute(attribute)
418
418
  # if the attribute is local, then delete it, otherwise filter out the superclass attribute
419
- prop = @local_prop_hash.delete(std_prop)
420
- if prop then
419
+ sp = @local_prop_hash.delete(sa)
420
+ if sp then
421
421
  # clear the inverse, if any
422
- clear_inverse(prop)
422
+ clear_inverse(sp)
423
423
  # remove from the mandatory attributes, if necessary
424
- @local_mndty_flt.delete(std_prop)
424
+ @local_mndty_attrs.delete(sa)
425
425
  # remove from the attribute => metadata hash
426
- @local_std_prop_hash.delete_if { |aliaz, pa| pa == std_prop }
426
+ @local_std_prop_hash.delete_if { |aliaz, pa| pa == sa }
427
427
  else
428
428
  # Filter the superclass hashes.
429
429
  anc_prop_hash = @prop_hash.components[1]
@@ -478,8 +478,8 @@ module Jinx
478
478
  #
479
479
  # @see #mandatory_attributes
480
480
  def collect_mandatory_attributes
481
- @local_mndty_flt.merge!(default_mandatory_local_attributes)
482
- append_ancestor_enum(@local_mndty_flt) { |par| par.mandatory_attributes }
481
+ @local_mndty_attrs.merge!(default_mandatory_local_attributes)
482
+ append_ancestor_enum(@local_mndty_attrs) { |par| par.mandatory_attributes }
483
483
  end
484
484
 
485
485
  def default_mandatory_local_attributes
@@ -77,15 +77,6 @@ module Jinx
77
77
  @inv_prop.attribute if @inv_prop
78
78
  end
79
79
 
80
- # An attribute is unidirectional if both of the following is true:
81
- # * there is no distinct {#inverse} attribute
82
- # * the attribute is not a {#dependent?} with more than one owner
83
- #
84
- # @return [Boolean] whether this attribute does not have an inverse
85
- def unidirectional?
86
- inverse.nil? and not (dependent? and type.owner_attributes.size > 1)
87
- end
88
-
89
80
  # @param [Class] the attribute return type
90
81
  def type=(klass)
91
82
  return if klass == @type
@@ -135,11 +126,6 @@ module Jinx
135
126
  @inv_prop.qualify(:disjoint) if disjoint?
136
127
  logger.debug { "Assigned #{@declarer.qp}.#{self} attribute inverse to #{type.qp}.#{attribute}." }
137
128
  end
138
-
139
- # @return [Boolean] whether this property has an inverse
140
- def bidirectional?
141
- !!@inv_prop
142
- end
143
129
 
144
130
  # @return [Property, nil] the property for the {#inverse} attribute, if any
145
131
  def inverse_property
@@ -156,91 +142,10 @@ module Jinx
156
142
  if @restrictions then @restrictions.each { |prop| prop.qualify(*flags) } end
157
143
  end
158
144
 
159
- # @return [Boolean] whether the subject attribute encapsulates a Java attribute
160
- def java_property?
161
- JavaProperty === self
162
- end
163
-
164
- # @return [Boolean] whether the subject attribute returns a domain object or collection of domain objects
165
- def domain?
166
- # the type must be a Ruby class rather than a Java Class, and include the Domain mix-in
167
- Class === type and type < Resource
168
- end
169
-
170
- # @return [Boolean] whether the subject attribute is not a domain object attribute
171
- def nondomain?
172
- not domain?
173
- end
174
-
175
- # @return [Boolean] whether the subject attribute return type is a collection
176
- def collection?
177
- @flags.include?(:collection)
178
- end
179
-
180
- # Returns whether the subject attribute is a dependent on a parent. See the Jinx configuration
181
- # documentation for a dependency description.
182
- #
183
- # @return [Boolean] whether the attribute references a dependent
184
- def dependent?
185
- @flags.include?(:dependent)
186
- end
187
-
188
- # Returns whether the subject attribute must have a value when it is saved
189
- #
190
- # @return [Boolean] whether the attribute is mandatory
191
- def mandatory?
192
- @declarer.mandatory_attributes.include?(attribute)
193
- end
194
-
195
- # An attribute is derived if the attribute value is set by setting another attribute, e.g. if this
196
- # attribute is the inverse of a dependent owner attribute.
197
- #
198
- # @return [Boolean] whether this attribute is derived from another attribute
199
- def derived?
200
- dependent? and !!inverse
201
- end
202
-
203
145
  # @return [Boolean] this attribute's inverse attribute if the inverse is a derived attribute, or nil otherwise
204
146
  def derived_inverse
205
147
  @inv_prop.attribute if @inv_prop and @inv_prop.derived?
206
148
  end
207
-
208
- # An independent attribute is a reference to one or more non-dependent Resource objects.
209
- # An {#owner?} attribute is independent.
210
- #
211
- # @return [Boolean] whether the subject attribute is a non-dependent domain attribute
212
- def independent?
213
- domain? and not dependent?
214
- end
215
-
216
- # @return [Boolean] whether this attribute is a collection with a collection inverse
217
- def many_to_many?
218
- return false unless collection?
219
- inv_prop = inverse_property
220
- inv_prop and inv_prop.collection?
221
- end
222
-
223
- # @return [Boolean] whether the subject attribute is a dependency owner
224
- def owner?
225
- @flags.include?(:owner)
226
- end
227
-
228
- # @return [Boolean] whether this is a dependent attribute which has exactly one owner value
229
- # chosen from several owner attributes
230
- def disjoint?
231
- @flags.include?(:disjoint)
232
- end
233
-
234
- # @return [Boolean] whether this attribute is a dependent which does not have a Java
235
- # inverse owner attribute
236
- def unidirectional_java_dependent?
237
- dependent? and java_property? and not bidirectional_java_association?
238
- end
239
-
240
- # @return [Boolean] whether this is a Java attribute which has a Java inverse
241
- def bidirectional_java_association?
242
- inverse and java_property? and inverse_property.java_property?
243
- end
244
149
 
245
150
  # Creates a new declarer attribute which restricts this attribute.
246
151
  # This method should only be called by a {Resource} class, since the class is responsible
@@ -27,7 +27,8 @@ module Jinx
27
27
  JavaProperty === self
28
28
  end
29
29
 
30
- # @return [Boolean] whether the subject attribute returns a domain object or collection of domain objects
30
+ # @return [Boolean] whether the subject attribute returns a domain object or a collection
31
+ # of domain objects
31
32
  def domain?
32
33
  # the type must be a Ruby class rather than a Java Class, and include the Domain mix-in
33
34
  Class === type and type < Resource
@@ -27,8 +27,12 @@ module Jinx
27
27
  @copier = opts.delete(:copier)
28
28
  # the source => target matches
29
29
  @matches = {}
30
- # Apply a filter to the selected references so that only a matched reference is visited.
31
- opts[:filter] = Proc.new { |src| @matches[src] }
30
+ # Apply a filter to the visited reference so that only a matched reference is visited.
31
+ # the reference filter
32
+ flt = opts[:filter]
33
+ opts[:filter] = Proc.new do |src|
34
+ (flt.nil? or flt.call(src)) and !!@matches[src]
35
+ end
32
36
  # the class => {id => target} hash
33
37
  @id_mtchs = LazyHash.new { Hash.new }
34
38
  # Match the source references before navigating from the source to its references, since
@@ -60,7 +60,7 @@ module Jinx
60
60
  # merge the non-domain attributes
61
61
  target.merge_attributes(source)
62
62
  # merge the source domain attributes into the target
63
- target.merge(source, mas, @matches)
63
+ target.merge_attributes(source, mas, @matches, &@filter)
64
64
  end
65
65
  end
66
66
  end
@@ -23,21 +23,19 @@ module Jinx
23
23
  # {Propertied#mergeable_attributes}.
24
24
  #
25
25
  # The merge is performed by calling {#merge_attribute} on each attribute with the matches
26
- # and merger block given to this method.
26
+ # and filter block given to this method.
27
27
  #
28
28
  # @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
29
29
  # @param [<Symbol>, nil] attributes the attributes to merge (default {Propertied#nondomain_attributes})
30
30
  # @param [{Resource => Resource}, nil] the optional merge source => target reference matches
31
- # @yield [attribute, oldval, newval] the optional merger block
32
- # @yieldparam [Symbol] attribute the merge target attribute
33
- # @yieldparam oldval the current merge attribute value
34
- # @yieldparam newval the new merge attribute value
31
+ # @yield [value] the optional filter block
32
+ # @yieldparam value the source merge attribute value
35
33
  # @return [Mergeable] self
36
34
  # @raise [ArgumentError] if none of the following are true:
37
35
  # * other is a Hash
38
36
  # * attributes is non-nil
39
37
  # * the other class responds to +mergeable_attributes+
40
- def merge_attributes(other, attributes=nil, matches=nil, &merger)
38
+ def merge_attributes(other, attributes=nil, matches=nil, &filter)
41
39
  return self if other.nil? or other.equal?(self)
42
40
  attributes = [attributes] if Symbol === attributes
43
41
  attributes ||= self.class.mergeable_attributes
@@ -45,11 +43,13 @@ module Jinx
45
43
  # if the source object is not a hash, then convert it to an attribute => value hash
46
44
  vh = Hashable === other ? other : other.value_hash(attributes)
47
45
  # merge the value hash
48
- vh.each { |pa, value| merge_attribute(pa, value, matches, &merger) }
46
+ vh.each { |pa, value| merge_attribute(pa, value, matches, &filter) }
49
47
  self
50
48
  end
51
49
 
52
- alias :merge :merge_attributes
50
+ def merge(*args, &filter)
51
+ merge_attributes(*args, &filter)
52
+ end
53
53
 
54
54
  alias :merge! :merge
55
55
 
@@ -77,19 +77,26 @@ module Jinx
77
77
  def merge_attribute(attribute, newval, matches=nil)
78
78
  # the previous value
79
79
  oldval = send(attribute)
80
- # If nothing to merge or a block can take over, then bail.
81
- if newval.nil? or mergeable__equal?(oldval, newval) then
82
- return oldval
83
- elsif block_given? then
84
- return yield(attribute, oldval, value)
80
+ # Filter the newval into the srcval.
81
+ srcval = if newval and block_given? then
82
+ if newval.collection? then
83
+ newval.select { |v| yield(v) }
84
+ elsif yield(newval) then
85
+ newval
86
+ end
87
+ else
88
+ newval
85
89
  end
86
90
 
91
+ # If there is no point in merging, then bail.
92
+ return oldval if srcval.nil_or_empty? or mergeable__equal?(oldval, newval)
93
+
87
94
  # Discriminate between a domain and non-domain attribute.
88
95
  prop = self.class.property(attribute)
89
96
  if prop.domain? then
90
- merge_domain_property_value(prop, oldval, newval, matches)
97
+ merge_domain_property_value(prop, oldval, srcval, matches)
91
98
  else
92
- merge_nondomain_property_value(prop, oldval, newval)
99
+ merge_nondomain_property_value(prop, oldval, srcval)
93
100
  end
94
101
  end
95
102
 
@@ -14,31 +14,31 @@ module Jinx
14
14
  # The required selector block given to this initializer determines which attributes to
15
15
  # visit. The references to visit next thus consist of the current domain object's selector
16
16
  # attributes' values. If the :filter option is set, then the given filter block is applied
17
- # to the selected attribute references to restrict which domain objects will be visited.
17
+ # to each selected attribute reference to determine which domain objects will be visited.
18
18
  #
19
19
  # @param opts (see Visitor#initialize)
20
- # @option opts [Proc] :filter an optional filter on the references to visit
20
+ # @option opts [Proc] :filter an optional filter on the reference to visit
21
21
  # @yield [obj] returns the {AttributeEnumerator} of attributes to visit next from the
22
22
  # current domain object
23
23
  # @yieldparam [Resource] obj the current domain object
24
24
  def initialize(opts=nil, &selector)
25
25
  raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
26
26
  # the property selector
27
- @flt_sel = selector
27
+ @selector = selector
28
28
  # the reference filter
29
- flt = Options.get(:filter, opts)
29
+ @filter = Options.get(:filter, opts)
30
30
  # Initialize the Visitor with a reference enumerator which selects the reference
31
31
  # attributes and applies the optional filter if necessary.
32
32
  @ref_enums = {}
33
- super do |ref|
33
+ super do |obj|
34
34
  # the reference property filter
35
- ras = attributes_to_visit(ref)
35
+ ras = attributes_to_visit(obj)
36
36
  if ras then
37
- logger.debug { "#{qp} visiting #{ref} attributes #{ras.pp_s(:single_line)}..." } if @verbose
37
+ logger.debug { "#{qp} visiting #{obj} attributes #{ras.pp_s(:single_line)}..." } if @verbose
38
38
  # an enumerator on the reference properties
39
- enum = ReferenceEnumerator.new(ref, ras.properties)
39
+ enum = ReferenceEnumerator.new(obj, ras.properties)
40
40
  # If there is a reference filter, then apply it to the enum references.
41
- flt ? enum.filter(&flt) : enum
41
+ @filter ? enum.filter(&@filter) : enum
42
42
  end
43
43
  end
44
44
  end
@@ -48,7 +48,7 @@ module Jinx
48
48
  # @param [Resource] obj the visiting object
49
49
  # @return [Propertied::Filter] the attributes to visit
50
50
  def attributes_to_visit(obj)
51
- @flt_sel.call(obj)
51
+ @selector.call(obj)
52
52
  end
53
53
  end
54
54
  end
@@ -1,32 +1,29 @@
1
- require 'jinx/helpers/uniquifier'
1
+ require 'jinx/resource/uniquifier_cache'
2
2
 
3
3
  module Jinx
4
4
  # The Unique mix-in makes values unique within the scope of a Resource class.
5
5
  module Unique
6
- # Makes the given String value unique in the context of this object's class.
7
- # @return nil if value is nil
8
- # Raises TypeError if value is neither nil nor a String.
9
- def uniquify_value(value)
10
- unless String === value or value.nil? then
11
- raise TypeError.new("Cannot uniquify #{qp} non-String value #{value}")
12
- end
13
- Uniquifier.instance.uniquify(self, value)
14
- end
15
-
16
- # Makes the secondary key unique by replacing each String key attribute value
17
- # with a unique value.
6
+ # Replaces each String secondary and alternate key property with a unique
7
+ # value. Successive calls to this method for domain objects of the same class
8
+ # replace the same String key property values with the same unique value.
9
+ #
10
+ # @return [Resource] self
18
11
  def uniquify
19
12
  uniquify_attributes(self.class.secondary_key_attributes)
20
13
  uniquify_attributes(self.class.alternate_key_attributes)
14
+ self
21
15
  end
22
-
23
- # Makes the given attribute values unique by replacing each String value
24
- # with a unique value.
16
+
17
+ private
18
+
19
+ # Makes this domain object's String values for the given attributes unique.
20
+ #
21
+ # @param [<Symbol>] the key attributes to uniquify
25
22
  def uniquify_attributes(attributes)
26
23
  attributes.each do |ka|
27
24
  oldval = send(ka)
28
25
  next unless String === oldval
29
- newval = uniquify_value(oldval)
26
+ newval = UniquifierCache.instance.get(self, oldval)
30
27
  set_property_value(ka, newval)
31
28
  logger.debug { "Reset #{qp} #{ka} from #{oldval} to unique value #{newval}." }
32
29
  end
@@ -0,0 +1,34 @@
1
+ require 'singleton'
2
+ require 'jinx/helpers/lazy_hash'
3
+ require 'jinx/helpers/string_uniquifier'
4
+
5
+ module Jinx
6
+ # A utility class to cache key value qualifiers.
7
+ class UniquifierCache
8
+ include Singleton
9
+
10
+ def initialize
11
+ @cache = Jinx::LazyHash.new { Hash.new }
12
+ end
13
+
14
+ # Returns the unique value generated for the given object and value.
15
+ # Successive calls to this method for domain objects of the same class
16
+ # and a given value return the same result.
17
+ #
18
+ # @example
19
+ # Jinx::UniquifierCache.instance.get(person, 'Groucho') #=> Groucho_wy874e6e
20
+ # Jinx::UniquifierCache.instance.get(person.copy, 'Groucho') #=> Groucho_wy87ye6e
21
+ #
22
+ # @param [Resource] obj the domain object containing the value
23
+ # @param [String] value the value to make unique
24
+ # @return [String] the unique value
25
+ def get(obj, value)
26
+ @cache[obj.class][value] ||= StringUniquifier.uniquify(value)
27
+ end
28
+
29
+ # Clears all cache entries.
30
+ def clear
31
+ @cache.clear
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Jinx
2
- VERSION = '2.1.2'
2
+ VERSION = '2.1.3'
3
3
  end
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/../../../helper'
2
+ require 'test/unit'
3
+ require 'jinx/helpers/string_uniquifier'
4
+
5
+ class StringUniquifierTest < Test::Unit::TestCase
6
+ def test_uniquify
7
+ u1 = Jinx::StringUniquifier.uniquify('Groucho')
8
+ u2 = Jinx::StringUniquifier.uniquify('Groucho')
9
+ assert_not_equal(u1, u2, "Consecutive uniquifier calls not unique")
10
+ end
11
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: jinx
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 2.1.2
5
+ version: 2.1.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - OHSU
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-06-12 00:00:00 Z
13
+ date: 2012-07-17 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -142,9 +142,10 @@ files:
142
142
  - lib/jinx/helpers/pretty_print.rb
143
143
  - lib/jinx/helpers/set.rb
144
144
  - lib/jinx/helpers/stopwatch.rb
145
+ - lib/jinx/helpers/string_uniquifier.rb
145
146
  - lib/jinx/helpers/transformer.rb
146
147
  - lib/jinx/helpers/transitive_closure.rb
147
- - lib/jinx/helpers/uniquifier.rb
148
+ - lib/jinx/helpers/uid.rb
148
149
  - lib/jinx/helpers/validation.rb
149
150
  - lib/jinx/helpers/visitor.rb
150
151
  - lib/jinx/import/class_path_modifier.rb
@@ -171,6 +172,7 @@ files:
171
172
  - lib/jinx/resource/reference_path_visitor.rb
172
173
  - lib/jinx/resource/reference_visitor.rb
173
174
  - lib/jinx/resource/unique.rb
175
+ - lib/jinx/resource/uniquifier_cache.rb
174
176
  - lib/jinx/version.rb
175
177
  - spec/defaults_spec.rb
176
178
  - spec/definitions/model/alias/child.rb
@@ -212,8 +214,8 @@ files:
212
214
  - test/lib/jinx/helpers/partial_order_test.rb
213
215
  - test/lib/jinx/helpers/pretty_print_test.rb
214
216
  - test/lib/jinx/helpers/stopwatch_test.rb
217
+ - test/lib/jinx/helpers/string_uniquifier_test.rb
215
218
  - test/lib/jinx/helpers/transitive_closure_test.rb
216
- - test/lib/jinx/helpers/uniquifier_test.rb
217
219
  - test/lib/jinx/helpers/visitor_test.rb
218
220
  - test/lib/jinx/import/java_test.rb
219
221
  - test/lib/jinx/import/mixed_case_test.rb
@@ -265,8 +267,8 @@ test_files:
265
267
  - test/lib/jinx/helpers/partial_order_test.rb
266
268
  - test/lib/jinx/helpers/pretty_print_test.rb
267
269
  - test/lib/jinx/helpers/stopwatch_test.rb
270
+ - test/lib/jinx/helpers/string_uniquifier_test.rb
268
271
  - test/lib/jinx/helpers/transitive_closure_test.rb
269
- - test/lib/jinx/helpers/uniquifier_test.rb
270
272
  - test/lib/jinx/helpers/visitor_test.rb
271
273
  - test/lib/jinx/import/java_test.rb
272
274
  - test/lib/jinx/import/mixed_case_test.rb
@@ -1,83 +0,0 @@
1
- require 'singleton'
2
- require 'jinx/helpers/lazy_hash'
3
-
4
- module Jinx
5
- # A utility class to generate value qualifiers.
6
- class Uniquifier
7
- include Singleton
8
-
9
- # Returns a relatively unique integral qualifier. Successive calls to this method
10
- # within the same time zone spaced more than a millisecond apart return different
11
- # integers. Each generated qualifier is greater than the previous by an unspecified
12
- # amount.
13
- def self.qualifier
14
- # the first date that this method could be called
15
- @first ||= Date.new(2011, 12, 01)
16
- # days as integer + milliseconds as fraction since the first date
17
- diff = DateTime.now - @first
18
- # shift a tenth of a milli up into the integer portion
19
- decimillis = diff * 24 * 60 * 60 * 10000
20
- # truncate the fraction
21
- decimillis.truncate
22
- end
23
-
24
- def initialize
25
- @cache = Jinx::LazyHash.new { Hash.new }
26
- end
27
-
28
- # Returns a relatively unique String for the given base String object or
29
- # (object, String value) pair. In the former case, each call returns a distinct value.
30
- # In the latter case, successive calls of the same String value for the same object
31
- # class return the same unique value.
32
- #
33
- # This method is useful to transform a String object key to a unique value for testing
34
- # purposes.
35
- #
36
- # The unique value is comprised of a prefix and suffix. The prefix is the base value
37
- # with spaces replaced by an underscore. The suffix is a {Jinx::Uniquifier.qualifier}
38
- # converted to digits and lower-case letters, excluding the digits 0, 1 and characters
39
- # l, o to avoid confusion.
40
- #
41
- # @example
42
- # Jinx::Uniquifier.instance.uniquify('Groucho') #=> Groucho_wiafye6e
43
- # Jinx::Uniquifier.instance.uniquify('Groucho') #=> Groucho_uqafye6e
44
- # Jinx::Uniquifier.instance.uniquify('Groucho Marx') #=> Groucho_ay23ye6e
45
- # Jinx::Uniquifier.instance.uniquify(person, 'Groucho') #=> Groucho_wy874e6e
46
- # Jinx::Uniquifier.instance.uniquify(person, 'Groucho') #=> Groucho_wy87ye6e
47
- #
48
- # @param obj the object containing the value to uniquify
49
- # @param value [String, nil] the value to make unique, or nil if the containing object is a String
50
- # @return [String, nil] the new unique value, or nil if the containing object is a String
51
- # and the given value is nil
52
- def uniquify(obj, value=nil)
53
- if String === obj then
54
- to_unique(obj)
55
- elsif value then
56
- @cache[obj.class][value] ||= to_unique(value)
57
- end
58
- end
59
-
60
- def clear
61
- @cache.clear
62
- end
63
-
64
- private
65
-
66
- CHARS = 'abcdefghijkmnpqrstuvwxyz23456789'
67
-
68
- # @param value [String, nil] the value to make unique
69
- # @return [String] the new unique value, or nil if the given value is nil
70
- # @raise [ArgumentError] if value is neither a String nor nil
71
- def to_unique(value)
72
- return if value.nil?
73
- raise ArgumentError.new("#{value.qp} is not a String") unless String === value
74
- s = ''
75
- n = Jinx::Uniquifier.qualifier
76
- while n > 0 do
77
- n, m = n.divmod(32)
78
- s << CHARS[m]
79
- end
80
- [value.gsub(' ', '_'), s].join('_')
81
- end
82
- end
83
- end
@@ -1,11 +0,0 @@
1
- require File.dirname(__FILE__) + '/../../../helper'
2
- require 'test/unit'
3
- require 'jinx/helpers/uniquifier'
4
-
5
- class UniquifierTest < Test::Unit::TestCase
6
- def test_uniquify
7
- u1 = Jinx::Uniquifier.instance.uniquify('Groucho')
8
- u2 = Jinx::Uniquifier.instance.uniquify('Groucho')
9
- assert_not_equal(u1, u2, "Consequetive uniquifier calls not unique")
10
- end
11
- end