active-triples 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,23 +1,34 @@
1
1
  require 'active_support/core_ext/module/delegation'
2
2
 
3
3
  module ActiveTriples
4
- class Term
5
-
6
- attr_accessor :parent, :value_arguments, :node_cache, :term_args
4
+ ##
5
+ # A `Relation` represents the values of a specific property/predicate on an
6
+ # {RDFSource}. Each relation is a set ({Array}) of {RDF::Terms} that are
7
+ # objects in the of source's triples of the form:
8
+ #
9
+ # <{#parent}> <{#predicate}> [term] .
10
+ #
11
+ # Relations, then, express n binary relationships between the parent node and
12
+ # a term.
13
+ #
14
+ # @see RDF::Term
15
+ class Relation
16
+
17
+ attr_accessor :parent, :value_arguments, :node_cache, :rel_args
7
18
  attr_reader :reflections
8
19
 
9
20
  delegate *(Array.public_instance_methods - [:send, :__send__, :__id__, :class, :object_id] + [:as_json]), :to => :result
10
21
 
11
- def initialize(parent_resource, value_arguments)
12
- self.parent = parent_resource
13
- @reflections = parent_resource.reflections
14
- self.term_args ||= {}
22
+ def initialize(parent_source, value_arguments)
23
+ self.parent = parent_source
24
+ @reflections = parent_source.reflections
25
+ self.rel_args ||= {}
15
26
  self.value_arguments = value_arguments
16
27
  end
17
28
 
18
29
  def value_arguments=(value_args)
19
30
  if value_args.kind_of?(Array) && value_args.last.kind_of?(Hash)
20
- self.term_args = value_args.pop
31
+ self.rel_args = value_args.pop
21
32
  end
22
33
  @value_arguments = value_args
23
34
  end
@@ -36,7 +47,7 @@ module ActiveTriples
36
47
 
37
48
  def set(values)
38
49
  values = [values].compact unless values.kind_of?(Array)
39
- values = values.to_a if values.class == Term
50
+ values = values.to_a if values.class == Relation
40
51
  empty_property
41
52
  values.each do |val|
42
53
  set_value(val)
@@ -91,7 +102,7 @@ module ActiveTriples
91
102
  end
92
103
 
93
104
  def property_config
94
- return type_property if (property == RDF.type || property.to_s == "type") && (!reflections.kind_of?(Resource) || !reflections.reflect_on_property(property))
105
+ return type_property if (property == RDF.type || property.to_s == "type") && (!reflections.kind_of?(RDFSource) || !reflections.reflect_on_property(property))
95
106
  reflections.reflect_on_property(property)
96
107
  end
97
108
 
@@ -116,7 +127,7 @@ module ActiveTriples
116
127
  object = val
117
128
  val = val.resource if val.respond_to?(:resource)
118
129
  val = value_to_node(val)
119
- if val.kind_of? Resource
130
+ if val.kind_of? RDFSource
120
131
  node_cache[val.rdf_subject] = nil
121
132
  add_child_node(val, object)
122
133
  return
@@ -178,13 +189,13 @@ module ActiveTriples
178
189
  end
179
190
 
180
191
  def cast?
181
- return true unless property_config || (term_args && term_args[:cast])
182
- return term_args[:cast] if term_args.has_key?(:cast)
192
+ return true unless property_config || (rel_args && rel_args[:cast])
193
+ return rel_args[:cast] if rel_args.has_key?(:cast)
183
194
  !!property_config[:cast]
184
195
  end
185
196
 
186
197
  def return_literals?
187
- term_args && term_args[:literal]
198
+ rel_args && rel_args[:literal]
188
199
  end
189
200
 
190
201
  def final_parent
@@ -211,7 +222,8 @@ module ActiveTriples
211
222
  def class_for_property
212
223
  klass = property_config[:class_name] if property_config
213
224
  klass ||= Resource
