Class: Tilia::VObject::Component

Inherits:
Node
  • Object
show all
Defined in:
lib/tilia/v_object/component.rb,
lib/tilia/v_object/component/v_card.rb,
lib/tilia/v_object/component/v_todo.rb,
lib/tilia/v_object/component/v_alarm.rb,
lib/tilia/v_object/component/v_event.rb,
lib/tilia/v_object/component/available.rb,
lib/tilia/v_object/component/v_journal.rb,
lib/tilia/v_object/component/v_calendar.rb,
lib/tilia/v_object/component/v_free_busy.rb,
lib/tilia/v_object/component/v_time_zone.rb,
lib/tilia/v_object/component/v_availability.rb

Overview

Component.

A component represents a group of properties, such as VCALENDAR, VEVENT, or VCARD.

Defined Under Namespace

Classes: Available, VAlarm, VAvailability, VCalendar, VCard, VEvent, VFreeBusy, VJournal, VTimeZone, VTodo

Constant Summary

Constants inherited from Node

Node::PROFILE_CALDAV, Node::PROFILE_CARDDAV, Node::REPAIR

Instance Attribute Summary collapse

Attributes inherited from Node

#iterator, #parent

Instance Method Summary collapse

Methods inherited from Node

#==, #each, #size

Constructor Details

#initialize(root, name, children = {}, defaults = true) ⇒ void

Creates a new component.

You can specify the children either in key=>value syntax, in which case properties will automatically be created, or you can just pass a list of Component and Property object.

By default, a set of sensible values will be added to the component. For an iCalendar object, this may be something like CALSCALE:GREGORIAN. To ensure that this does not happen, set defaults to false.

Parameters:

  • root (Document)
  • name (String)

    such as VCALENDAR, VEVENT.

  • children (array) (defaults to: {})
  • defaults (Boolean) (defaults to: true)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/tilia/v_object/component.rb', line 48

def initialize(root, name, children = {}, defaults = true)
  @children = {}
  @name = name.to_s.upcase
  @root = root

  # Try to handle some PHP quirks
  if children.is_a?(Array)
    new_children = {}
    children.each_with_index { |c, i| new_children[i] = c }
    children = new_children
  end

  if defaults
    # This is a terribly convoluted way to do this, but this ensures
    # that the order of properties as they are specified in both
    # defaults and the childrens list, are inserted in the object in a
    # natural way.
    list = self.defaults
    nodes = []

    children.each do |key, value|
      if value.is_a?(Node)
        list.delete value.name if list.key?(value.name)
        nodes << value
      else
        list[key] = value
      end
    end

    list.each do |key, value|
      add(key, value)
    end

    nodes.each do |node|
      add(node)
    end
  else
    children.each do |k, child|
      if child.is_a?(Node)
        # Component or Property
        add(child)
      else
        # Property key=>value
        add(k, child)
      end
    end
  end
end

Instance Attribute Details

#nameString

Component name.

This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.

Returns:

  • (String)


25
26
27
# File 'lib/tilia/v_object/component.rb', line 25

def name
  @name
end

Instance Method Details

#[](name) ⇒ Property

Using ‘get’ you will either get a property or component.

If there were no child-elements found with the specified name, null is returned.

To use this, this may look something like this:

event = calendar->VEVENT

Parameters:

  • name (String)

Returns:



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/tilia/v_object/component.rb', line 393

def [](name)
  return super(name) if name.is_a?(Fixnum)

  if name == 'children'
    fail 'Starting sabre/vobject 4.0 the children property is now protected. You should use the children method instead'
  end

  matches = select(name)

  if matches.empty?
    return nil
  else
    first_match = matches.first
    # @return [first_match] Property
    first_match.iterator = ElementList.new(matches.to_a)
    return first_match
  end
end

#[]=(name, value) ⇒ void

This method returns an undefined value.

Using the setter method you can add properties or subcomponents.

You can either pass a Component, Property object, or a string to automatically create a Property.

If the item already exists, it will be removed. If you want to add a new item with the same name, always use the add method.

Parameters:

  • name (String)
  • value


434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/tilia/v_object/component.rb', line 434

def []=(name, value)
  return super(name, value) if name.is_a?(Fixnum)
  name = name.upcase

  remove(name)

  if value.is_a?(Component) || value.is_a?(Property)
    add(value)
  else
    add(name, value)
  end
end

#add(*arguments) ⇒ Node

Adds a new property or component, and returns the new item.

This method has 3 possible signatures:

add(Component comp) // Adds a new component add(Property prop) // Adds a new property add(name, value, array parameters = []) // Adds a new property add(name, array children = []) // Adds a new component by name.

Returns:



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/tilia/v_object/component.rb', line 108

def add(*arguments)
  if arguments[0].is_a?(Node)
    if arguments[1]
      fail ArgumentError, 'The second argument must not be specified, when passing a VObject Node'
    end
    arguments[0].parent = self
    new_node = arguments[0]
  elsif arguments[0].is_a?(String)
    new_node = @root.create(*arguments)
  else
    fail ArgumentError, 'The first argument must either be a Node or a string'
  end

  name = new_node.name
  if @children.key?(name)
    @children[name] << new_node
  else
    @children[name] = [new_node]
  end

  new_node
