class Tilia::VObject::Component

Component.

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

Attributes

name[RW]

Component name.

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

@return [String]

Public Class Methods

new(root, name, children = {}, defaults = true) click to toggle source

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.

@param [Document] root @param [String] name such as VCALENDAR, VEVENT. @param [array] children @param [Boolean] defaults

@return [void]

# 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

Public Instance Methods

[](name) click to toggle source

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

@param [String] name

@return [Property]

Calls superclass method
# 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) click to toggle source

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.

@param [String] name @param value

@return [void]

Calls superclass method
# 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) click to toggle source

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.

@return [Node]

# 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
children() click to toggle source

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

@return [array]

# File lib/tilia/v_object/component.rb, line 171
def children
  result = []
  @children.each do |_, child_group|
    result.concat(child_group)
  end

  result
end
components() click to toggle source

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

@return [array]

# 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) click to toggle source

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

@param [String] name

@return [void]

Calls superclass method
# File lib/tilia/v_object/component.rb, line 453
def delete(name)
  return super(name) if name.is_a?(Fixnum)
  remove(name)
end
destroy() click to toggle source

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.

@return [void]

Calls superclass method
# 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) click to toggle source

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

@return [void]

# 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_serialize() click to toggle source

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

@return [array]

# 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) click to toggle source

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

@param [String] name

@return [Boolean]

# File lib/tilia/v_object/component.rb, line 417
def key?(name)
  matches = select(name)
  matches.any?
end
remove(item) click to toggle source

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.

@param [String|Property|Component] item @return [void]

# 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) click to toggle source

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”.

@param [String] name @return [array]

# 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
serialize() click to toggle source

Turns the object back into a serialized blob.

@return [String]

# 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_s() click to toggle source

TODO: document

# File lib/tilia/v_object/component.rb, line 601
def to_s
  serialize
end
validate(options = 0) click to toggle source

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.

@param [Fixnum] options

@return [array]

# 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_rules() click to toggle source

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.

@return [array]

# File lib/tilia/v_object/component.rb, line 495
def validation_rules
  []
end
xml_serialize(writer) click to toggle source

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

@param [XmlWriter] writer XML writer.

@return [void]

# 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

Protected Instance Methods

defaults() click to toggle source

This method should return a list of default property values.

@return [array]

# File lib/tilia/v_object/component.rb, line 375
def defaults
  []
end