splunk-sdk-ruby 0.1.0
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.
- data/CHANGELOG.md +160 -0
- data/Gemfile +8 -0
- data/LICENSE +177 -0
- data/README.md +310 -0
- data/Rakefile +40 -0
- data/examples/1_connect.rb +51 -0
- data/examples/2_manage.rb +103 -0
- data/examples/3_blocking_searches.rb +82 -0
- data/examples/4_asynchronous_searches.rb +79 -0
- data/examples/5_stream_data_to_splunk.rb +79 -0
- data/lib/splunk-sdk-ruby.rb +47 -0
- data/lib/splunk-sdk-ruby/ambiguous_entity_reference.rb +28 -0
- data/lib/splunk-sdk-ruby/atomfeed.rb +323 -0
- data/lib/splunk-sdk-ruby/collection.rb +417 -0
- data/lib/splunk-sdk-ruby/collection/apps.rb +35 -0
- data/lib/splunk-sdk-ruby/collection/case_insensitive_collection.rb +58 -0
- data/lib/splunk-sdk-ruby/collection/configuration_file.rb +50 -0
- data/lib/splunk-sdk-ruby/collection/configurations.rb +80 -0
- data/lib/splunk-sdk-ruby/collection/jobs.rb +136 -0
- data/lib/splunk-sdk-ruby/collection/messages.rb +51 -0
- data/lib/splunk-sdk-ruby/context.rb +522 -0
- data/lib/splunk-sdk-ruby/entity.rb +260 -0
- data/lib/splunk-sdk-ruby/entity/index.rb +191 -0
- data/lib/splunk-sdk-ruby/entity/job.rb +339 -0
- data/lib/splunk-sdk-ruby/entity/message.rb +36 -0
- data/lib/splunk-sdk-ruby/entity/saved_search.rb +71 -0
- data/lib/splunk-sdk-ruby/entity/stanza.rb +45 -0
- data/lib/splunk-sdk-ruby/entity_not_ready.rb +26 -0
- data/lib/splunk-sdk-ruby/illegal_operation.rb +27 -0
- data/lib/splunk-sdk-ruby/namespace.rb +239 -0
- data/lib/splunk-sdk-ruby/resultsreader.rb +716 -0
- data/lib/splunk-sdk-ruby/service.rb +339 -0
- data/lib/splunk-sdk-ruby/splunk_http_error.rb +49 -0
- data/lib/splunk-sdk-ruby/synonyms.rb +50 -0
- data/lib/splunk-sdk-ruby/version.rb +27 -0
- data/lib/splunk-sdk-ruby/xml_shim.rb +117 -0
- data/splunk-sdk-ruby.gemspec +27 -0
- data/test/atom_test_data.rb +472 -0
- data/test/data/atom/atom_feed_with_message.xml +19 -0
- data/test/data/atom/atom_with_feed.xml +99 -0
- data/test/data/atom/atom_with_several_entries.xml +101 -0
- data/test/data/atom/atom_with_simple_entries.xml +30 -0
- data/test/data/atom/atom_without_feed.xml +248 -0
- data/test/data/export/4.2.5/export_results.xml +88 -0
- data/test/data/export/4.3.5/export_results.xml +87 -0
- data/test/data/export/5.0.1/export_results.xml +78 -0
- data/test/data/export/5.0.1/nonreporting.xml +232 -0
- data/test/data/results/4.2.5/results-empty.xml +0 -0
- data/test/data/results/4.2.5/results-preview.xml +255 -0
- data/test/data/results/4.2.5/results.xml +336 -0
- data/test/data/results/4.3.5/results-empty.xml +0 -0
- data/test/data/results/4.3.5/results-preview.xml +1057 -0
- data/test/data/results/4.3.5/results.xml +626 -0
- data/test/data/results/5.0.2/results-empty.xml +1 -0
- data/test/data/results/5.0.2/results-empty_preview.xml +1 -0
- data/test/data/results/5.0.2/results-preview.xml +448 -0
- data/test/data/results/5.0.2/results.xml +501 -0
- data/test/export_test_data.json +360 -0
- data/test/resultsreader_test_data.json +1119 -0
- data/test/services.server.info.xml +43 -0
- data/test/services.xml +111 -0
- data/test/test_atomfeed.rb +71 -0
- data/test/test_collection.rb +278 -0
- data/test/test_configuration_file.rb +124 -0
- data/test/test_context.rb +119 -0
- data/test/test_entity.rb +95 -0
- data/test/test_helper.rb +250 -0
- data/test/test_http_error.rb +52 -0
- data/test/test_index.rb +91 -0
- data/test/test_jobs.rb +319 -0
- data/test/test_messages.rb +17 -0
- data/test/test_namespace.rb +188 -0
- data/test/test_restarts.rb +49 -0
- data/test/test_resultsreader.rb +106 -0
- data/test/test_roles.rb +41 -0
- data/test/test_saved_searches.rb +119 -0
- data/test/test_service.rb +65 -0
- data/test/test_users.rb +33 -0
- data/test/test_xml_shim.rb +28 -0
- data/test/testfile.txt +1 -0
- metadata +200 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2011-2013 Splunk, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
5
|
+
# not use this file except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations
|
14
|
+
# under the License.
|
15
|
+
#++
|
16
|
+
|
17
|
+
module Splunk
|
18
|
+
##
|
19
|
+
# Exception thrown when fetching from an entity returns HTTP code 204.
|
20
|
+
#
|
21
|
+
# This primarily comes up with jobs. When a job is not yet ready, fetching
|
22
|
+
# it from the server returns code 204, and we want to handle it specially.
|
23
|
+
#
|
24
|
+
class EntityNotReady < StandardError
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2011-2013 Splunk, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
5
|
+
# not use this file except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations
|
14
|
+
# under the License.
|
15
|
+
#++
|
16
|
+
|
17
|
+
module Splunk
|
18
|
+
##
|
19
|
+
# Exception thrown when a call is known statically to fail.
|
20
|
+
#
|
21
|
+
# +IllegalOperation+ is meant to be thrown when a call can be statically
|
22
|
+
# inferred to fail, such as trying to delete an index on versions of Splunk
|
23
|
+
# before 5.0. It implies that no round trips to the server were made.
|
24
|
+
#
|
25
|
+
class IllegalOperation < StandardError
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2011-2013 Splunk, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
5
|
+
# not use this file except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations
|
14
|
+
# under the License.
|
15
|
+
#++
|
16
|
+
|
17
|
+
##
|
18
|
+
# Ruby representations of Splunk namespaces.
|
19
|
+
#
|
20
|
+
# Splunk's namespaces give access paths to objects. Each application, user,
|
21
|
+
# search job, saved search, or other entity in Splunk has a namespace, and
|
22
|
+
# when you access an entity via the REST API, you include a namespace in your
|
23
|
+
# query. What entities are visible to your query depends on the namespace you
|
24
|
+
# use for the query.
|
25
|
+
#
|
26
|
+
# Some namespaces can contain wildcards or default values filled in by Splunk.
|
27
|
+
# We call such namespaces _wildcard_, since they cannot be the namespace of an
|
28
|
+
# entity, only a query. Namespaces that can be the namespace of an entity are
|
29
|
+
# called _exact_.
|
30
|
+
#
|
31
|
+
# We distinguish six kinds of namespace, each of which is represented by a
|
32
|
+
# separate class:
|
33
|
+
#
|
34
|
+
# * +DefaultNamespace+, used for queries where you want to use
|
35
|
+
# whatever would be default for the user you are logged into Splunk as,
|
36
|
+
# and is the namespace of applications (which themselves determine namespaces,
|
37
|
+
# and so have to have a special one).
|
38
|
+
# * +GlobalNamespace+, which makes an entity visible anywhere in Splunk.
|
39
|
+
# * +SystemNamespace+, which is used for entities like users and roles that
|
40
|
+
# are part of Splunk. Entities in the system namespace are visible anywhere
|
41
|
+
# in Splunk.
|
42
|
+
# * +AppNamespace+, one per application installed in the Splunk instance.
|
43
|
+
# * +AppReferenceNamespace+, which is the namespace that applications themselves
|
44
|
+
# live in. It differs from +DefaultNamespace+ only in that it is a exact
|
45
|
+
# namespace.
|
46
|
+
# * The user namespaces, which are defined by a user _and_ an application.
|
47
|
+
#
|
48
|
+
# In the user and application namespaces, you can use +"-"+ as a wildcard
|
49
|
+
# in place of an actual user or application name.
|
50
|
+
#
|
51
|
+
# These are all represented in the Ruby SDK by correspondingly named classes:
|
52
|
+
# +DefaultNamespace+, +GlobalNamespace+, +SystemNamespace+, +AppNamespace+,
|
53
|
+
# and +UserNamespace+. Each of these have an empty mixin +Namespace+, so an
|
54
|
+
# instance of any of them will respond to +#is_a?(Namespace)+ with +true+.
|
55
|
+
#
|
56
|
+
# Some of these classes are singletons, some aren't, and to avoid confusion or
|
57
|
+
# having to remember which is which, you should create namespaces with the
|
58
|
+
# +namespace+ function.
|
59
|
+
#
|
60
|
+
# What namespace the +eai:acl+ fields in an entity map to is determined by what
|
61
|
+
# the path to that entity should be. In the end, a namespace is a way to
|
62
|
+
# calculate the initial path to access an entity. For example, applications all
|
63
|
+
# have +sharing="app"+ and +app=""+ in their +eai:acl+ fields, but their path
|
64
|
+
# uses the +services/+ prefix, so that particular combination, despite what it
|
65
|
+
# appears to be, is actually an +AppReferenceNamespace+.
|
66
|
+
#
|
67
|
+
|
68
|
+
require 'singleton'
|
69
|
+
|
70
|
+
module Splunk
|
71
|
+
##
|
72
|
+
# Convert a hash of +eai:acl+ fields from Splunk's REST API into a namespace.
|
73
|
+
#
|
74
|
+
# _eai_acl_ should be a hash containing at least the key +"sharing"+, and,
|
75
|
+
# depending on the value associated with +"sharing"+, possibly keys +"app"+
|
76
|
+
# and +"owner"+.
|
77
|
+
#
|
78
|
+
# Returns: a +Namespace+.
|
79
|
+
#
|
80
|
+
def self.eai_acl_to_namespace(eai_acl)
|
81
|
+
namespace(:sharing => eai_acl["sharing"],
|
82
|
+
:app => eai_acl["app"],
|
83
|
+
:owner => eai_acl["owner"])
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Create a +Namespace+.
|
88
|
+
#
|
89
|
+
#
|
90
|
+
# +namespace+ takes a hash of arguments, recognizing the keys +:sharing+,
|
91
|
+
# +:owner+, and +:app+. Among them, +:sharing+ is
|
92
|
+
# required, and depending on its value, the others may be required or not.
|
93
|
+
#
|
94
|
+
# +:sharing+ determines what kind of namespace is produced. It can have the
|
95
|
+
# values +"default"+, +"global"+, +"system"+, +"user"+, or +"app"+.
|
96
|
+
#
|
97
|
+
# If +:sharing+ is +"default"+, +"global"+, or +"system"+, the other two
|
98
|
+
# arguments are ignored. If +:sharing+ is +"app"+, only +:app+ is used,
|
99
|
+
# specifying the application of the namespace. If +:sharing+ is +"user"+,
|
100
|
+
# then both the +:app+ and +:owner+ arguments are used.
|
101
|
+
#
|
102
|
+
# If +:sharing+ is +"app"+ but +:app+ is +""+, it returns an
|
103
|
+
# +AppReferenceNamespace+.
|
104
|
+
#
|
105
|
+
# Returns: a +Namespace+.
|
106
|
+
#
|
107
|
+
def self.namespace(args)
|
108
|
+
sharing = args.fetch(:sharing, "default")
|
109
|
+
owner = args.fetch(:owner, nil)
|
110
|
+
app = args.fetch(:app, nil)
|
111
|
+
|
112
|
+
if sharing == "system"
|
113
|
+
return SystemNamespace.instance
|
114
|
+
elsif sharing == "global"
|
115
|
+
return GlobalNamespace.instance
|
116
|
+
elsif sharing == "user"
|
117
|
+
if owner.nil? or owner == ""
|
118
|
+
raise ArgumentError.new("Must provide an owner for user namespaces.")
|
119
|
+
elsif app.nil? or app == ""
|
120
|
+
raise ArgumentError.new("Must provide an app for user namespaces.")
|
121
|
+
else
|
122
|
+
return UserNamespace.new(owner, app)
|
123
|
+
end
|
124
|
+
elsif sharing == "app"
|
125
|
+
if app.nil?
|
126
|
+
raise ArgumentError.new("Must specify an application for application sharing")
|
127
|
+
elsif args[:app] == ""
|
128
|
+
return AppReferenceNamespace.instance
|
129
|
+
else
|
130
|
+
return AppNamespace.new(args[:app])
|
131
|
+
end
|
132
|
+
elsif sharing == "default"
|
133
|
+
return DefaultNamespace.instance
|
134
|
+
else
|
135
|
+
raise ArgumentError.new("Unknown sharing value: #{sharing}")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# A mixin that fills the role of an abstract base class.
|
141
|
+
#
|
142
|
+
# Namespaces have two methods: +is_exact?+ and +to_path_fragment+, and
|
143
|
+
# can be compared for equality.
|
144
|
+
#
|
145
|
+
module Namespace
|
146
|
+
##
|
147
|
+
# Is this a exact namespace?
|
148
|
+
#
|
149
|
+
# Returns: +true+ or +false+.
|
150
|
+
#
|
151
|
+
def is_exact?() end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Returns the URL prefix corresponding to this namespace.
|
155
|
+
#
|
156
|
+
# The prefix is returned as a list of strings. The strings
|
157
|
+
# are _not_ URL encoded. You need to URL encode them when
|
158
|
+
# you construct your URL.
|
159
|
+
#
|
160
|
+
# Returns: an +Array+ of +Strings+.
|
161
|
+
#
|
162
|
+
def to_path_fragment() end
|
163
|
+
end
|
164
|
+
|
165
|
+
class GlobalNamespace # :nodoc:
|
166
|
+
include Singleton
|
167
|
+
include Namespace
|
168
|
+
def is_exact?() true end
|
169
|
+
def to_path_fragment() ["servicesNS", "nobody", "system"] end
|
170
|
+
end
|
171
|
+
|
172
|
+
class SystemNamespace # :nodoc:
|
173
|
+
include Singleton
|
174
|
+
include Namespace
|
175
|
+
def is_exact?() true end
|
176
|
+
def to_path_fragment() ["servicesNS", "nobody", "system"] end
|
177
|
+
end
|
178
|
+
|
179
|
+
class DefaultNamespace # :nodoc:
|
180
|
+
include Singleton
|
181
|
+
include Namespace
|
182
|
+
# A services/ namespace always uses the current user
|
183
|
+
# and current app, neither of which are wildcards, so this
|
184
|
+
# namespace is guaranteed to be exact.
|
185
|
+
def is_exact?() true end
|
186
|
+
def to_path_fragment() ["services"] end
|
187
|
+
end
|
188
|
+
|
189
|
+
class AppReferenceNamespace # :nodoc:
|
190
|
+
include Singleton
|
191
|
+
include Namespace
|
192
|
+
def is_exact?() true end
|
193
|
+
def to_path_fragment() ["services"] end
|
194
|
+
end
|
195
|
+
|
196
|
+
class AppNamespace # :nodoc:
|
197
|
+
include Namespace
|
198
|
+
attr_reader :app
|
199
|
+
|
200
|
+
def initialize(app)
|
201
|
+
@app = app
|
202
|
+
end
|
203
|
+
|
204
|
+
def ==(other)
|
205
|
+
other.is_a?(AppNamespace) && @app == other.app
|
206
|
+
end
|
207
|
+
|
208
|
+
def is_exact?()
|
209
|
+
@app != "-"
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_path_fragment()
|
213
|
+
["servicesNS", "nobody", @app]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class UserNamespace # :nodoc:
|
218
|
+
include Namespace
|
219
|
+
attr_reader :user, :app
|
220
|
+
|
221
|
+
def initialize(user, app)
|
222
|
+
@user = user
|
223
|
+
@app = app
|
224
|
+
end
|
225
|
+
|
226
|
+
def ==(other)
|
227
|
+
other.is_a?(UserNamespace) && @app == other.app &&
|
228
|
+
@user == other.user
|
229
|
+
end
|
230
|
+
|
231
|
+
def is_exact?()
|
232
|
+
(@app != "-") && (@user != "-")
|
233
|
+
end
|
234
|
+
|
235
|
+
def to_path_fragment()
|
236
|
+
["servicesNS", @user, @app]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,716 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2011-2013 Splunk, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
5
|
+
# not use this file except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations
|
14
|
+
# under the License.
|
15
|
+
#++
|
16
|
+
|
17
|
+
##
|
18
|
+
# +resultsreader.rb+ provides classes to incrementally parse the XML output from
|
19
|
+
# Splunk search jobs. For most search jobs you will want +ResultsReader+, which
|
20
|
+
# handles a single results set. However, the running a blocking export job from
|
21
|
+
# the +search/jobs/export endpoint+ sends back a stream of results sets, all but
|
22
|
+
# the last of which are previews. In this case, you should use the
|
23
|
+
# +MultiResultsReader+, which will let you iterate over the results sets.
|
24
|
+
#
|
25
|
+
# By default, +ResultsReader+ will try to use Nokogiri for XML parsing. If
|
26
|
+
# Nokogiri isn't available, it will fall back to REXML, which ships with Ruby
|
27
|
+
# 1.9. See +xml_shim.rb+ for how to alter this behavior.
|
28
|
+
#
|
29
|
+
|
30
|
+
#--
|
31
|
+
# There are two basic designs we could have used for handling the
|
32
|
+
# search/jobs/export output. We could either have the user call
|
33
|
+
# +ResultsReader#each+ multiple times, each time going through the next results
|
34
|
+
# set, or we could do what we have here and have an outer iterator that yields
|
35
|
+
# distinct +ResultsReader+ objects for each results set.
|
36
|
+
#
|
37
|
+
# The outer iterator is syntactically somewhat clearer, but you must invalidate
|
38
|
+
# the previous +ResultsReader+ objects before yielding a new one so that code
|
39
|
+
# like
|
40
|
+
#
|
41
|
+
# readers = []
|
42
|
+
# outer_iter.each do |reader|
|
43
|
+
# readers << reader
|
44
|
+
# end
|
45
|
+
# readers[2].each do |result|
|
46
|
+
# puts result
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# will throw an error on the second each. The right behavior is to throw an
|
50
|
+
# exception in the +ResultsReader+ each if it is invoked out of order. This
|
51
|
+
# problem doesn't affect the all-in-one design.
|
52
|
+
#
|
53
|
+
# However, in the all-in-one design, it is impossible to set the is_preview and
|
54
|
+
# fields instance variables of the +ResultsReader+ correctly between invocations
|
55
|
+
# of each. This makes code with the all-in-one design such as
|
56
|
+
#
|
57
|
+
# while reader.is_preview
|
58
|
+
# reader.each do |result|
|
59
|
+
# ...
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# If the ... contains a break, then there is no way to set is_preview correctly
|
64
|
+
# before the next iteration of the while loop. This problem does not affect
|
65
|
+
# the outer iterator design, and Fred Ross and Yunxin Wu were not able to come
|
66
|
+
# up with a way to make it work in the all-in-one design, so the SDK uses the
|
67
|
+
# outer iterator design.
|
68
|
+
#++
|
69
|
+
|
70
|
+
require 'stringio'
|
71
|
+
|
72
|
+
require_relative 'xml_shim'
|
73
|
+
require_relative 'collection/jobs' # To access ExportStream
|
74
|
+
|
75
|
+
module Splunk
|
76
|
+
# +ResultsReader+ parses Splunk's XML format for results into Ruby objects.
|
77
|
+
#
|
78
|
+
# You can use both Nokogiri and REXML. By default, the +ResultsReader+ will
|
79
|
+
# try to use Nokogiri, and if it is not available will fall back to REXML. If
|
80
|
+
# you want other behavior, see +xml_shim.rb+ for how to set the XML library.
|
81
|
+
#
|
82
|
+
# +ResultsReader is an +Enumerable+, so it has methods such as +each+ and
|
83
|
+
# +each_with_index+. However, since it's a stream parser, once you iterate
|
84
|
+
# through it once, it will thereafter be empty.
|
85
|
+
#
|
86
|
+
# Do not use +ResultsReader+ with the results of the +create_export+ or
|
87
|
+
# +create_stream+ methods on +Service+ or +Jobs+. These methods use endpoints
|
88
|
+
# which return a different set of data structures. Use +MultiResultsReader+
|
89
|
+
# instead for those cases. If you do use +ResultsReader+, it will return
|
90
|
+
# a concatenation of all non-preview events in the stream, but that behavior
|
91
|
+
# should be considered deprecated, and will result in a warning.
|
92
|
+
#
|
93
|
+
# The ResultsReader object has two additional methods:
|
94
|
+
#
|
95
|
+
# * +is_preview?+ returns a Boolean value that indicates whether these
|
96
|
+
# results are a preview from an unfinished search or not
|
97
|
+
# * +fields+ returns an array of all the fields that may appear in a result
|
98
|
+
# in this set, in the order they should be displayed (if you're going
|
99
|
+
# to make a table or the like)
|
100
|
+
#
|
101
|
+
# *Example*:
|
102
|
+
#
|
103
|
+
# require 'splunk-sdk-ruby'
|
104
|
+
#
|
105
|
+
# service = Splunk::connect(:username => "admin", :password => "changeme")
|
106
|
+
#
|
107
|
+
# stream = service.jobs.create_oneshot("search index=_internal | head 10")
|
108
|
+
# reader = ResultsReader.new(stream)
|
109
|
+
# puts reader.is_preview?
|
110
|
+
# # Prints: false
|
111
|
+
# reader.each do |result|
|
112
|
+
# puts result
|
113
|
+
# end
|
114
|
+
# # Prints a sequence of Hashes containing events.
|
115
|
+
#
|
116
|
+
class ResultsReader
|
117
|
+
include Enumerable
|
118
|
+
|
119
|
+
##
|
120
|
+
# Are the results in this reader a preview from an unfinished search?
|
121
|
+
#
|
122
|
+
# Returns: +true+ or +false+, or +nil+ if the stream is empty.
|
123
|
+
#
|
124
|
+
def is_preview?
|
125
|
+
return @is_preview
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# An +Array+ of all the fields that may appear in each result.
|
130
|
+
#
|
131
|
+
# Note that any given result will contain a subset of these fields.
|
132
|
+
#
|
133
|
+
# Returns: an +Array+ of +Strings+.
|
134
|
+
#
|
135
|
+
attr_reader :fields
|
136
|
+
|
137
|
+
def initialize(text_or_stream)
|
138
|
+
if text_or_stream.nil?
|
139
|
+
stream = StringIO.new("")
|
140
|
+
elsif stream.is_a?(ExportStream)
|
141
|
+
# The sensible behavior on streams from the export endpoints is to
|
142
|
+
# skip all preview results and concatenate all others. The export
|
143
|
+
# functions wrap their streams in ExportStream to mark that they need
|
144
|
+
# this special handling.
|
145
|
+
@is_export = true
|
146
|
+
@reader = MultiResultsReader.new(text_or_stream).final_results()
|
147
|
+
@is_preview = @reader.is_preview?
|
148
|
+
@fields = @reader.fields
|
149
|
+
return
|
150
|
+
elsif !text_or_stream.respond_to?(:read)
|
151
|
+
# Strip because the XML libraries can be pissy.
|
152
|
+
stream = StringIO.new(text_or_stream.strip)
|
153
|
+
else
|
154
|
+
stream = text_or_stream
|
155
|
+
end
|
156
|
+
|
157
|
+
if stream.eof?
|
158
|
+
@is_preview = nil
|
159
|
+
@fields = []
|
160
|
+
elsif stream.is_a?(ExportStream)
|
161
|
+
|
162
|
+
else
|
163
|
+
# We use a SAX parser. +listener+ is the event handler, but a SAX
|
164
|
+
# parser won't usually transfer control during parsing.
|
165
|
+
# To incrementally return results as we parse, we have to put
|
166
|
+
# the parser into a +Fiber+ from which we can yield.
|
167
|
+
listener = ResultsListener.new()
|
168
|
+
@iteration_fiber = Fiber.new do
|
169
|
+
if $splunk_xml_library == :nokogiri
|
170
|
+
parser = Nokogiri::XML::SAX::Parser.new(listener)
|
171
|
+
parser.parse(stream)
|
172
|
+
else # Use REXML
|
173
|
+
REXML::Document.parse_stream(stream, listener)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
@is_preview = @iteration_fiber.resume
|
178
|
+
@fields = @iteration_fiber.resume
|
179
|
+
@reached_end = false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def each()
|
184
|
+
# If we have been passed a stream from an export endpoint, it should be
|
185
|
+
# marked as such, and we handle it differently.
|
186
|
+
if @is_export
|
187
|
+
warn "[DEPRECATED] Do not use ResultsReader on the output of the " +
|
188
|
+
"export endpoint. Use MultiResultsReader instead."
|
189
|
+
reader = MultiResultsReader.new(@stream).final_results()
|
190
|
+
enum = reader.each()
|
191
|
+
else
|
192
|
+
enum = Enumerator.new() do |yielder|
|
193
|
+
if !@iteration_fiber.nil? # Handle the case of empty files
|
194
|
+
@reached_end = false
|
195
|
+
while true
|
196
|
+
result = @iteration_fiber.resume
|
197
|
+
break if result.nil? or result == :end_of_results_set
|
198
|
+
yielder << result
|
199
|
+
end
|
200
|
+
end
|
201
|
+
@reached_end = true
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
if block_given? # Apply the enumerator to a block if we have one
|
206
|
+
enum.each() { |e| yield e }
|
207
|
+
else
|
208
|
+
enum # Otherwise return the enumerator itself
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
##
|
213
|
+
# Skips the rest of the events in this ResultsReader.
|
214
|
+
#
|
215
|
+
def skip_remaining_results()
|
216
|
+
if !@reached_end
|
217
|
+
each() { |result|}
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# +ResultsListener+ is the SAX event handler for +ResultsReader+.
|
224
|
+
#
|
225
|
+
# The authors of Nokogiri decided to make their SAX interface
|
226
|
+
# slightly incompatible with that of REXML. For example, REXML
|
227
|
+
# uses tag_start and passes attributes as a dictionary, while
|
228
|
+
# Nokogiri calls the same thing start_element, and passes
|
229
|
+
# attributes as an association list.
|
230
|
+
#
|
231
|
+
# This is a classic finite state machine parser. The `@states` variable
|
232
|
+
# contains a hash with the states as its values. Each hash contains
|
233
|
+
# functions giving the behavior of the state machine in that state.
|
234
|
+
# The actual methods on the function dispatch to these functions
|
235
|
+
# based upon the current state (as stored in `@state`).
|
236
|
+
#
|
237
|
+
# The parser initially runs until it has determined if the results are
|
238
|
+
# a preview, then calls +Fiber.yield+ to return it. Then it continues and
|
239
|
+
# tries to yield a field order, and then any results. (It will always yield
|
240
|
+
# a field order, even if it is empty). At the end of a results set, it yields
|
241
|
+
# +:end_of_results_set+.
|
242
|
+
#
|
243
|
+
class ResultsListener # :nodoc:
|
244
|
+
def initialize()
|
245
|
+
# @fields holds the accumulated list of fields from the fieldOrder
|
246
|
+
# element. If there has been no accumulation, it is set to
|
247
|
+
# :no_fieldOrder_found. For empty results sets, there is often no
|
248
|
+
# fieldOrder element, but we still want to yield an empty Array at the
|
249
|
+
# right point, so if we reach the end of a results element and @fields
|
250
|
+
# is still :no_fieldOrder_found, we yield an empty array at that point.
|
251
|
+
@fields = :no_fieldOrder_found
|
252
|
+
@concatenate = false
|
253
|
+
@is_preview = nil
|
254
|
+
@state = :base
|
255
|
+
@states = {
|
256
|
+
# Toplevel state.
|
257
|
+
:base => {
|
258
|
+
:start_element => lambda do |name, attributes|
|
259
|
+
if name == "results"
|
260
|
+
if !@concatenate
|
261
|
+
@is_preview = attributes["preview"] == "1"
|
262
|
+
Fiber.yield(@is_preview)
|
263
|
+
end
|
264
|
+
elsif name == "fieldOrder"
|
265
|
+
if !@concatenate
|
266
|
+
@state = :field_order
|
267
|
+
@fields = []
|
268
|
+
end
|
269
|
+
elsif name == "result"
|
270
|
+
@state = :result
|
271
|
+
@current_offset = Integer(attributes["offset"])
|
272
|
+
@current_result = {}
|
273
|
+
end
|
274
|
+
end,
|
275
|
+
:end_element => lambda do |name|
|
276
|
+
if name == "results" and !@concatenate
|
277
|
+
Fiber.yield([]) if @fields == :no_fieldOrder_found
|
278
|
+
|
279
|
+
if !@is_preview # Start concatenating events
|
280
|
+
@concatenate = true
|
281
|
+
else
|
282
|
+
# Reset the fieldOrder
|
283
|
+
@fields = :no_fieldOrder_found
|
284
|
+
Fiber.yield(:end_of_results_set)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
},
|
289
|
+
# Inside a `fieldOrder` element. Recognizes only
|
290
|
+
# the `field` element, and returns to the `:base` state
|
291
|
+
# when it encounters `</fieldOrder>`.
|
292
|
+
:field_order => {
|
293
|
+
:start_element => lambda do |name, attributes|
|
294
|
+
if name == "field"
|
295
|
+
@state = :field_order_field
|
296
|
+
end
|
297
|
+
end,
|
298
|
+
:end_element => lambda do |name|
|
299
|
+
if name == "fieldOrder"
|
300
|
+
@state = :base
|
301
|
+
Fiber.yield(@fields)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
},
|
305
|
+
# When the parser in `:field_order` state encounters
|
306
|
+
# a `field` element, it jumps to this state to record it.
|
307
|
+
# When `</field>` is encountered, jumps back to `:field_order`.
|
308
|
+
:field_order_field => {
|
309
|
+
:characters => lambda do |text|
|
310
|
+
@fields << text.strip
|
311
|
+
end,
|
312
|
+
:end_element => lambda do |name|
|
313
|
+
if name == "field"
|
314
|
+
@state = :field_order
|
315
|
+
end
|
316
|
+
end
|
317
|
+
},
|
318
|
+
# When the parser has hit the `result` element, it jumps here.
|
319
|
+
# When this state hits `</result>`, it calls `Fiber.yield` to
|
320
|
+
# send the completed result back, and, when the fiber is
|
321
|
+
# resumed, jumps back to the `:base` state.
|
322
|
+
:result => {
|
323
|
+
:start_element => lambda do |name, attributes|
|
324
|
+
if name == "field"
|
325
|
+
@current_field = attributes["k"]
|
326
|
+
@current_value = nil
|
327
|
+
elsif name == "text" || name == "v"
|
328
|
+
@state = :field_values
|
329
|
+
@current_scratch = ""
|
330
|
+
end
|
331
|
+
end,
|
332
|
+
:end_element => lambda do |name|
|
333
|
+
if name == "result"
|
334
|
+
Fiber.yield @current_result
|
335
|
+
@current_result = nil
|
336
|
+
@current_offset = nil
|
337
|
+
@state = :base
|
338
|
+
elsif name == "field"
|
339
|
+
if @current_result.has_key?(@current_field)
|
340
|
+
if @current_result[@current_field].is_a?(Array)
|
341
|
+
@current_result[@current_field] << @current_value
|
342
|
+
elsif @current_result[@current_field] != nil
|
343
|
+
@current_result[@current_field] =
|
344
|
+
[@current_result[@current_field], @current_value]
|
345
|
+
end
|
346
|
+
else
|
347
|
+
@current_result[@current_field] = @current_value
|
348
|
+
end
|
349
|
+
@current_field = nil
|
350
|
+
@current_value = nil
|
351
|
+
end
|
352
|
+
end
|
353
|
+
},
|
354
|
+
# Parse the values inside a results field.
|
355
|
+
:field_values => {
|
356
|
+
:end_element => lambda do |name|
|
357
|
+
if name == "text" || name == "v"
|
358
|
+
if @current_value == nil
|
359
|
+
@current_value = @current_scratch
|
360
|
+
elsif @current_value.is_a?(Array)
|
361
|
+
@current_value << @current_scratch
|
362
|
+
else
|
363
|
+
@current_value = [@current_value, @current_scratch]
|
364
|
+
end
|
365
|
+
|
366
|
+
@current_scratch = nil
|
367
|
+
@state = :result
|
368
|
+
elsif name == "sg"
|
369
|
+
# <sg> is emitted to delimit text that should be displayed
|
370
|
+
# highlighted. We preserve it in field values.
|
371
|
+
@current_scratch << "</sg>"
|
372
|
+
end
|
373
|
+
end,
|
374
|
+
:start_element => lambda do |name, attributes|
|
375
|
+
if name == "sg"
|
376
|
+
s = ["sg"] + attributes.sort.map do |entry|
|
377
|
+
key, value = entry
|
378
|
+
"#{key}=\"#{value}\""
|
379
|
+
end
|
380
|
+
text = "<" + s.join(" ") + ">"
|
381
|
+
@current_scratch << text
|
382
|
+
end
|
383
|
+
end,
|
384
|
+
:characters => lambda do |text|
|
385
|
+
@current_scratch << text
|
386
|
+
end
|
387
|
+
}
|
388
|
+
}
|
389
|
+
end
|
390
|
+
|
391
|
+
# Nokogiri methods - all dispatch to the REXML methods.
|
392
|
+
def start_element(name, attributes)
|
393
|
+
# attributes is an association list. Turn it into a hash
|
394
|
+
# that tag_start can use.
|
395
|
+
attribute_dict = {}
|
396
|
+
attributes.each do |attribute|
|
397
|
+
key = attribute.localname
|
398
|
+
value = attribute.value
|
399
|
+
attribute_dict[key] = value
|
400
|
+
end
|
401
|
+
|
402
|
+
tag_start(name, attribute_dict)
|
403
|
+
end
|
404
|
+
|
405
|
+
def start_element_namespace(name, attributes=[], prefix=nil, uri=nil, ns=[])
|
406
|
+
start_element(name, attributes)
|
407
|
+
end
|
408
|
+
|
409
|
+
def end_element(name)
|
410
|
+
tag_end(name)
|
411
|
+
end
|
412
|
+
|
413
|
+
def end_element_namespace(name, prefix = nil, uri = nil)
|
414
|
+
end_element(name)
|
415
|
+
end
|
416
|
+
|
417
|
+
def characters(text)
|
418
|
+
text(text)
|
419
|
+
end
|
420
|
+
|
421
|
+
# REXML methods - all dispatch is done here
|
422
|
+
def tag_start(name, attributes)
|
423
|
+
# attributes is a hash.
|
424
|
+
if @states[@state].has_key?(:start_element)
|
425
|
+
@states[@state][:start_element].call(name, attributes)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
def tag_end(name)
|
430
|
+
if @states[@state].has_key?(:end_element)
|
431
|
+
@states[@state][:end_element].call(name)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def text(text)
|
436
|
+
if @states[@state].has_key?(:characters)
|
437
|
+
@states[@state][:characters].call(text)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Unused methods in Nokogiri
|
442
|
+
def cdata_block(string) end
|
443
|
+
def comment(string) end
|
444
|
+
def end_document() end
|
445
|
+
def error(string) end
|
446
|
+
def start_document() end
|
447
|
+
def warning(string) end
|
448
|
+
# xmldecl declared in REXML list below.
|
449
|
+
|
450
|
+
# Unused methods in REXML
|
451
|
+
def attlistdecl(element_name, attributes, raw_content) end
|
452
|
+
def cdata(content) end
|
453
|
+
def comment(comment) end
|
454
|
+
def doctype(name, pub_sys, long_name, uri) end
|
455
|
+
def doctype_end() end
|
456
|
+
def elementdecl(content) end
|
457
|
+
def entity(content) end
|
458
|
+
def entitydecl(content) end
|
459
|
+
def instruction(name, instruction) end
|
460
|
+
def notationdecl(content) end
|
461
|
+
def xmldecl(version, encoding, standalone) end
|
462
|
+
end
|
463
|
+
|
464
|
+
##
|
465
|
+
# Version of +ResultsReader+ that accepts an external parsing state.
|
466
|
+
#
|
467
|
+
# +ResultsReader+ sets up its own Fiber for doing SAX parsing of the XML,
|
468
|
+
# but for the +MultiResultsReader+, we want to share a single fiber among
|
469
|
+
# all the results readers that we create. +PuppetResultsReader+ takes
|
470
|
+
# the fiber, is_preview, and fields information from its constructor
|
471
|
+
# and then exposes the same methods as ResultsReader.
|
472
|
+
#
|
473
|
+
# You should never create an instance of +PuppetResultsReader+ by hand. It
|
474
|
+
# will be passed back from iterating over a +MultiResultsReader+.
|
475
|
+
#
|
476
|
+
class PuppetResultsReader < ResultsReader
|
477
|
+
def initialize(fiber, is_preview, fields)
|
478
|
+
@valid = true
|
479
|
+
@iteration_fiber = fiber
|
480
|
+
@is_preview = is_preview
|
481
|
+
@fields = fields
|
482
|
+
end
|
483
|
+
|
484
|
+
def each()
|
485
|
+
if !@valid
|
486
|
+
raise StandardError.new("Cannot iterate on ResultsReaders out of order.")
|
487
|
+
else
|
488
|
+
super()
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def invalidate()
|
493
|
+
@valid = false
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
##
|
498
|
+
# Parser for the XML results sets returned by blocking export jobs.
|
499
|
+
#
|
500
|
+
# The methods +create_export+ and +create_stream+ on +Jobs+ and +Service+
|
501
|
+
# do not return data in quite the same format as other search jobs in Splunk.
|
502
|
+
# They will return a sequence of preview results sets, and then (if they are
|
503
|
+
# not real time searches) a final results set.
|
504
|
+
#
|
505
|
+
# +MultiResultsReader+ takes the stream returned by such a call, and provides
|
506
|
+
# iteration over each results set, or access to only the final, non-preview
|
507
|
+
# results set.
|
508
|
+
#
|
509
|
+
#
|
510
|
+
# *Examples*:
|
511
|
+
# require 'splunk-sdk-ruby'
|
512
|
+
#
|
513
|
+
# service = Splunk::connect(:username => "admin", :password => "changeme")
|
514
|
+
#
|
515
|
+
# stream = service.jobs.create_export("search index=_internal | head 10")
|
516
|
+
#
|
517
|
+
# readers = MultiResultsReader.new(stream)
|
518
|
+
# readers.each do |reader|
|
519
|
+
# puts "New result set (preview=#{reader.is_preview?})"
|
520
|
+
# reader.each do |result|
|
521
|
+
# puts result
|
522
|
+
# end
|
523
|
+
# end
|
524
|
+
#
|
525
|
+
# # Alternately
|
526
|
+
# reader = readers.final_results()
|
527
|
+
# reader.each do |result|
|
528
|
+
# puts result
|
529
|
+
# end
|
530
|
+
#
|
531
|
+
class MultiResultsReader
|
532
|
+
include Enumerable
|
533
|
+
|
534
|
+
def initialize(text_or_stream)
|
535
|
+
if text_or_stream.nil?
|
536
|
+
stream = StringIO.new("")
|
537
|
+
elsif !text_or_stream.respond_to?(:read)
|
538
|
+
# Strip because the XML libraries can be pissy.
|
539
|
+
stream = StringIO.new(text_or_stream.strip)
|
540
|
+
else
|
541
|
+
stream = text_or_stream
|
542
|
+
end
|
543
|
+
|
544
|
+
listener = ResultsListener.new()
|
545
|
+
@iteration_fiber = Fiber.new do
|
546
|
+
if $splunk_xml_library == :nokogiri
|
547
|
+
parser = Nokogiri::XML::SAX::Parser.new(listener)
|
548
|
+
# Nokogiri requires a unique root element, which we are fabricating
|
549
|
+
# here, while REXML is fine with multiple root elements in a stream.
|
550
|
+
edited_stream = ConcatenatedStream.new(
|
551
|
+
StringIO.new("<fake-root-element>"),
|
552
|
+
XMLDTDFilter.new(stream),
|
553
|
+
StringIO.new("</fake-root-element>")
|
554
|
+
)
|
555
|
+
parser.parse(edited_stream)
|
556
|
+
else # Use REXML
|
557
|
+
REXML::Document.parse_stream(stream, listener)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def each()
|
563
|
+
enum = Enumerator.new() do |yielder|
|
564
|
+
if !@iteration_fiber.nil? # Handle the case of empty files
|
565
|
+
begin
|
566
|
+
while true
|
567
|
+
is_preview = @iteration_fiber.resume
|
568
|
+
fields = @iteration_fiber.resume
|
569
|
+
reader = PuppetResultsReader.new(@iteration_fiber, is_preview, fields)
|
570
|
+
yielder << reader
|
571
|
+
# Finish extracting any events that the user didn't read.
|
572
|
+
# Otherwise the next results reader will start in the middle of
|
573
|
+
# the previous results set.
|
574
|
+
reader.skip_remaining_results()
|
575
|
+
reader.invalidate()
|
576
|
+
end
|
577
|
+
rescue FiberError
|
578
|
+
# After the last result element, the next evaluation of
|
579
|
+
# 'is_preview = @iteration_fiber.resume' above will throw a
|
580
|
+
# +FiberError+ when the fiber terminates without yielding any
|
581
|
+
# additional values. We handle the control flow in this way so
|
582
|
+
# that the final code in the fiber to handle cleanup always gets
|
583
|
+
# run.
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
if block_given? # Apply the enumerator to a block if we have one
|
589
|
+
enum.each() { |e| yield e }
|
590
|
+
else
|
591
|
+
enum # Otherwise return the enumerator itself
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
##
|
596
|
+
# Returns a +ResultsReader+ over only the non-preview results.
|
597
|
+
#
|
598
|
+
# If you run this method against a real time search job, which only ever
|
599
|
+
# produces preview results, it will loop forever. If you run it against
|
600
|
+
# a non-reporting system (that is, one that filters and extracts fields
|
601
|
+
# from events, but doesn't calculate a whole new set of events), you will
|
602
|
+
# get only the first few results, since you should be using the normal
|
603
|
+
# +ResultsReader+, not +MultiResultsReader+, in that case.
|
604
|
+
#
|
605
|
+
def final_results()
|
606
|
+
each do |reader|
|
607
|
+
if reader.is_preview?
|
608
|
+
reader.skip_remaining_results()
|
609
|
+
else
|
610
|
+
return reader
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
|
617
|
+
##
|
618
|
+
# Stream transformer that filters out XML DTD definitions.
|
619
|
+
#
|
620
|
+
# +XMLDTDFilter+ takes anything between <? and > to be a DTD. It does no
|
621
|
+
# escaping of quoted text.
|
622
|
+
#
|
623
|
+
class XMLDTDFilter < IO
|
624
|
+
def initialize(stream)
|
625
|
+
@stream = stream
|
626
|
+
@peeked_char = nil
|
627
|
+
end
|
628
|
+
|
629
|
+
def close()
|
630
|
+
@stream.close()
|
631
|
+
end
|
632
|
+
|
633
|
+
def read(n=nil)
|
634
|
+
response = ""
|
635
|
+
|
636
|
+
while n.nil? or n > 0
|
637
|
+
# First use any element we already peeked at.
|
638
|
+
if !@peeked_char.nil?
|
639
|
+
response << @peeked_char
|
640
|
+
@peeked_char = nil
|
641
|
+
if !n.nil?
|
642
|
+
n -= 1
|
643
|
+
end
|
644
|
+
next
|
645
|
+
end
|
646
|
+
|
647
|
+
c = @stream.read(1)
|
648
|
+
if c.nil? # We've reached the end of the stream
|
649
|
+
break
|
650
|
+
elsif c == "<" # We might have a DTD definition
|
651
|
+
d = @stream.read(1) || ""
|
652
|
+
if d == "?" # It's a DTD. Skip until we've consumed a >.
|
653
|
+
while true
|
654
|
+
q = @stream.read(1)
|
655
|
+
if q == ">"
|
656
|
+
break
|
657
|
+
end
|
658
|
+
end
|
659
|
+
else # It's not a DTD. Push that ? into lookahead.
|
660
|
+
@peeked_char = d
|
661
|
+
response << c
|
662
|
+
if !n.nil?
|
663
|
+
n = n-1
|
664
|
+
end
|
665
|
+
end
|
666
|
+
else # No special behavior
|
667
|
+
response << c
|
668
|
+
if !n.nil?
|
669
|
+
n -= 1
|
670
|
+
end
|
671
|
+
end
|
672
|
+
end
|
673
|
+
return response
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
##
|
678
|
+
# Returns a stream which concatenates all the streams passed to it.
|
679
|
+
#
|
680
|
+
class ConcatenatedStream < IO
|
681
|
+
def initialize(*streams)
|
682
|
+
@streams = streams
|
683
|
+
end
|
684
|
+
|
685
|
+
def close()
|
686
|
+
@streams.each do |stream|
|
687
|
+
stream.close()
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
def read(n=nil)
|
692
|
+
response = ""
|
693
|
+
while n.nil? or n > 0
|
694
|
+
if @streams.empty? # No streams left
|
695
|
+
break
|
696
|
+
else # We have streams left.
|
697
|
+
chunk = @streams[0].read(n) || ""
|
698
|
+
found_n = chunk.length()
|
699
|
+
if n.nil? or chunk.length() < n
|
700
|
+
@streams.shift()
|
701
|
+
end
|
702
|
+
if !n.nil?
|
703
|
+
n -= chunk.length()
|
704
|
+
end
|
705
|
+
|
706
|
+
response << chunk
|
707
|
+
end
|
708
|
+
end
|
709
|
+
if response == ""
|
710
|
+
return nil
|
711
|
+
else
|
712
|
+
return response
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|