libis-tools 0.9.20 → 0.9.21

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -233
  3. data/Rakefile +5 -0
  4. data/lib/libis/tools.rb +1 -0
  5. data/lib/libis/tools/assert.rb +11 -0
  6. data/lib/libis/tools/checksum.rb +22 -5
  7. data/lib/libis/tools/command.rb +24 -3
  8. data/lib/libis/tools/config.rb +61 -33
  9. data/lib/libis/tools/config_file.rb +0 -1
  10. data/lib/libis/tools/deep_struct.rb +10 -2
  11. data/lib/libis/tools/extend/empty.rb +2 -2
  12. data/lib/libis/tools/extend/hash.rb +37 -18
  13. data/lib/libis/tools/extend/kernel.rb +9 -0
  14. data/lib/libis/tools/extend/string.rb +17 -8
  15. data/lib/libis/tools/logger.rb +95 -44
  16. data/lib/libis/tools/metadata.rb +5 -1
  17. data/lib/libis/tools/metadata/dublin_core_record.rb +22 -4
  18. data/lib/libis/tools/metadata/field_format.rb +49 -9
  19. data/lib/libis/tools/metadata/fix_field.rb +5 -0
  20. data/lib/libis/tools/metadata/mapper.rb +2 -1
  21. data/lib/libis/tools/metadata/mappers/flandrica.rb +8 -1
  22. data/lib/libis/tools/metadata/mappers/kuleuven.rb +6 -2
  23. data/lib/libis/tools/metadata/marc21_record.rb +1 -0
  24. data/lib/libis/tools/metadata/marc_record.rb +31 -12
  25. data/lib/libis/tools/metadata/parser/basic_parser.rb +2 -0
  26. data/lib/libis/tools/metadata/parser/dublin_core_parser.rb +2 -1
  27. data/lib/libis/tools/metadata/parser/marc21_parser.rb +2 -1
  28. data/lib/libis/tools/metadata/parser/marc_format_parser.rb +2 -1
  29. data/lib/libis/tools/metadata/parser/marc_rules.rb +2 -1
  30. data/lib/libis/tools/metadata/parser/marc_select_parser.rb +2 -1
  31. data/lib/libis/tools/metadata/parser/patch.rb +1 -0
  32. data/lib/libis/tools/metadata/parser/subfield_criteria_parser.rb +2 -1
  33. data/lib/libis/tools/metadata/sharepoint_mapping.rb +1 -0
  34. data/lib/libis/tools/metadata/sharepoint_record.rb +2 -0
  35. data/lib/libis/tools/metadata/var_field.rb +8 -0
  36. data/lib/libis/tools/mets_dnx.rb +61 -0
  37. data/lib/libis/tools/mets_file.rb +87 -604
  38. data/lib/libis/tools/mets_objects.rb +534 -0
  39. data/lib/libis/tools/parameter.rb +144 -21
  40. data/lib/libis/tools/thread_safe.rb +31 -0
  41. data/lib/libis/tools/version.rb +1 -1
  42. data/lib/libis/tools/xml_document.rb +18 -24
  43. data/libis-tools.gemspec +6 -2
  44. data/spec/config_spec.rb +3 -4
  45. data/spec/logger_spec.rb +13 -30
  46. data/spec/mets_file_spec.rb +17 -17
  47. metadata +53 -7
@@ -1,33 +1,64 @@
1
1
  # encoding: utf-8
2
2
  require 'date'
3
3
  require 'libis/tools/extend/struct'
4
+ require 'concurrent/hash'
4
5
 
5
6
  module Libis
6
7
  module Tools
7
8
 
9
+ # Exception that will be raised when a parameter value does not pass the validation checks.
8
10
  class ParameterValidationError < RuntimeError;
9
11
  end
12
+
13
+ # Exception that will be raised when an attempt is made to change the value of a frozen parameter.
10
14
  class ParameterFrozenError < RuntimeError;
11
15
  end
12
16
 
13
17
  # noinspection RubyConstantNamingConvention
14
- Parameter = ::Struct.new(:name, :default, :datatype, :description, :constraint, :frozen, :options) do
15
18
 
