activedocument 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ActiveDocument/active_document.rb +225 -0
- data/lib/ActiveDocument/finder.rb +100 -0
- data/lib/ActiveDocument/inheritable.rb +25 -0
- data/lib/ActiveDocument/mark_logic_http.rb +97 -0
- data/lib/ActiveDocument/mark_logic_query_builder.rb +76 -0
- data/lib/ActiveDocument/search_match.rb +54 -0
- data/lib/ActiveDocument/search_options.rb +59 -0
- data/lib/ActiveDocument/search_result.rb +65 -0
- data/lib/ActiveDocument/search_results.rb +65 -0
- data/lib/active_document.rb +10 -0
- data/lib/activedocument.rb +1 -0
- metadata +84 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
require 'ActiveDocument/mark_logic_http'
|
17
|
+
require 'rubygems'
|
18
|
+
require 'nokogiri'
|
19
|
+
require 'yaml'
|
20
|
+
require 'ActiveDocument/mark_logic_query_builder'
|
21
|
+
require 'ActiveDocument/search_results'
|
22
|
+
require 'ActiveDocument/finder'
|
23
|
+
require "ActiveDocument/inheritable"
|
24
|
+
|
25
|
+
# The ActiveXML module is used as a namespace for all classes relating to the ActiveXML functionality.
|
26
|
+
# ActiveXML::Base is the class that should be extended in order to make use of this functionality in your own
|
27
|
+
# domain objects.
|
28
|
+
#
|
29
|
+
module ActiveDocument
|
30
|
+
|
31
|
+
# Developers should extend this class to create their own domain classes
|
32
|
+
# -------------------
|
33
|
+
# = Usage
|
34
|
+
# -------------------
|
35
|
+
# == Dynamic Finders
|
36
|
+
# ActiveDocument::Base provides extensive methods for finding matching documents based on a variety of criteria.
|
37
|
+
# === Find By Element
|
38
|
+
# Accessed via find_by_ELEMENT method where Element = the name of your element. Executes a search for all documents
|
39
|
+
# with an element ELEMENT that contains the value passed in to the method call.
|
40
|
+
# The signature of this dynamic finder is:
|
41
|
+
# <tt>find_by_ELEMENT(value, root [optional], element_namespace [optional], root_namespace [optional])</tt>
|
42
|
+
#
|
43
|
+
# Parameters details are as follows:
|
44
|
+
# <b>Value:</b> the text to be found within the given element. This is a mandatory parameter
|
45
|
+
# <b>Namespace:</b> The namespace in which the element being searched occurs. This is an optional element. If provided,
|
46
|
+
# it will be used in the search and will override any default values. If no namespace if provided then the code will
|
47
|
+
# attempt to dynamically determine the namespace. First, if the element name is contained in the namespaces hash
|
48
|
+
# then that namespace is used. If the element name is not found then the _default_namespace_
|
49
|
+
# is used. If there is no default namespace, then no namespace is used.
|
50
|
+
# -------------------
|
51
|
+
# == Dynamic Accessors
|
52
|
+
# In addition to the ability to access the underlying XML document (as a Nokogiri XML Document) you have the ability
|
53
|
+
# to access the XML as attributes of your domain object via dynamic attribute accessors (eg. domain_object.element_name) The rules for using accessors
|
54
|
+
# are as follows:
|
55
|
+
# 1. If the element_name is a simple type (i.e. text node with no children <tt><example>text</exmample></tt>)
|
56
|
+
# 1. If there is only one occurence of the element, return its text value
|
57
|
+
# 2. If there are multiple occurences of the element, return a list of each text value
|
58
|
+
# 2. If the element_name is a complex type (e.g. <tt><example><text>hi</text></example></tt>)
|
59
|
+
# 1. If there is only one ocurence of the element then return it as a Nokoguri Element
|
60
|
+
# 2. If there are multiple occurences of the element, return a list of Nokoguri Elements
|
61
|
+
#
|
62
|
+
# More complex dynamic accessors are also supported. They still adhere to the rules above, but instead of just looking
|
63
|
+
# for an element anywhere in the document, you can be more specific. For example, domain_object.chapter.paragraph
|
64
|
+
# will find all paragraph elements that are children of chapter elements.
|
65
|
+
# -------------------
|
66
|
+
class Base < Finder
|
67
|
+
include ClassLevelInheritableAttributes
|
68
|
+
inheritable_attributes_list :namespaces, :default_namespace, :root
|
69
|
+
@namespaces = Hash.new
|
70
|
+
@default_namespace = String.new
|
71
|
+
attr_reader :document, :uri
|
72
|
+
|
73
|
+
|
74
|
+
# create a new instance with an optional xml string to use for constructing the model
|
75
|
+
def initialize(xml_string = nil, uri = nil)
|
76
|
+
unless xml_string.nil?
|
77
|
+
@document = Nokogiri::XML(xml_string) do |config|
|
78
|
+
config.noblanks
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@root = self.class.to_s
|
82
|
+
@uri = uri
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the root element for this object
|
86
|
+
def root
|
87
|
+
@root
|
88
|
+
end
|
89
|
+
|
90
|
+
# Sets the root element for this object
|
91
|
+
def root=(value)
|
92
|
+
@root = value
|
93
|
+
end
|
94
|
+
|
95
|
+
# enables the dynamic finders
|
96
|
+
def method_missing(method_id, *arguments, &block)
|
97
|
+
@@log.debug("ActiveDocument::Base at line #{__LINE__}: method called is #{method_id} with arguments #{arguments}")
|
98
|
+
method = method_id.to_s
|
99
|
+
if method =~ /^(\w*)$/ # methods with no '.' in them and not ending in '='
|
100
|
+
if arguments.length > 0
|
101
|
+
super
|
102
|
+
end
|
103
|
+
access_element $1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def access_element(element)
|
108
|
+
xpath = ""
|
109
|
+
xpath = "//" unless self.instance_of? PartialResult
|
110
|
+
namespace = self.class.namespace_for_element(element)
|
111
|
+
element = "ns:#{element}" unless namespace.nil? || namespace.empty?
|
112
|
+
xpath << element
|
113
|
+
if namespace.nil?
|
114
|
+
nodes = @document.xpath(xpath)
|
115
|
+
else
|
116
|
+
nodes = @document.xpath(xpath, {'ns' => namespace})
|
117
|
+
end
|
118
|
+
evaluate_nodeset(nodes)
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def evaluate_nodeset(result_nodeset)
|
124
|
+
if result_nodeset.length == 1 # found one match
|
125
|
+
if result_nodeset[0].children.length == 1 and result_nodeset[0].children[0].type == Nokogiri::XML::Node::TEXT_NODE
|
126
|
+
result_nodeset[0].text
|
127
|
+
elsif result_nodeset[0].children.length >1 # we are now dealing with complex nodes
|
128
|
+
PartialResult.new(result_nodeset)
|
129
|
+
end
|
130
|
+
elsif result_nodeset.length >1 # multiple matches
|
131
|
+
if result_nodeset.all? {|node| node.children.length == 1} and result_nodeset.all? {|node| node.children[0].type == Nokogiri::XML::Node::TEXT_NODE}
|
132
|
+
# we have multiple simple text nodes
|
133
|
+
result_nodeset.collect {|node| node.text}
|
134
|
+
else
|
135
|
+
# we have multiple complex elements
|
136
|
+
PartialResult.new(result_nodeset)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class PartialResult < self
|
142
|
+
def initialize(nodeset)
|
143
|
+
@document = nodeset
|
144
|
+
@root = nodeset[0].name
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_s
|
148
|
+
@document.to_s
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
class << self
|
155
|
+
attr_reader :namespaces, :default_namespace, :root
|
156
|
+
|
157
|
+
def namespaces(namespace_hash)
|
158
|
+
@namespaces = namespace_hash
|
159
|
+
end
|
160
|
+
|
161
|
+
def add_namespace(element, uri)
|
162
|
+
@namespaces[element.to_s] == uri
|
163
|
+
end
|
164
|
+
|
165
|
+
def remove_namespace(element)
|
166
|
+
@namespaces.delete element
|
167
|
+
end
|
168
|
+
|
169
|
+
def default_namespace(namespace)
|
170
|
+
@default_namespace = namespace # todo should this just be an entry in namespaces?
|
171
|
+
end
|
172
|
+
|
173
|
+
# enables the dynamic finders
|
174
|
+
def method_missing(method_id, *arguments, &block)
|
175
|
+
@@log.debug("ActiveDocument::Base at line #{__LINE__}: method called is #{method_id} with arguments #{arguments}")
|
176
|
+
method = method_id.to_s
|
177
|
+
# identify element search methods
|
178
|
+
if method =~ /find_by_(.*)$/ and arguments.length > 0
|
179
|
+
value = arguments[0]
|
180
|
+
element = $1.to_sym
|
181
|
+
if arguments[1]
|
182
|
+
root = arguments[1]
|
183
|
+
else
|
184
|
+
root = @root
|
185
|
+
end
|
186
|
+
if arguments[2]
|
187
|
+
element_namespace = arguments[2]
|
188
|
+
else
|
189
|
+
element_namespace = namespace_for_element(element)
|
190
|
+
end
|
191
|
+
if arguments[3]
|
192
|
+
root_namespace = arguments[3]
|
193
|
+
else
|
194
|
+
root_namespace = namespace_for_element(root)
|
195
|
+
end
|
196
|
+
execute_finder(element, value, root, element_namespace, root_namespace)
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns an ActiveXML object representing the requested information
|
202
|
+
def load(uri)
|
203
|
+
self.new(@@ml_http.send_xquery(@@xquery_builder.load(uri)), uri)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Finds all documents of this type that contain the word anywhere in their structure
|
207
|
+
def find_by_word(word, root=@root, namespace=@default_namespace)
|
208
|
+
SearchResults.new(@@ml_http.send_xquery(@@xquery_builder.find_by_word(word, root, namespace)))
|
209
|
+
end
|
210
|
+
|
211
|
+
def namespace_for_element(element)
|
212
|
+
namespace = nil
|
213
|
+
if @namespaces[element]
|
214
|
+
namespace = @namespaces[element]
|
215
|
+
else
|
216
|
+
namespace = @default_namespace unless @default_namespace.nil?
|
217
|
+
end
|
218
|
+
namespace
|
219
|
+
end
|
220
|
+
end # end inner class
|
221
|
+
|
222
|
+
end # end class
|
223
|
+
|
224
|
+
|
225
|
+
end # end module
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'ActiveDocument/mark_logic_http'
|
16
|
+
require 'ActiveDocument/mark_logic_query_builder'
|
17
|
+
require 'rubygems'
|
18
|
+
require 'nokogiri'
|
19
|
+
require 'ActiveDocument/search_results'
|
20
|
+
require 'logger'
|
21
|
+
|
22
|
+
module ActiveDocument
|
23
|
+
|
24
|
+
class Finder
|
25
|
+
|
26
|
+
|
27
|
+
@@xquery_builder = ActiveDocument::MarkLogicQueryBuilder.new
|
28
|
+
|
29
|
+
def self.config(yaml_file)
|
30
|
+
config = YAML.load_file(yaml_file)
|
31
|
+
@@ml_http = ActiveDocument::MarkLogicHTTP.new(config['uri'], config['user_name'], config['password'])
|
32
|
+
configure_logger(config)
|
33
|
+
end
|
34
|
+
|
35
|
+
# enables the dynamic finders
|
36
|
+
def self.method_missing(method_id, *arguments, &block)
|
37
|
+
@@log.debug("ActiveDocument::Finder at line #{__LINE__}: method called is #{method_id} with arguments #{arguments}")
|
38
|
+
method = method_id.to_s
|
39
|
+
# identify finder methods
|
40
|
+
if method =~ /find_by_(.*)$/ and arguments.length > 0
|
41
|
+
namespace = arguments[1] if arguments.length == 2
|
42
|
+
execute_finder($1.to_sym, arguments[0], nil, namespace)
|
43
|
+
else
|
44
|
+
puts "missed"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.execute_finder(element, value, root = nil, element_namespace = nil, root_namespace = nil)
|
49
|
+
xquery = @@xquery_builder.find_by_element(element, value, root, element_namespace, root_namespace)
|
50
|
+
@@log.info("Finder.execute_finder at line #{__LINE__}: #{xquery}")
|
51
|
+
SearchResults.new(@@ml_http.send_xquery(xquery))
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.search(search_string, start = 1, page_length = 10, options = nil)
|
55
|
+
if start.nil? : start = 1 end
|
56
|
+
if page_length.nil? : page_length = 10 end
|
57
|
+
search_text = @@xquery_builder.search(search_string, start, page_length, options)
|
58
|
+
SearchResults.new(@@ml_http.send_xquery(search_text)) # todo support options node
|
59
|
+
end
|
60
|
+
private
|
61
|
+
|
62
|
+
def self.configure_logger(config)
|
63
|
+
|
64
|
+
begin
|
65
|
+
log_location = if config['logger']['file']
|
66
|
+
config['logger']['file']
|
67
|
+
else
|
68
|
+
STDERR
|
69
|
+
end
|
70
|
+
log_level = case config['logger']['level']
|
71
|
+
when "debug" then
|
72
|
+
Logger::DEBUG
|
73
|
+
when "info" then
|
74
|
+
Logger::INFO
|
75
|
+
when "warn" then
|
76
|
+
Logger::WARN
|
77
|
+
when "error" then
|
78
|
+
Logger::ERROR
|
79
|
+
when "fatal" then
|
80
|
+
Logger::FATAL
|
81
|
+
else
|
82
|
+
Logger::WARN
|
83
|
+
end
|
84
|
+
|
85
|
+
rotation = if config['logger']['rotation']
|
86
|
+
config['logger']['rotation']
|
87
|
+
else
|
88
|
+
"daily"
|
89
|
+
end
|
90
|
+
file = open(log_location, File::WRONLY | File::APPEND | File::CREAT)
|
91
|
+
@@log = Logger.new(file, rotation)
|
92
|
+
@@log.level = log_level
|
93
|
+
rescue StandardError => oops
|
94
|
+
@@log = Logger.new(STDERR)
|
95
|
+
@@log.level = Logger::WARN
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ClassLevelInheritableAttributes
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def inheritable_attributes_list(*args)
|
8
|
+
@inheritable_attributes_list ||= [:inheritable_attributes_list]
|
9
|
+
@inheritable_attributes_list += args
|
10
|
+
args.each do |arg|
|
11
|
+
class_eval %(
|
12
|
+
class << self; attr_accessor :#{arg} end
|
13
|
+
)
|
14
|
+
end
|
15
|
+
@inheritable_attributes_list
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherited(subclass)
|
19
|
+
@inheritable_attributes_list.each do |inheritable_attribute|
|
20
|
+
instance_var = "@#{inheritable_attribute}"
|
21
|
+
subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'net/http'
|
16
|
+
require 'uri'
|
17
|
+
require 'digest/md5'
|
18
|
+
|
19
|
+
module Net
|
20
|
+
module HTTPHeader
|
21
|
+
@@nonce_count = -1
|
22
|
+
CNONCE = Digest::MD5.new.update("%x" % (Time.now.to_i + rand(65535))).hexdigest
|
23
|
+
|
24
|
+
def digest_auth(user, password, response)
|
25
|
+
# based on http://segment7.net/projects/ruby/snippets/digest_auth.rb
|
26
|
+
@@nonce_count += 1
|
27
|
+
|
28
|
+
response['www-authenticate'] =~ /^(\w+) (.*)/
|
29
|
+
|
30
|
+
params = {}
|
31
|
+
$2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
|
32
|
+
|
33
|
+
a_1 = "#{user}:#{params['realm']}:#{password}"
|
34
|
+
a_2 = "#{@method}:#{@path}"
|
35
|
+
request_digest = ''
|
36
|
+
request_digest << Digest::MD5.new.update(a_1).hexdigest
|
37
|
+
request_digest << ':' << params['nonce']
|
38
|
+
request_digest << ':' << ('%08x' % @@nonce_count)
|
39
|
+
request_digest << ':' << CNONCE
|
40
|
+
request_digest << ':' << params['qop']
|
41
|
+
request_digest << ':' << Digest::MD5.new.update(a_2).hexdigest
|
42
|
+
|
43
|
+
header = []
|
44
|
+
header << "Digest username=\"#{user}\""
|
45
|
+
header << "realm=\"#{params['realm']}\""
|
46
|
+
|
47
|
+
header << "qop=#{params['qop']}"
|
48
|
+
|
49
|
+
header << "algorithm=MD5"
|
50
|
+
header << "uri=\"#{@path}\""
|
51
|
+
header << "nonce=\"#{params['nonce']}\""
|
52
|
+
header << "nc=#{'%08x' % @@nonce_count}"
|
53
|
+
header << "cnonce=\"#{CNONCE}\""
|
54
|
+
header << "response=\"#{Digest::MD5.new.update(request_digest).hexdigest}\""
|
55
|
+
|
56
|
+
@header['Authorization'] = header
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ActiveDocument
|
62
|
+
|
63
|
+
class MarkLogicHTTP
|
64
|
+
|
65
|
+
def initialize(uri, user_name, password)
|
66
|
+
@url = URI.parse(uri)
|
67
|
+
@user_name = user_name
|
68
|
+
@password = password
|
69
|
+
end
|
70
|
+
|
71
|
+
def send_xquery(xquery)
|
72
|
+
if xquery.nil? or xquery.empty? then
|
73
|
+
return nil
|
74
|
+
end
|
75
|
+
req = authenticate()
|
76
|
+
req.set_form_data({'request'=>"#{xquery}"})
|
77
|
+
res = Net::HTTP.new(@url.host, @url.port).start {|http| http.request(req) }
|
78
|
+
case res
|
79
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
80
|
+
# puts res.body
|
81
|
+
res.body
|
82
|
+
else
|
83
|
+
res.error!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def authenticate
|
89
|
+
req = Net::HTTP::Post.new(@url.path)
|
90
|
+
Net::HTTP.start(@url.host, @url.port) do |http|
|
91
|
+
res = http.head(@url.request_uri)
|
92
|
+
req.digest_auth(@user_name, @password, res)
|
93
|
+
end
|
94
|
+
return req
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module ActiveDocument
|
16
|
+
|
17
|
+
class MarkLogicQueryBuilder
|
18
|
+
|
19
|
+
def load(uri)
|
20
|
+
"fn:doc('#{uri}')"
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method does a full text search
|
24
|
+
def find_by_word(word, root, namespace)
|
25
|
+
xquery = <<GENERATED
|
26
|
+
import module namespace search = "http://marklogic.com/appservices/search"at "/MarkLogic/appservices/search/search.xqy";
|
27
|
+
search:search("#{word}",
|
28
|
+
<options xmlns="http://marklogic.com/appservices/search">
|
29
|
+
GENERATED
|
30
|
+
unless root.nil?
|
31
|
+
xquery << "<searchable-expression"
|
32
|
+
|
33
|
+
xquery << " xmlns:a=\"#{namespace}\"" unless namespace.nil? or namespace.empty?
|
34
|
+
xquery << '>/'
|
35
|
+
xquery << "a:" unless namespace.nil? or namespace.empty?
|
36
|
+
xquery << "#{root}</searchable-expression>"
|
37
|
+
end
|
38
|
+
xquery << "</options>)"
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_by_element(element, value, root = nil, element_namespace = nil, root_namespace = nil)
|
42
|
+
xquery = <<GENERATED
|
43
|
+
import module namespace search = "http://marklogic.com/appservices/search"at "/MarkLogic/appservices/search/search.xqy";
|
44
|
+
search:search("word:#{value}",
|
45
|
+
<options xmlns="http://marklogic.com/appservices/search">
|
46
|
+
GENERATED
|
47
|
+
unless root.nil?
|
48
|
+
xquery << "<searchable-expression"
|
49
|
+
xquery << " xmlns:a=\"#{root_namespace}\"" unless root_namespace.nil?
|
50
|
+
xquery << '>/'
|
51
|
+
xquery << "a:" unless root_namespace.nil?
|
52
|
+
xquery << "#{root}</searchable-expression>)"
|
53
|
+
end
|
54
|
+
xquery << <<CONSTRAINT
|
55
|
+
<constraint name="word">
|
56
|
+
<word>
|
57
|
+
<element ns="#{element_namespace unless element_namespace.nil?}" name="#{element}"/>
|
58
|
+
</word>
|
59
|
+
</constraint></options>)
|
60
|
+
CONSTRAINT
|
61
|
+
end
|
62
|
+
|
63
|
+
def search(search_text, start, page_length, options)
|
64
|
+
if options.nil?
|
65
|
+
option = '()'
|
66
|
+
else
|
67
|
+
option = options.to_s
|
68
|
+
end
|
69
|
+
<<-GENERATED
|
70
|
+
import module namespace search = "http://marklogic.com/appservices/search"at "/MarkLogic/appservices/search/search.xqy";
|
71
|
+
search:search("#{search_text}",#{option},#{start}, #{page_length})
|
72
|
+
GENERATED
|
73
|
+
end
|
74
|
+
|
75
|
+
end # end class
|
76
|
+
end # end module
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'nokogiri'
|
17
|
+
|
18
|
+
module ActiveDocument
|
19
|
+
class SearchMatch
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
def initialize(node)
|
23
|
+
@node = node
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
@node.xpath("./@path").to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
value = @node.xpath("./node()").to_s
|
32
|
+
begin
|
33
|
+
value[/<search:highlight>/] = ""
|
34
|
+
value[/<\/search:highlight>/] = ""
|
35
|
+
rescue IndexError
|
36
|
+
end
|
37
|
+
return value
|
38
|
+
end
|
39
|
+
|
40
|
+
def highlighted_match(highlight_tag = nil)
|
41
|
+
value = @node.xpath("./node()").to_s
|
42
|
+
unless highlight_tag.nil?
|
43
|
+
begin
|
44
|
+
value[/<search:highlight>/] = "<#{highlight_tag}>"
|
45
|
+
value[/<\/search:highlight>/] = "</#{highlight_tag}>"
|
46
|
+
rescue IndexError
|
47
|
+
value
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
return value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
# SearchOptions allow you to control exactly how the ActiveDocument::Finder#search method behaves and what additional
|
17
|
+
# information may be returned in the ActiveDocument::SearchResults object
|
18
|
+
# == Attributes
|
19
|
+
# * return_facets - if true then facet information is returned in the resultant ActiveDocument::SearchResults object. Default is true
|
20
|
+
# * value_constraints - this is a #Hash of value constraint names to their options. e.g. search_options_object.value_constraints["Region"] = {"namespace" => "http://wits.nctc.gov", "element" => "Region"}
|
21
|
+
module ActiveDocument
|
22
|
+
class SearchOptions
|
23
|
+
attr_accessor :return_facets, :value_constraints
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@return_facets = true;
|
27
|
+
@value_constraints = Hash.new
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# outputs the object in correctly formatted XML suitable for use in a search
|
32
|
+
def to_s
|
33
|
+
value_constraints = String.new
|
34
|
+
@value_constraints.each do |key, value|
|
35
|
+
value_constraints << <<-XML
|
36
|
+
<constraint name="#{key}">
|
37
|
+
<value>
|
38
|
+
<element ns="#{value["namespace"]}" name="#{value["element"]}"/>
|
39
|
+
</value>
|
40
|
+
</constraint>
|
41
|
+
XML
|
42
|
+
end
|
43
|
+
|
44
|
+
value = <<-XML
|
45
|
+
<options xmlns="http://marklogic.com/appservices/search">
|
46
|
+
<return-facets>#{@return_facets}</return-facets>
|
47
|
+
XML
|
48
|
+
|
49
|
+
# add in constraints
|
50
|
+
unless value_constraints.empty?
|
51
|
+
value << value_constraints
|
52
|
+
end
|
53
|
+
|
54
|
+
# close the options node
|
55
|
+
value << "</options>"
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'nokogiri'
|
17
|
+
require 'ActiveDocument/search_match'
|
18
|
+
|
19
|
+
module ActiveDocument
|
20
|
+
class SearchResult
|
21
|
+
include Enumerable
|
22
|
+
|
23
|
+
def initialize(node)
|
24
|
+
@node = node
|
25
|
+
end
|
26
|
+
|
27
|
+
def index
|
28
|
+
Integer(@node.xpath("./@index").to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def uri
|
32
|
+
@node.xpath("./@uri").to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def path
|
36
|
+
@node.xpath("./@path").to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def score
|
40
|
+
Float(@node.xpath("./@score").to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def confidence
|
44
|
+
Float(@node.xpath("./@confidence").to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
def fitness
|
48
|
+
Integer(@node.xpath("./@fitness").to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
def each(&block)
|
52
|
+
@node.xpath("./search:snippet/search:match").each {|node| yield SearchMatch.new(node)}
|
53
|
+
end
|
54
|
+
|
55
|
+
def root_type
|
56
|
+
full_path = @node.xpath("./search:snippet/search:match")[1].xpath("./@path").to_s
|
57
|
+
root = full_path.match(/:[[:alpha:]]+\//) # find the first :something/ which should indicate the root
|
58
|
+
root.to_s.delete(":/") # remove the : and / to get the root element name
|
59
|
+
end
|
60
|
+
|
61
|
+
def realize(klass)
|
62
|
+
klass.load(uri)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright 2010 Mark Logic, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'nokogiri'
|
17
|
+
require 'ActiveDocument/search_result'
|
18
|
+
|
19
|
+
module ActiveDocument
|
20
|
+
class SearchResults
|
21
|
+
include Enumerable
|
22
|
+
|
23
|
+
def initialize(results)
|
24
|
+
@results_document = Nokogiri::XML(results)
|
25
|
+
end
|
26
|
+
|
27
|
+
def total
|
28
|
+
Integer(@results_document.xpath("/search:response/@total").to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def start
|
32
|
+
Integer(@results_document.xpath("/search:response/@start").to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
def page_length
|
36
|
+
Integer(@results_document.xpath("/search:response/@page-length").to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def search_text
|
40
|
+
@results_document.xpath("/search:response/search:qtext/text()").to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def query_resolution_time
|
44
|
+
@results_document.xpath("/search:response/search:metrics/search:query-resolution-time/text()").to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def snippet_resolution_time
|
48
|
+
@results_document.xpath("/search:response/search:metrics/search:snippet-resolution-time/text()").to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def facet_resolution_time
|
52
|
+
@results_document.xpath("/search:response/search:metrics/search:facet-resolution-time/text()").to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def total_time
|
56
|
+
@results_document.xpath("/search:response/search:metrics/search:total-time/text()").to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def each(&block)
|
60
|
+
@results_document.xpath("/search:response/search:result").each {|node| yield SearchResult.new(node)}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'ActiveDocument/active_document'
|
2
|
+
require 'ActiveDocument/finder'
|
3
|
+
require 'ActiveDocument/inheritable'
|
4
|
+
require 'ActiveDocument/mark_logic_http'
|
5
|
+
require 'ActiveDocument/mark_logic_query_builder'
|
6
|
+
require 'ActiveDocument/search_match'
|
7
|
+
require 'ActiveDocument/search_options'
|
8
|
+
require 'ActiveDocument/search_result'
|
9
|
+
require 'ActiveDocument/search_results'
|
10
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_document'
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activedocument
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
version: "0.1"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Clark D. Richey, Jr.
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-04-02 00:00:00 -04:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: nokogiri
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 4
|
29
|
+
- 1
|
30
|
+
version: 1.4.1
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: Object Mapper for XML Database. Initially setup for connection to MarkLogic
|
34
|
+
email: clark@clarkrichey.com
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- lib/active_document.rb
|
43
|
+
- lib/ActiveDocument/active_document.rb
|
44
|
+
- lib/ActiveDocument/finder.rb
|
45
|
+
- lib/ActiveDocument/inheritable.rb
|
46
|
+
- lib/ActiveDocument/mark_logic_http.rb
|
47
|
+
- lib/ActiveDocument/mark_logic_query_builder.rb
|
48
|
+
- lib/ActiveDocument/search_match.rb
|
49
|
+
- lib/ActiveDocument/search_options.rb
|
50
|
+
- lib/ActiveDocument/search_result.rb
|
51
|
+
- lib/ActiveDocument/search_results.rb
|
52
|
+
- lib/activedocument.rb
|
53
|
+
has_rdoc: true
|
54
|
+
homepage: http://github.com/crichey/ActiveDocument
|
55
|
+
licenses: []
|
56
|
+
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
|
60
|
+
require_paths:
|
61
|
+
- .
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: Object Mapper for XML Database
|
83
|
+
test_files: []
|
84
|
+
|