Module: Jinx::Resource

Includes:
Inversible, Mergeable
Defined in:
lib/jinx/resource.rb,
lib/jinx/resource/matcher.rb

Overview

This Resource module enhances application domain classes with the following features:

  • meta-data introspection

  • dependency

  • inverse integrity

  • defaults

  • validation

  • copy/merge

A application domain module becomes jinxed by including Resource and specifying the Java package and optional JRuby class mix-in definitions.

Examples:

# The application domain module
module Domain
  include Jinx::Resource  
  # The caTissue Java package name.
  package 'app.domain'
  # The JRuby mix-ins directory.
  definitions File.expand_path('domain', dirname(__FILE__))
end

Defined Under Namespace

Classes: DetailPrinter, Matcher, ReferencePrinter

Constant Summary collapse

COPY_MERGE_OPTS =

The copy merge call options.

{:inverse => false}
DEPENDENT_VISITOR =

The dependent attribute visitor.

See Also:

Jinx::ReferenceVisitor.new { |obj| obj.class.dependent_attributes }
DEF_MATCHER =
Matcher.new

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mergeable

#merge, #merge_attribute, #merge_attributes, #merge_domain_property_value, #merge_nondomain_property_value, #mergeable__equal?, #value_hash

Methods included from Inversible

#add_to_inverse_collection, #set_inverse, #set_inversible_noncollection_attribute

Class Method Details

.collection_value_equal?(value, other, matches = nil) ⇒ Boolean (private)

Returns:



756
757
758
# File 'lib/jinx/resource.rb', line 756

def self.collection_value_equal?(value, other, matches=nil)
  value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
end

.match_all(sources, targets) ⇒ {Resouce => Resource}

Returns a source => target hash of the given sources which match the targets using the #match_in method.

Returns:

  • ({Resouce => Resource})

    a source => target hash of the given sources which match the targets using the #match_in method



420
421
422
# File 'lib/jinx/resource.rb', line 420

def self.match_all(sources, targets)
  DEF_MATCHER.match(sources, targets)
end

.value_equal?(value, other, matches = nil) ⇒ Boolean

Returns whether value equals other modulo the given matches according to the following tests:

  • value == other

  • value and other are Resource instances and value is a #match? with other.

  • value and other are Enumerable with members equal according to the above conditions.

  • value and other are DateTime instances and are equal to within one second.

The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip of a date attribute, which loses the seconds fraction.

Returns:

  • (Boolean)

    whether value and other are equal according to the above tests



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/jinx/resource.rb', line 576

def self.value_equal?(value, other, matches=nil)
  value = value.to_ruby_date if Java::JavaUtil::Date === value
  other = other.to_ruby_date if Java::JavaUtil::Date === other
  if value == other then
    true
  elsif value.collection? and other.collection? then
    collection_value_equal?(value, other, matches)
  elsif Date === value and Date === other then
    (value - other).abs.floor.zero?
  elsif Resource === value and value.class === other then
    value.matches?(other)
  elsif matches then
    matches[value] == other
  else
    false
  end
end

Instance Method Details

#add_defaultsResource

Sets the default attribute values for this domain object and its dependents. If this Resource does not have an identifier, then missing attributes are set to the values defined by Propertied#add_attribute_defaults.

Subclasses should override the private #add_defaults_local method rather than this method.

Returns:



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/jinx/resource.rb', line 73

def add_defaults
  # If there is an owner, then delegate to the owner.
  # Otherwise, add defaults to this object.
  par = owner
  if par and par.identifier.nil? then
    logger.debug { "Adding defaults to #{qp} owner #{par.qp}..." }
    par.add_defaults
  else
    logger.debug { "Adding defaults to #{qp} and its dependents..." }
    # apply the local and dependent defaults
    add_defaults_recursive
  end
  self
end

#add_defaults_localObject (private)

Sets the default attribute values for this domain object. Unlike #add_defaults, this method does not set defaults for dependents. This method sets the configuration values for this domain object as described in #add_defaults, but does not set defaults for dependents.

This method is the integration point for subclasses to augment defaults with programmatic logic. If a subclass overrides this method, then it should call super before setting the local default attributes. This ensures that configuration defaults takes precedence.



685
686
687
688
# File 'lib/jinx/resource.rb', line 685

def add_defaults_local
  logger.debug { "Adding defaults to #{qp}..." }
  merge_attributes(self.class.defaults)
end

#add_defaults_recursiveObject (protected)

Adds the default values to this object, if necessary, and its dependents.



616
617
618
619
620
621
# File 'lib/jinx/resource.rb', line 616

def add_defaults_recursive
  # Add the local defaults.
  add_defaults_local
  # Recurse to the dependents.
  each_defaultable_reference { |ref| ref.add_defaults_recursive }
end

#alternate_keyArray, ...

Returns the key value or values.

Returns:

See Also:



200
201
202
# File 'lib/jinx/resource.rb', line 200