19
+ # A {Parameter} is like a class instance attribute on steroids. Contrary to regular attributes, {Parameter}s are
20
+ # type-safe, can have a descriptive text explaining their use, a constraint that limits the values and any other
21
+ # properties for an application to use for their needs.
22
+ #
23
+ # Parameters are inherited from base classes and can be overwritten without affecting the parameters in the parent
24
+ # class. For instance, a regular parameter in the parent class can be given a fixed value in the child class by
25
+ # giving it a default value and setting it's frozen property to true. The same paremter in the parent class
26
+ # instances will still be modifieable. But the parameter in the child class instances will be frozen, even if
27
+ # accessed via the methods on parent class.
28
+ #
29
+ # Important: the parameter will exist both on the class level as on the instance level, but the parameter on the
30
+ # class level is the parameter definition as described in the {Parameter} class. On the instance level, there are
31
+ # merely some parameter methods that access the parameter instance values with the help of the parameter definitions
32
+ # on the class. The implementation of the parameter instances is dealt with by the {ParameterContainer} module.
33
+ class Parameter < Struct.new(:name, :default, :datatype, :description, :constraint, :frozen, :options)
34
+
35
+ # Create a Parameter instance.
36
+ # @param [Array] args The values for:
37
+ # * name - Required. String for the name of the parameter. Any valid attribute name is acceptable.
38
+ # * default value - Any value. Will be coverted to the given datatype if present. Default is nil.
39
+ # * datatype - String. One of: bool, string, int, float, datetime, array, hash. If omitted it will be derived
40
+ # from the default value or set to the default 'string'.
41
+ # * description - String describing the parameter's use.
42
+ # * constraint - Array, Range, RegEx or single value. Default is nil meaning no constraint.
43
+ # * frozen - Boolean. Default is false; if true the parameter value cannot be changed from the default value.
44
+ # * options - Any Hash. It's up to the applcation to interprete and use this info.
45
+ # datatype can be omitted if the type can be derived from the
16
46
  def initialize(*args)
17
- # noinspection RubySuperCallWithoutSuperclassInspection
18
47
  super(*args)
19
48
  self[:options] ||= {}
20
49
  self[:datatype] ||= guess_datatype
21
50
  end
22
51
 
52
+ # Duplicates the parameter
23
53
  def dup
24
- # noinspection RubySuperCallWithoutSuperclassInspection
25
54
  new_obj = super
26
55
  # noinspection RubyResolve
27
56
  new_obj[:options] = Marshal.load(Marshal.dump(self[:options]))
28
57
  new_obj
29
58
  end
30
59
 
60
+ # Merges other parameter data into the current parameter
61
+ # @param [::Libis::Tools::Parameter] other parameter definition to copy properties from
31
62
  def merge!(other)
32
63
  other.each do |k,v|
33
64
  if k == :options
@@ -39,39 +70,58 @@ module Libis
39
70
  self
40
71
  end
41
72
 
73
+ # Retrieve a specific property of the parameter.
74
+ # If not found in the regular properties, the options Hash is scanned for the property.
75
+ # @param [Symbol] key name of the property
42
76
  def [](key)
43
- # noinspection RubySuperCallWithoutSuperclassInspection
44
77
  return super(key) if members.include?(key)
45
78
  self[:options][key]
46
79
  end
47
80
 
81
+ # Set a property of the parameter.
82
+ # If the property is not one of the regular properties, the property will be set in the options Hash.
83
+ # @param (see #[])
84
+ # @param [Object] value value for the property. No type checking happens on this value
48
85
  def []=(key, value)
49
- # noinspection RubySuperCallWithoutSuperclassInspection
50
86
  return super(key, value) if members.include?(key)
51
87
  self[:options][key] = value
52
88
  end
53
89
 
90
+ # Convience method to create a new {Parameter} from a Hash.
91
+ # @param [Hash] h Hash with parameter definition properties
54
92
  def self.from_hash(h)
55
93
  h.each { |k, v| self[k.to_s.to_sym] = v }
56
94
  end
57
95
 
96
+ # Dumps the parameter properties into a Hash.
97
+ # The options properties are merged into the hash. If you do not want that, use Struct#to_h instead.
98
+ #
99
+ # @return [Hash] parameter definition properties
58
100
  def to_h
59
- # noinspection RubySuperCallWithoutSuperclassInspection
60
101
  super.inject({}) do |hash, key, value|
61
102
  key == :options ? value.each { |k, v| hash[k] = v } : hash[key] = value