214
- klass = ActiveTriples.class_from_string(klass, final_parent.class) if klass.kind_of? String
225
+ klass = ActiveTriples.class_from_string(klass, final_parent.class) if
226
+ klass.kind_of? String
215
227
  klass
216
228
  end
217
229
 
@@ -223,6 +235,19 @@ module ActiveTriples
223
235
  parent.rdf_subject
224
236
  end
225
237
  end
238
+ end
226
239
 
240
+ class Term < Relation
241
+ def self.inherited(*)
242
+ warn 'ActiveTriples::Term is deprecated! ' \
243
+ 'Use ActiveTriples::Relation instead.'
244
+ super
245
+ end
246
+
247
+ def initialize(*)
248
+ warn 'ActiveTriples::Term is deprecated! ' \
249
+ 'Use ActiveTriples::Relation instead.'
250
+ super
251
+ end
227
252
  end
228
253
  end
@@ -1,547 +1,22 @@
1
1
  require 'deprecation'
2
- require 'active_model'
3
- require 'active_support/core_ext/hash'
4
2
 
5
3
  module ActiveTriples
6
4
  ##
7
- # Defines a generic RDF `Resource` as an RDF::Graph with property
8
- # configuration, accessors, and some other methods for managing
9
- # resources as discrete subgraphs which can be maintained by a Hydra
10
- # datastream model.
11
- #
12
- # Resources can be instances of ActiveTriples::Resource
13
- # directly, but more often they will be instances of subclasses with
14
- # registered properties and configuration. e.g.
15
- #
16
- # class License < Resource
17
- # configure repository: :default
18
- # property :title, predicate: RDF::DC.title, class_name: RDF::Literal do |index|
19
- # index.as :displayable, :facetable
20
- # end
21
- # end
22
- class Resource < RDF::Graph
23
- extend Configurable
24
- include Properties
25
- extend Deprecation
26
- extend ActiveModel::Naming
27
- extend ActiveModel::Translation
28
- extend ActiveModel::Callbacks
29
- include ActiveModel::Validations
30
- include ActiveModel::Conversion
31
- include ActiveModel::Serialization
32
- include ActiveModel::Serializers::JSON
33
- include NestedAttributes
34
- include Reflection
35
- attr_accessor :parent
36
- define_model_callbacks :persist
5
+ # Defines a generic RDF `Resource` as an `ActiveTriples::Entity`. This
6
+ # provides a basic `Entity` type for classless resources.
7
+ class Resource
8
+ include RDFSource
37
9
 
38
10
  class << self
39
11
  def type_registry
40
- @@type_registry ||= {}
12
+ RDFSource.type_registry
41
13
  end
42
14
 
43
- ##
44
- # Adapter for a consistent interface for creating a new Resource
45
- # from a URI. Similar functionality should exist in all objects
46
- # which can become a Resource.
47
- #
48
- # @param uri [#to_uri, String]
49
- # @param vals values to pass as arguments to ::new
50
- #
51
- # @return [ActiveTriples::Resource] a Resource with
52
- def from_uri(uri,vals=nil)
53
- new(uri, vals)
15
+ def property(*)
16
+ raise "Properties not definable directly on ActiveTriples::Resource, use a subclass" if
17
+ self == ActiveTriples::Resource
18
+ super
54
19
  end
55
20
  end
