summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx/rels/relationship.rb
blob: 62860f5051a1121f48f0797e2640d6a3ed084353 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

module Axlsx
  # A relationship defines a reference between package parts.
  # @note Packages automatically manage relationships.
  class Relationship
    class << self
      # Keeps track of relationship ids in use.
      # @return [Array]
      def ids_cache
        Thread.current[:axlsx_relationship_ids_cache] ||= {}
      end

      # Initialize cached ids.
      #
      # This should be called before serializing a package (see {Package#serialize} and
      # {Package#to_stream}) to make sure that serialization is idempotent (i.e.
      # Relationship instances are generated with the same IDs everytime the package
      # is serialized).
      def initialize_ids_cache
        Thread.current[:axlsx_relationship_ids_cache] = {}
      end

      # Clear cached ids.
      #
      # This should be called after serializing a package (see {Package#serialize} and
      # {Package#to_stream}) to free the memory allocated for cache.
      #
      # Also, calling this avoids memory leaks (cached ids lingering around
      # forever).
      def clear_ids_cache
        Thread.current[:axlsx_relationship_ids_cache] = nil
      end

      # Generate and return a unique id (eg. `rId123`) Used for setting {#Id}.
      #
      # The generated id depends on the number of previously cached ids, so using
      # {clear_ids_cache} will automatically reset the generated ids, too.
      # @return [String]
      def next_free_id
        "rId#{ids_cache.size + 1}"
      end
    end

    # The id of the relationship (eg. "rId123"). Most instances get their own unique id.
    # However, some instances need to share the same id – see {#ids_cache_key}
    # for details.
    # @return [String]
    attr_reader :Id

    # The location of the relationship target
    # @return [String]
    attr_reader :Target

    # The type of relationship
    # @note Supported types are defined as constants in Axlsx:
    # @see XML_NS_R
    # @see TABLE_R
    # @see PIVOT_TABLE_R
    # @see WORKBOOK_R
    # @see WORKSHEET_R
    # @see APP_R
    # @see RELS_R
    # @see CORE_R
    # @see STYLES_R
    # @see CHART_R
    # @see DRAWING_R
    # @return [String]
    attr_reader :Type

    # The target mode of the relationship
    # used for hyperlink type relationships to mark the relationship to an external resource
    # TargetMode can be specified during initialization by passing in a :target_mode option
    # Target mode must be :external for now.
    attr_reader :TargetMode

    # The source object the relations belongs to (e.g. a hyperlink, drawing, ...). Needed when
    # looking up the relationship for a specific object (see {Relationships#for}).
    attr_reader :source_obj

    # Initializes a new relationship.
    # @param [Object] source_obj see {#source_obj}
    # @param [String] type The type of the relationship
    # @param [String] target The target for the relationship
    # @option [Symbol] :target_mode only accepts :external.
    def initialize(source_obj, type, target, options = {})
      @source_obj = source_obj
      self.Target = target
      self.Type = type
      self.TargetMode = options[:target_mode] if options[:target_mode]
      @Id = (self.class.ids_cache[ids_cache_key] ||= self.class.next_free_id)
    end

    # @see Target
    def Target=(v) Axlsx.validate_string v; @Target = v end
    # @see Type
    def Type=(v) Axlsx.validate_relationship_type v; @Type = v end

    # @see TargetMode
    def TargetMode=(v) RestrictionValidator.validate 'Relationship.TargetMode', [:External, :Internal], v; @TargetMode = v; end

    # serialize relationship
    # @param [String] str
    # @return [String]
    def to_xml_string(str = +'')
      h = Axlsx.instance_values_for(self).reject { |k, _| k == "source_obj" }
      str << '<Relationship '
      h.each_with_index do |key_value, index|
        str << ' ' unless index.zero?
        str << key_value.first.to_s << '="' << Axlsx.coder.encode(key_value.last.to_s) << '"'
      end
      str << '/>'
    end

    # A key that determines whether this relationship should use already generated id.
    #
    # Instances designating the same relationship need to use the same id. We can not simply
    # compare the {#Target} attribute, though: `foo/bar.xml`, `../foo/bar.xml`,
    # `../../foo/bar.xml` etc. are all different but probably mean the same file (this
    # is especially an issue for relationships in the context of pivot tables). So lets
    # just ignore this attribute for now (except when {#TargetMode} is set to `:External` –
    # then {#Target} will be an absolute URL and thus can safely be compared).
    #
    # @todo Implement comparison of {#Target} based on normalized path names.
    # @return [Array]
    def ids_cache_key
      key = [source_obj, self.Type, self.TargetMode]
      key << self.Target if self.TargetMode == :External
      key
    end
  end
end