def alternate_key
  key_value(self.class.alternate_key_attributes)
end

#clear_attribute(attribute) ⇒ Object

Clears the given attribute value. If the current value responds to the clear method, then the current value is cleared. Otherwise, the value is set to Metadata#empty_value.

Parameters:

  • attribute (Symbol)

    the attribute to clear



127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/jinx/resource.rb', line 127

def clear_attribute(attribute)
  # the current value to clear
  current = send(attribute)
  return if current.nil?
  # call the current value clear if possible.
  # otherwise, set the attribute to the empty value.
  if current.respond_to?(:clear) then
    current.clear
  else
    writer = self.class.property(attribute).writer
    value = self.class.empty_value(attribute)
    send(writer, value)
  end
end

#content_matches?(other) ⇒ Boolean

Matches this domain object with the other domain object. The match succeeds if and only if the object classes match and for each attribute, at least one of the following conditions hold:

  • this object’s attribute value is nil or empty

  • the other object’s attribute value is nil or empty

  • if the attribute is a nondomain attribute, then the respective values are equal

  • if the attribute value is a Resource, then the value recursively matches the other value

  • if the attribute value is a Resource collection, then every item in the collection matches some item in the other collection

Parameters:

  • the (Resource)

    domain object to match

Returns:

  • (Boolean)

    whether this object matches the other object on class and content



373
374
375
376
# File 'lib/jinx/resource.rb', line 373

def content_matches?(other)
  logger.debug { "Matching #{self} content against #{other}..." }
  content_matches_recursive?(other)
end

#content_matches_recursive?(other, visited = Set.new) ⇒ Boolean (protected)

Returns whether this object matches the other object on class and content.

Parameters:

  • visited (<Resource>) (defaults to: Set.new)

    the matched references

Returns:

  • (Boolean)

    whether this object matches the other object on class and content



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
# File 'lib/jinx/resource.rb', line 626

def content_matches_recursive?(other, visited=Set.new)
  return false unless self.class == other.class
  return false unless self.class.nondomain_attributes.all? do |pa|
    v = send(pa)
    ov = other.send(pa)
    if v.nil? || ov.nil? or Resource.value_equal?(v, ov) then
      true
    else
      logger.debug { "#{self} does not match #{other} on property #{pa}: #{v.qp} vs #{ov.qp}" }
      false
    end
  end  
  self.class.domain_attributes.all? do |pa|
    v = send(pa)
    ov = other.send(pa)
    if v.nil_or_empty? or ov.nil_or_empty? or visited.include?(v) then
      true
    else
      logger.debug { "Matching #{self} #{pa} value #{v.qp} against #{other} #{pa} value #{ov.qp}..." }
      if Enumerable === v then
        v.all? do |ref|
          oref = ref.match_in(ov)
          if oref.nil? then
            logger.debug { "#{self} does not match #{other} on property #{pa}: #{v.pp_s} vs #{ov.pp_s}" }
            false
          else
            ref.content_matches_recursive?(oref, visited)
          end
        end
      else
        visited << v
        v.content_matches_recursive?(ov, visited)
      end
    end
  end  
end

#copy(*attributes) ⇒ Resource

Returns a new domain object with the given attributes copied from this domain object. The attributes argument consists of either attribute Symbols or a single Enumerable consisting of Symbols. The default attributes are the Propertied#nondomain_attributes.

Parameters:

  • attributes (<Symbol>, (<Symbol>))

    the attributes to copy

Returns:

  • (Resource)

    a copy of this domain object



113
114
115
116
117
118
119
120
121
# File 'lib/jinx/resource.rb', line 113

def copy(*attributes)
  if attributes.empty? then
    attributes = self.class.nondomain_attributes
  elsif Enumerable === attributes.first then
    raise ArgumentError.new("#{qp} copy attributes argument is not a Symbol: #{attributes.first}") unless attributes.size == 1
    attributes = attributes.first
  end
  self.class.new.merge_attributes(self, attributes)
end

#delegate_to_inverse_setter(prop, ref, writer) ⇒ Object (private)

Parameters:

  • prop (Property)

    the attribute to set

  • ref (Resource)

    the inverse value

  • the (Symbol)

    inverse => self writer method



863
864
865
866
# File 'lib/jinx/resource.rb', line 863

def delegate_to_inverse_setter(prop, ref, writer)
  logger.debug { "Setting #{qp} #{prop} by setting the #{ref.qp} inverse attribute #{prop.inverse}..." }
  ref.send(writer, self)
end

#dependent?Boolean

Returns whether this domain object is dependent on another entity.

Returns:

  • (Boolean)

    whether this domain object is dependent on another entity



283
284
285
# File 'lib/jinx/resource.rb', line 283

def dependent?
  self.class.dependent?
end

#dependent_update_only?(other) ⇒ Boolean

Returns whether the other domain object is a dependent of this object and has an update-only non-domain attribute.