62
103
  hash
63
104
  end
64
105
  end
65
106
 
107
+ # Valid input strings for boolean parameter value, all converted to 'true'
66
108
  TRUE_BOOL = %w'true yes t y 1'
109
+ # Valid input strings for boolean parameter value, all converted to 'false'
67
110
  FALSE_BOOL = %w'false no f n 0'
68
111
 
112
+ # Parse any value and try to convert to the correct datatype and check the constraints.
113
+ # Will throw an exception if not valid.
114
+ # @param [Object] value Any value to parse, strings are best supported.
115
+ # @return [Object] checked and converted value
69
116
  def parse(value = nil)
70
117
  result = value.nil? ? self[:default] : convert(value)
71
118
  check_constraint(result)
72
119
  result
73
120
  end
74
121
 
122
+ # Parse any value and try to convert to the correct datatype and check the constraints.
123
+ # Will return false if not valid, true otherwise.
124
+ # @param [Object] value Any value to check
75
125
  def valid_value?(value)
76
126
  begin
77
127
  parse(value)
@@ -80,7 +130,6 @@ module Libis
80
130
  end
81
131
  true
82
132
  end
83
-
84
133
  private
85
134
 
86
135
  def guess_datatype
@@ -105,14 +154,14 @@ module Libis
105
154
  end
106
155
 
107
156
  def convert(v)
108
- case self[:datatype]
157
+ case self[:datatype].to_s.downcase
109
158
  when 'boolean', 'bool'
110
159
  return true if TRUE_BOOL.include?(v.to_s.downcase)
111
160
  return false if FALSE_BOOL.include?(v.to_s.downcase)
112
161
  raise ParameterValidationError, "No boolean information in '#{v.to_s}'. " +
113
162
  "Valid values are: '#{TRUE_BOOL.join('\', \'')}" +
114
163
  "' and '#{FALSE_BOOL.join('\', \'')}'."
115
- when 'string', nil
164
+ when 'string', 'nil'
116
165
  return v.to_s
117
166
  when 'int'
118
167
  return Integer(v)
@@ -162,28 +211,88 @@ module Libis
162
211
 
163
212
  end # Parameter
164
213
 
214
+ # To use the parameters a class should include the ParameterContainer module and add parameter
215
+ # statements to the body of the class definition.
216
+ #
217
+ # Besides enabling the {::Libis::Tools::ParameterContainer::ClassMethods#parameter parameter} class method to
218
+ # define parameters, the module adds the class method
219
+ # {::Libis::Tools::ParameterContainer::ClassMethods#parameter_defs parameter_defs} that will return
220
+ # a Hash with parameter names as keys and their respective parameter definitions as values.
221
+ #
222
+ # On each class instance the {::Libis::Tools::ParameterContainer#parameter parameter} method is added and serves
223
+ # as both getter and setter for parameter values.
224
+ # The methods {::Libis::Tools::ParameterContainer#[] []} and {::Libis::Tools::ParameterContainer#[]= []=} serve as
225
+ # aliases for the getter and setter calls.
226
+ #
227
+ # Additionally two protected methods are available on the instance:
228
+ # * {::Libis::Tools::ParameterContainer#parameters parameters}: returns the Hash that keeps track of the current
229
+ # parameter values for the instance.
230
+ # * {::Libis::Tools::ParameterContainer#get_parameter_definition get_parameter_defintion}: retrieves the parameter
231
+ # definition from the instance's class for the given parameter name.
232
+ #
233
+ # Any class that derives from a class that included the ParameterContainer module will automatically inherit all
234
+ # parameter definitions from all of it's base classes and can override any of these parameter definitions e.g. to
235
+ # change the default values for the parameter.
236
+ #
165
237
  module ParameterContainer
166
238
 
239
+ # Methods created on class level.
167
240
  module ClassMethods
168
241
 
242
+ # Get a list of all parameter definitions.
243
+ # The list is initialized with duplicates of the parameter definitions of the parent class and
244
+ # each new parameter definition updates or appends the list.
245
+ # @return [Hash] with parameter names as keys and {Parameter} instance as value.
169
246
  def parameter_defs
170
247
  return @parameters if @parameters