end

#childrenarray

Returns a flat list of all the properties and components in this component.

Returns:

  • (array)


171
172
173
174
175
176
177
178
# File 'lib/tilia/v_object/component.rb', line 171

def children
  result = []
  @children.each do |_, child_group|
    result.concat(child_group)
  end

  result
end

#componentsarray

This method only returns a list of sub-components. Properties are ignored.

Returns:

  • (array)


184
185
186
187
188
189
190
191
192
193
194
# File 'lib/tilia/v_object/component.rb', line 184

def components
  result = []

  @children.each do |_key, child_group|
    child_group.each do |child|
      result << child if child.is_a?(Component)
    end
  end

  result
end

#delete(name) ⇒ void

This method returns an undefined value.

Removes all properties and components within this component with the specified name.

Parameters:

  • name (String)


453
454
455
456
# File 'lib/tilia/v_object/component.rb', line 453

def delete(name)
  return super(name) if name.is_a?(Fixnum)
  remove(name)
end

#destroyvoid

This method returns an undefined value.

Call this method on a document if you’re done using it.

It’s intended to remove all circular references, so PHP can easily clean it up.



590
591
592
593
594
595
596
597
598
# File 'lib/tilia/v_object/component.rb', line 590

def destroy
  super

  @children.each do |_child_name, child_group|
    child_group.each(&:destroy)
  end

  @children = {}
end

#initialize_copy(_original) ⇒ void

This method returns an undefined value.

This method is automatically called when the object is cloned. Specifically, this will ensure all child elements are also cloned.



462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/tilia/v_object/component.rb', line 462

def initialize_copy(_original)
  new_children = {}
  @children.each do |child_name, child_group|
    new_children[child_name] = []
    child_group.each do |child|
      cloned_child = child.clone
      cloned_child.parent = self
      # cloned_child.root = @root
      new_children[child_name] << cloned_child
    end
  end
  @children = new_children
end

#json_serializearray

This method returns an array, with the representation as it should be encoded in JSON. This is used to create jCard or jCal documents.

Returns:

  • (array)


308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/tilia/v_object/component.rb', line 308

def json_serialize
  components = []
  properties = []

  @children.each do |_, child_group|
    child_group.each do |child|
      if child.is_a?(Component)
        components << child.json_serialize
      else
        properties << child.json_serialize
      end
    end
  end

  [
    @name.downcase,
    properties,
    components
  ]
end

#key?(name) ⇒ Boolean

This method checks if a sub-element with the specified name exists.

Parameters:

  • name (String)

Returns:

  • (Boolean)


417
418
419
420
# File 'lib/tilia/v_object/component.rb', line 417

def key?(name)
  matches = select(name)
  matches.any?
end

#remove(item) ⇒ void

This method returns an undefined value.

This method removes a component or property from this component.

You can either specify the item by name (like DTSTART), in which case all properties/components with that name will be removed, or you can pass an instance of a property or component, in which case only that exact item will be removed.

Parameters:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/tilia/v_object/component.rb', line 140

def remove(item)
  if item.is_a?(String)
    # If there's no dot in the name, it's an exact property name and
    # we can just wipe out all those properties.
    #
    unless item.index('.')
      @children.delete(item.upcase)
      return nil
    end

    # If there was a dot, we need to ask select to help us out and
    # then we just call remove recursively.
    select(item).each do |child|
      remove(child)
    end
  else
    select(item.name).each_with_index do |child, _k|
      if child == item
        @children[item.name].delete(item)
        return nil
      end
    end
  end

  fail ArgumentError, 'The item you passed to remove was not a child of this component'
end

#select(name) ⇒ array

Returns an array with elements that match the specified name.

This function is also aware of MIME-Directory groups (as they appear in vcards). This means that if a property is grouped as “HOME.EMAIL”, it will also be returned when searching for just “EMAIL”. If you want to search for a property in a specific group, you can select on the entire string (“HOME.EMAIL”). If you want to search on a specific property that has not been assigned a group, specify “.EMAIL”.

Parameters:

  • name (String)

Returns:

  • (array)


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/tilia/v_object/component.rb', line 207

def select(name)
  group = nil
  name = name.upcase

  (group, name) = name.split('.', 2) if name.index('.')

  name = nil if name.blank?

  if name
    result = @children.key?(name) ? @children[name] : []

    if group.nil?
      return result
    else
      # If we have a group filter as well, we need to narrow it down
      # more.
      return result.select do |child|
        child.is_a?(Property) && (child.group || '').upcase == group
      end
    end
  end

  # If we got to this point, it means there was no 'name' specified for
  # searching, implying that this is a group-only search.
  result = []
  @children.each do |_, child_group|
    child_group.each do |child|
      if child.is_a?(Property) && (child.group || '').upcase == group
        result << child
      end
    end
  end

  result
end

#serializeString

Turns the object back into a serialized blob.