Parameters:

  • other (Resource)

    the domain object to check

Returns:

  • (Boolean)

    whether the other domain object is a dependent of this object and has an update-only non-domain attribute.



266
267
268
269
270
271
# File 'lib/jinx/resource.rb', line 266

def dependent_update_only?(other)
  other.owner == self and
  other.class.nondomain_attributes.detect_attribute_with_property do |prop|
    prop.updatable? and not prop.creatable?
  end
end

#dependents(properties = nil) ⇒ Enumerable

Returns this domain object’s dependents. Dependents which have an alternate preferred owner, as described in #effective_owner_property_value, are not included in the result.

Parameters:

  • property (<Property>, Property, nil)

    the dependent property or properties (default is all dependent properties)

Returns:

  • (Enumerable)

    this domain object’s direct dependents



299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/jinx/resource.rb', line 299

def dependents(properties=nil)
  properties ||= self.class.dependent_attributes.properties
  # Make a reference enumerator that selects only those dependents which do not have
  # an alternate preferred owner.
  ReferenceEnumerator.new(self, properties).filter do |dep|
    # dep is a candidate dependent. dep could have a preferred owner which differs
    # from self. If there is a different preferred owner, then don't call the
    # iteration block.
    oref = dep.owner
    oref.nil? or oref == self
  end
end

#difference(other, attributes = nil) ⇒ {Object => (Object,Object)} Also known as: diff

Returns the difference between this Persistable and the other Persistable for the given attributes. The default attributes are the Propertied#nondomain_attributes.

Parameters:

  • other (Resource)

    the domain object to compare

  • attributes (<Symbol>, nil) (defaults to: nil)

    the attributes to compare

Returns:



430
431
432
433
434
435
# File 'lib/jinx/resource.rb', line 430

def difference(other, attributes=nil)
  attributes ||= self.class.nondomain_attributes
  vh = value_hash(attributes)
  ovh = other.value_hash(attributes)
  vh.diff(ovh) { |key, v1, v2| Resource.value_equal?(v1, v2) }
end

#direct_dependents(attribute) ⇒ <Resource>

Returns the attribute references which directly depend on this owner. The default is the attribute value.

Returns an Enumerable. If the value is not already an Enumerable, then this method returns an empty array if value is nil, or a singelton array with value otherwise.

If there is more than one owner of a dependent, then subclasses should override this method to select dependents whose dependency path is shorter than an alternative dependency path, e.g. if a Node is owned by both a Graph and a parent Node. In that case, the Graph direct dependents consist of the top-level nodes owned by the Graph but not referenced by another Node.

Parameters:

  • attribute (Symbol)

    the dependent attribute

Returns:

  • (<Resource>)

    the attribute value, wrapped in an array if necessary



335
336
337
338
339
340
341
342
# File 'lib/jinx/resource.rb', line 335

def direct_dependents(attribute)
  deps = send(attribute)
  case deps
    when Enumerable then deps
    when nil then Array::EMPTY_ARRAY
    else [deps]
  end
end

#dump {|owner| ... } ⇒ String

Prints this domain object’s content and recursively prints the referenced content. The optional selector block determines the attributes to print. The default is the Propertied#java_attributes.

Yields:

  • (owner)

    the owner attribute selector

Yield Parameters:

  • owner (Resource)

    the domain object to print

Returns:

  • (String)

    the domain object content



532
533
534
# File 'lib/jinx/resource.rb', line 532

def dump(&selector)
  DetailPrinter.new(self, &selector).pp_s
end

#each_defaultable_reference {|dep| ... } ⇒ Object (private)

Enumerates referenced domain objects for setting defaults. This base implementation includes the #dependents. Subclasses can override this# method to add references which should be defaulted or to set the order in which defaults are applied.

Yields:

  • (dep)

    operate on the dependent

Yield Parameters:

  • dep (<Resource>)

    the dependent to which the defaults are applied



752
753
754
# File 'lib/jinx/resource.rb', line 752

def each_defaultable_reference(&block)
  dependents.each(&block)
end

#effective_owner_property_value(Property, Resource)?

Returns the (property, value) pair for which there is an owner reference, or nil if this domain object does not reference an owner.

Returns:



214
215
216
217
218
219
# File 'lib/jinx/resource.rb', line 214

def effective_owner_property_value
  self.class.owner_properties.detect_value do |op|
    ref = send(op.attribute)
    [op, ref] if ref
  end
end

#empty_value(attribute) ⇒ Object (private)

Returns 0 if attribute is a Java primitive number, false if attribute is a Java primitive boolean, an empty collectin if the Java attribute is a collection, nil otherwise.



872
873
874
875
876
877
878
879
# File 'lib/jinx/resource.rb', line 872

def empty_value(attribute)
  type = java_type(attribute) || return
  if type.primitive? then
    type.name == 'boolean' ? false : 0
  else
    self.class.empty_value(attribute)
  end
end