171
- @parameters = Hash.new
172
- begin
173
- self.superclass.parameter_defs.
174
- each_with_object(@parameters) do |(name, param), hash|
175
- if hash.has_key?(name)
176
- hash[name].merge!(param)
177
- else
178
- hash[name] = param.dup
248
+ @parameters = ::Concurrent::Hash.new
249
+ begin
250
+ self.superclass.parameter_defs.
251
+ each_with_object(@parameters) do |(name, param), hash|
252
+ hash[name] = param.dup
179
253
  end
254
+ rescue NoMethodError
255
+ # ignored
180
256
  end
181
- rescue NoMethodError
182
- # ignored
183
- end
184
- @parameters
257
+ @parameters
185
258
  end
186
259
 
260
+ # DSL method that allows creating parameter definitions on the class level.
261
+ #
262
+ # It takes only one mandatory argument which is a Hash. The first entry is interpreted as '<name>: <default>'.
263
+ # The name for the parameter should be unique and the default value can be any value
264
+ # of type TrueClass, FalseClass, String, Integer, Float, Date, Time, DateTime, Array, Hash or nil.
265
+ #
266
+ # The second up to last Hash entries are optional properties for the parameter. These are:
267
+ # * datatype: the type of values the parameter will accept. Valid values are:
268
+ # * 'bool' or 'boolean'
269
+ # * 'string'
270
+ # * 'int'
271
+ # * 'float'
272
+ # * 'datetime'
273
+ # * 'array'
274
+ # * 'hash'
275
+ # Any other value will raise an Exception when the parameter is used. The value is case-insensitive and
276
+ # if not present, the datatype will be derived from the default value with 'string' being the default for
277
+ # NilClass. In any case the parameter will try its best to convert supplied values to the proper data type.
278
+ # For instance, an Integer parameter will accept 3, 3.1415, '3' and Rational(10/3) as valid values and
279
+ # store them as the integer value 3. Likewise DateTime parameters will try to interprete date and time strings.
280
+ # * description: any descriptive text you want to add to clarify what this parameter is used for.
281
+ # Any tool can ask the class for its parameters and - for instance - can use this property to provide help
282
+ # in a GUI when asking the user for input.
283
+ # * constraint: adds a validation condition to the parameter. The condition value can be:
284
+ # * an array: only values that convert to a value in the list are considered valid.
285
+ # * a range: only values that convert to a value in the given range are considered valid.
286
+ # * a regular expression: only values that match the regular expression are considered valid.
287
+ # * a string: only values that are '==' to the constraint are considered valid.
288
+ # * frozen: if set to true, prevents the class instance to set the parameter to any value other than
289
+ # the default. Mostly useful when a derived class needs a parameter in the parent class to be set to a
290
+ # specific value. Setting a value on a frozen parameter with the 'parameter(name,value)' method throws a
291
+ # {::Libis::Tools::ParameterFrozenError}.
292
+ # * options: a hash with any additional properties that you want to associate to the parameter. Any key-value pair in this
293
+ # hash is added to the retrievable properties of the parameter. Likewise any property defined, that is not in the list of
294
+ # known properties is added to the options hash. In this aspect the ::Libis::Tools::Parameter class behaves much like an
295
+ # OpenStruct even though it is implemented as a Struct.
187
296
  def parameter(options = {})
188
297
  return self.parameter_defs[options] unless options.is_a? Hash
189
298
  return nil if options.keys.empty?
@@ -198,12 +307,22 @@ module Libis
198
307
 
199
308
  end
200
309
 
310
+ # @!visibility private
201
311
  def self.included(base)
202
312
  base.extend(ClassMethods)
203
313
  end
204
314
 
315
+ # Special constant to indicate a parameter has no value set. Nil cannot be used as it is a valid value.
205
316
  NO_VALUE = '##NAV##'
206
317
 
318
+ # Getter/setter for parameter instances
319
+ # With only one argument (the parameter name) it returns the current value for the parameter, but the optional
320
+ # second argument will cause the method to set the parameter value. If the parameter is not available or
321
+ # the given value is not a valid value for the parameter, the method will return the special constant
322
+ # {::Libis::Tools::ParameterContainer::NO_VALUE NO_VALUE}.
323
+ #
324
+ # Setting a value on a frozen parameter with the 'parameter(name,value)' method throws a
325
+ # {::Libis::Tools::ParameterFrozenError} exception.
207
326
  def parameter(name, value = NO_VALUE)