56
-
57
- ##
58
- # Specifies whether the object is currently writable.
59
- #
60
- # @return [true, false]
61
- def writable?
62
- !frozen?
63
- end
64
-
65
- ##
66
- # Initialize an instance of this resource class. Defaults to a
67
- # blank node subject. In addition to RDF::Graph parameters, you
68
- # can pass in a URI and/or a parent to build a resource from a
69
- # existing data.
70
- #
71
- # You can pass in only a parent with:
72
- # Resource.new(nil, parent)
73
- #
74
- # @see RDF::Graph
75
- def initialize(*args, &block)
76
- resource_uri = args.shift unless args.first.is_a?(Hash)
77
- self.parent = args.shift unless args.first.is_a?(Hash)
78
- set_subject!(resource_uri) if resource_uri
79
- super(*args, &block)
80
- reload
81
- # Append type to graph if necessary.
82
- self.get_values(:type) << self.class.type if self.class.type.kind_of?(RDF::URI) && type.empty?
83
- end
84
-
85
- ##
86
- # Returns the current object.
87
- #
88
- # @deprecated redundant, simply returns self.
89
- #
90
- # @return [self]
91
- def graph
92
- Deprecation.warn Resource, "graph is redundant & deprecated. It will be removed in ActiveTriples 0.2.0.", caller
93
- self
94
- end
95
-
96
- def final_parent
97
- @final_parent ||= begin
98
- parent = self.parent
99
- while parent && parent.parent && parent.parent != parent
100
- parent = parent.parent
101
- end
102
- parent
103
- end
104
- end
105
-
106
- def attributes
107
- attrs = {}
108
- attrs['id'] = id if id
109
- fields.map { |f| attrs[f.to_s] = get_values(f) }
110
- unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) }
111
- attrs
112
- end
113
-
114
- def serializable_hash(options = nil)
115
- attrs = (fields.map { |f| f.to_s }) << 'id'
116
- hash = super(:only => attrs)
117
- unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
118
- hash
119
- end
120
-
121
- def reflections
122
- self.class
123
- end
124
-
125
- def attributes=(values)
126
- raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
127
- values = values.with_indifferent_access
128
- id = values.delete(:id)
129
- set_subject!(id) if id && node?
130
- values.each do |key, value|
131
- if reflections.reflect_on_property(key)
132
- set_value(rdf_subject, key, value)
133
- elsif nested_attributes_options.keys.map { |k| "#{k}_attributes" }.include?(key)
134
- send("#{key}=".to_sym, value)
135
- else
136
- raise ArgumentError, "No association found for name `#{key}'. Has it been defined yet?"
137
- end
138
- end
139
- end
140
-
141
- ##
142
- # Returns a serialized string representation of self.
143
- # Extends the base implementation builds a JSON-LD context if the
144
- # specified format is :jsonld and a context is provided by
145
- # #jsonld_context
146
- #
147
- # @see RDF::Enumerable#dump
148
- #
149
- # @param args [Array<Object>]
150
- # @return [String]
151
- def dump(*args)
152
- if args.first == :jsonld and respond_to?(:jsonld_context)
153
- args << {} unless args.last.is_a?(Hash)
154
- args.last[:context] ||= jsonld_context
155
- end
156
- super
157
- end
158
-
159
- ##
160
- # @return [RDF::URI, RDF::Node] a URI or Node which the resource's
161
- # properties are about.
162
- def rdf_subject
163
- @rdf_subject ||= RDF::Node.new
164
- end
165
- alias_method :to_term, :rdf_subject
166
-
167
- ##
168
- # A string identifier for the resource
169
- def id
170
- node? ? nil : rdf_subject.to_s
171
- end
172
-
173
- def node?
174
- return true if rdf_subject.kind_of? RDF::Node
175
- false
176
- end
177
-
178
- ##
179
- # @return [String, nil] the base URI the resource will use when
180
- # setting its subject. `nil` if none is used.
181
- def base_uri
182
- self.class.base_uri
183
- end
184
-
185
- def type
186
- self.get_values(:type).to_a
187
- end
188
-
189
- def type=(type)
190
- raise "Type must be an RDF::URI" unless type.kind_of? RDF::URI
191
- self.update(RDF::Statement.new(rdf_subject, RDF.type, type))
192
- end
193
-
194
- ##
195
- # Looks for labels in various default fields, prioritizing
196
- # configured label fields.
197
- def rdf_label
198
- labels = Array(self.class.rdf_label)
199
- labels += default_labels
200
- labels.each do |label|
201
- values = get_values(label)
202
- return values unless values.empty?
203
- end
204
- node? ? [] : [rdf_subject.to_s]
205
- end
206
-
207
- ##
208
- # Lists fields registered as properties on the object.
209
- #
210
- # @return [Array<Symbol>] the list of registered properties.
211
- def fields
212
- properties.keys.map(&:to_sym).reject{|x| x == :type}
213
- end
214
-
215
- ##
216
- # Load data from the #rdf_subject URI. Retrieved data will be
217
- # parsed into the Resource's graph from available RDF::Readers
218
- # and available from property accessors if if predicates are
219
- # registered.
220
- #
221
- # osu = ActiveTriples::Resource.new('http://dbpedia.org/resource/Oregon_State_University')
222
- # osu.fetch
223
- # osu.rdf_label.first
224
- # # => "Oregon State University"
225
- #
226
- # @return [ActiveTriples::Resource] self
227
- def fetch
228
- load(rdf_subject)
229
- self
230
- end
231
-
232
- def persist!(opts={})
233
- return if @persisting
234
- return false if opts[:validate] && !valid?
235
- @persisting = true
236
- run_callbacks :persist do
237
- raise "failed when trying to persist to non-existant repository or parent resource" unless repository
238
- erase_old_resource
239
- repository << self
240
- @persisted = true
241
- end
242
- @persisting = false
243
- true
244
- end
245
-
246
- ##
247
- # Indicates if the resource is persisted.
248
- #
249
- # @see #persist
250
- # @return [true, false]
251
- def persisted?
252
- @persisted ||= false
253
- return (@persisted and parent.persisted?) if parent
254
- @persisted
255
- end
256
-
257
- ##
258
- # Repopulates the graph from the repository or parent resource.
259
- #
260
- # @return [true, false]
261
- def reload
262
- @term_cache ||= {}
263
- return false unless repository
264
- self << repository.query(subject: rdf_subject)
265
- unless empty?
266
- @persisted = true
267
- end
268
- true
269
- end
270
-
271
- ##
272
- # Adds or updates a property with supplied values.
273
- #
274
- # Handles two argument patterns. The recommended pattern is:
275
- # set_value(property, values)
276
- #
277
- # For backwards compatibility, there is support for explicitly
278
- # passing the rdf_subject to be used in the statement:
279
- # set_value(uri, property, values)
280
- #
281
- # @note This method will delete existing statements with the correct subject and predicate from the graph
282
- def set_value(*args)
283
- # Add support for legacy 3-parameter syntax
284
- if args.length > 3 || args.length < 2
285
- raise ArgumentError, "wrong number of arguments (#{args.length} for 2-3)"
286
- end
287
- values = args.pop
288
- get_term(args).set(values)
289
- end
290
-
291
- ##
292
- # Adds or updates a property with supplied values.
293
- #
294
- # @note This method will delete existing statements with the correct subject and predicate from the graph
295
- def []=(uri_or_term_property, value)
296
- self[uri_or_term_property].set(value)
297
- end
298
-
299
- ##
300
- # Returns an array of values belonging to the property
301
- # requested. Elements in the array may RdfResource objects or a
302
- # valid datatype.
303
- #
304
- # Handles two argument patterns. The recommended pattern is:
305
- # get_values(property)
306
- #
307
- # For backwards compatibility, there is support for explicitly
308
- # passing the rdf_subject to be used in th statement:
309
- # get_values(uri, property)
310
- def get_values(*args)
311
- get_term(args)
312
- end
313
-
314
- ##
315
- # Returns an array of values belonging to the property
316
- # requested. Elements in the array may RdfResource objects or a
317
- # valid datatype.
318
- def [](uri_or_term_property)
319
- get_term([uri_or_term_property])
320
- end
321
-
322
-
323
- def get_term(args)
324
- @term_cache ||= {}
325
- term = Term.new(self, args)
326
- @term_cache["#{term.send(:rdf_subject)}/#{term.property}/#{term.term_args}"] ||= term
327
- @term_cache["#{term.send(:rdf_subject)}/#{term.property}/#{term.term_args}"]
328
- end
329
-
330
- ##
331
- # Set a new rdf_subject for the resource.
332
- #
333
- # This raises an error if the current subject is not a blank node,
334
- # and returns false if it can't figure out how to make a URI from
335
- # the param. Otherwise it creates a URI for the resource and
336
- # rebuilds the graph with the updated URI.
337
- #
338
- # Will try to build a uri as an extension of the class's base_uri
339
- # if appropriate.
340
- #
341
- # @param [#to_uri, #to_s] uri_or_str the uri or string to use
342
- def set_subject!(uri_or_str)
343
- raise "Refusing update URI when one is already assigned!" unless node? or rdf_subject == RDF::URI(nil)
344
- # Refusing set uri to an empty string.
345
- return false if uri_or_str.nil? or (uri_or_str.to_s.empty? and not uri_or_str.kind_of? RDF::URI)
346
- # raise "Refusing update URI! This object is persisted to a datastream." if persisted?
347
- old_subject = rdf_subject
348
- @rdf_subject = get_uri(uri_or_str)
349
-
350
- each_statement do |statement|
351
- if statement.subject == old_subject
352
- delete(statement)
353
- self << RDF::Statement.new(rdf_subject, statement.predicate, statement.object)
354
- elsif statement.object == old_subject
355
- delete(statement)
356
- self << RDF::Statement.new(statement.subject, statement.predicate, rdf_subject)
357
- end
358
- end
359
- end
360
-
361
- def destroy
362
- clear
363
- persist! if repository
364
- parent.destroy_child(self) if parent
365
- @destroyed = true
366
- end
367
- alias_method :destroy!, :destroy
368
-
369
- ##
370
- # Indicates if the Resource has been destroyed.
371
- #
372
- # @return [true, false]
373
- def destroyed?
374
- @destroyed ||= false
375
- end
376
-
377
- def destroy_child(child)
378
- statements.each do |statement|
379
- delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject
380
- end
381
- end
382
-
383
- ##
384
- # Indicates if the record is 'new' (has not yet been persisted).
385
- #
386
- # @return [true, false]
387
- def new_record?
388
- not persisted?
389
- end
390
-
391
- ##
392
- # @return [String] the string representation of the resource
393
- def solrize
394
- node? ? rdf_label : rdf_subject.to_s
395
- end
396
-
397
- def mark_for_destruction
398
- @marked_for_destruction = true
399
- end
400
-
401
- def marked_for_destruction?
402
- @marked_for_destruction
403
- end
404
-
405
- protected
406
-
407
- #Clear out any old assertions in the repository about this node or statement
408
- # thus preparing to receive the updated assertions.
409
- def erase_old_resource
410
- if node?
411
- repository.statements.each do |statement|
412
- repository.send(:delete_statement, statement) if statement.subject == rdf_subject
413
- end
414
- else
415
- repository.delete [rdf_subject, nil, nil]
416
- end
417
- end
418
-
419
- ##
420
- # Test if the rdf_subject that would be generated using a
421
- # specific ID is already in use in the triplestore.
422
- #
423
- # @param [Integer, #read] ID to test
424
- #
425
- # @return [TrueClass, FalseClass] true, if the ID is in
426
- # use in the triplestore; otherwise, false.
427
- # NOTE: If the ID is in use in an object not yet
428
- # persisted, false will be returned presenting
429
- # a window of opportunity for an ID clash.
430
- def self.id_persisted?(test_id)
431
- rdf_subject = self.new(test_id).rdf_subject
432
- ActiveTriples::Repositories.has_subject?(rdf_subject)
433
- end
434
-
435
- ##
436
- # Test if the rdf_subject that would be generated using a
437
- # specific URI is already in use in the triplestore.
438
- #
439
- # @param [String, RDF::URI, #read] URI to test
440
- #
441
- # @return [TrueClass, FalseClass] true, if the URI is in
442
- # use in the triplestore; otherwise, false.
443
- # NOTE: If the URI is in use in an object not yet
444
- # persisted, false will be returned presenting
445
- # a window of opportunity for an ID clash.
446
- def self.uri_persisted?(test_uri)
447
- rdf_subject = test_uri.kind_of?(RDF::URI) ? test_uri : RDF::URI(test_uri)
448
- ActiveTriples::Repositories.has_subject?(rdf_subject)
449
- end
450
-
451
- private
452
-
453
- ##
454
- # Returns the properties registered and their configurations.
455
- #
456
- # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveTriples::NodeConfig}]
457
- def properties
458
- _active_triples_config
459
- end
460
-
461
- ##
462
- # List of RDF predicates registered as properties on the object.
463
- #
464
- # @return [Array<RDF::URI>]
465
- def registered_predicates
466
- properties.values.map { |config| config.predicate }
467
- end
468
-
469
- ##
470
- # List of RDF predicates used in the Resource's triples, but not
471
- # mapped to any property or accessor methods.
472
- #
473
- # @return [Array<RDF::URI>]
474
- def unregistered_predicates
475
- preds = registered_predicates
476
- preds << RDF.type
477
- predicates.select { |p| !preds.include? p }
478
- end
479
-
480
- ##
481
- # Given a predicate which has been registered to a property,
482
- # returns the name of the matching property.
483
- #
484
- # @param predicate [RDF::URI]
485
- #
486
- # @return [String, nil] the name of the property mapped to the
487
- # predicate provided
488
- def property_for_predicate(predicate)
489
- properties.each do |property, values|
490
- return property if values[:predicate] == predicate
491
- end
492
- return nil
493
- end
494
-
495
- def default_labels
496
- [RDF::SKOS.prefLabel,
497
- RDF::DC.title,
498
- RDF::RDFS.label,
499
- RDF::SKOS.altLabel,
500
- RDF::SKOS.hiddenLabel]
501
- end
502
-
503
- ##
504
- # Return the repository (or parent) that this resource should
505
- # write to when persisting.
506
- #
507
- # @return [RDF::Repository, ActiveTriples::Resource] the target
508
- # repository
509
- def repository
510
- @repository ||=
511
- if self.class.repository == :parent
512
- final_parent
513
- else
514
- repo = Repositories.repositories[self.class.repository]
515
- raise RepositoryNotFoundError, "The class #{self.class} expects a repository called #{self.class.repository}, but none was declared" unless repo
516
- repo
517
- end
518
- end
519
-
520
- ##
521
- # Takes a URI or String and aggressively tries to convert it into
522
- # an RDF term. If a String is given, first tries to interpret it
523
- # as a valid URI, then tries to append it to base_uri. Finally,
524
- # raises an error if no valid term can be built.
525
- #
526
- # The argument must be an RDF::Node, an object that responds to
527
- # #to_uri, a String that represents a valid URI, or a String that
528
- # appends to the Resource's base_uri to create a valid URI.
529
- #
530
- # @TODO: URI.scheme_list is naive and incomplete. Find a better
531
- # way to check for an existing scheme.
532
- #
533
- # @param uri_or_str [RDF::Resource, String]
534
- #
535
- # @return [RDF::Resource] A term
536
- # @raise [RuntimeError] no valid RDF term could be built
537
- def get_uri(uri_or_str)
538
- return uri_or_str.to_uri if uri_or_str.respond_to? :to_uri
539
- return uri_or_str if uri_or_str.kind_of? RDF::Node
540
- uri_or_str = uri_or_str.to_s
541
- return RDF::Node(uri_or_str[2..-1]) if uri_or_str.start_with? '_:'
542
- return RDF::URI(uri_or_str) if RDF::URI(uri_or_str).valid? and (URI.scheme_list.include?(RDF::URI.new(uri_or_str).scheme.upcase) or RDF::URI.new(uri_or_str).scheme == 'info')
543
- return RDF::URI(self.base_uri.to_s + (self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/') + uri_or_str) if base_uri && !uri_or_str.start_with?(base_uri.to_s)
544
- raise RuntimeError, "could not make a valid RDF::URI from #{uri_or_str}"
545
- end
546
21
  end
547
22
  end