#independent?Boolean

Returns whether this domain object is not dependent on another entity.

Returns:

  • (Boolean)

    whether this domain object is not dependent on another entity



288
289
290
# File 'lib/jinx/resource.rb', line 288

def independent?
  not dependent?
end

#java_type(attribute) ⇒ Object (private)

Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.



882
883
884
885
# File 'lib/jinx/resource.rb', line 882

def java_type(attribute)
  prop = self.class.property(attribute)
  prop.property_descriptor.attribute_type if JavaProperty === prop
end

#key(attributes = nil) ⇒ Array, ...

Returns the first non-nil #key_value for the primary, secondary and alternate key attributes.

Returns:



163
164
165
# File 'lib/jinx/resource.rb', line 163

def key(attributes=nil)
  primary_key or secondary_key or alternate_key
end

#key_value(attributes) ⇒ Array, ...

Returns the key for the given key attributes as follows:

  • If there are no key attributes, then nil.

  • Otherwise, if any key attribute value is missing, then nil.

  • Otherwise, if the key attributes is a singleton Array, then the key is the value of the sole key attribute.

  • Otherwise, the key is an Array of the key attribute values.

Parameters:

  • attributes (<Symbol>)

    the key attributes, or nil for the primary key

Returns:



176
177
178
179
180
181
182
183
184
185
# File 'lib/jinx/resource.rb', line 176

def key_value(attributes)
  attributes ||= self.class.primary_key_attributes
  case attributes.size
  when 0 then nil
  when 1 then send(attributes.first)
  else
    key = attributes.map { |pa| send(pa) || return }
    key unless key.empty?
  end
end

#mandatory_attributes<Symbol>

Returns the attributes which are required for save. This base implementation returns the class Propertied#mandatory_attributes. Subclasses can override this method for domain object state-specific refinements.

Returns:

  • (<Symbol>)

    the required attributes for a save operation



317
318
319
# File 'lib/jinx/resource.rb', line 317

def mandatory_attributes
  self.class.mandatory_attributes
end

#match_attribute_value(prop, newval, oldval) {|sources, targets| ... } ⇒ {Resource => Resource} (private)

Returns the source => target hash of matches for the given prop newval sources and oldval targets. If the matcher block is given, then that block is called on the sources and targets. Otherwise, match_all is called.

Parameters:

  • prop (Property)

    the attribute to match

  • newval

    the source value

  • oldval

    the target value

Yields:

  • (sources, targets)

    matches sources to targets

Yield Parameters:

  • sources (<Resource>)

    an Enumerable on the source value

  • targets (<Resource>)

    an Enumerable on the target value

Returns:



898
899
900
901
902
903
904
905
906
907
908
909
910
# File 'lib/jinx/resource.rb', line 898

def match_attribute_value(prop, newval, oldval)
  # make Enumerable targets and sources for matching
  sources = newval.to_enum
  targets = oldval.to_enum
  
  # match sources to targets
  unless oldval.nil_or_empty? then
    logger.debug { "Matching source #{newval.qp} to target #{qp} #{prop} #{oldval.qp}..." }
  end
  matches = block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
  logger.debug { "Matched #{qp} #{prop}: #{matches.qp}." } unless matches.empty?
  matches
end

#match_in(others) ⇒ Resource?

Matches this dependent domain object with the others on type and key attributes in the scope of a parent object. Returns the object in others which matches this domain object, or nil if none.

The match attributes are, in order:

  • the primary key

  • the secondary key

  • the alternate key

This domain object is matched against the others on the above attributes in succession until a unique match is found. The key attribute matches are strict, i.e. each key attribute value must be non-nil and match the other value.

Parameters:

  • the (<Resource>)

    candidate domain object matches

Returns:

  • (Resource, nil)

    the matching domain object, or nil if no match



393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/jinx/resource.rb', line 393

def match_in(others)
  # trivial case: self is in others
  return self if others.include?(self)
  # filter for the same type
  unless others.all? { |other| self.class === other } then
    others = others.filter { |other| self.class === other }
  end
  # match on primary, secondary or alternate key
  match_unique_object_with_attributes(others, self.class.primary_key_attributes) or
  match_unique_object_with_attributes(others, self.class.secondary_key_attributes) or
  match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
end

#match_in_owner_scope(others) ⇒ Resource?

Returns the match of this domain object in the scope of a matching owner as follows:

  • If #match_in returns a match, then that match is the result is used.

  • Otherwise, if this is a dependent attribute then the match is attempted on a secondary key without owner attributes. Defaults are added to this object in order to pick up potential secondary key values.

Parameters:

  • the (<Resource>)

    candidate domain object matches

Returns:

  • (Resource, nil)

    the matching domain object, or nil if no match



414
415
416
# File 'lib/jinx/resource.rb', line 414

def match_in_owner_scope(others)
  match_in(others) or others.detect { |other| matches_without_owner_attribute?(other) }
end