208
327
  param_def = get_parameter_definition(name)
209
328
  return NO_VALUE unless param_def
@@ -219,10 +338,14 @@ module Libis
219
338
  end
220
339
  end
221
340
 
341
+ # Alias for the {#parameter} getter.
222
342
  def [](name)
223
343
  parameter(name)
224
344
  end
225
345
 
346
+ # Alias for the {#parameter} setter.
347
+ # The only difference is that in case of a frozen parameter, this method silently ignores the exception,
348
+ # but the default value still will not be changed.
226
349
  def []=(name, value)
227
350
  parameter name, value
228
351
  rescue ParameterFrozenError
@@ -0,0 +1,31 @@
1
+ require 'monitor'
2
+
3
+ module Libis
4
+ module Tools
5
+
6
+ # Module to safely create a mutex for creating thread safe classes.
7
+ #
8
+ # Usage: include this module in a class or extend a module with this one.
9
+ module ThreadSafe
10
+
11
+ # Access the instance mutex
12
+ def mutex
13
+ self.class.class_mutex.synchronize do
14
+ @mutex ||= Monitor.new
15
+ end
16
+ end
17
+
18
+ # @!visibility private
19
+ module MutexCreator
20
+ attr_accessor :class_mutex
21
+ end
22
+
23
+ # @!visibility private
24
+ def self.included(klass)
25
+ klass.extend(MutexCreator)
26
+ # noinspection RubyResolve
27
+ klass.class_mutex = Monitor.new
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  module Libis
2
2
  module Tools
3
- VERSION = '0.9.20'
3
+ VERSION = '0.9.21'
4
4
  end
5
5
  end
@@ -9,21 +9,19 @@ module Libis
9
9
 
10
10
  # noinspection RubyTooManyMethodsInspection
11
11
 
12
-
13
12
  # This class embodies most used features of Nokogiri, Nori and Gyoku in one convenience class. The Nokogiri document
14
13
  # is stored in the class variable 'document' and can be accessed and manipulated directly - if required.
15
14
  #
16
15
  # In the examples we assume the following XML code:
17
16
  #
18
- # <?xml version="1.0" encoding="utf-8"?>
19
- # <patron>
20
- # <name>Harry Potter</name>
21
- # <barcode library='Hogwarts Library'>1234567890</barcode>
22
- # <access_level>student</access_level>
23
- # <email>harry.potter@hogwarts.edu</email>
24
- # <email>hpotter@JKRowling.com</email>
25
- # </patron>
26
-
17
+ # <?xml version="1.0" encoding="utf-8"?>
18
+ # <patron>
19
+ # <name>Harry Potter</name>
20
+ # <barcode library='Hogwarts Library'>1234567890</barcode>
21
+ # <access_level>student</access_level>
22
+ # <email>harry.potter@hogwarts.edu</email>
23
+ # <email>hpotter@JKRowling.com</email>
24
+ # </patron>
27
25
  class XmlDocument
28
26
 
29
27
  attr_accessor :document
@@ -38,8 +36,9 @@ module Libis
38
36
  !invalid?
39
37
  end
40
38
 
41
- # Create new XmlDocument instance. The object will contain a new and emtpy Nokogiri XML Document. The object will
42
- # not be valid until a root node is added.
39
+ # Create new XmlDocument instance.
40
+ # The object will contain a new and emtpy Nokogiri XML Document.
41
+ # The object will not be valid until a root node is added.
43
42
  # @param [String] encoding character encoding for the XML content; default value is 'utf-8'
44
43
  # @return [XmlDocument] new instance
45
44
  def initialize(encoding = 'utf-8')
@@ -53,7 +52,6 @@ module Libis
53
52
  def self.open(file)
54
53
  doc = XmlDocument.new
55
54
  doc.document = Nokogiri::XML(File.open(file))
56
- # doc.document = Nokogiri::XML(File.open(file), &:noblanks)
57
55
  doc
58
56
  end
59
57
 
@@ -63,7 +61,6 @@ module Libis
63
61
  def self.parse(xml)
64
62
  doc = XmlDocument.new
65
63
  doc.document = Nokogiri::XML.parse(xml)
66
- # doc.document = Nokogiri::XML.parse(xml, &:noblanks)
67
64
  doc
68
65
  end
