om 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/History.textile +1 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/lib/om/xml/accessors.rb +130 -0
- data/lib/om/xml/container.rb +32 -0
- data/lib/om/xml/properties.rb +380 -0
- data/lib/om/xml/property_value_operators.rb +111 -0
- data/lib/om/xml/validation.rb +63 -0
- data/lib/om/xml.rb +23 -0
- data/lib/om.rb +9 -0
- data/om.gemspec +89 -0
- data/spec/fixtures/CBF_MODS/ARS0025_016.xml +94 -0
- data/spec/fixtures/RUBRIC_mods_article_template.xml +89 -0
- data/spec/fixtures/mods-3-2.xsd +1 -24
- data/spec/fixtures/mods_articles/hydrangea_article1.xml +90 -0
- data/spec/fixtures/test_dummy_mods.xml +36 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/unit/accessors_spec.rb +156 -0
- data/spec/unit/container_spec.rb +60 -0
- data/spec/unit/properties_spec.rb +247 -0
- data/spec/unit/property_value_operators_spec.rb +245 -0
- data/spec/unit/validation_spec.rb +78 -0
- data/spec/unit/xml_spec.rb +21 -0
- metadata +174 -0
data/.document
ADDED
data/.gitignore
ADDED
data/History.textile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Note: OX v.1 Does not handle treating attribute values as the changing "value" of a node
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Matt Zumwalt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= opinionated-xml
|
2
|
+
|
3
|
+
A library to help you tame sprawling XML schemas like MODS.
|
4
|
+
|
5
|
+
== 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
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Matt Zumwalt. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "om"
|
8
|
+
gem.summary = %Q{OM (Opinionated Metadata): A library to help you tame sprawling XML schemas like MODS.}
|
9
|
+
gem.description = %Q{OM (Opinionated Metadata): A library to help you tame sprawling XML schemas like MODS. Wraps Nokogiri documents in objects with miscellaneous helper methods for doing things like retrieve generated xpath queries or look up properties based on a simplified DSL}
|
10
|
+
gem.email = "matt.zumwalt@yourmediashelf.com"
|
11
|
+
gem.homepage = "http://github.com/mediashelf/om"
|
12
|
+
gem.authors = ["Matt Zumwalt"]
|
13
|
+
|
14
|
+
gem.add_dependency('nokogiri')
|
15
|
+
gem.add_dependency('facets')
|
16
|
+
|
17
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
18
|
+
gem.add_development_dependency "mocha", ">= 0.9.8"
|
19
|
+
gem.add_development_dependency "ruby-debug"
|
20
|
+
|
21
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
22
|
+
end
|
23
|
+
Jeweler::GemcutterTasks.new
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'spec/rake/spectask'
|
29
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
35
|
+
spec.libs << 'lib' << 'spec'
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :spec => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :spec
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
47
|
+
|
48
|
+
rdoc.rdoc_dir = 'rdoc'
|
49
|
+
rdoc.title = "om #{version}"
|
50
|
+
rdoc.rdoc_files.include('README*')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module OM::XML::Accessors
|
2
|
+
|
3
|
+
module ClassMethods
|
4
|
+
attr_accessor :accessors
|
5
|
+
|
6
|
+
def accessor(accessor_name, opts={})
|
7
|
+
@accessors ||= {}
|
8
|
+
insert_accessor(accessor_name, opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
def insert_accessor(accessor_name, accessor_opts, parent_names=[])
|
12
|
+
unless accessor_opts.has_key?(:relative_xpath)
|
13
|
+
accessor_opts[:relative_xpath] = "oxns:#{accessor_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
destination = @accessors
|
17
|
+
parent_names.each do |parent_name|
|
18
|
+
destination = destination[parent_name][:children]
|
19
|
+
end
|
20
|
+
|
21
|
+
destination[accessor_name] = accessor_opts
|
22
|
+
|
23
|
+
# Recursively call insert_accessor for any children
|
24
|
+
if accessor_opts.has_key?(:children)
|
25
|
+
children_array = accessor_opts[:children].dup
|
26
|
+
accessor_opts[:children] = {}
|
27
|
+
children_array.each do |child|
|
28
|
+
if child.kind_of?(Hash)
|
29
|
+
child_name = child.keys.first
|
30
|
+
child_opts = child.values.first
|
31
|
+
else
|
32
|
+
child_name = child
|
33
|
+
child_opts = {}
|
34
|
+
end
|
35
|
+
insert_accessor(child_name, child_opts, parent_names+[accessor_name] )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the configuration info for the selected accessor.
|
42
|
+
# Ingores any integers in the array (ie. nodeset indices intended for use in other accessor convenience methods)
|
43
|
+
def accessor_info(*args)
|
44
|
+
# Ignore any nodeset indexes in the args array
|
45
|
+
keys = args.select {|x| !x.kind_of?(Integer) }
|
46
|
+
info = @accessors
|
47
|
+
keys.each do |k|
|
48
|
+
unless keys.index(k) == 0
|
49
|
+
info = info.fetch(:children, nil)
|
50
|
+
if info.nil?
|
51
|
+
debugger
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
info = info.fetch(k, nil)
|
57
|
+
if info.nil?
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
return info
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def accessor_xpath(*args)
|
66
|
+
keys = even_values(args)
|
67
|
+
indices = odd_values(args)
|
68
|
+
xpath = "//"
|
69
|
+
keys.each do |k|
|
70
|
+
key_index = keys.index(k)
|
71
|
+
accessor_info = accessor_info(*keys[0..key_index])
|
72
|
+
relative_path = accessor_info[:relative_xpath]
|
73
|
+
|
74
|
+
if relative_path.kind_of?(Hash)
|
75
|
+
if relative_path.has_key?(:attribute)
|
76
|
+
relative_path = "@"+relative_path[:attribute]
|
77
|
+
end
|
78
|
+
else
|
79
|
+
|
80
|
+
if indices[key_index]
|
81
|
+
add_position_predicate!(relative_path, indices[key_index])
|
82
|
+
end
|
83
|
+
|
84
|
+
if accessor_info.has_key?(:default_content_path)
|
85
|
+
relative_path << "/"+accessor_info[:default_content_path]
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
if key_index > 0
|
90
|
+
relative_path.insert(0, "/")
|
91
|
+
end
|
92
|
+
xpath << relative_path
|
93
|
+
end
|
94
|
+
|
95
|
+
return xpath
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_position_predicate!(xpath_query, array_index_value)
|
99
|
+
position_function = "position()=#{array_index_value + 1}"
|
100
|
+
|
101
|
+
if xpath_query.include?("]")
|
102
|
+
xpath_query.insert(xpath_query.rindex("]"), " and #{position_function}")
|
103
|
+
else
|
104
|
+
xpath_query << "[#{position_function}]"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def odd_values(array)
|
109
|
+
array.values_at(* array.each_index.select {|i| i.odd?})
|
110
|
+
end
|
111
|
+
def even_values(array)
|
112
|
+
array.values_at(* array.each_index.select {|i| i.even?})
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Instance Methods -- These methods will be available on instances of OM classes (ie. the actual xml documents)
|
117
|
+
|
118
|
+
def self.included(klass)
|
119
|
+
klass.extend(ClassMethods)
|
120
|
+
end
|
121
|
+
|
122
|
+
# *args Variable length array of values in format [:accessor_name, index, :accessor_name ...]
|
123
|
+
# example: [:person, 1, :first_name]
|
124
|
+
# Currently, indexes must be integers.
|
125
|
+
def retrieve(*args)
|
126
|
+
xpath = self.class.accessor_xpath(*args)
|
127
|
+
ng_xml.xpath(xpath, "oxns"=>"http://www.loc.gov/mods/v3")
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module OM::XML::Container
|
2
|
+
|
3
|
+
attr_accessor :ng_xml
|
4
|
+
|
5
|
+
# Class Methods -- These methods will be available on classes that include this Module
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
# @xml Sting, File or Nokogiri::XML::Node
|
10
|
+
# @tmpl ActiveFedora::MetadataDatastream
|
11
|
+
def from_xml(xml, tmpl=self.new) # :nodoc:
|
12
|
+
if xml.kind_of? Nokogiri::XML::Node
|
13
|
+
tmpl.ng_xml = xml
|
14
|
+
else
|
15
|
+
tmpl.ng_xml = Nokogiri::XML::Document.parse(xml)
|
16
|
+
end
|
17
|
+
return tmpl
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
# Instance Methods -- These methods will be available on instances of classes that include this module
|
23
|
+
|
24
|
+
def self.included(klass)
|
25
|
+
klass.extend(ClassMethods)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_xml
|
29
|
+
ng_xml.to_xml
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,380 @@
|
|
1
|
+
module OM::XML::Properties
|
2
|
+
|
3
|
+
attr_accessor :ng_xml
|
4
|
+
|
5
|
+
# Class Methods -- These methods will be available on classes that include this Module
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_accessor :root_property_ref, :root_config, :ox_namespaces
|
9
|
+
attr_reader :properties
|
10
|
+
|
11
|
+
def root_property( property_ref, path, namespace, opts={})
|
12
|
+
# property property_ref, path, opts.merge({:path=>path, :ref=>property_ref})
|
13
|
+
@root_config = opts.merge({:namespace=>namespace, :path=>path, :ref=>property_ref})
|
14
|
+
@root_property_ref = property_ref
|
15
|
+
@ox_namespaces = {'oxns'=>root_config[:namespace]}
|
16
|
+
@schema_url = opts[:schema]
|
17
|
+
end
|
18
|
+
|
19
|
+
def property( property_ref, opts={})
|
20
|
+
@properties ||= {}
|
21
|
+
@properties[property_ref] = opts.merge({:ref=>property_ref})
|
22
|
+
configure_property @properties[property_ref]
|
23
|
+
configure_paths @properties[property_ref]
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure_property(prop_hash=root_config)
|
27
|
+
if prop_hash.has_key?(:variant_of)
|
28
|
+
properties[prop_hash[:ref]] = properties[prop_hash[:variant_of]].deep_copy.merge(prop_hash)
|
29
|
+
end
|
30
|
+
if !prop_hash.has_key?(:convenience_methods)
|
31
|
+
prop_hash[:convenience_methods] = {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates appropriate xpath queries for a property described by the given hash
|
36
|
+
# putting the results into the hash under keys like :xpath and :xpath_constrained
|
37
|
+
# This is also done recursively for all subelements and convenience methods described in the hash.
|
38
|
+
def configure_paths(prop_hash=root_config)
|
39
|
+
prop_hash[:path] ||= ""
|
40
|
+
# prop_hash[:path] = Array( prop_hash[:path] )
|
41
|
+
xpath_opts = {}
|
42
|
+
|
43
|
+
if prop_hash.has_key?(:variant_of)
|
44
|
+
xpath_opts[:variations] = prop_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
xpath_constrained_opts = xpath_opts.merge({:constraints=>:default})
|
48
|
+
|
49
|
+
relative_xpath = generate_xpath(prop_hash, xpath_opts.merge(:relative=>true))
|
50
|
+
# prop_hash[:xpath] = generate_xpath(prop_hash, xpath_opts)
|
51
|
+
prop_hash[:xpath] = "//#{relative_xpath}"
|
52
|
+
prop_hash[:xpath_relative] = relative_xpath
|
53
|
+
prop_hash[:xpath_constrained] = generate_xpath(prop_hash, xpath_constrained_opts).gsub('"', '\"')
|
54
|
+
|
55
|
+
prop_hash[:convenience_methods].each_pair do |cm_name, cm_props|
|
56
|
+
cm_xpath_relative_opts = cm_props.merge(:variations => cm_props, :relative=>true)
|
57
|
+
cm_constrained_opts = xpath_opts.merge(:constraints => cm_props)
|
58
|
+
|
59
|
+
cm_relative_xpath = generate_xpath(cm_props, cm_xpath_relative_opts)
|
60
|
+
prop_hash[:convenience_methods][cm_name][:xpath_relative] = cm_relative_xpath
|
61
|
+
# prop_hash[:convenience_methods][cm_name][:xpath] = generate_xpath(cm_xpath_hash, cm_xpath_opts)
|
62
|
+
prop_hash[:convenience_methods][cm_name][:xpath] = prop_hash[:xpath] + "/" + cm_relative_xpath
|
63
|
+
prop_hash[:convenience_methods][cm_name][:xpath_constrained] = generate_xpath(prop_hash, cm_constrained_opts).gsub('"', '\"')
|
64
|
+
end
|
65
|
+
|
66
|
+
if prop_hash.has_key?(:subelements)
|
67
|
+
prop_hash[:subelements].each do |se|
|
68
|
+
configure_subelement_paths(se, prop_hash, xpath_opts)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if properties[:unresolved].has_key?(prop_hash[:ref])
|
73
|
+
ref = prop_hash[:ref]
|
74
|
+
properties[:unresolved][ref].each do |parent_prop_hash|
|
75
|
+
logger.debug "Resolving #{ref} subelement for #{parent_prop_hash[:ref]} property"
|
76
|
+
configure_subelement_paths(ref, parent_prop_hash, xpath_opts)
|
77
|
+
end
|
78
|
+
properties[:unresolved].delete(ref)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def configure_subelement_paths(se, parent_prop_hash, parent_xpath_opts)
|
84
|
+
# se_xpath_opts = parent_xpath_opts.merge(:subelement_of => parent_prop_hash[:ref])
|
85
|
+
se_xpath_opts = parent_xpath_opts
|
86
|
+
new_se_props = {}
|
87
|
+
if parent_prop_hash.has_key?(:variant_of)
|
88
|
+
se_xpath_opts[:variations] = parent_prop_hash
|
89
|
+
end
|
90
|
+
|
91
|
+
if se.instance_of?(String)
|
92
|
+
|
93
|
+
new_se_props[:path] = se
|
94
|
+
new_se_props[:xpath_relative] = generate_xpath({:path=>se}, :relative=>true)
|
95
|
+
new_se_props[:xpath] = parent_prop_hash[:xpath] + "/" + new_se_props[:xpath_relative]
|
96
|
+
se_xpath_constrained_opts = se_xpath_opts.merge({:constraints=>{:path=>se}})
|
97
|
+
new_se_props[:xpath_constrained] = generate_xpath(parent_prop_hash, se_xpath_constrained_opts)
|
98
|
+
|
99
|
+
elsif se.instance_of?(Symbol)
|
100
|
+
|
101
|
+
if properties.has_key?(se)
|
102
|
+
|
103
|
+
se_props = properties[se]
|
104
|
+
|
105
|
+
new_se_props = se_props.deep_copy
|
106
|
+
# new_se_props[:path] = se_props[:path]
|
107
|
+
# new_se_props[:xpath_relative] = se_props[:xpath_relative]
|
108
|
+
new_se_props[:xpath] = parent_prop_hash[:xpath] + "/" + new_se_props[:xpath_relative]
|
109
|
+
se_xpath_constrained_opts = parent_xpath_opts.merge(:constraints => se_props, :subelement_of => parent_prop_hash[:ref])
|
110
|
+
new_se_props[:xpath_constrained] = generate_xpath(parent_prop_hash, se_xpath_constrained_opts)
|
111
|
+
|
112
|
+
else
|
113
|
+
properties[:unresolved] ||= {}
|
114
|
+
properties[:unresolved][se] ||= []
|
115
|
+
properties[:unresolved][se] << parent_prop_hash
|
116
|
+
logger.debug("Added #{se.inspect} to unresolved properties with parent #{parent_prop_hash[:ref]}")
|
117
|
+
end
|
118
|
+
else
|
119
|
+
logger.info("failed to generate path for #{se.inspect}")
|
120
|
+
se_xpath = ""
|
121
|
+
se_xpath_constrained = ""
|
122
|
+
end
|
123
|
+
|
124
|
+
if new_se_props.has_key?(:xpath_constrained)
|
125
|
+
new_se_props[:xpath_constrained].gsub!('"', '\"')
|
126
|
+
end
|
127
|
+
|
128
|
+
properties[ parent_prop_hash[:ref] ][:convenience_methods][se.to_sym] = new_se_props
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
def generate_xpath( property_info, opts={})
|
133
|
+
prefix = "oxns"
|
134
|
+
property_info[:path] ||= ""
|
135
|
+
path = property_info[:path]
|
136
|
+
path_array = Array( path )
|
137
|
+
template = ""
|
138
|
+
template << "//" unless opts[:relative]
|
139
|
+
template << "#{prefix}:"
|
140
|
+
template << delimited_list(path_array, "/#{prefix}:")
|
141
|
+
|
142
|
+
predicates = []
|
143
|
+
default_content_path = property_info.has_key?(:default_content_path) ? property_info[:default_content_path] : ""
|
144
|
+
subelement_path_parts = []
|
145
|
+
|
146
|
+
# Skip everything if a template was provided
|
147
|
+
if opts.has_key?(:template)
|
148
|
+
template = eval('"' + opts[:template] + '"')
|
149
|
+
else
|
150
|
+
# Apply variations
|
151
|
+
if opts.has_key?(:variations)
|
152
|
+
if opts[:variations].has_key?(:attributes)
|
153
|
+
opts[:variations][:attributes].each_pair do |attr_name, attr_value|
|
154
|
+
predicates << "@#{attr_name}=\"#{attr_value}\""
|
155
|
+
end
|
156
|
+
end
|
157
|
+
if opts[:variations].has_key?(:subelement_path)
|
158
|
+
if opts[:variations][:subelement_path].instance_of?(Array)
|
159
|
+
opts[:variations][:subelement_path].each do |se_path|
|
160
|
+
subelement_path_parts << "#{prefix}:#{se_path}"
|
161
|
+
end
|
162
|
+
else
|
163
|
+
subelement_path_parts << "#{prefix}:#{opts[:variations][:subelement_path]}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Apply constraints
|
169
|
+
if opts.has_key?(:constraints)
|
170
|
+
arguments_for_contains_function = []
|
171
|
+
if opts[:constraints] == :default
|
172
|
+
if property_info.has_key?(:default_content_path)
|
173
|
+
default_content_path = property_info[:default_content_path]
|
174
|
+
arguments_for_contains_function << "#{prefix}:#{default_content_path}"
|
175
|
+
end
|
176
|
+
elsif opts[:constraints].has_key?(:path)
|
177
|
+
constraint_predicates = []
|
178
|
+
if opts.has_key?(:subelement_of)
|
179
|
+
constraint_path = "#{prefix}:#{opts[:constraints][:path]}"
|
180
|
+
if opts[:constraints].has_key?(:default_content_path)
|
181
|
+
constraint_path << "/#{prefix}:#{opts[:constraints][:default_content_path]}"
|
182
|
+
end
|
183
|
+
else
|
184
|
+
constraint_path = "#{prefix}:#{opts[:constraints][:path]}"
|
185
|
+
end
|
186
|
+
arguments_for_contains_function << constraint_path
|
187
|
+
|
188
|
+
if opts[:constraints].has_key?(:attributes) && opts[:constraints][:attributes].kind_of?(Hash)
|
189
|
+
opts[:constraints][:attributes].each_pair do |attr_name, attr_value|
|
190
|
+
constraint_predicates << "@#{attr_name}=\"#{attr_value}\""
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
unless constraint_predicates.empty?
|
195
|
+
arguments_for_contains_function.last << "[#{delimited_list(constraint_predicates)}]"
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
arguments_for_contains_function << "\":::constraint_value:::\""
|
200
|
+
|
201
|
+
predicates << "contains(#{delimited_list(arguments_for_contains_function)})"
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
unless predicates.empty?
|
206
|
+
template << "["
|
207
|
+
template << delimited_list(predicates, " and ")
|
208
|
+
template << "]"
|
209
|
+
end
|
210
|
+
|
211
|
+
unless subelement_path_parts.empty?
|
212
|
+
subelement_path_parts.each {|path_part| template << "/#{path_part}"}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# result = eval( '"' + template + '"' )
|
217
|
+
return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }
|
218
|
+
end
|
219
|
+
|
220
|
+
#
|
221
|
+
# Convenience Methods for Retrieving Generated Info
|
222
|
+
#
|
223
|
+
|
224
|
+
def xpath_query_for( property_ref, query_opts={}, opts={} )
|
225
|
+
|
226
|
+
if property_ref.instance_of?(String)
|
227
|
+
xpath_query = property_ref
|
228
|
+
else
|
229
|
+
property_info = property_info_for( property_ref )
|
230
|
+
|
231
|
+
if !property_info.nil?
|
232
|
+
if query_opts.kind_of?(String)
|
233
|
+
constraint_value = query_opts
|
234
|
+
xpath_template = property_info[:xpath_constrained]
|
235
|
+
xpath_query = eval( '"' + xpath_template + '"' )
|
236
|
+
elsif query_opts.kind_of?(Hash) && !query_opts.empty?
|
237
|
+
key_value_pair = query_opts.first
|
238
|
+
constraint_value = key_value_pair.last
|
239
|
+
xpath_template = property_info[:convenience_methods][key_value_pair.first][:xpath_constrained]
|
240
|
+
xpath_query = eval( '"' + xpath_template + '"' )
|
241
|
+
else
|
242
|
+
xpath_query = property_info[:xpath]
|
243
|
+
end
|
244
|
+
else
|
245
|
+
xpath_query = nil
|
246
|
+
end
|
247
|
+
end
|
248
|
+
return xpath_query
|
249
|
+
end
|
250
|
+
|
251
|
+
def property_info_for(property_ref)
|
252
|
+
if property_ref.instance_of?(Array) && property_ref.length == 1
|
253
|
+
property_ref = property_ref[0]
|
254
|
+
end
|
255
|
+
if property_ref.instance_of?(Symbol)
|
256
|
+
property_info = properties[property_ref]
|
257
|
+
elsif property_ref.kind_of?(Array)
|
258
|
+
prop_ref = property_ref[0]
|
259
|
+
cm_name = property_ref[1]
|
260
|
+
if properties.has_key?(prop_ref)
|
261
|
+
property_info = properties[prop_ref][:convenience_methods][cm_name]
|
262
|
+
end
|
263
|
+
else
|
264
|
+
property_info = nil
|
265
|
+
end
|
266
|
+
return property_info
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
def delimited_list( values_array, delimiter=", ")
|
271
|
+
result = values_array.collect{|a| a + delimiter}.to_s.chomp(delimiter)
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
#
|
276
|
+
# Builder Support
|
277
|
+
#
|
278
|
+
|
279
|
+
def builder_template(property_ref, opts={})
|
280
|
+
property_info = property_info_for(property_ref)
|
281
|
+
|
282
|
+
prop_info = property_info.merge(opts)
|
283
|
+
|
284
|
+
if prop_info.nil?
|
285
|
+
return nil
|
286
|
+
else
|
287
|
+
node_options = []
|
288
|
+
node_child_template = ""
|
289
|
+
if prop_info.has_key?(:default_content_path)
|
290
|
+
node_child_options = ["\':::builder_new_value:::\'"]
|
291
|
+
node_child_template = " { xml.#{property_info[:default_content_path]}( #{delimited_list(node_child_options)} ) }"
|
292
|
+
else
|
293
|
+
node_options = ["\':::builder_new_value:::\'"]
|
294
|
+
end
|
295
|
+
# if opts.has_key?(:attributes) ...
|
296
|
+
# ...
|
297
|
+
if prop_info.has_key?(:attributes)
|
298
|
+
applicable_attributes( prop_info[:attributes] ).each_pair do |k,v|
|
299
|
+
node_options << ":#{k}=>\'#{v}\'"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
template = "xml.#{prop_info[:path]}( #{delimited_list(node_options)} )" + node_child_template
|
303
|
+
return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# @attributes_spec Array or Hash
|
308
|
+
# Returns a Hash where all of the values are strings
|
309
|
+
# {:type=>"date"} will return {:type=>"date"}
|
310
|
+
# {["authority", {:type=>["text","code"]}] will return {:type=>"text"}
|
311
|
+
def applicable_attributes(attributes)
|
312
|
+
|
313
|
+
if attributes.kind_of?(Hash)
|
314
|
+
attributes_hash = attributes
|
315
|
+
elsif attributes.kind_of?(Array)
|
316
|
+
attributes_hash = {}
|
317
|
+
attributes.each do |bute|
|
318
|
+
if bute.kind_of?(Hash)
|
319
|
+
attributes_hash.merge!(bute)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
applicable_attributes = {}
|
325
|
+
attributes_hash.each_pair {|k,v| applicable_attributes[k] = condense_value(v) }
|
326
|
+
|
327
|
+
|
328
|
+
return applicable_attributes
|
329
|
+
end
|
330
|
+
|
331
|
+
def condense_value(value)
|
332
|
+
if value.kind_of?(Array)
|
333
|
+
return value.first
|
334
|
+
else
|
335
|
+
return value
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def logger
|
340
|
+
@logger ||= defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : Logger.new(STDOUT)
|
341
|
+
end
|
342
|
+
|
343
|
+
private :applicable_attributes
|
344
|
+
end
|
345
|
+
|
346
|
+
# Instance Methods -- These methods will be available on instances of classes that include this module
|
347
|
+
|
348
|
+
def self.included(klass)
|
349
|
+
klass.extend(ClassMethods)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Applies the property's corresponding xpath query, returning the result Nokogiri::XML::NodeSet
|
353
|
+
def lookup( property_ref, query_opts={}, opts={} )
|
354
|
+
xpath_query = xpath_query_for( property_ref, query_opts, opts )
|
355
|
+
|
356
|
+
if xpath_query.nil?
|
357
|
+
result = []
|
358
|
+
else
|
359
|
+
result = ng_xml.xpath(xpath_query, ox_namespaces)
|
360
|
+
end
|
361
|
+
|
362
|
+
return result
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
# Returns a hash combining the current documents namespaces (provided by nokogiri) and any namespaces that have been set up by your class definiton.
|
367
|
+
# Most importantly, this matches the 'oxns' namespace to the namespace you provided in your root property config
|
368
|
+
def ox_namespaces
|
369
|
+
@ox_namespaces ||= ng_xml.namespaces.merge(self.class.ox_namespaces)
|
370
|
+
end
|
371
|
+
|
372
|
+
def xpath_query_for( property_ref, query_opts={}, opts={} )
|
373
|
+
self.class.xpath_query_for( property_ref, query_opts, opts )
|
374
|
+
end
|
375
|
+
|
376
|
+
def property_info_for(property_ref)
|
377
|
+
self.class.property_info_for(property_ref)
|
378
|
+
end
|
379
|
+
|
380
|
+
end
|