active-triples 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,132 @@
1
+ require 'active_support'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/class'
4
+
5
+ module ActiveTriples
6
+ module NestedAttributes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :nested_attributes_options, :instance_writer => false
11
+ self.nested_attributes_options = {}
12
+ end
13
+
14
+ private
15
+
16
+ UNASSIGNABLE_KEYS = %w(_destroy )
17
+
18
+ # @param [Symbol] association_name
19
+ # @param [Hash, Array] attributes_collection
20
+ # @example
21
+ #
22
+ # assign_nested_attributes_for_collection_association(:people, {
23
+ # '1' => { id: '1', name: 'Peter' },
24
+ # '2' => { name: 'John' },
25
+ # '3' => { id: '2', _destroy: true }
26
+ # })
27
+ #
28
+ # Will update the name of the Person with ID 1, build a new associated
29
+ # person with the name 'John', and mark the associated Person with ID 2
30
+ # for destruction.
31
+ #
32
+ # Also accepts an Array of attribute hashes:
33
+ #
34
+ # assign_nested_attributes_for_collection_association(:people, [
35
+ # { id: '1', name: 'Peter' },
36
+ # { name: 'John' },
37
+ # { id: '2', _destroy: true }
38
+ # ])
39
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
40
+ options = self.nested_attributes_options[association_name]
41
+
42
+ # TODO
43
+ #check_record_limit!(options[:limit], attributes_collection)
44
+
45
+ if attributes_collection.is_a?(Hash)
46
+ attributes_collection = attributes_collection.values
47
+ end
48
+
49
+ association = self.send(association_name)
50
+
51
+ attributes_collection.each do |attributes|
52
+ attributes = attributes.with_indifferent_access
53
+
54
+ if attributes['id'] && existing_record = association.detect { |record| record.rdf_subject.to_s == attributes['id'].to_s }
55
+ if !call_reject_if(association_name, attributes)
56
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
57
+ end
58
+ else
59
+ attributes = attributes.with_indifferent_access
60
+ association.build(attributes.except(*UNASSIGNABLE_KEYS))
61
+ end
62
+ end
63
+ end
64
+
65
+ # Updates a record with the +attributes+ or marks it for destruction if
66
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
67
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
68
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
69
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
70
+ end
71
+
72
+ def call_reject_if(association_name, attributes)
73
+ return false if has_destroy_flag?(attributes)
74
+ case callback = self.nested_attributes_options[association_name][:reject_if]
75
+ when Symbol
76
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
77
+ when Proc
78
+ callback.call(attributes)
79
+ end
80
+ end
81
+
82
+ # Determines if a hash contains a truthy _destroy key.
83
+ def has_destroy_flag?(hash)
84
+ ["1", "true"].include?(hash['_destroy'].to_s)
85
+ end
86
+
87
+ module ClassMethods
88
+ def accepts_nested_attributes_for *attr_names
89
+ options = { :allow_destroy => false, :update_only => false }
90
+ options.update(attr_names.extract_options!)
91
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
92
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
93
+
94
+ attr_names.each do |association_name|
95
+ nested_attributes_options = self.nested_attributes_options.dup
96
+ nested_attributes_options[association_name] = options
97
+ self.nested_attributes_options = nested_attributes_options
98
+
99
+ generate_association_writer(association_name)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Generates a writer method for this association. Serves as a point for
106
+ # accessing the objects in the association. For example, this method
107
+ # could generate the following:
108
+ #
109
+ # def pirate_attributes=(attributes)
110
+ # assign_nested_attributes_for_collection_association(:pirate, attributes)
111
+ # end
112
+ #
113
+ # This redirects the attempts to write objects in an association through
114
+ # the helper methods defined below. Makes it seem like the nested
115
+ # associations are just regular associations.
116
+ def generate_association_writer(association_name)
117
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
118
+ if method_defined?(:#{association_name}_attributes=)
119
+ remove_method(:#{association_name}_attributes=)
120
+ end
121
+ def #{association_name}_attributes=(attributes)
122
+ assign_nested_attributes_for_collection_association(:#{association_name}, attributes)
123
+ ## in lieu of autosave_association_callbacks just save all of em.
124
+ send(:#{association_name}).each {|obj| obj.marked_for_destruction? ? obj.destroy : nil}
125
+ send(:#{association_name}).reset!
126
+ end
127
+ eoruby
128
+ end
129
+ end
130
+ end
131
+ end
132
+
@@ -0,0 +1,56 @@
1
+ module ActiveTriples
2
+ class NodeConfig
3
+ attr_accessor :predicate, :term, :class_name, :type, :behaviors, :multivalue
4
+
5
+ def initialize(term, predicate, args={})
6
+ self.term = term
7
+ self.predicate = predicate
8
+ self.class_name = args.delete(:class_name)
9
+ self.multivalue = args.delete(:multivalue) { true }
10
+ raise ArgumentError, "Invalid arguments for Rdf Node configuration: #{args} on #{predicate}" unless args.empty?
11
+ end
12
+
13
+ def [](value)
14
+ value = value.to_sym
15
+ self.respond_to?(value) ? self.send(value) : nil
16
+ end
17
+
18
+ def class_name
19
+ if @class_name.kind_of?(String)
20
+ begin
21
+ new_class = @class_name.constantize
22
+ @class_name = new_class
23
+ rescue NameError
24
+ end
25
+ end
26
+ @class_name
27
+ end
28
+
29
+ def with_index (&block)
30
+ # needed for solrizer integration
31
+ iobj = IndexObject.new
32
+ yield iobj
33
+ self.type = iobj.data_type
34
+ self.behaviors = iobj.behaviors
35
+ end
36
+
37
+ # this enables a cleaner API for solr integration
38
+ class IndexObject
39
+ attr_accessor :data_type, :behaviors
40
+ def initialize
41
+ @behaviors = []
42
+ @data_type = :string
43
+ end
44
+ def as(*args)
45
+ @behaviors = args
46
+ end
47
+ def type(sym)
48
+ @data_type = sym
49
+ end
50
+ def defaults
51
+ :noop
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,96 @@
1
+ require 'deprecation'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module ActiveTriples
5
+ ##
6
+ # Implements property configuration common to Rdf::Resource,
7
+ # RDFDatastream, and others. It does its work at the class level,
8
+ # and is meant to be extended.
9
+ #
10
+ # Define properties at the class level with:
11
+ #
12
+ # property :title, predicate: RDF::DC.title, class_name: ResourceClass
13
+ #
14
+ # or with the 'old' style:
15
+ #
16
+ # map_predicates do |map|
17
+ # map.title(in: RDF::DC)
18
+ # end
19
+ #
20
+ # You can pass a block to either to set index behavior.
21
+ module Properties
22
+ extend Deprecation
23
+ attr_accessor :config
24
+
25
+ ##
26
+ # Registers properties for Resource-like classes
27
+ # @param [Symbol] name of the property (and its accessor methods)
28
+ # @param [Hash] opts for this property, must include a :predicate
29
+ # @yield [index] index sets solr behaviors for the property
30
+ def property(name, opts={}, &block)
31
+ self.config[name] = NodeConfig.new(name, opts[:predicate], opts.except(:predicate)).tap do |config|
32
+ config.with_index(&block) if block_given?
33
+ end
34
+ behaviors = config[name].behaviors.flatten if config[name].behaviors and not config[name].behaviors.empty?
35
+ register_property(name)
36
+ end
37
+
38
+ def config
39
+ @config ||= if superclass.respond_to? :config
40
+ superclass.config.dup
41
+ else
42
+ {}.with_indifferent_access
43
+ end
44
+ end
45
+
46
+ alias_method :properties, :config
47
+ alias_method :properties=, :config=
48
+
49
+ def config_for_term_or_uri(term)
50
+ return config[term.to_sym] unless term.kind_of? RDF::Resource
51
+ config.each { |k, v| return v if v.predicate == term.to_uri }
52
+ end
53
+
54
+ def fields
55
+ properties.keys.map(&:to_sym)
56
+ end
57
+
58
+ private
59
+
60
+ ##
61
+ # Private method for creating accessors for a given property.
62
+ # @param [#to_s] name Name of the accessor to be created, get/set_value is called on the resource using this.
63
+ def register_property(name)
64
+ parent = Proc.new{self}
65
+ # parent = Proc.new{resource} if self < ActiveFedora::Datastream
66
+ define_method "#{name}=" do |*args|
67
+ instance_eval(&parent).set_value(name.to_sym, *args)
68
+ end
69
+ define_method name do
70
+ instance_eval(&parent).get_values(name.to_sym)
71
+ end
72
+ end
73
+
74
+ public
75
+ # Mapper is for backwards compatibility with ActiveFedora::RDFDatastream
76
+ class Mapper
77
+ attr_accessor :parent
78
+ def initialize(parent)
79
+ @parent = parent
80
+ end
81
+ def method_missing(name, *args, &block)
82
+ properties = args.first || {}
83
+ vocab = properties.delete(:in)
84
+ to = properties.delete(:to) || name
85
+ predicate = vocab.send(to)
86
+ parent.property(name, properties.merge(predicate: predicate), &block)
87
+ end
88
+ end
89
+ def map_predicates
90
+ Deprecation.warn Properties, "map_predicates is deprecated and will be removed in active-fedora 8.0.0. Use property :name, predicate: predicate instead.", caller
91
+ mapper = Mapper.new(self)
92
+ yield(mapper)
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveTriples
2
+ ##
3
+ # Defines module methods for registering an RDF::Repository for
4
+ # persistence of Resources.
5
+ #
6
+ # This allows any triplestore (or other storage platform) with an
7
+ # RDF::Repository implementation to be used for persistence of
8
+ # resources that will be shared between ActiveFedora::Base objects.
9
+ #
10
+ # ActiveFedora::Rdf::Repositories.add_repository :blah, RDF::Repository.new
11
+ #
12
+ # Multiple repositories can be registered to keep different kinds of
13
+ # resources seperate. This is configurable on subclasses of Resource
14
+ # at the class level.
15
+ #
16
+ # @see Configurable
17
+ module Repositories
18
+
19
+ def add_repository(name, repo)
20
+ raise "Repositories must be an RDF::Repository" unless repo.kind_of? RDF::Repository
21
+ repositories[name] = repo
22
+ end
23
+ module_function :add_repository
24
+
25
+ def clear_repositories!
26
+ @repositories = {}
27
+ end
28
+ module_function :clear_repositories!
29
+
30
+ def repositories
31
+ @repositories ||= {}
32
+ end
33
+ module_function :repositories
34
+
35
+ end
36
+ end
@@ -0,0 +1,369 @@
1
+ require 'deprecation'
2
+ require 'active_model'
3
+ require 'active_support/core_ext/hash'
4
+
5
+ module ActiveTriples
6
+ ##
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
+ @@type_registry
24
+ extend Configurable
25
+ extend Properties
26
+ extend Deprecation
27
+ extend ActiveModel::Naming
28
+ include ActiveModel::Conversion
29
+ include ActiveModel::Serialization
30
+ include ActiveModel::Serializers::JSON
31
+ include NestedAttributes
32
+ attr_accessor :parent
33
+
34
+ class << self
35
+ def type_registry
36
+ @@type_registry ||= {}
37
+ end
38
+
39
+ ##
40
+ # Adapter for a consistent interface for creating a new node from a URI.
41
+ # Similar functionality should exist in all objects which can become a node.
42
+ def from_uri(uri,vals=nil)
43
+ new(uri, vals)
44
+ end
45
+ end
46
+
47
+ def writable?
48
+ !frozen?
49
+ end
50
+
51
+ ##
52
+ # Initialize an instance of this resource class. Defaults to a
53
+ # blank node subject. In addition to RDF::Graph parameters, you
54
+ # can pass in a URI and/or a parent to build a resource from a
55
+ # existing data.
56
+ #
57
+ # You can pass in only a parent with:
58
+ # Resource.new(nil, parent)
59
+ #
60
+ # @see RDF::Graph
61
+ def initialize(*args, &block)
62
+ resource_uri = args.shift unless args.first.is_a?(Hash)
63
+ self.parent = args.shift unless args.first.is_a?(Hash)
64
+ set_subject!(resource_uri) if resource_uri
65
+ super(*args, &block)
66
+ reload
67
+ # Append type to graph if necessary.
68
+ self.get_values(:type) << self.class.type if self.class.type.kind_of?(RDF::URI) && type.empty?
69
+ end
70
+
71
+ def graph
72
+ Deprecation.warn Resource, "graph is redundant & deprecated. It will be removed in active-fedora 8.0.0.", caller
73
+ self
74
+ end
75
+
76
+ def final_parent
77
+ @final_parent ||= begin
78
+ parent = self.parent
79
+ while parent && parent.parent && parent.parent != parent
80
+ parent = parent.parent
81
+ end
82
+ parent
83
+ end
84
+ end
85
+
86
+ def attributes
87
+ attrs = {}
88
+ attrs['id'] = id if id
89
+ fields.map { |f| attrs[f.to_s] = get_values(f) }
90
+ unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) }
91
+ attrs
92
+ end
93
+
94
+ def serializable_hash(options = nil)
95
+ attrs = (fields.map { |f| f.to_s }) << 'id'
96
+ hash = super(:only => attrs)
97
+ unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
98
+ hash
99
+ end
100
+
101
+ def attributes=(values)
102
+ raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
103
+ values = values.with_indifferent_access
104
+ set_subject!(values.delete(:id)) if values.has_key?(:id) and node?
105
+ values.each do |key, value|
106
+ if properties.keys.include?(key)
107
+ set_value(rdf_subject, key, value)
108
+ elsif self.singleton_class.nested_attributes_options.keys.map{ |k| "#{k}_attributes"}.include?(key)
109
+ send("#{key}=".to_sym, value)
110
+ else
111
+ raise ArgumentError, "No association found for name `#{key}'. Has it been defined yet?"
112
+ end
113
+ end
114
+ end
115
+
116
+ def rdf_subject
117
+ @rdf_subject ||= RDF::Node.new
118
+ end
119
+
120
+ def id
121
+ node? ? nil : rdf_subject.to_s
122
+ end
123
+
124
+ def node?
125
+ return true if rdf_subject.kind_of? RDF::Node
126
+ false
127
+ end
128
+
129
+ def to_term
130
+ rdf_subject
131
+ end
132
+
133
+ def base_uri
134
+ self.class.base_uri
135
+ end
136
+
137
+ def type
138
+ self.get_values(:type).to_a.map{|x| x.rdf_subject}
139
+ end
140
+
141
+ def type=(type)
142
+ raise "Type must be an RDF::URI" unless type.kind_of? RDF::URI
143
+ self.update(RDF::Statement.new(rdf_subject, RDF.type, type))
144
+ end
145
+
146
+ ##
147
+ # Look for labels in various default fields, prioritizing
148
+ # configured label fields
149
+ def rdf_label
150
+ labels = Array(self.class.rdf_label)
151
+ labels += default_labels
152
+ labels.each do |label|
153
+ values = get_values(label)
154
+ return values unless values.empty?
155
+ end
156
+ node? ? [] : [rdf_subject.to_s]
157
+ end
158
+
159
+ def fields
160
+ properties.keys.map(&:to_sym).reject{|x| x == :type}
161
+ end
162
+
163
+ ##
164
+ # Load data from URI
165
+ def fetch
166
+ load(rdf_subject)
167
+ self
168
+ end
169
+
170
+ def persist!
171
+ raise "failed when trying to persist to non-existant repository or parent resource" unless repository
172
+ repository.delete [rdf_subject,nil,nil] unless node?
173
+ if node?
174
+ repository.statements.each do |statement|
175
+ repository.send(:delete_statement, statement) if statement.subject == rdf_subject
176
+ end
177
+ end
178
+ repository << self
179
+ @persisted = true
180
+ end
181
+
182
+ def persisted?
183
+ @persisted ||= false
184
+ return (@persisted and parent.persisted?) if parent
185
+ @persisted
186
+ end
187
+
188
+ ##
189
+ # Repopulates the graph from the repository or parent resource.
190
+ def reload
191
+ @term_cache ||= {}
192
+ if self.class.repository == :parent
193
+ return false if final_parent.nil?
194
+ end
195
+ self << repository.query(subject: rdf_subject)
196
+ unless empty?
197
+ @persisted = true
198
+ end
199
+ true
200
+ end
201
+
202
+ ##
203
+ # Adds or updates a property with supplied values.
204
+ #
205
+ # Handles two argument patterns. The recommended pattern is:
206
+ # set_value(property, values)
207
+ #
208
+ # For backwards compatibility, there is support for explicitly
209
+ # passing the rdf_subject to be used in the statement:
210
+ # set_value(uri, property, values)
211
+ #
212
+ # @note This method will delete existing statements with the correct subject and predicate from the graph
213
+ def set_value(*args)
214
+ # Add support for legacy 3-parameter syntax
215
+ if args.length > 3 || args.length < 2
216
+ raise ArgumentError("wrong number of arguments (#{args.length} for 2-3)")
217
+ end
218
+ values = args.pop
219
+ get_term(args).set(values)
220
+ end
221
+
222
+ ##
223
+ # Returns an array of values belonging to the property
224
+ # requested. Elements in the array may RdfResource objects or a
225
+ # valid datatype.
226
+ #
227
+ # Handles two argument patterns. The recommended pattern is:
228
+ # get_values(property)
229
+ #
230
+ # For backwards compatibility, there is support for explicitly
231
+ # passing the rdf_subject to be used in th statement:
232
+ # get_values(uri, property)
233
+ def get_values(*args)
234
+ get_term(args)
235
+ end
236
+
237
+ def get_term(args)
238
+ @term_cache ||= {}
239
+ term = Term.new(self, args)
240
+ @term_cache["#{term.rdf_subject}/#{term.property}"] ||= term
241
+ @term_cache["#{term.rdf_subject}/#{term.property}"]
242
+ end
243
+
244
+ ##
245
+ # Set a new rdf_subject for the resource.
246
+ #
247
+ # This raises an error if the current subject is not a blank node,
248
+ # and returns false if it can't figure out how to make a URI from
249
+ # the param. Otherwise it creates a URI for the resource and
250
+ # rebuilds the graph with the updated URI.
251
+ #
252
+ # Will try to build a uri as an extension of the class's base_uri
253
+ # if appropriate.
254
+ #
255
+ # @param [#to_uri, #to_s] uri_or_str the uri or string to use
256
+ def set_subject!(uri_or_str)
257
+ raise "Refusing update URI when one is already assigned!" unless node?
258
+ # Refusing set uri to an empty string.
259
+ return false if uri_or_str.nil? or uri_or_str.to_s.empty?
260
+ # raise "Refusing update URI! This object is persisted to a datastream." if persisted?
261
+ old_subject = rdf_subject
262
+ @rdf_subject = get_uri(uri_or_str)
263
+
264
+ each_statement do |statement|
265
+ if statement.subject == old_subject
266
+ delete(statement)
267
+ self << RDF::Statement.new(rdf_subject, statement.predicate, statement.object)
268
+ elsif statement.object == old_subject
269
+ delete(statement)
270
+ self << RDF::Statement.new(statement.subject, statement.predicate, rdf_subject)
271
+ end
272
+ end
273
+ end
274
+
275
+ def destroy
276
+ clear
277
+ persist! if repository
278
+ parent.destroy_child(self) if parent
279
+ @destroyed = true
280
+ end
281
+ alias_method :destroy!, :destroy
282
+
283
+ def destroyed?
284
+ @destroyed ||= false
285
+ end
286
+
287
+ def destroy_child(child)
288
+ statements.each do |statement|
289
+ delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject
290
+ end
291
+ end
292
+
293
+ def new_record?
294
+ not persisted?
295
+ end
296
+
297
+ ##
298
+ # @return [String] the string to index in solr
299
+ def solrize
300
+ node? ? rdf_label : rdf_subject.to_s
301
+ end
302
+
303
+ def mark_for_destruction
304
+ @marked_for_destruction = true
305
+ end
306
+
307
+ def marked_for_destruction?
308
+ @marked_for_destruction
309
+ end
310
+
311
+ private
312
+
313
+ def properties
314
+ self.singleton_class.properties
315
+ end
316
+
317
+ def registered_predicates
318
+ properties.values.map { |config| config.predicate }
319
+ end
320
+
321
+ def unregistered_predicates
322
+ preds = registered_predicates
323
+ preds << RDF.type
324
+ predicates.select { |p| !preds.include? p }
325
+ end
326
+
327
+ def property_for_predicate(predicate)
328
+ properties.each do |property, values|
329
+ return property if values[:predicate] == predicate
330
+ end
331
+ return nil
332
+ end
333
+
334
+ def default_labels
335
+ [RDF::SKOS.prefLabel,
336
+ RDF::DC.title,
337
+ RDF::RDFS.label,
338
+ RDF::SKOS.altLabel,
339
+ RDF::SKOS.hiddenLabel]
340
+ end
341
+
342
+ ##
343
+ # Return the repository (or parent) that this resource should
344
+ # write to when persisting.
345
+ def repository
346
+ @repository ||=
347
+ if self.class.repository == :parent
348
+ final_parent
349
+ else
350
+ Repositories.repositories[self.class.repository]
351
+ end
352
+ end
353
+
354
+ ##
355
+ # Takes a URI or String and aggressively tries to create a valid RDF URI.
356
+ # Combines the input with base_uri if appropriate.
357
+ #
358
+ # @TODO: URI.scheme_list is naive and incomplete. Find a better way to check for an existing scheme.
359
+ def get_uri(uri_or_str)
360
+ return uri_or_str.to_uri if uri_or_str.respond_to? :to_uri
361
+ return uri_or_str if uri_or_str.kind_of? RDF::Node
362
+ uri_or_str = uri_or_str.to_s
363
+ return RDF::Node(uri_or_str[2..-1]) if uri_or_str.start_with? '_:'
364
+ 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')
365
+ 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)
366
+ raise RuntimeError, "could not make a valid RDF::URI from #{uri_or_str}"
367
+ end
368
+ end
369
+ end