69
66
 
@@ -83,7 +80,6 @@ module Libis
83
80
  # @param [String] file name of the file to save to
84
81
  # @param [Integer] indent amount of space for indenting; default 2
85
82
  # @param [String] encoding character encoding; default 'utf-8'
86
- # @return [nil]
87
83
  def save(file, indent = 2, encoding = 'utf-8')
88
84
  fd = File.open(file, 'w')
89
85
  @document.write_xml_to(fd, :indent => indent, :encoding => encoding)
@@ -92,7 +88,7 @@ module Libis
92
88
 
93
89
  # Export the XML Document to an XML string.
94
90
  # @param [Hash] options options passed to the underlying Nokogiri::XML::Document#to_xml; default is:
95
- # {indent: 2, encoding: 'utf-8'}
91
+ # !{indent: 2, encoding: 'utf-8'}
96
92
  # @return [String] a string
97
93
  def to_xml(options = {})
98
94
  options = {indent: 2, encoding: 'utf-8', save_with: Nokogiri::XML::Node::SaveOptions::DEFAULT_XML}.merge(options)
@@ -102,7 +98,8 @@ module Libis
102
98
  # Export the XML Document to a Hash.
103
99
  #
104
100
  # @note The hash is generated using the Nori gem. The options passed to this call are used to configure Nori in
105
- # the constructor call. For content and syntax see the {Nori} documentation. Nori also uses an enhanced
101
+ # the constructor call. For content and syntax see the
102
+ # {http://www.rubydoc.info/gems/nori/2.6.0 Nori documentation}. Nori also uses an enhanced
106
103
  # String class with an extra method #attributes that will return a Hash containing tag-value pairs for each
107
104
  # attribute of the XML element.
108
105
  #
@@ -252,6 +249,7 @@ module Libis
252
249
  # <book title="Quidditch Through the Ages" author="Kennilworthy Whisp" due_date="1992-4-23"/>
253
250
  # </books>
254
251
  # </patron>
252
+ #
255
253
  # @param [Code block] block Build instructions
256
254
  # @return [XmlDocument] the new XML Document
257
255
  def self.build(&block)
@@ -333,7 +331,7 @@ module Libis
333
331
  #
334
332
  # @param [Nokogiri::XML::Node] node node to add the attributes to
335
333
  # @param [Hash] attributes a Hash with tag - value pairs for each attribute
336
- # @return [{Nokogiri::XML::Node}] the node
334
+ # @return [Nokogiri::XML::Node] the node
337
335
  def add_attributes(node, attributes)
338
336
  XmlDocument.add_attributes node, attributes
339
337
  end
@@ -373,10 +371,6 @@ module Libis
373
371
  # @param [Hash] namespaces a Hash with prefix - URI pairs for each namespace definition that should be added. The
374
372
  # special key +:node_ns+ is reserved for specifying the prefix for the node itself. To set the default
375
373
  # namespace, use the prefix +nil+
376
- # Example:
377
- # node = xml_doc.create_text_node 'address', 'content'
378
- # xml_doc.add_namespaces node, node_ns: 'abc', abc: 'http://abc.org', xyz: 'http://xyz.org'
379
- # # node => <abc:sample abc="http://abc.org" xyz="http://xyz.org">content</abc:sample>
380
374
  def add_namespaces(node, namespaces)
381
375
  XmlDocument.add_namespaces node, namespaces
382
376
  end
@@ -565,7 +559,7 @@ module Libis
565
559
  node
566
560
  end
567
561
 
568
- # Get the first node matching the tag. The node will be seached with XPath search term = "//#{tag}".
562
+ # Get the first node matching the tag. The node will be seached with XPath search term = "//#!{tag}".
569
563
  #
570
564
  # @param [String] tag XML tag to look for; XPath syntax is allowed
571
565
  # @param [Node] parent
@@ -573,7 +567,7 @@ module Libis
573
567
  get_nodes(tag, parent).first
574
568
  end
575
569
 
576
- # Get all the nodes matching the tag. The node will be seached with XPath search term = "//#{tag}".
570
+ # Get all the nodes matching the tag. The node will be seached with XPath search term = "//#!{tag}".
577
571
  #
578
572
  # @param [String] tag XML tag to look for; XPath syntax is allowed
579
573
  # @param [Node] parent