om 0.1.10 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.textile +27 -0
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/om/samples/mods_article.rb +64 -0
- data/lib/om/samples.rb +2 -0
- data/lib/om/tree_node.rb +24 -0
- data/lib/om/xml/document.rb +68 -0
- data/lib/om/xml/named_term_proxy.rb +37 -0
- data/lib/om/xml/node_generator.rb +14 -0
- data/lib/om/xml/term.rb +223 -0
- data/lib/om/xml/term_value_operators.rb +182 -0
- data/lib/om/xml/term_xpath_generator.rb +224 -0
- data/lib/om/xml/terminology.rb +216 -0
- data/lib/om/xml/vocabulary.rb +17 -0
- data/lib/om/xml.rb +17 -0
- data/lib/om.rb +2 -0
- data/om.gemspec +37 -6
- data/spec/fixtures/mods_articles/hydrangea_article1.xml +0 -1
- data/spec/integration/rights_metadata_integration_example_spec.rb +27 -15
- data/spec/unit/document_spec.rb +141 -0
- data/spec/unit/generator_spec.rb +7 -1
- data/spec/unit/named_term_proxy_spec.rb +39 -0
- data/spec/unit/node_generator_spec.rb +27 -0
- data/spec/unit/properties_spec.rb +1 -1
- data/spec/unit/term_builder_spec.rb +195 -0
- data/spec/unit/term_spec.rb +180 -0
- data/spec/unit/term_value_operators_spec.rb +361 -0
- data/spec/unit/term_xpath_generator_spec.rb +111 -0
- data/spec/unit/terminology_builder_spec.rb +178 -0
- data/spec/unit/terminology_spec.rb +322 -0
- data/spec/unit/validation_spec.rb +4 -0
- metadata +40 -7
data/README.textile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
h1. opinionated-xml
|
2
|
+
|
3
|
+
A library to help you tame sprawling XML schemas like MODS.
|
4
|
+
|
5
|
+
h2. Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
h2. Acknowledgements
|
16
|
+
|
17
|
+
Creator: Matt Zumwalt ("MediaShelf":http://yourmediashelf.com)
|
18
|
+
|
19
|
+
Thanks to
|
20
|
+
|
21
|
+
Bess Sadler, who enabled us to take knowledge gleaned from developing Blacklight and apply it to OM metadata indexing
|
22
|
+
Ross Singer
|
23
|
+
Those who participated in the Opinionated MODS breakout session at Code4Lib 2010
|
24
|
+
|
25
|
+
h2. Copyright
|
26
|
+
|
27
|
+
Copyright (c) 2010 Matt Zumwalt. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -11,7 +11,7 @@ begin
|
|
11
11
|
gem.homepage = "http://github.com/mediashelf/om"
|
12
12
|
gem.authors = ["Matt Zumwalt"]
|
13
13
|
|
14
|
-
gem.add_dependency('nokogiri')
|
14
|
+
gem.add_dependency('nokogiri', ">= 1.4.2")
|
15
15
|
gem.add_dependency('facets')
|
16
16
|
|
17
17
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class OM::Samples::ModsArticle
|
2
|
+
|
3
|
+
include OM::XML::Document
|
4
|
+
|
5
|
+
set_terminology do |t|
|
6
|
+
t.root(:path=>"mods", :xmlns=>"http://www.loc.gov/mods/v3", :schema=>"http://www.loc.gov/standards/mods/v3/mods-3-2.xsd")
|
7
|
+
|
8
|
+
t.title_info(:path=>"titleInfo") {
|
9
|
+
t.main_title(:path=>"title", :label=>"title")
|
10
|
+
t.language(:path=>{:attribute=>"lang"})
|
11
|
+
}
|
12
|
+
t.abstract
|
13
|
+
t.topic_tag(:path=>"subject", :default_content_path=>"topic")
|
14
|
+
# This is a mods:name. The underscore is purely to avoid namespace conflicts.
|
15
|
+
t.name_ {
|
16
|
+
# this is a namepart
|
17
|
+
t.namePart(:index_as=>[:searchable, :displayable, :facetable, :sortable], :required=>:true, :type=>:string, :label=>"generic name")
|
18
|
+
# affiliations are great
|
19
|
+
t.affiliation
|
20
|
+
t.displayForm
|
21
|
+
t.role(:ref=>[:role])
|
22
|
+
t.description
|
23
|
+
t.date(:path=>"namePart", :attributes=>{:type=>"date"})
|
24
|
+
t.last_name(:path=>"namePart", :attributes=>{:type=>"family"})
|
25
|
+
t.first_name(:path=>"namePart", :attributes=>{:type=>"given"}, :label=>"first name")
|
26
|
+
t.terms_of_address(:path=>"namePart", :attributes=>{:type=>"termsOfAddress"})
|
27
|
+
}
|
28
|
+
# lookup :person, :first_name
|
29
|
+
t.person(:ref=>:name, :attributes=>{:type=>"personal"})
|
30
|
+
t.organizaton(:ref=>:name, :attributes=>{:type=>"institutional"})
|
31
|
+
t.conference(:ref=>:name, :attributes=>{:type=>"conference"})
|
32
|
+
|
33
|
+
t.role {
|
34
|
+
t.text(:path=>"roleTerm",:attributes=>{:type=>"text"})
|
35
|
+
t.code(:path=>"roleTerm",:attributes=>{:type=>"code"})
|
36
|
+
}
|
37
|
+
t.journal(:path=>'relatedItem', :attributes=>{:type=>"host"}) {
|
38
|
+
t.title_info
|
39
|
+
t.origin_info(:path=>"originInfo") {
|
40
|
+
t.publisher
|
41
|
+
t.date_issued(:path=>"dateIssued")
|
42
|
+
}
|
43
|
+
t.issn(:path=>"identifier", :attributes=>{:type=>"issn"})
|
44
|
+
t.issue(:path=>"part") {
|
45
|
+
t.volume(:path=>"detail", :attributes=>{:type=>"volume"}, :default_content_path=>"number")
|
46
|
+
t.level(:path=>"detail", :attributes=>{:type=>"number"}, :default_content_path=>"number")
|
47
|
+
t.extent
|
48
|
+
t.pages(:path=>"extent", :attributes=>{:unit=>"pages"}) {
|
49
|
+
t.start
|
50
|
+
t.end
|
51
|
+
}
|
52
|
+
t.publication_date(:path=>"date")
|
53
|
+
t.start_page(:proxy=>[:pages, :start])
|
54
|
+
t.end_page(:proxy=>[:pages, :end])
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
# Changes from OM::Properties implementation
|
61
|
+
# renamed family_name => last_name
|
62
|
+
# start_page & end_page now accessible as [:journal, :issue, :pages, :start] (etc.)
|
63
|
+
|
64
|
+
end
|
data/lib/om/samples.rb
ADDED
data/lib/om/tree_node.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module OM::TreeNode
|
2
|
+
|
3
|
+
attr_accessor :ancestors
|
4
|
+
|
5
|
+
# insert the mapper into the given parent
|
6
|
+
def set_parent(parent_mapper)
|
7
|
+
parent_mapper.children[@name] = self
|
8
|
+
@ancestors << parent_mapper
|
9
|
+
end
|
10
|
+
|
11
|
+
# insert the given mapper into the current mappers children
|
12
|
+
def add_child(child_mapper)
|
13
|
+
child_mapper.ancestors << self
|
14
|
+
@children[child_mapper.name.to_sym] = child_mapper
|
15
|
+
end
|
16
|
+
|
17
|
+
def retrieve_child(child_name)
|
18
|
+
child = @children.fetch(child_name, nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parent
|
22
|
+
ancestors.last
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module OM::XML::Document
|
2
|
+
|
3
|
+
|
4
|
+
# Class Methods -- These methods will be available on classes that include this Module
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
attr_accessor :terminology
|
9
|
+
|
10
|
+
# Sets the OM::XML::Terminology for the Document
|
11
|
+
# Expects +&block+ that will be passed into OM::XML::Terminology::Builder.new
|
12
|
+
def set_terminology &block
|
13
|
+
@terminology = OM::XML::Terminology::Builder.new( &block ).build
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns any namespaces defined by the Class' Terminology
|
17
|
+
def ox_namespaces
|
18
|
+
if @terminology.nil?
|
19
|
+
return {}
|
20
|
+
else
|
21
|
+
return @terminology.namespaces
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# Instance Methods -- These methods will be available on instances of classes that include this module
|
28
|
+
|
29
|
+
attr_accessor :ox_namespaces
|
30
|
+
|
31
|
+
def self.included(klass)
|
32
|
+
klass.extend(ClassMethods)
|
33
|
+
|
34
|
+
klass.send(:include, OM::XML::Container)
|
35
|
+
klass.send(:include, OM::XML::TermValueOperators)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Applies the property's corresponding xpath query, returning the result Nokogiri::XML::NodeSet
|
39
|
+
def find_by_terms_and_value(*term_pointer)
|
40
|
+
xpath = self.class.terminology.xpath_for(*term_pointer)
|
41
|
+
if xpath.nil?
|
42
|
+
return nil
|
43
|
+
else
|
44
|
+
return ng_xml.xpath(xpath, ox_namespaces)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# +term_pointer+ Variable length array of values in format [:accessor_name, :accessor_name ...] or [{:accessor_name=>index}, :accessor_name ...]
|
50
|
+
# example: {:person => 1}, :first_name
|
51
|
+
# example: [:person, 1, :first_name]
|
52
|
+
# Currently, indexes must be integers.
|
53
|
+
def find_by_terms(*term_pointer)
|
54
|
+
xpath = self.class.terminology.xpath_with_indexes(*term_pointer)
|
55
|
+
if xpath.nil?
|
56
|
+
return nil
|
57
|
+
else
|
58
|
+
return ng_xml.xpath(xpath, ox_namespaces)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a hash combining the current documents namespaces (provided by nokogiri) and any namespaces that have been set up by your Terminology.
|
63
|
+
# Most importantly, this matches the 'oxns' namespace to the namespace you provided in your Terminology's root term config
|
64
|
+
def ox_namespaces
|
65
|
+
@ox_namespaces ||= ng_xml.namespaces.merge(self.class.ox_namespaces)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class OM::XML::NamedTermProxy
|
2
|
+
|
3
|
+
attr_accessor :proxy_pointer, :name
|
4
|
+
|
5
|
+
include OM::TreeNode
|
6
|
+
|
7
|
+
def initialize(name, proxy_pointer, opts={})
|
8
|
+
opts = {:namespace_prefix=>"oxns", :ancestors=>[], :children=>{}}.merge(opts)
|
9
|
+
[:children, :ancestors].each do |accessor_name|
|
10
|
+
instance_variable_set("@#{accessor_name}", opts.fetch(accessor_name, nil) )
|
11
|
+
end
|
12
|
+
@name = name
|
13
|
+
@proxy_pointer = proxy_pointer
|
14
|
+
end
|
15
|
+
|
16
|
+
def proxied_term
|
17
|
+
self.parent.retrieve_term(*self.proxy_pointer)
|
18
|
+
end
|
19
|
+
|
20
|
+
# do nothing -- this is to prevent errors when the parent term calls generate_xpath_queries! on its children
|
21
|
+
def generate_xpath_queries!
|
22
|
+
# do nothing
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing
|
26
|
+
end
|
27
|
+
|
28
|
+
# Any unknown method calls will be proxied to the proxied term
|
29
|
+
def method_missing method, *args, &block
|
30
|
+
if args.empty?
|
31
|
+
return self.proxied_term.send(method)
|
32
|
+
else
|
33
|
+
return self.proxied_term.send(method, args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module OM::XML::NodeGenerator
|
2
|
+
|
3
|
+
# Module Methods -- These methods can be called directly on the Module itself
|
4
|
+
def self.generate(term, builder_new_value, opts={})
|
5
|
+
template = term.xml_builder_template(opts)
|
6
|
+
builder_call_body = eval('"' + template + '"')
|
7
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
8
|
+
eval( builder_call_body )
|
9
|
+
end
|
10
|
+
|
11
|
+
return builder.doc
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/om/xml/term.rb
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
class OM::XML::Term
|
2
|
+
|
3
|
+
# Term::Builder Class Definition
|
4
|
+
|
5
|
+
class Builder
|
6
|
+
attr_accessor :name, :settings, :children, :terminology_builder
|
7
|
+
|
8
|
+
def initialize(name, terminology_builder=nil)
|
9
|
+
@name = name.to_sym
|
10
|
+
@terminology_builder = terminology_builder
|
11
|
+
@settings = {:required=>false, :data_type=>:string}
|
12
|
+
@children = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_child(child)
|
16
|
+
@children[child.name] = child
|
17
|
+
end
|
18
|
+
|
19
|
+
def retrieve_child(child_name)
|
20
|
+
child = @children.fetch(child_name, nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lookup_refs(nodes_visited=[])
|
24
|
+
result = []
|
25
|
+
if @settings[:ref]
|
26
|
+
# Fail if we do not have terminology builder
|
27
|
+
if self.terminology_builder.nil?
|
28
|
+
raise "Cannot perform lookup_ref for the #{self.name} builder. It doesn't have a reference to any terminology builder"
|
29
|
+
end
|
30
|
+
begin
|
31
|
+
target = self.terminology_builder.retrieve_term_builder(*@settings[:ref])
|
32
|
+
rescue OM::XML::Terminology::BadPointerError
|
33
|
+
# Clarify message on BadPointerErrors
|
34
|
+
raise OM::XML::Terminology::BadPointerError, "#{self.name} refers to a Term Builder that doesn't exist. The bad pointer is #{@settings[:ref].inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fail on circular references and return an intelligible error message
|
38
|
+
if nodes_visited.contains?(target)
|
39
|
+
nodes_visited << self
|
40
|
+
nodes_visited << target
|
41
|
+
trail = ""
|
42
|
+
nodes_visited.each_with_index do |node, z|
|
43
|
+
trail << node.name.inspect
|
44
|
+
unless z == nodes_visited.length-1
|
45
|
+
trail << " => "
|
46
|
+
end
|
47
|
+
end
|
48
|
+
raise OM::XML::Terminology::CircularReferenceError, "Circular reference in Terminology: #{trail}"
|
49
|
+
end
|
50
|
+
result << target
|
51
|
+
result.concat( target.lookup_refs(nodes_visited << self) )
|
52
|
+
end
|
53
|
+
return result
|
54
|
+
end
|
55
|
+
|
56
|
+
# If a :ref value has been set, looks up the target of that ref and merges the target's settings & children with the current builder's settings & children
|
57
|
+
# operates recursively, so it is possible to apply refs that in turn refer to other nodes.
|
58
|
+
def resolve_refs!
|
59
|
+
name_of_last_ref = nil
|
60
|
+
lookup_refs.each_with_index do |ref,z|
|
61
|
+
@settings = two_layer_merge(@settings, ref.settings)
|
62
|
+
@children.merge!(ref.children)
|
63
|
+
name_of_last_ref = ref.name
|
64
|
+
end
|
65
|
+
if @settings[:path].nil? && !name_of_last_ref.nil?
|
66
|
+
@settings[:path] = name_of_last_ref.to_s
|
67
|
+
end
|
68
|
+
@settings.delete :ref
|
69
|
+
return self
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a new Hash that merges +downstream_hash+ with +upstream_hash+
|
73
|
+
# similar to calling +upstream_hash+.merge(+downstream_hash+) only it also merges
|
74
|
+
# any internal values that are themselves Hashes.
|
75
|
+
def two_layer_merge(downstream_hash, upstream_hash)
|
76
|
+
up = upstream_hash.dup
|
77
|
+
dn = downstream_hash.dup
|
78
|
+
up.each_pair do |setting_name, value|
|
79
|
+
if value.kind_of?(Hash) && downstream_hash.has_key?(setting_name)
|
80
|
+
dn[setting_name] = value.merge(downstream_hash[setting_name])
|
81
|
+
up.delete(setting_name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
return up.merge(dn)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Builds a new OM::XML::Term based on the Builder object's current settings
|
88
|
+
# If no path has been provided, uses the Builder object's name as the term's path
|
89
|
+
# Recursively builds any children, appending the results as children of the Term that's being built.
|
90
|
+
def build
|
91
|
+
self.resolve_refs!
|
92
|
+
if term.self.settings.has_key?(:proxy)
|
93
|
+
term = OM::XML::NamedTermProxy.new(self.name, self.settings[:proxy])
|
94
|
+
else
|
95
|
+
term = OM::XML::Term.new(self.name)
|
96
|
+
|
97
|
+
self.settings.each do |name, values|
|
98
|
+
if term.respond_to?(name.to_s+"=")
|
99
|
+
term.instance_variable_set("@#{name}", values)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@children.each_value do |child|
|
103
|
+
term.add_child child.build
|
104
|
+
end
|
105
|
+
term.generate_xpath_queries!
|
106
|
+
end
|
107
|
+
|
108
|
+
return term
|
109
|
+
end
|
110
|
+
|
111
|
+
# Any unknown method calls will add an entry to the settings hash and return the current object
|
112
|
+
def method_missing method, *args, &block
|
113
|
+
if args.length == 1
|
114
|
+
args = args.first
|
115
|
+
end
|
116
|
+
@settings[method] = args
|
117
|
+
return self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Term Class Definition
|
122
|
+
|
123
|
+
attr_accessor :name, :xpath, :xpath_constrained, :xpath_relative, :path, :index_as, :required, :data_type, :variant_of, :path, :attributes, :default_content_path, :namespace_prefix, :is_root_term
|
124
|
+
attr_accessor :children, :internal_xml, :terminology
|
125
|
+
|
126
|
+
include OM::TreeNode
|
127
|
+
|
128
|
+
def initialize(name, opts={})
|
129
|
+
opts = {:namespace_prefix=>"oxns", :ancestors=>[], :children=>{}}.merge(opts)
|
130
|
+
[:children, :ancestors,:path, :index_as, :required, :type, :variant_of, :path, :attributes, :default_content_path, :namespace_prefix].each do |accessor_name|
|
131
|
+
instance_variable_set("@#{accessor_name}", opts.fetch(accessor_name, nil) )
|
132
|
+
end
|
133
|
+
@name = name
|
134
|
+
if @path.nil? || @path.empty?
|
135
|
+
@path = name.to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.from_node(mapper_xml)
|
140
|
+
name = mapper_xml.attribute("name").text.to_sym
|
141
|
+
attributes = {}
|
142
|
+
mapper_xml.xpath("./attribute").each do |a|
|
143
|
+
attributes[a.attribute("name").text.to_sym] = a.attribute("value").text
|
144
|
+
end
|
145
|
+
new_mapper = self.new(name, :attributes=>attributes)
|
146
|
+
[:index_as, :required, :type, :variant_of, :path, :default_content_path, :namespace_prefix].each do |accessor_name|
|
147
|
+
attribute = mapper_xml.attribute(accessor_name.to_s)
|
148
|
+
unless attribute.nil?
|
149
|
+
new_mapper.instance_variable_set("@#{accessor_name}", attribute.text )
|
150
|
+
end
|
151
|
+
end
|
152
|
+
new_mapper.internal_xml = mapper_xml
|
153
|
+
|
154
|
+
mapper_xml.xpath("./mapper").each do |child_node|
|
155
|
+
child = self.from_node(child_node)
|
156
|
+
new_mapper.add_child(child)
|
157
|
+
end
|
158
|
+
|
159
|
+
return new_mapper
|
160
|
+
end
|
161
|
+
|
162
|
+
# crawl down into mapper's children hash to find the desired mapper
|
163
|
+
# ie. @test_mapper.retrieve_mapper(:conference, :role, :text)
|
164
|
+
def retrieve_term(*pointers)
|
165
|
+
children_hash = self.children
|
166
|
+
pointers.each do |p|
|
167
|
+
if children_hash.has_key?(p)
|
168
|
+
target = children_hash[p]
|
169
|
+
if pointers.index(p) == pointers.length-1
|
170
|
+
return target
|
171
|
+
else
|
172
|
+
children_hash = target.children
|
173
|
+
end
|
174
|
+
else
|
175
|
+
return nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
return target
|
179
|
+
end
|
180
|
+
|
181
|
+
def is_root_term?
|
182
|
+
@is_root_term == true
|
183
|
+
end
|
184
|
+
|
185
|
+
def xpath_absolute
|
186
|
+
@xpath
|
187
|
+
end
|
188
|
+
|
189
|
+
# +term_pointers+ reference to the property you want to generate a builder template for
|
190
|
+
# @opts
|
191
|
+
def xml_builder_template(extra_opts = {})
|
192
|
+
extra_attributes = extra_opts.fetch(:attributes, {})
|
193
|
+
|
194
|
+
node_options = []
|
195
|
+
node_child_template = ""
|
196
|
+
if !self.default_content_path.nil?
|
197
|
+
node_child_options = ["\':::builder_new_value:::\'"]
|
198
|
+
node_child_template = " { xml.#{self.default_content_path}( #{OM::XML.delimited_list(node_child_options)} ) }"
|
199
|
+
else
|
200
|
+
node_options = ["\':::builder_new_value:::\'"]
|
201
|
+
end
|
202
|
+
if !self.attributes.nil?
|
203
|
+
self.attributes.merge(extra_attributes).each_pair do |k,v|
|
204
|
+
node_options << ":#{k}=>\'#{v}\'"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
template = "xml.#{self.path}( #{OM::XML.delimited_list(node_options)} )" + node_child_template
|
208
|
+
return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }
|
209
|
+
end
|
210
|
+
|
211
|
+
# Generates absolute, relative, and constrained xpaths for the term, setting xpath, xpath_relative, and xpath_constrained accordingly.
|
212
|
+
# Also triggers update_xpath_values! on all child nodes, as their absolute paths rely on those of their parent nodes.
|
213
|
+
def generate_xpath_queries!
|
214
|
+
self.xpath = OM::XML::TermXpathGenerator.generate_absolute_xpath(self)
|
215
|
+
self.xpath_constrained = OM::XML::TermXpathGenerator.generate_constrained_xpath(self)
|
216
|
+
self.xpath_relative = OM::XML::TermXpathGenerator.generate_relative_xpath(self)
|
217
|
+
self.children.each_value {|child| child.generate_xpath_queries! }
|
218
|
+
return self
|
219
|
+
end
|
220
|
+
|
221
|
+
# private :update_xpath_values
|
222
|
+
|
223
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require "open-uri"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
class OM::XML::ParentNodeNotFoundError < RuntimeError; end
|
5
|
+
module OM::XML::TermValueOperators
|
6
|
+
|
7
|
+
# Retrieves all of the nodes from the current document that match +term_pointer+ and returns an array of their values
|
8
|
+
def term_values(*term_pointer)
|
9
|
+
result = []
|
10
|
+
find_by_terms(*term_pointer).each {|node| result << node.text }
|
11
|
+
# find_by_terms(*OM.destringify(term_pointer)).each {|node| result << node.text }
|
12
|
+
return result
|
13
|
+
end
|
14
|
+
|
15
|
+
# alias for term_values
|
16
|
+
def property_values(*lookup_args)
|
17
|
+
term_values(*lookup_args)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# example term values hash: {[{":person"=>"0"}, "role", "text"]=>{"0"=>"role1", "1"=>"role2", "2"=>"role3"}, [{:person=>1}, :family_name]=>"Andronicus", [{"person"=>"1"},:given_name]=>["Titus"],[{:person=>1},:role,:text]=>["otherrole1","otherrole2"] }
|
22
|
+
def update_values(params={})
|
23
|
+
# remove any terms from params that this datastream doesn't recognize
|
24
|
+
|
25
|
+
params.delete_if do |term_pointer,new_values|
|
26
|
+
if term_pointer.kind_of?(String)
|
27
|
+
true
|
28
|
+
else
|
29
|
+
!self.class.terminology.has_term?(*OM.destringify(term_pointer))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
result = params.dup
|
34
|
+
|
35
|
+
params.each_pair do |term_pointer,new_values|
|
36
|
+
pointer = OM.destringify(term_pointer)
|
37
|
+
template = OM.pointers_to_flat_array(pointer,false)
|
38
|
+
hn = OM::XML::Terminology.term_hierarchical_name(*pointer)
|
39
|
+
|
40
|
+
# Sanitize new_values to always be a hash with indexes
|
41
|
+
case new_values
|
42
|
+
when Hash
|
43
|
+
when Array
|
44
|
+
nv = new_values.dup
|
45
|
+
new_values = {}
|
46
|
+
nv.each {|v| new_values[nv.index(v).to_s] = v}
|
47
|
+
else
|
48
|
+
new_values = {"0"=>new_values}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Populate the response hash appropriately, using hierarchical names for terms as keys rather than the given pointers.
|
52
|
+
result.delete(term_pointer)
|
53
|
+
result[hn] = new_values.dup
|
54
|
+
|
55
|
+
# Skip any submitted values if the new value matches the current values
|
56
|
+
current_values = term_values(*pointer)
|
57
|
+
new_values.delete_if do |y,z|
|
58
|
+
if current_values[y.to_i]==z and y.to_i > -1
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Fill out the pointer completely if the final term is a NamedTermProxy
|
66
|
+
term = self.class.terminology.retrieve_term( *OM.pointers_to_flat_array(pointer,false) )
|
67
|
+
if term.kind_of? OM::XML::NamedTermProxy
|
68
|
+
pointer.pop
|
69
|
+
pointer = pointer.concat(term.proxy_pointer)
|
70
|
+
end
|
71
|
+
|
72
|
+
xpath = self.class.terminology.xpath_with_indexes(*pointer)
|
73
|
+
parent_pointer = pointer.dup
|
74
|
+
parent_pointer.pop
|
75
|
+
parent_xpath = self.class.terminology.xpath_with_indexes(*parent_pointer)
|
76
|
+
|
77
|
+
# If the value doesn't exist yet, append it. Otherwise, update the existing value.
|
78
|
+
new_values.each do |y,z|
|
79
|
+
if find_by_terms(*pointer)[y.to_i].nil? || y.to_i == -1
|
80
|
+
result[hn].delete(y)
|
81
|
+
term_values_append(:parent_select=>parent_xpath,:child_index=>0,:template=>template,:values=>z)
|
82
|
+
new_array_index = find_by_terms(*pointer).length - 1
|
83
|
+
result[hn][new_array_index.to_s] = z
|
84
|
+
else
|
85
|
+
term_value_update(xpath, y.to_i, z)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
return result
|
90
|
+
end
|
91
|
+
|
92
|
+
def term_values_append(opts={})
|
93
|
+
parent_select = Array( opts[:parent_select] )
|
94
|
+
child_index = opts[:child_index]
|
95
|
+
template = opts[:template]
|
96
|
+
new_values = Array( opts[:values] )
|
97
|
+
|
98
|
+
# If template is a string, use it as the template, otherwise use it as arguments to xml_builder_template
|
99
|
+
unless template.instance_of?(String)
|
100
|
+
template_args = Array(template)
|
101
|
+
if template_args.last.kind_of?(Hash)
|
102
|
+
template_opts = template_args.delete_at(template_args.length - 1)
|
103
|
+
template_args << template_opts
|
104
|
+
end
|
105
|
+
template = self.class.terminology.xml_builder_template( *template_args )
|
106
|
+
end
|
107
|
+
|
108
|
+
parent_nodeset = find_by_terms(*parent_select)
|
109
|
+
parent_node = node_from_set(parent_nodeset, child_index)
|
110
|
+
|
111
|
+
if parent_node.nil?
|
112
|
+
raise OM::XML::ParentNodeNotFoundError, "Failed to find a parent node to insert values into based on :parent_select #{parent_select.inspect} with :child_index #{child_index.inspect}"
|
113
|
+
end
|
114
|
+
|
115
|
+
builder = Nokogiri::XML::Builder.with(parent_node) do |xml|
|
116
|
+
new_values.each do |builder_new_value|
|
117
|
+
builder_arg = eval('"'+ template + '"') # this inserts builder_new_value into the builder template
|
118
|
+
eval(builder_arg)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Nokogiri::XML::Node.new(builder.to_xml, foo)
|
123
|
+
|
124
|
+
return parent_node
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
def term_value_update(node_select,child_index,new_value,opts={})
|
129
|
+
# template = opts.fetch(:template,nil)
|
130
|
+
|
131
|
+
node = find_by_terms_and_value(*node_select)[child_index]
|
132
|
+
if new_value == "" || new_value == :delete
|
133
|
+
node.remove
|
134
|
+
else
|
135
|
+
node.content = new_value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# def term_value_set(term_ref, query_opts, node_index, new_value)
|
140
|
+
# end
|
141
|
+
|
142
|
+
def term_value_delete(opts={})
|
143
|
+
parent_select = Array( opts[:parent_select] )
|
144
|
+
parent_index = opts[:parent_index]
|
145
|
+
child_index = opts[:child_index]
|
146
|
+
xpath_select = opts[:select]
|
147
|
+
|
148
|
+
if !xpath_select.nil?
|
149
|
+
node = find_by_terms_and_value(xpath_select).first
|
150
|
+
else
|
151
|
+
# parent_nodeset = find_by_terms_and_value(parent_select, parent_select)
|
152
|
+
parent_nodeset = find_by_terms_and_value(*parent_select)
|
153
|
+
|
154
|
+
if parent_index.nil?
|
155
|
+
node = node_from_set(parent_nodeset, child_index)
|
156
|
+
else
|
157
|
+
parent = node_from_set(parent_nodeset, parent_index)
|
158
|
+
# this next line is a hack around the fact that element_children() sometimes doesn't work.
|
159
|
+
node = node_from_set(parent.xpath("*"), child_index)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
node.remove
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# Allows you to provide an array index _or_ a symbol representing the function to call on the nodeset in order to retrieve the node.
|
168
|
+
def node_from_set(nodeset, index)
|
169
|
+
if index.kind_of?(Integer)
|
170
|
+
node = nodeset[index]
|
171
|
+
elsif index.kind_of?(Symbol) && nodeset.respond_to?(index)
|
172
|
+
node = nodeset.send(index)
|
173
|
+
else
|
174
|
+
raise "Could not retrieve node using index #{index}."
|
175
|
+
end
|
176
|
+
|
177
|
+
return node
|
178
|
+
end
|
179
|
+
|
180
|
+
private :node_from_set
|
181
|
+
|
182
|
+
end
|