#match_unique_object_with_attributes(others, attributes) ⇒ Object (private)

Returns the object in others which uniquely matches this domain object on the given attributes, or nil if there is no unique match. This method returns nil if any attributes value is nil.



930
931
932
933
934
935
936
937
938
# File 'lib/jinx/resource.rb', line 930

def match_unique_object_with_attributes(others, attributes)
  vh = value_hash(attributes)
  return if vh.empty? or vh.size < attributes.size
  matches = others.select do |other|
    self.class == other.class and
      vh.all? { |pa, v| other.matches_attribute_value?(pa, v) }
  end
  matches.first if matches.size == 1
end

#matches?(other) ⇒ Boolean

Returns whether this object matches the fetched other object on class and a primary, secondary or alternate key.

Parameters:

  • the (Resource)

    domain object to match

Returns:

  • (Boolean)

    whether this object matches the fetched other object on class and a primary, secondary or alternate key



347
348
349
350
351
352
353
354
355
356
# File 'lib/jinx/resource.rb', line 347

def matches?(other)
  # trivial case
  return true if equal?(other)
  # check the type
  return false unless self.class == other.class
  # match on primary, secondary or alternate key
  matches_key_attributes?(other, self.class.primary_key_attributes) or
  matches_key_attributes?(other, self.class.secondary_key_attributes) or
  matches_key_attributes?(other, self.class.alternate_key_attributes)
end

#matches_attribute_value?(attribute, value) ⇒ Boolean (protected)

Returns whether this Resource’s attribute value matches the given value. A domain attribute match is determined by #match?. A non-domain attribute match is determined by an equality comparison.

Parameters:

  • attribute (Symbol)

    the attribute to match

  • value

    the value to compare

Returns:

  • (Boolean)

    whether the values match



603
604
605
606
# File 'lib/jinx/resource.rb', line 603

def matches_attribute_value?(attribute, value)
  v = send(attribute)
  Resource === v ? value.matches?(v) : value == v
end

#matches_key_attributes?(other, attributes) ⇒ Boolean (private)

Returns whether there is a non-nil value for each attribute and the value matches the other attribute value.

Parameters:

  • attributes (<Symbol>)

    the attributes to match

Returns:

  • (Boolean)

    whether there is a non-nil value for each attribute and the value matches the other attribute value



915
916
917
918
919
920
921
922
923
924
925
926
# File 'lib/jinx/resource.rb', line 915

def matches_key_attributes?(other, attributes)
  return false if attributes.empty?
  attributes.all? do |pa|
    v = send(pa)
    if v.nil? then
      false
    else
      ov = other.send(pa)
      Resource === v ? v.matches?(ov) : v == ov
    end
  end
end

#matches_without_owner_attribute?(other) ⇒ Boolean (private)

Returns whether this domain object matches the other domain object as follows:

  • The classes are the same.

  • There are not conflicting primary key values.

  • Each non-owner secondary key value matches.

Note that objects without a secondary key match.

Parameters:

  • the (<Resource>)

    candidate domain object matches

Returns:

  • (Boolean)

    whether there is a non-owner match



845
846
847
848
849
850
851
852
853
854
855
856
857
858
# File 'lib/jinx/resource.rb', line 845

def matches_without_owner_attribute?(other)
  return false unless other.class == self.class
  # check the primary key
  return false unless self.class.primary_key_attributes.all? do |ka|
    kv = send(ka)
    okv = other.send(ka)
    kv.nil? or okv.nil? or kv == okv
  end
  # match on the non-owner secondary key
  oas = self.class.owner_attributes
  self.class.secondary_key_attributes.all? do |ka|
    oas.include?(ka) or other.matches_attribute_value?(ka, send(ka))
  end
end

#minimal_match?(other) ⇒ Boolean

Returns the domain object in others which matches this dependent domain object within the scope of a parent on a minimally acceptable constraint. This method is used when this object might be partially complete–say, lacking a secondary key value–but is expected to match one of the others, e.g. when matching a referenced object to its fetched counterpart.

This base implementation returns whether the following conditions hold:

  1. other is the same class as this domain object

  2. if both identifiers are non-nil, then they are equal

Subclasses can override this method to impose additional minimal consistency constraints.

Parameters:

  • other (Resource)

    the domain object to match against

Returns:

  • (Boolean)

    whether this Resource equals other



453
454
455
456
# File 'lib/jinx/resource.rb', line 453

def minimal_match?(other)
  self.class === other and
    (identifier.nil? or other.identifier.nil? or identifier == other.identifier)
end

#missing_mandatory_attributes<Symbol> (protected)

Returns the required attributes for this domain object which are nil or empty.

Returns:

  • (<Symbol>)

    the required attributes for this domain object which are nil or empty



609
610
611
# File 'lib/jinx/resource.rb', line 609

def missing_mandatory_attributes
  mandatory_attributes.select { |pa| send(pa).nil_or_empty? }
end