Returns:

  • (String)


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/tilia/v_object/component.rb', line 246

def serialize
  str = "BEGIN:#{@name}\r\n"

  # Gives a component a 'score' for sorting purposes.
  #
  # This is solely used by the childrenSort method.
  #
  # A higher score means the item will be lower in the list.
  # To avoid score collisions, each "score category" has a reasonable
  # space to accomodate elements. The key is added to the score to
  # preserve the original relative order of elements.
  #
  # @param [Fixnum] key
  # @param [array] array
  #
  # @return [Fixnum]
  sort_score = lambda do |key, array|
    key = array.index(key)
    if array[key].is_a?(Component)
      # We want to encode VTIMEZONE first, this is a personal
      # preference.
      if array[key].name == 'VTIMEZONE'
        score = 300_000_000
        return score + key
      else
        score = 400_000_000
        return score + key
      end
    else
      # Properties get encoded first
      # VCARD version 4.0 wants the VERSION property to appear first
      if array[key].is_a?(Property)
        if array[key].name == 'VERSION'
          score = 100_000_000
          return score + key
        else
          # All other properties
          score = 200_000_000
          return score + key
        end
      end
    end
  end

  tmp = children.sort do |a, b|
    s_a = sort_score.call(a, children)
    s_b = sort_score.call(b, children)
    s_a - s_b
  end

  tmp.each do |child|
    str += child.serialize.encode('utf-8', 'utf-8')
  end
  str += "END:#{@name}\r\n"

  str
end

#to_sObject

TODO: document



601
602
603
# File 'lib/tilia/v_object/component.rb', line 601

def to_s
  serialize
end

#validate(options = 0) ⇒ array

Validates the node for correctness.

The following options are supported:

Node::REPAIR - May attempt to automatically repair the problem.
Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.

This method returns an array with detected problems. Every element has the following properties:

* level - problem level.
* message - A human-readable string describing the issue.
* node - A reference to the problematic node.

The level means:

1 - The issue was repaired (only happens if REPAIR was turned on).
2 - A warning.
3 - An error.

Parameters:

  • options (Fixnum) (defaults to: 0)

Returns:

  • (array)


521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/tilia/v_object/component.rb', line 521

def validate(options = 0)
  rules = validation_rules
  defaults = self.defaults

  property_counters = {}

  messages = []

  children.each do |child|
    name = child.name.upcase

    if !property_counters.key?(name)
      property_counters[name] = 1
    else
      property_counters[name] += 1
    end
    messages.concat child.validate(options)
  end

  rules.each do |prop_name, rule|
    case rule.to_s
    when '0'
      if property_counters.key?(prop_name)
        messages << {
          'level'   => 3,
          'message' => "#{prop_name} MUST NOT appear in a #{@name} component",
          'node'    => self
        }
      end
    when '1'
      if !property_counters.key?(prop_name) || property_counters[prop_name] != 1
        repaired = false
        add(prop_name, defaults[prop_name]) if options & REPAIR > 0 && defaults.key?(prop_name)

        messages << {
          'level'   => repaired ? 1 : 3,
          'message' => "#{prop_name} MUST appear exactly once in a #{@name} component",
          'node'    => self
        }
      end
    when '+'
      if !property_counters.key?(prop_name) || property_counters[prop_name] < 1
        messages << {
          'level'   => 3,
          'message' => "#{prop_name} MUST appear at least once in a #{@name} component",
          'node'    => self
        }
      end
    when '*'
    when '?'
      if property_counters.key?(prop_name) && property_counters[prop_name] > 1
        messages << {
          'level'   => 3,
          'message' => "#{prop_name} MUST NOT appear more than once in a #{@name} component",
          'node'    => self
        }
      end
    end
  end

  messages
end

#validation_rulesarray

A simple list of validation rules.

This is simply a list of properties, and how many times they either must or must not appear.

Possible values per property:

* 0 - Must not appear.
* 1 - Must appear exactly once.
* + - Must appear at least once.
* * - Can appear any number of times.
* ? - May appear, but not more than once.

It is also possible to specify defaults and severity levels for violating the rule.

See the VEVENT implementation for getValidationRules for a more complex example.

Returns:

  • (array)


495
496
497
# File 'lib/tilia/v_object/component.rb', line 495

def validation_rules
  []
end

#xml_serialize(writer) ⇒ void

This method returns an undefined value.

This method serializes the data into XML. This is used to create xCard or xCal documents.

Parameters:

  • writer (Xml\Writer)

    XML writer.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/tilia/v_object/component.rb', line 335

def xml_serialize(writer)
  components = []
  properties = []

  @children.each do |_, child_group|
    child_group.each do |child|
      if child.is_a?(Component)
        components << child
      else
        properties << child
      end
    end
  end

  writer.start_element(@name.downcase)

  if properties.any?
    writer.start_element('properties')
    properties.each do |property|
      property.xml_serialize(writer)
    end
    writer.end_element
  end

  if components.any?
    writer.start_element('components')
    components.each do |component|
      component.xml_serialize(writer)
    end
    writer.end_element
  end

  writer.end_element
end