saxon 0.1.0-java

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.
@@ -0,0 +1,129 @@
1
+ require_relative 'xdm_node'
2
+ require_relative 'xdm_atomic_value'
3
+
4
+ module Saxon
5
+ # An XPath Data Model Value object, representing a Sequence.
6
+ class XdmValue
7
+ include Enumerable
8
+
9
+ def self.wrap_s9_xdm_value(s9_xdm_value)
10
+ return new(s9_xdm_value) if s9_xdm_value.instance_of?(Saxon::S9API::XdmValue)
11
+ case s9_xdm_value
12
+ when Saxon::S9API::XdmEmptySequence
13
+ new([])
14
+ else
15
+ wrap_s9_xdm_item(s9_xdm_value)
16
+ end
17
+ end
18
+
19
+ def self.wrap_s9_xdm_item(s9_xdm_item)
20
+ if s9_xdm_item.isAtomicValue
21
+ XdmAtomicValue.new(s9_xdm_item)
22
+ else
23
+ case s9_xdm_item
24
+ when Saxon::S9API::XdmNode
25
+ XdmNode.new(s9_xdm_item)
26
+ else
27
+ XdmUnhandledItem.new(s9_xdm_item)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Create a new XdmValue sequence containing the items passed in as a Ruby enumerable.
33
+ #
34
+ # @param items [Enumerable] A list of XDM Item members
35
+ # @return [Saxon::XdmValue] The XDM value
36
+ def self.create(items)
37
+ new(Saxon::S9API::XdmValue.makeSequence(items.map(&:to_java)))
38
+ end
39
+
40
+ attr_reader :s9_xdm_value
41
+ private :s9_xdm_value
42
+
43
+ # @api private
44
+ def initialize(s9_xdm_value)
45
+ @s9_xdm_value = s9_xdm_value
46
+ end
47
+
48
+ # @return [Fixnum] The size of the sequence
49
+ def size
50
+ s9_xdm_value.size
51
+ end
52
+
53
+ # Calls the given block once for each Item in the sequence, passing that
54
+ # item as a parameter. Returns the value itself.
55
+ #
56
+ # If no block is given, an Enumerator is returned.
57
+ #
58
+ # @overload
59
+ # @yield [item] The current XDM Item
60
+ # @yieldparam item [Saxon::XdmAtomicValue, Saxon::XdmNode] the item.
61
+ def each(&block)
62
+ to_enum.each(&block)
63
+ end
64
+
65
+ # @return [Saxon::S9API::XdmValue] The underlying Saxon Java XDM valuee object.
66
+ def to_java
67
+ @s9_xdm_value
68
+ end
69
+
70
+ # Compare this XdmValue with another. Currently this requires iterating
71
+ # across the sequence, and the other sequence and comparing each member
72
+ # with the corresponding member in the other sequence.
73
+ #
74
+ # @param other [Saxon::XdmValue] The XdmValue to be compare against
75
+ # @return [Boolean] whether the two XdmValues are equal
76
+ def ==(other)
77
+ return false unless other.is_a?(XdmValue)
78
+ return false unless other.size == size
79
+ not_okay = to_enum.zip(other.to_enum).find { |mine, theirs|
80
+ mine != theirs
81
+ }
82
+ not_okay.nil? || !not_okay
83
+ end
84
+
85
+ alias_method :eql?, :==
86
+
87
+ # The hash code for the XdmValue
88
+ # @return [Fixnum] The hash code
89
+ def hash
90
+ @hash ||= to_a.hash
91
+ end
92
+
93
+ # Returns a lazy Enumerator over the sequence
94
+ # @return [Enumerator::Lazy] the enumerator
95
+ def to_enum
96
+ s9_xdm_value.each.lazy.map { |s9_xdm_item|
97
+ wrap_s9_xdm_item(s9_xdm_item)
98
+ }.each
99
+ end
100
+
101
+ alias_method :enum_for, :to_enum
102
+
103
+ private
104
+
105
+ def wrap_s9_xdm_item(s9_xdm_item)
106
+ if s9_xdm_item.isAtomicValue
107
+ XdmAtomicValue.new(s9_xdm_item)
108
+ else
109
+ case s9_xdm_item
110
+ when Saxon::S9API::XdmNode
111
+ XdmNode.new(s9_xdm_item)
112
+ else
113
+ XdmUnhandledItem.new(s9_xdm_item)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ # Placeholder class for Saxon Items that we haven't gotten to yet
120
+ class XdmUnhandledItem
121
+ def initialize(s9_xdm_item)
122
+ @s9_xdm_item = s9_xdm_item
123
+ end
124
+
125
+ def to_java
126
+ @s9_xdm_item
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,8 @@
1
+ require_relative './xpath/compiler'
2
+
3
+ module Saxon
4
+ # Classes for compiling, configuring, and executing XPath queries against
5
+ # XDM nodes or documents
6
+ module XPath
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ require 'forwardable'
2
+ require_relative './static_context'
3
+ require_relative './executable'
4
+
5
+ module Saxon
6
+ module XPath
7
+ # Compiles XPath expressions so they can be executed
8
+ class Compiler
9
+ # Create a new <tt>XPath::Compiler</tt> using the supplied Processor.
10
+ # Passing a block gives access to a DSL for setting up the compiler's
11
+ # static context.
12
+ #
13
+ # @param processor [Saxon::Processor] the {Saxon::Processor} to use
14
+ # @yield An XPath compiler DSL block
15
+ # @return [Saxon::XPath::Compiler] the new compiler instance
16
+ def self.create(processor, &block)
17
+ static_context = XPath::StaticContext.define(block)
18
+ new(processor.to_java, static_context)
19
+ end
20
+
21
+ extend Forwardable
22
+
23
+ attr_reader :static_context
24
+ private :static_context
25
+
26
+ # @api private
27
+ # @param s9_processor [net.sf.saxon.s9api.Processor] the Saxon
28
+ # <tt>Processor</tt> to wrap
29
+ # @param static_context [Saxon::XPath::StaticContext] the static context
30
+ # XPaths compiled using this compiler will have
31
+ def initialize(s9_processor, static_context)
32
+ @s9_processor, @static_context = s9_processor, static_context
33
+ end
34
+
35
+ def_delegators :static_context, :default_collation, :declared_namespaces, :declared_variables
36
+ # @!attribute [r] default_collation
37
+ # @return [String] the URI of the default declared collation
38
+ # @!attribute [r] declared_namespaces
39
+ # @return [Hash<String => String>] declared namespaces as prefix => URI hash
40
+ # @!attribute [r] declared_variables
41
+ # @return [Hash<Saxon::QName => Saxon::XPath::VariableDeclaration>] declared variables as QName => Declaration hash
42
+
43
+ # @param expression [String] the XPath expression to compile
44
+ # @return [Saxon::XPath::Executable] the executable query
45
+ def compile(expression)
46
+ Saxon::XPath::Executable.new(new_compiler.compile(expression), static_context)
47
+ end
48
+
49
+ def create(&block)
50
+ new_static_context = static_context.define(block)
51
+ self.class.new(@s9_processor, new_static_context)
52
+ end
53
+
54
+ private
55
+
56
+ def new_compiler
57
+ compiler = @s9_processor.newXPathCompiler
58
+ declared_namespaces.each do |prefix, uri|
59
+ compiler.declareNamespace(prefix, uri)
60
+ end
61
+ declared_variables.each do |_, decl|
62
+ compiler.declareVariable(*decl.compiler_args)
63
+ end
64
+ compiler.declareDefaultCollation(default_collation) unless default_collation.nil?
65
+ compiler
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../xdm_node'
2
+ require_relative '../xdm_atomic_value'
3
+
4
+ module Saxon
5
+ module XPath
6
+ # Represents a compiled XPath query ready to be executed
7
+ class Executable
8
+ # @return [XPath::StaticContext] the XPath's static context
9
+ attr_reader :static_context
10
+
11
+ # @api private
12
+ # @param s9_xpath_executable [net.sf.saxon.s9api.XPathExecutable] the
13
+ # Saxon compiled XPath object
14
+ # @param static_context [XPath::StaticContext] the XPath's static
15
+ # context
16
+ def initialize(s9_xpath_executable, static_context)
17
+ @s9_xpath_executable, @static_context = s9_xpath_executable, static_context
18
+ end
19
+
20
+ # Run the compiled query using a passed-in node as the context item.
21
+ # @param context_item [Saxon::XdmNode] the context item node
22
+ # @return [Saxon::XPath::Result] the result of the query as an
23
+ # enumerable
24
+ def run(context_item, variables = {})
25
+ selector = to_java.load
26
+ selector.setContextItem(context_item.to_java)
27
+ variables.each do |qname_or_string, value|
28
+ selector.setVariable(static_context.resolve_variable_qname(qname_or_string).to_java, Saxon::XdmAtomicValue.create(value).to_java)
29
+ end
30
+ Result.new(selector.iterator)
31
+ end
32
+
33
+ # @return [net.sf.saxon.s9api.XPathExecutable] the underlying Saxon
34
+ # <tt>XPathExecutable</tt>
35
+ def to_java
36
+ @s9_xpath_executable
37
+ end
38
+ end
39
+
40
+ # The result of executing an XPath query as an enumerable object
41
+ class Result
42
+ include Enumerable
43
+
44
+ # @api private
45
+ # @param result_iterator [java.util.Iterator] the result of calling
46
+ # <tt>#iterator</tt> on a Saxon <tt>XPathSelector</tt>
47
+ def initialize(result_iterator)
48
+ @result_iterator = result_iterator
49
+ end
50
+
51
+ # Yields <tt>XdmNode</tt>s from the query result. If no block is passed,
52
+ # returns an <tt>Enumerator</tt>
53
+ # @yieldparam xdm_node [Saxon::XdmNode] the name that is yielded
54
+ def each(&block)
55
+ @result_iterator.lazy.map { |s9_xdm_node| Saxon::XdmNode.new(s9_xdm_node) }.each(&block)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,204 @@
1
+ require_relative '../qname'
2
+ require_relative './variable_declaration'
3
+ module Saxon
4
+ module XPath
5
+ # Raised when an attempt to declare a variable is made using a string for
6
+ # the qname with a namespace prefix that has not been declared in the
7
+ # context yet
8
+ class MissingVariableNamespaceError < StandardError
9
+ def initialize(variable_name, prefix)
10
+ @variable_name, @prefix = variable_name, prefix
11
+ end
12
+
13
+ def to_s
14
+ "Namespace prefix ‘#{@prefix}’ for variable name ‘#{@variable_name}’ is not bound to a URI"
15
+ end
16
+ end
17
+
18
+ # Represents the static context for a compiled XPath. {StaticContext}s are immutable.
19
+ class StaticContext
20
+ # methods used by both {StaticContext} and {StaticContext::DSL}
21
+ module Common
22
+ # @param args [Hash]
23
+ # @option args [Hash<Saxon::QName => Saxon::XPath::VariableDeclaration>] :declared_variables Hash of declared variables
24
+ # @option args [Hash<String => String>] :declared_namespaces Hash of namespace bindings prefix => URI
25
+ # @option args [Hash<String => java.text.Collator>] :declared_collations Hash of URI => Collator bindings
26
+ # @option args [String] :default_collation URI of the default collation
27
+ def initialize(args = {})
28
+ @declared_variables = args.fetch(:declared_variables, {}).freeze
29
+ @declared_namespaces = args.fetch(:declared_namespaces, {}).freeze
30
+ @default_collation = args.fetch(:default_collation, nil).freeze
31
+ end
32
+
33
+ # returns the context details in a hash suitable for initializing a new one
34
+ # @return [Hash<Symbol => Hash,null>] the args hash
35
+ def args_hash
36
+ {
37
+ declared_namespaces: @declared_namespaces,
38
+ declared_variables: @declared_variables,
39
+ default_collation: @default_collation
40
+ }
41
+ end
42
+ end
43
+
44
+ # methods for resolving a QName represented as a <tt>prefix:local-name</tt> string into a {Saxon::QName} by looking the prefix up in declared namespaces
45
+ module Resolver
46
+ # Resolve a QName string into a {Saxon::QName}.
47
+ #
48
+ # If the arg is a {Saxon::QName} already, it just gets returned. If
49
+ # it's an instance of the underlying Saxon Java QName, it'll be wrapped
50
+ # into a {Saxon::QName}
51
+ #
52
+ # If the arg is a string, it's resolved by using {resolve_variable_name}
53
+ #
54
+ # @param qname_or_string [String, Saxon::QName] the qname to resolve
55
+ # @return [Saxon::QName]
56
+ def self.resolve_variable_qname(qname_or_string, namespaces)
57
+ case qname_or_string
58
+ when String
59
+ resolve_variable_name(qname_or_string, namespaces)
60
+ when Saxon::QName
61
+ qname_or_string
62
+ when Saxon::S9API::QName
63
+ Saxon::QName.new(qname_or_string)
64
+ end
65
+ end
66
+
67
+ # Resolve a QName string of the form <tt>"prefix:local-name"</tt> into a
68
+ # {Saxon::QName} by looking up the namespace URI in a hash of
69
+ # <tt>"prefix" => "namespace-uri"</tt>
70
+ #
71
+ # @param qname_string [String] the QName as a <tt>"prefix:local-name"</tt> string
72
+ # @param namespaces [Hash<String => String>] the set of namespaces as a hash of <tt>"prefix" => "namespace-uri"</tt>
73
+ # @return [Saxon::QName]
74
+ def self.resolve_variable_name(qname_string, namespaces)
75
+ local_name, prefix = qname_string.split(':').reverse
76
+ uri = nil
77
+
78
+ if prefix
79
+ uri = namespaces[prefix]
80
+ raise MissingVariableNamespaceError.new(qname_string, prefix) if uri.nil?
81
+ end
82
+
83
+ Saxon::QName.create(prefix: prefix, uri: uri, local_name: local_name)
84
+ end
85
+ end
86
+
87
+ # Provides the hooks for constructing a {StaticContext} with a DSL.
88
+ # @api private
89
+ class DSL
90
+ include Common
91
+
92
+ # Create an instance based on the args hash, and execute the passed in Proc/lambda against it using <tt>#instance_exec</tt> and return a
93
+ # new {StaticContext} with the results
94
+ # @param block [Proc] a Proc/lambda (or <tt>to_proc</tt>'d containing DSL calls
95
+ # @return [Saxon::XPath::StaticContext]
96
+ def self.define(block, args = {})
97
+ dsl = new(args)
98
+ dsl.instance_exec(&block) unless block.nil?
99
+ StaticContext.new(dsl.args_hash)
100
+ end
101
+
102
+ # Set the default Collation to use. This should be one of the special
103
+ # collation URIs Saxon recognises, or one that has been registered
104
+ # using Saxon::Processor#declare_collations on the Processor that
105
+ # created the {XPath::Compiler} this context is for.
106
+ #
107
+ # @param collation_uri [String] The URI of the Collation to set as the default
108
+ def default_collation(collation_uri)
109
+ @default_collation = collation_uri
110
+ end
111
+
112
+ # Bind prefixes to namespace URIs
113
+ #
114
+ # @param namespaces [Hash{String, Symbol => String}]
115
+ def namespace(namespaces = {})
116
+ @declared_namespaces = @declared_namespaces.merge(namespaces.transform_keys(&:to_s)).freeze
117
+ end
118
+
119
+ # Declare a XPath variable's existence in the context
120
+ #
121
+ # @param qname [String, Saxon::QName] The name of the variable as
122
+ # explicit QName or prefix:name string form. The string form requires
123
+ # the namespace prefix to have already been declared with {#namespace}
124
+ # @param type [String, Hash{Symbol => String, Class}, null] The type of the
125
+ # variable, either as a string using the same form as an XSLT
126
+ # <tt>as=""</tt> type definition, or as a hash of one key/value where
127
+ # that key is a Symbol taken from {Saxon::OccurenceIndicator} and the
128
+ # value is either a Class that {Saxon::ItemType} can convert to its
129
+ # XDM equivalent (e.g. {::String}), or a string that {Saxon::ItemType}
130
+ # can parse into an XDM type (e.g. <tt>xs:string</tthat
131
+ # {Saxon::ItemType} can parse into an XDM type (e.g.
132
+ # <tt>xs:string</tt> or <tt>element()</tt>).
133
+ # If it's nil, then the default <tt>item()*</tt> – anything – type declaration is used
134
+ def variable(qname, type = nil)
135
+ qname = resolve_variable_qname(qname)
136
+ @declared_variables = @declared_variables.merge({
137
+ qname => resolve_variable_declaration(qname, type)
138
+ }).freeze
139
+ end
140
+
141
+ private
142
+
143
+ def resolve_variable_qname(qname_or_string)
144
+ Resolver.resolve_variable_qname(qname_or_string, @declared_namespaces)
145
+ end
146
+
147
+ def resolve_variable_type_decl(type_decl)
148
+ case type_decl
149
+ when String
150
+ occurence_char = type_decl[-1]
151
+ occurence = case occurence_char
152
+ when '?'
153
+ {zero_or_one: type_decl[0..-2]}
154
+ when '+'
155
+ {one_or_more: type_decl[0..-2]}
156
+ when '*'
157
+ {zero_or_more: type_decl[0..-2]}
158
+ else
159
+ {one: type_decl}
160
+ end
161
+ when Hash
162
+ type_decl
163
+ end
164
+ end
165
+
166
+ def resolve_variable_declaration(qname, type)
167
+ args_hash = resolve_variable_type_decl(type) || {}
168
+ args_hash[:qname] = qname
169
+ Saxon::XPath::VariableDeclaration.new(args_hash)
170
+ end
171
+ end
172
+
173
+ include Common
174
+
175
+ # Executes the Proc/lambda passed in with a new instance of
176
+ # {StaticContext} as <tt>self</tt>, allowing the DSL methods to be
177
+ # called in a DSL-ish way
178
+ #
179
+ # @param block [Proc] the block of DSL calls to be executed
180
+ # @return [Saxon::XPath::StaticContext] the static context created by the block
181
+ def self.define(block)
182
+ DSL.define(block)
183
+ end
184
+
185
+ attr_reader :declared_variables, :declared_namespaces, :default_collation
186
+
187
+ # @return [Saxon::QName]
188
+ # @overload resolve_variable_qname(qname)
189
+ # returns the QName
190
+ # @param qname_or_string [Saxon::QName] the name as a QName
191
+ # @overload resolve_variable_qname(string)
192
+ # resolve the <tt>prefix:local_name</tt> string into a proper QName by
193
+ # looking up the prefix in the {#declared_namespaces}
194
+ # @param qname_or_string [String] the name as a string
195
+ def resolve_variable_qname(qname_or_string)
196
+ Resolver.resolve_variable_qname(qname_or_string, declared_namespaces)
197
+ end
198
+
199
+ def define(block)
200
+ DSL.define(block, args_hash)
201
+ end
202
+ end
203
+ end
204
+ end