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.
  packages '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_attribute, #merge_attributes, #merge_domain_property_value, #merge_nondomain_property_value, #mergeable__equal?

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:



726
727
728
# File 'lib/jinx/resource.rb', line 726

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



392
393
394
# File 'lib/jinx/resource.rb', line 392

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



546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/jinx/resource.rb', line 546

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:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/jinx/resource.rb', line 63

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.



655
656
657
658
# File 'lib/jinx/resource.rb', line 655

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.



586
587
588
589
590
591
# File 'lib/jinx/resource.rb', line 586

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:



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

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



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/jinx/resource.rb', line 117

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



345
346
347
348
# File 'lib/jinx/resource.rb', line 345

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



596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/jinx/resource.rb', line 596

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



103
104
105
106
107
108
109
110
111
# File 'lib/jinx/resource.rb', line 103

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



833
834
835
836
# File 'lib/jinx/resource.rb', line 833

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



255
256
257
# File 'lib/jinx/resource.rb', line 255

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.



229
230
231
232
# File 'lib/jinx/resource.rb', line 229

def dependent_update_only?(other)
  other.owner == self and
  other.class.nondomain_attributes.detect_with_property { |prop| prop.updatable? and not prop.creatable? }
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



271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/jinx/resource.rb', line 271

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

#diff(other, attributes = nil) ⇒ {Object => (Object,Object)}

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:



402
403
404
405
406
407
# File 'lib/jinx/resource.rb', line 402

def diff(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



307
308
309
310
311
312
313
314
# File 'lib/jinx/resource.rb', line 307

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



502
503
504
# File 'lib/jinx/resource.rb', line 502

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



722
723
724
# File 'lib/jinx/resource.rb', line 722

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

#effective_owner_property_value(Property, Resource)?

owner reference, or nil if this domain object does not reference an owner

Returns:

  • ((Property, Resource), nil)

    the (property, value) pair for which there is an



201
202
203
204
205
206
# File 'lib/jinx/resource.rb', line 201

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.



842
843
844
845
846
847
848
849
# File 'lib/jinx/resource.rb', line 842

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



260
261
262
# File 'lib/jinx/resource.rb', line 260

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.



852
853
854
855
# File 'lib/jinx/resource.rb', line 852

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:



152
153
154
# File 'lib/jinx/resource.rb', line 152

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:



165
166
167
168
169
170
171
172
173
174
# File 'lib/jinx/resource.rb', line 165

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



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

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:



868
869
870
871
872
873
874
875
876
877
878
879
880
# File 'lib/jinx/resource.rb', line 868

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



365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/jinx/resource.rb', line 365

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



386
387
388
# File 'lib/jinx/resource.rb', line 386

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.



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

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



319
320
321
322
323
324
325
326
327
328
# File 'lib/jinx/resource.rb', line 319

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



573
574
575
576
# File 'lib/jinx/resource.rb', line 573

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



885
886
887
888
889
890
891
892
893
894
895
896
# File 'lib/jinx/resource.rb', line 885

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



815
816
817
818
819
820
821
822
823
824
825
826
827
828
# File 'lib/jinx/resource.rb', line 815

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



423
424
425
426
# File 'lib/jinx/resource.rb', line 423

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



579
580
581
# File 'lib/jinx/resource.rb', line 579

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.



923
924
925
926
927
928
929
930
931
932
# File 'lib/jinx/resource.rb', line 923

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



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

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

#owner=(owner) ⇒ Object

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

Parameters:

  • owner (Resource)

    the owner domain object

Raises:

  • (NoMethodError)

    if this Resource’s class does not have exactly one owner attribute



212
213
214
215
216
# File 'lib/jinx/resource.rb', line 212

def owner=(owner)
  pa = self.class.owner_attribute
  if pa.nil? then raise NoMethodError.new("#{self.class.qp} does not have a unique owner attribute") end
  set_property_value(pa, owner)
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:



221
222
223
224
# File 'lib/jinx/resource.rb', line 221

def owner_ancestor?(other)
  ownr = self.owner
  ownr and (ownr == other or ownr.owner_ancestor?(other))
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



448
449
450
451
452
453
454
455
# File 'lib/jinx/resource.rb', line 448

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

#pretty_print(q) ⇒ String

Returns the formatted content of this Resource.

Parameters:

  • q

    the PrettyPrint queue

Returns:

  • (String)

    the formatted content of this Resource



489
490
491
492
493
# File 'lib/jinx/resource.rb', line 489

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:



177
178
179
# File 'lib/jinx/resource.rb', line 177

def primary_key
  key_value(self.class.primary_key_attributes)
end

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



50
51
52
# File 'lib/jinx/resource.rb', line 50

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:



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

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.



777
778
779
780
781
782
783
784
785
# File 'lib/jinx/resource.rb', line 777

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



794
795
796
797
798
799
800
801
802
803
804
# File 'lib/jinx/resource.rb', line 794

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.



44
45
46
47
# File 'lib/jinx/resource.rb', line 44

def proxy_object_id
  # make a hash code on demand
  @_hc ||= (Object.new.object_id * 31) + 17
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



435
436
437
# File 'lib/jinx/resource.rb', line 435

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



249
250
251
252
# File 'lib/jinx/resource.rb', line 249

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.



916
917
918
919
# File 'lib/jinx/resource.rb', line 916

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:



183
184
185
# File 'lib/jinx/resource.rb', line 183

def secondary_key
  key_value(self.class.secondary_key_attributes)
end

#set_property_value(attribute, 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.



138
139
140
141
142
143
144
145
146
# File 'lib/jinx/resource.rb', line 138

def set_property_value(attribute, value)
  prop = self.class.property(attribute)
  if prop.domain? and prop.collection? then
    clear_attribute(attribute)
    merge_attribute(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



707
708
709
710
711
712
713
714
# File 'lib/jinx/resource.rb', line 707

def set_typed_property_value(property, value)
  begin
    send(property.writer, value)
  rescue TypeError
    # Add the attribute to the error message.
    raise TypeError.new("Cannot set #{self.class.qp} #{property} to #{value.qp} - " + $!)
  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



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

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:



87
88
89
90
91
92
93
94
# File 'lib/jinx/resource.rb', line 87

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.



665
666
667
668
# File 'lib/jinx/resource.rb', line 665

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:



673
674
675
676
677
678
679
680
# File 'lib/jinx/resource.rb', line 673

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:



687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'lib/jinx/resource.rb', line 687

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_dependent? then
    raise ValidationError.new("Dependent #{self} does not reference an owner")
  end
end

#value_hash(attributes = nil) ⇒ {Symbol => Object}

Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value. The default attributes are this domain object’s class Propertied#attributes. Only non-nil attributes defined by this Resource are included in the result hash.

Parameters:

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

    the attributes to merge

Returns:



240
241
242
243
# File 'lib/jinx/resource.rb', line 240

def value_hash(attributes=nil)
  attributes ||= self.class.attributes
  attributes.to_compact_hash { |pa| send(pa) if self.class.method_defined?(pa) }
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



474
475
476
# File 'lib/jinx/resource.rb', line 474

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



482
483
484
485
# File 'lib/jinx/resource.rb', line 482

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:



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

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