#non_id_search_attribute_valuesObject (private)

Returns the attribute => value hash to use for matching this domain object.



953
954
955
956
957
958
959
960
961
962
# File 'lib/jinx/resource.rb', line 953

def non_id_search_attribute_values
  # if there is a secondary key, then search on those attributes.
  # otherwise, search on all attributes.
  key_props = self.class.secondary_key_attributes
  pas = key_props.empty? ? self.class.nondomain_java_attributes : key_props
  # associate the values
  attr_values = pas.to_compact_hash { |pa| send(pa) }
  # if there is no secondary key, then cull empty values
  key_props.empty? ? attr_values.delete_if { |pa, value| value.nil? } : attr_values
end

#ownerResource?

Returns the domain object that owns this object, or nil if this object is not dependent on an owner.

Returns:

  • (Resource, nil)

    the domain object that owns this object, or nil if this object is not dependent on an owner



206
207
208
# File 'lib/jinx/resource.rb', line 206

def owner
  self.class.owner_attributes.detect_value { |pa| send(pa) }
end

#owner=(obj) ⇒ Object

Sets this dependent’s owner attribute to the given domain object.

Parameters:

  • obj (Resource)

    the owner domain object

Raises:

  • (NoMethodError)

    if this Resource’s class does not have an owner property which accepts the given domain object



226
227
228
229
230
231
232
233
234
# File 'lib/jinx/resource.rb', line 226

def owner=(obj)
  if obj.nil? then
    op, ov = effective_owner_property_value || return
  else
    op = self.class.owner_properties.detect { |prop| prop.type === obj }
  end
  if op.nil? then raise NoMethodError.new("#{self.class.qp} does not have an owner attribute for #{obj}") end
  set_property_value(op.attribute, obj)
end

#owner_ancestor?(other) ⇒ Boolean

Returns whether the other domain object is this object’s #owner or an #owner_ancestor? of this object’s #owner.

Parameters:

  • other (Resource)

    the domain object to check

Returns:



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/jinx/resource.rb', line 239

def owner_ancestor?(other)
  ownr = self.owner
  if ownr then
    ownr == other or ownr.owner_ancestor?(other)
  elsif self.class.owner_types.size == self.class.owner_properties.size then
    false
  else
    path = other.class.dependency_path_to(self.class)
    !!path and other.reachable?(self, path)
  end
end

#path_value(path) ⇒ Object

Returns the value for the given attribute path Array or String expression, e.g.:

study.path_value("site.address.state")

follows the study -> site -> address -> state accessors and returns the state value, or nil if any intermediate reference is nil. The array form for the above example is:

study.path_value([:site, :address, :state])

Parameters:

  • path (<Symbol>)

    the attributes to navigate

Returns:

  • the attribute navigation result



478
479
480
481
482
483
484
485
# File 'lib/jinx/resource.rb', line 478

def path_value(path)
  path = path.split('.').map { |pa| pa.to_sym } if String === path
  path.inject(self) do |parent, pa|
    value = parent.send(pa)
    return if value.nil?
    value
  end
end

#post_initializeObject

Initialization hook. This default implementation is a no-op.

Technology idiosyncracy:

  • JRuby

    calling super in a module initialize method results in an infinite loop. The work-around is to add a post_initialize method that mix-in modules can override.



56
57
# File 'lib/jinx/resource.rb', line 56

def post_initialize
end

#pretty_print(q) ⇒ String

Returns the formatted content of this Resource.

Parameters:

  • q

    the PrettyPrint queue

Returns:

  • (String)

    the formatted content of this Resource



519
520
521
522
523
# File 'lib/jinx/resource.rb', line 519

def pretty_print(q)
  q.text(qp)
  content = printable_content
  q.pp_hash(content) unless content.empty?
end

#primary_keyArray, ...

Returns the key value or values.

Returns:



188
189
190
# File 'lib/jinx/resource.rb', line 188

def primary_key
  key_value(self.class.primary_key_attributes)
end

Prints this object’s class demodulized name and object id.



60
61
62
# File 'lib/jinx/resource.rb', line 60

def print_class_and_id
  "#{self.class.qp}@#{proxy_object_id}"
end

#printable_content(attributes = nil) {|ref| ... } ⇒ {Symbol => String}

Returns this domain object’s attributes content as an attribute => value hash suitable for printing.

The default attributes are this object’s saved attributes. The optional reference_printer is used to print a referenced domain object.

Parameters:

  • attributes (<Symbol>, nil) (defaults to: nil)

    the attributes to print

Yields:

  • (ref)

    the reference print formatter

Yield Parameters:

  • ref (Resource)

    the referenced domain object to print

Returns:



560
561
562
563
564
# File 'lib/jinx/resource.rb', line 560

def printable_content(attributes=nil, &reference_printer)
  attributes ||= printworthy_attributes
  vh = value_hash(attributes)
  vh.transform_value { |value| printable_value(value, &reference_printer) }
