om 0.1.10 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|