saxon 0.1.0-java

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