end

#printable_value(value, &reference_printer) ⇒ Object (private)

Returns a value suitable for printing. If value is a domain object, then the block provided to this method is called. The default block creates a new ReferencePrinter on the value.



807
808
809
810
811
812
813
814
815
# File 'lib/jinx/resource.rb', line 807

def printable_value(value, &reference_printer)
  Jinx::Collector.on(value) do |item|
    if Resource === item then
      block_given? ? yield(item) : printable_value(item) { |ref| ReferencePrinter.new(ref) }
    else
      item
    end
  end
end

#printworthy_attributes<Symbol] the attributes to print (private)

Returns an attribute => value hash which identifies the object. If this object has a complete primary key, than the primary key attributes are returned. Otherwise, if there are secondary key attributes, then they are returned. Otherwise, if there are nondomain attributes, then they are returned. Otherwise, if there are fetched attributes, then they are returned.

Returns:

  • (<Symbol] the attributes to print)

    <Symbol] the attributes to print



824
825
826
827
828
829
830
831
832
833
834
# File 'lib/jinx/resource.rb', line 824

def printworthy_attributes
  if self.class.primary_key_attributes.all? { |pa| !!send(pa) } then
    self.class.primary_key_attributes
  elsif not self.class.secondary_key_attributes.empty? then
    self.class.secondary_key_attributes
  elsif not self.class.nondomain_java_attributes.empty? then
    self.class.nondomain_java_attributes
  else
    self.class.fetched_attributes
  end
end

#proxy_object_idInteger

Returns the object id.

Returns:

  • (Integer)

    the object id

Technology idiosyncracy:

  • JRuby

    Bug #5090 - JRuby 1.5 object_id is no longer a reserved method, and results in a String value rather than an Integer (cf. jira.codehaus.org/browse/JRUBY-5090). Work-around is to make a proxy object id.



47
48
49
50
# File 'lib/jinx/resource.rb', line 47

def proxy_object_id
  # make a hash code on demand
  @_hc ||= (Object.new.object_id * 31) + 17
end

#reachable?(other, path) ⇒ Boolean

Returns whether following there is a path from this object through the given properties to the other object.

Parameters:

  • other (Resource)

    the domain object to check

  • the (<Property>)

    property path to follow

Returns:

  • (Boolean)

    whether following there is a path from this object through the given properties to the other object



255
256
257
258
259
260
261
# File 'lib/jinx/resource.rb', line 255

def reachable?(other, path)
  return false if path.empty?
  prop = path.shift
  send(prop.attribute).to_enum.any? do |ref|
    ref == other or ref.reachable?(other, path)
  end
end

#reference_hierarchy {|ref| ... } ⇒ Enumerable

Returns an enumerator on the transitive closure of the reference attributes. If a block is given to this method, then the block called on each reference determines which attributes to visit. Otherwise, all saved references are visited.

Yields:

  • (ref)

    reference visit attribute selector

Yield Parameters:

  • ref (Resource)

    the domain object to visit

Returns:

  • (Enumerable)

    the reference transitive closure



465
466
467
# File 'lib/jinx/resource.rb', line 465

def reference_hierarchy
  ReferenceVisitor.new { |ref| yield ref }.to_enum(self)
end

#references(attributes = nil) ⇒ <Resource>

Returns the domain object references for the given attributes.

Parameters:

  • the (<Symbol>, nil)

    domain attributes to include, or nil to include all domain attributes

Returns:

  • (<Resource>)

    the referenced attribute domain object values



277
278
279
280
# File 'lib/jinx/resource.rb', line 277

def references(attributes=nil)
  attributes ||= self.class.domain_attributes
  attributes.map { |pa| send(pa) }.flatten.compact
end

#search_attribute_valuesObject (private)

Returns the attribute => value hash to use for matching this domain object as follows:

  • If this domain object has a database identifier, then the identifier is the sole match criterion attribute.

  • Otherwise, if a secondary key is defined for the object’s class, then those attributes are used.

  • Otherwise, all attributes are used.

If any secondary key value is nil, then this method returns an empty hash, since the search is ambiguous.



946
947
948
949
# File 'lib/jinx/resource.rb', line 946

def search_attribute_values
  # if this object has a database identifier, then the identifier is the search criterion
  identifier.nil? ? non_id_search_attribute_values : { :identifier => identifier }
end

#secondary_keyArray, ...

Returns the key value or values.

Returns:

See Also:



194
195
196
# File 'lib/jinx/resource.rb', line 194

def secondary_key
  key_value(self.class.secondary_key_attributes)
end

#set_property_value(prop, value) ⇒ Object

Sets this domain object’s attribute to the value. This method clears the current attribute value, if any, and merges the new value. Merge rather than assignment ensures that a collection type is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set and then merging the array content into the set.

Parameters:

  • prop (Property, Symbol)

    the property or attribute to set

  • value

    the new value



149
150
151
152
153
154
155
156
157
# File 'lib/jinx/resource.rb', line 149

def set_property_value(prop, value)
  prop = self.class.property(prop) if Symbol === prop
  if prop.domain? and prop.collection? then
    clear_attribute(prop.attribute)
    merge_attribute(prop.attribute, value)
  else
    set_typed_property_value(prop, value)
  end
end

#set_typed_property_value(property, value) ⇒ Object (private)

Parameters:

  • property (Property)

    the property to set

  • value

    the new value

Raises:

  • (TypeError)

    if the value is incompatible with the property



737
738
739
740
741
742
743
744
# File 'lib/jinx/resource.rb', line 737

def set_typed_property_value(property, value)
  begin
    send(property.writer, value)
  rescue TypeError
    logger.error("Cannot set #{self.class.qp} #{property} to #{value.qp} - " + $!)
    raise
  end
end

#to_s(attributes = nil) ⇒ String Also known as: inspect

Prints this domain object in the format:

class_name@object_id{attribute => value ...}

The default attributes include identifying attributes.

Parameters:

  • attributes (<Symbol>) (defaults to: nil)

    the attributes to print

Returns:

  • (String)

    the formatted content



542
543
544
545
546
# File 'lib/jinx/resource.rb', line 542

def to_s(attributes=nil)
  content = printable_content(attributes)
  content_s = content.pp_s(:single_line) unless content.empty?
  "#{print_class_and_id}#{content_s}"
end

#validateResource

Validates this domain object and its ##dependents for consistency and completeness. An object is valid if it contains a non-nil value for each mandatory attribute. Objects which have already been validated are skipped.

A Resource class should not override this method, but override the private #validate_local method instead.

Returns:



97
98
99
100
101
102
103
104
# File 'lib/jinx/resource.rb', line 97

def validate
  unless @validated then
    validate_local
    @validated = true
  end
  dependents.each { |dep| dep.validate }
  self
end

#validate_localObject (private)

Validates that this domain object is internally consistent. Subclasses override this method for additional validation, but should call super first.



695
696
697
698
# File 'lib/jinx/resource.rb', line 695

def validate_local
  validate_mandatory_attributes
  validate_owner
end

#validate_mandatory_attributesObject (private)

Validates that this domain object contains a non-nil value for each mandatory attribute.

Raises:



703
704
705
706
707
708
709
710
# File 'lib/jinx/resource.rb', line 703

def validate_mandatory_attributes
  invalid = missing_mandatory_attributes
  unless invalid.empty? then
    logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
    raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
  end
  validate_owner
end

#validate_ownerObject (private)

Validates that this domain object either doesn’t have an owner attribute or has a unique effective owner.

Raises:



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
# File 'lib/jinx/resource.rb', line 717

def validate_owner
  # If there is an unambigous owner, then we are done.
  return unless owner.nil?
  # If there is more than one owner attribute, then check that there is at most one
  # unambiguous owner reference. The owner method returns nil if the owner is ambiguous.
  if self.class.owner_attributes.size > 1 then
    vh = value_hash(self.class.owner_attributes)
    if vh.size > 1 then
      raise ValidationError.new("Dependent #{self} references multiple owners #{vh.pp_s}:\n#{dump}")
    end
  end
  # If there is an owner reference attribute, then there must be an owner.
  if self.class.bidirectional_java_dependent? then
    raise ValidationError.new("Dependent #{self} does not reference an owner")
  end
end

#visit_dependents {|dep| ... } ⇒ Object

Applies the operator block to the transitive closure of this domain object’s dependency relation. The block argument is a dependent.

Yields:

  • (dep)

    operation on the visited domain object

Yield Parameters:

  • dep (Resource)

    the domain object to visit



504
505
506
# File 'lib/jinx/resource.rb', line 504

def visit_dependents(&operator)
  DEPENDENT_VISITOR.visit(self, &operator)
end

#visit_owners {|dep| ... } ⇒ Object

Applies the operator block to the transitive closure of this domain object’s owner relation.

Yields:

  • (dep)

    operation on the visited domain object

Yield Parameters:

  • dep (Resource)

    the domain object to visit



512
513
514
515
# File 'lib/jinx/resource.rb', line 512

def visit_owners(&operator) # :yields: owner
  ref = owner
  yield(ref) and ref.visit_owners(&operator) if ref
end

#visit_path(*path) {|attribute| ... } ⇒ Object

Applies the operator block to this object and each domain object in the reference path. This method visits the transitive closure of each recursive path attribute.

Parameters:

  • path (<Symbol>)

    the attributes to visit

Yield Parameters:

  • attribute (Symbol)

    the attribute to visit

Returns:

  • the visit result

See Also:



494
495
496
497
# File 'lib/jinx/resource.rb', line 494

def visit_path(*path, &operator)
  visitor = ReferencePathVisitor.new(self.class, path)
  visitor.visit(self, &operator)
end