sdague-icalendar 1.0.2.1

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. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +263 -0
  4. data/Rakefile +110 -0
  5. data/docs/api/STUB +0 -0
  6. data/docs/rfcs/itip_notes.txt +69 -0
  7. data/docs/rfcs/rfc2425.pdf +0 -0
  8. data/docs/rfcs/rfc2426.pdf +0 -0
  9. data/docs/rfcs/rfc2445.pdf +0 -0
  10. data/docs/rfcs/rfc2446.pdf +0 -0
  11. data/docs/rfcs/rfc2447.pdf +0 -0
  12. data/docs/rfcs/rfc3283.txt +738 -0
  13. data/examples/create_cal.rb +45 -0
  14. data/examples/parse_cal.rb +20 -0
  15. data/examples/single_event.ics +18 -0
  16. data/lib/hash_attrs.rb +34 -0
  17. data/lib/icalendar.rb +36 -0
  18. data/lib/icalendar/base.rb +43 -0
  19. data/lib/icalendar/calendar.rb +111 -0
  20. data/lib/icalendar/component.rb +438 -0
  21. data/lib/icalendar/component/alarm.rb +44 -0
  22. data/lib/icalendar/component/event.rb +129 -0
  23. data/lib/icalendar/component/freebusy.rb +37 -0
  24. data/lib/icalendar/component/journal.rb +61 -0
  25. data/lib/icalendar/component/timezone.rb +105 -0
  26. data/lib/icalendar/component/todo.rb +64 -0
  27. data/lib/icalendar/conversions.rb +144 -0
  28. data/lib/icalendar/helpers.rb +109 -0
  29. data/lib/icalendar/parameter.rb +33 -0
  30. data/lib/icalendar/parser.rb +485 -0
  31. data/lib/meta.rb +32 -0
  32. data/test/calendar_test.rb +71 -0
  33. data/test/component/event_test.rb +220 -0
  34. data/test/component/timezone_test.rb +67 -0
  35. data/test/component/todo_test.rb +13 -0
  36. data/test/component_test.rb +66 -0
  37. data/test/conversions_test.rb +97 -0
  38. data/test/coverage/STUB +0 -0
  39. data/test/fixtures/folding.ics +23 -0
  40. data/test/fixtures/life.ics +46 -0
  41. data/test/fixtures/simplecal.ics +119 -0
  42. data/test/fixtures/single_event.ics +23 -0
  43. data/test/interactive.rb +17 -0
  44. data/test/parameter_test.rb +29 -0
  45. data/test/parser_test.rb +84 -0
  46. data/test/read_write.rb +23 -0
  47. metadata +108 -0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ ## Need this so we can require the library from the samples directory
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'rubygems' # Unless you install from the tarball or zip.
6
+ require 'icalendar'
7
+ require 'date'
8
+
9
+ include Icalendar # Probably do this in your class to limit namespace overlap
10
+
11
+ ## Creating calendars and events is easy.
12
+
13
+ # Create a calendar with an event (standard method)
14
+ cal = Calendar.new
15
+ cal.event do
16
+ dtstart Date.new(2005, 04, 29)
17
+ dtend Date.new(2005, 04, 28)
18
+ summary "Meeting with the man."
19
+ description "Have a long lunch meeting and decide nothing..."
20
+ klass "PRIVATE"
21
+ end
22
+
23
+ ## Or you can make events like this
24
+ event = Event.new
25
+ event.start = DateTime.civil(2006, 6, 23, 8, 30)
26
+ event.summary = "A great event!"
27
+ cal.add_event(event)
28
+
29
+ event2 = cal.event # This automatically adds the event to the calendar
30
+ event2.start = DateTime.civil(2006, 6, 24, 8, 30)
31
+ event2.summary = "Another great event!"
32
+
33
+ # Now with support for property parameters
34
+ params = {"ALTREP" =>['"http://my.language.net"'], "LANGUAGE" => ["SPANISH"]}
35
+
36
+ cal.event do
37
+ dtstart Date.new(2005, 04, 29)
38
+ dtend Date.new(2005, 04, 28)
39
+ summary "This is a summary with params.", params
40
+ end
41
+
42
+ # We can output the calendar as a string to write to a file,
43
+ # network port, database etc.
44
+ cal_string = cal.to_ical
45
+ puts cal_string
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ ## Need this so we can require the library from the samples directory
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'icalendar'
6
+ require 'date'
7
+
8
+ # Open a file or string to parse
9
+ cal_file = File.open("../test/life.ics")
10
+
11
+ # Parser returns an array of calendars because a single file
12
+ # can have multiple calendar objects.
13
+ cals = Icalendar::parse(cal_file)
14
+ cal = cals.first
15
+
16
+ # Now you can access the cal object in just the same way I created it
17
+ event = cal.events.first
18
+
19
+ puts "start date-time: " + event.dtstart.to_s
20
+ puts "summary: " + event.summary
@@ -0,0 +1,18 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:bsprodidfortestabc123
4
+ BEGIN:VEVENT
5
+ UID:bsuidfortestabc123
6
+ SUMMARY:This is a really long summary
7
+ to test the method of unfolding lines
8
+ so I'm just going to ma
9
+ ke it
10
+ a whol
11
+ e
12
+ bunch of lines.
13
+ CLASS:PRIVATE
14
+ DTSTART;TZID=US-Mountain:20050120T170000
15
+ DTEND:20050120T184500
16
+ DTSTAMP:20050118T211523Z
17
+ END:VEVENT
18
+ END:VCALENDAR
data/lib/hash_attrs.rb ADDED
@@ -0,0 +1,34 @@
1
+ # A module which adds some generators for hash based accessors.
2
+ module HashAttrs
3
+
4
+ def hash_reader(hash_sym, syms)
5
+ syms.each do |id|
6
+ id = id.to_s.downcase
7
+ func = Proc.new do
8
+ hash = instance_variable_get(hash_sym)
9
+ hash[id.to_sym]
10
+ end
11
+
12
+ self.send(:define_method, id, func)
13
+ end
14
+ end
15
+
16
+ def hash_writer(hash_sym, syms)
17
+ syms.each do |id|
18
+ id = id.to_s.downcase
19
+
20
+ func = Proc.new do |val|
21
+ hash = instance_variable_get(hash_sym)
22
+ hash[id.to_sym] = val
23
+ end
24
+
25
+ self.send(:define_method, id+'=', func)
26
+ end
27
+ end
28
+
29
+ def hash_accessor(hash, *syms)
30
+ hash_reader(hash, syms)
31
+ hash_writer(hash, syms)
32
+ end
33
+ end
34
+
data/lib/icalendar.rb ADDED
@@ -0,0 +1,36 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ $:.unshift(File.dirname(__FILE__))
10
+
11
+ ### Base classes and mixin modules
12
+
13
+ # to_ical methods for built-in classes
14
+ require 'icalendar/conversions'
15
+
16
+ # Meta-programming helper methods
17
+ require 'meta'
18
+
19
+ # Hash attributes mixin module
20
+ require 'hash_attrs'
21
+
22
+ require 'icalendar/base'
23
+ require 'icalendar/component'
24
+
25
+ # Calendar and components
26
+ require 'icalendar/calendar'
27
+ require 'icalendar/component/event'
28
+ require 'icalendar/component/journal'
29
+ require 'icalendar/component/todo'
30
+ require 'icalendar/component/freebusy'
31
+ require 'icalendar/component/timezone'
32
+ require 'icalendar/component/alarm'
33
+
34
+ # Calendar parser
35
+ require 'icalendar/parser'
36
+
@@ -0,0 +1,43 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+ require 'logger'
9
+
10
+ module Icalendar #:nodoc:
11
+
12
+ # A simple error class to differentiate iCalendar library exceptions
13
+ # from ruby language exceptions or others.
14
+ class IcalendarError < StandardError #:nodoc:
15
+ end
16
+
17
+ # Exception used when the library encounters a bogus calendar component.
18
+ class UnknownComponentClass < IcalendarError
19
+ end
20
+
21
+ # Exception used when the library encounters a bogus property type.
22
+ class UnknownPropertyMethod< IcalendarError
23
+ end
24
+
25
+ # Exception used when the library encounters a bogus property value.
26
+ class InvalidPropertyValue < IcalendarError
27
+ end
28
+
29
+ # This class serves as the base class for just about everything in
30
+ # the library so that the logging system can be configured in one place.
31
+ class Base
32
+ @@logger = Logger.new(STDERR)
33
+ @@logger.level = Logger::FATAL
34
+
35
+ def self.debug
36
+ @@logger.level = Logger::DEBUG
37
+ end
38
+
39
+ def self.quiet
40
+ @@logger.level = Logger::FATAL
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,111 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Icalendar
10
+
11
+ class Calendar < Component
12
+ ical_component :events, :todos, :journals, :freebusys, :timezones
13
+
14
+ ical_property :calscale, :calendar_scale
15
+ ical_property :prodid, :product_id
16
+ ical_property :version
17
+ ical_property :ip_method
18
+
19
+ def initialize()
20
+ super("VCALENDAR")
21
+
22
+ # Set some defaults
23
+ self.calscale = "GREGORIAN" # Who knows, but this is the only one in the spec.
24
+ self.prodid = "iCalendar-Ruby" # Current product... Should be overwritten by apps that use the library
25
+ self.version = "2.0" # Version of the specification
26
+ end
27
+
28
+ def event(&block)
29
+ e = Event.new
30
+ # Note: I'm not sure this is the best way to pass this down, but it works
31
+ e.tzid = self.timezones[0].tzid
32
+
33
+ self.add_component e
34
+
35
+ if block
36
+ e.instance_eval &block
37
+ e.dtstart.ical_params = { "TZID" => e.tzid }
38
+ e.dtend.ical_params = { "TZID" => e.tzid }
39
+ end
40
+
41
+ e
42
+ end
43
+
44
+ def find_event(uid)
45
+ self.events.find {|e| e.uid == uid}
46
+ end
47
+
48
+ def todo(&block)
49
+ e = Todo.new
50
+ self.add_component e
51
+
52
+ e.instance_eval &block if block
53
+
54
+ e
55
+ end
56
+
57
+ def find_todo(uid)
58
+ self.todos.find {|t| t.uid == uid}
59
+ end
60
+
61
+ def journal(&block)
62
+ e = Journal.new
63
+ self.add_component e
64
+
65
+ e.instance_eval &block if block
66
+
67
+ e
68
+ end
69
+
70
+ def find_journal(uid)
71
+ self.journals.find {|j| j.uid == uid}
72
+ end
73
+
74
+ def freebusy(&block)
75
+ e = Freebusy.new
76
+ self.add_component e
77
+
78
+ e.instance_eval &block if block
79
+
80
+ e
81
+ end
82
+
83
+ def find_freebusy(uid)
84
+ self.freebusys.find {|f| f.uid == uid}
85
+ end
86
+
87
+ def timezone(&block)
88
+ e = Timezone.new
89
+ self.add_component e
90
+
91
+ e.instance_eval &block if block
92
+
93
+ e
94
+ end
95
+
96
+ # The "PUBLISH" method in a "VEVENT" calendar component is an
97
+ # unsolicited posting of an iCalendar object. Any CU may add published
98
+ # components to their calendar. The "Organizer" MUST be present in a
99
+ # published iCalendar component. "Attendees" MUST NOT be present. Its
100
+ # expected usage is for encapsulating an arbitrary event as an
101
+ # iCalendar object. The "Organizer" may subsequently update (with
102
+ # another "PUBLISH" method), add instances to (with an "ADD" method),
103
+ # or cancel (with a "CANCEL" method) a previously published "VEVENT"
104
+ # calendar component.
105
+ def publish
106
+ self.ip_method = "PUBLISH"
107
+ end
108
+
109
+ end # class Calendar
110
+
111
+ end # module Icalendar
@@ -0,0 +1,438 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Icalendar
10
+ require 'socket'
11
+
12
+ MAX_LINE_LENGTH = 75
13
+
14
+ class Geo < Icalendar::Base
15
+ attr_accessor :latitude, :longitude
16
+ alias :lat :latitude
17
+ alias :long :longitude
18
+
19
+ def initialize(lat, long)
20
+ @lat = lat
21
+ @long = long
22
+ end
23
+
24
+ def to_ical
25
+ "#{@lat.to_ical};#{@long.to_ical}"
26
+ end
27
+ end
28
+
29
+ # The body of the iCalendar object consists of a sequence of calendar
30
+ # properties and one or more calendar components. The calendar
31
+ # properties are attributes that apply to the calendar as a whole. The
32
+ # calendar components are collections of properties that express a
33
+ # particular calendar semantic. For example, the calendar component can
34
+ # specify an Event, a Todo, a Journal entry, Timezone information, or
35
+ # Freebusy time information, or an Alarm.
36
+ class Component < Icalendar::Base
37
+
38
+ meta_include HashAttrs
39
+
40
+ attr_reader :name
41
+ attr_accessor :properties
42
+
43
+ @@multi_properties = {}
44
+ @@multiline_properties = {}
45
+
46
+ def initialize(name)
47
+ @name = name
48
+ @components = Hash.new([])
49
+ @properties = {}
50
+
51
+ @@logger.info("New #{@name[1,@name.size].capitalize}...")
52
+ end
53
+
54
+ # Add a sub-component to the current component object.
55
+ def add_component(component)
56
+ key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
57
+
58
+ unless @components.has_key? key
59
+ @components[key] = []
60
+ end
61
+
62
+ @components[key] << component
63
+ end
64
+
65
+ # Add a component to the calendar.
66
+ alias add add_component
67
+
68
+ # Add an event to the calendar.
69
+ alias add_event add_component
70
+
71
+ # Add a todo item to the calendar.
72
+ alias add_todo add_component
73
+
74
+ # Add a journal item to the calendar.
75
+ alias add_journal add_component
76
+
77
+ def remove_component(component)
78
+ key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
79
+
80
+ if @components.has_key? key
81
+ @components[key].delete(component)
82
+ end
83
+ end
84
+
85
+ # Remove a component from the calendar.
86
+ alias remove remove_component
87
+
88
+ # Remove an event from the calendar.
89
+ alias remove_event remove_component
90
+
91
+ # Remove a todo item from the calendar.
92
+ alias remove_todo remove_component
93
+
94
+ # Remove a journal item from the calendar.
95
+ alias remove_journal remove_component
96
+
97
+ # Used to generate unique component ids
98
+ def new_uid
99
+ "#{DateTime.now}_#{rand(999999999)}@#{Socket.gethostname}"
100
+ end
101
+
102
+ # Output in the icalendar format
103
+ def to_ical
104
+ print_component do
105
+ s = ""
106
+ @components.each_value do |comps|
107
+ comps.each { |component| s << component.to_ical }
108
+ end
109
+ s
110
+ end
111
+ end
112
+
113
+ # Print this icalendar component
114
+ def print_component
115
+ # Begin a new component
116
+ "BEGIN:#{@name.upcase}\r\n" +
117
+
118
+ # Then the properties
119
+ print_properties +
120
+
121
+ # sub components
122
+ yield +
123
+
124
+ # End of this component
125
+ "END:#{@name.upcase}\r\n"
126
+ end
127
+
128
+ # Print this components properties
129
+ def print_properties
130
+ s = ""
131
+
132
+ @properties.each do |key,val|
133
+ # Take out underscore for property names that conflicted
134
+ # with built-in words.
135
+ if key =~ /ip_.*/
136
+ key = key[3..-1]
137
+ end
138
+
139
+ # Property name
140
+ unless multiline_property?(key)
141
+ prelude = "#{key.gsub(/_/, '-').upcase}" +
142
+
143
+ # Possible parameters
144
+ print_parameters(val)
145
+
146
+ # Property value
147
+ value = ":#{val.to_ical}"
148
+ escaped = prelude + value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
149
+ s << escaped.slice!(0, MAX_LINE_LENGTH) << "\r\n " while escaped.size > MAX_LINE_LENGTH
150
+ s << escaped << "\r\n"
151
+ s.gsub!(/ *$/, '')
152
+ else
153
+ prelude = "#{key.gsub(/_/, '-').upcase}"
154
+ val.each do |v|
155
+ params = print_parameters(v)
156
+ value = ":#{v.to_ical}"
157
+ escaped = prelude + params + value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
158
+ s << escaped.slice!(0, MAX_LINE_LENGTH) << "\r\n " while escaped.size > MAX_LINE_LENGTH
159
+ s << escaped << "\r\n"
160
+ s.gsub!(/ *$/, '')
161
+ end
162
+ end
163
+ end
164
+ s
165
+ end
166
+
167
+ # Print the parameters for a specific property
168
+ def print_parameters(val)
169
+ s = ""
170
+ return s unless val.respond_to?(:ical_params) and not val.ical_params.nil?
171
+
172
+ val.ical_params.each do |key,val|
173
+ s << ";#{key}"
174
+ val = [ val ] unless val.is_a?(Array)
175
+
176
+ # Possible parameter values
177
+ unless val.empty?
178
+ s << "="
179
+ sep = "" # First entry comes after = sign, but then we need commas
180
+ val.each do |pval|
181
+ if pval.respond_to? :to_ical
182
+ s << sep << pval.to_ical
183
+ sep = ","
184
+ end
185
+ end
186
+ end
187
+ end
188
+ s
189
+ end
190
+
191
+ # TODO: Look into the x-property, x-param stuff...
192
+ # This would really only be needed for subclassing to add additional
193
+ # properties to an application using the API.
194
+ def custom_property(name, value)
195
+ @properties[name] = value
196
+ end
197
+
198
+ def multi_property?(name)
199
+ @@multi_properties.has_key?(name.downcase)
200
+ end
201
+
202
+ def multiline_property?(name)
203
+ @@multiline_properties.has_key?(name.downcase)
204
+ end
205
+
206
+ # Make it protected so we can monitor usage...
207
+ protected
208
+
209
+ def Component.ical_component(*syms)
210
+ hash_accessor :@components, *syms
211
+ end
212
+
213
+ # Define a set of methods supporting a new property
214
+ def Component.ical_property(property, alias_name = nil, prop_name = nil)
215
+ property = "#{property}".strip.downcase
216
+ alias_name = "#{alias_name}".strip.downcase unless alias_name.nil?
217
+ # If a prop_name was given then we use that for the actual storage
218
+ property = "#{prop_name}".strip.downcase unless prop_name.nil?
219
+
220
+ generate_getter(property, alias_name)
221
+ generate_setter(property, alias_name)
222
+ generate_query(property, alias_name)
223
+ end
224
+
225
+ # Define a set of methods defining a new property, which
226
+ # supports multiple values for the same property name.
227
+ def Component.ical_multi_property(property, singular, plural)
228
+ property = "#{property}".strip.downcase.gsub(/-/, '_')
229
+ plural = "#{plural}".strip.downcase
230
+
231
+ # Set this key so the parser knows to use an array for
232
+ # storing this property type.
233
+ @@multi_properties["#{property}"] = true
234
+
235
+ generate_multi_getter(property, plural)
236
+ generate_multi_setter(property, plural)
237
+ generate_multi_query(property, plural)
238
+ generate_multi_adder(property, singular)
239
+ generate_multi_remover(property, singular)
240
+ end
241
+
242
+ # Define a set of methods defining a new property, which
243
+ # supports multiple values in multiple lines with same property name
244
+ def Component.ical_multiline_property(property, singular, plural)
245
+ @@multiline_properties["#{property}"] = true
246
+ ical_multi_property(property, singular, plural)
247
+ end
248
+
249
+
250
+ private
251
+
252
+ def Component.generate_getter(property, alias_name)
253
+ unless instance_methods.include? property
254
+ code = <<-code
255
+ def #{property}(val = nil, params = nil)
256
+ return @properties["#{property}"] if val.nil?
257
+
258
+ unless val.respond_to?(:to_ical)
259
+ raise(NotImplementedError, "Value of type (" + val.class.to_s + ") does not support to_ical method!")
260
+ end
261
+
262
+ unless params.nil?
263
+ # Extend with the parameter methods only if we have to...
264
+ unless val.respond_to?(:ical_params)
265
+ val.class.class_eval { attr_accessor :ical_params }
266
+ end
267
+ val.ical_params = params
268
+ end
269
+
270
+ @properties["#{property}"] = val
271
+ end
272
+ code
273
+
274
+ class_eval code, "component.rb", 219
275
+ alias_method("#{alias_name}", "#{property}") unless alias_name.nil?
276
+ end
277
+ end
278
+
279
+ def Component.generate_setter(property, alias_name)
280
+ setter = property + '='
281
+ unless instance_methods.include? setter
282
+ code = <<-code
283
+ def #{setter}(val)
284
+ #{property}(val)
285
+ end
286
+ code
287
+
288
+ class_eval code, "component.rb", 233
289
+ alias_method("#{alias_name}=", "#{property+'='}") unless alias_name.nil?
290
+ end
291
+ end
292
+
293
+ def Component.generate_query(property, alias_name)
294
+ query = "#{property}?"
295
+ unless instance_methods.include? query
296
+ code = <<-code
297
+ def #{query}
298
+ @properties.has_key?("#{property.downcase}")
299
+ end
300
+ code
301
+
302
+ class_eval code, "component.rb", 226
303
+
304
+ alias_method("#{alias_name}\?", "#{query}") unless alias_name.nil?
305
+ end
306
+ end
307
+
308
+ def Component.generate_multi_getter(property, plural)
309
+ # Getter for whole array
310
+ unless instance_methods.include? plural
311
+ code = <<-code
312
+ def #{plural}(a = nil)
313
+ if a.nil?
314
+ @properties["#{property}"] || []
315
+ else
316
+ self.#{plural}=(a)
317
+ end
318
+ end
319
+ code
320
+
321
+ class_eval code, "component.rb", 186
322
+ end
323
+ end
324
+
325
+ def Component.generate_multi_setter(property, plural)
326
+ # Setter for whole array
327
+ unless instance_methods.include? plural+'+'
328
+ code = <<-code
329
+ def #{plural}=(a)
330
+ if a.respond_to?(:to_ary)
331
+ a.to_ary.each do |val|
332
+ unless val.respond_to?(:to_ical)
333
+ raise(NotImplementedError, "Property values do not support to_ical method!")
334
+ end
335
+ end
336
+
337
+ @properties["#{property}"] = a.to_ary
338
+ else
339
+ raise ArgumentError, "#{plural} is a multi-property that must be an array! Use the add_[property] method to add single entries."
340
+ end
341
+ end
342
+ code
343
+
344
+ class_eval code, "component.rb", 198
345
+ end
346
+ end
347
+
348
+ def Component.generate_multi_query(property, plural)
349
+ # Query for any of these properties
350
+ unless instance_methods.include? plural+'?'
351
+ code = <<-code
352
+ def #{plural}?
353
+ @properties.has_key?("#{property}")
354
+ end
355
+ code
356
+
357
+ class_eval code, "component.rb", 210
358
+ end
359
+ end
360
+
361
+ def Component.generate_multi_adder(property, singular)
362
+ adder = "add_"+singular.to_s
363
+ # Add another item to this properties array
364
+ unless instance_methods.include? adder
365
+ code = <<-code
366
+ def #{adder}(val, params = {})
367
+ unless val.respond_to?(:to_ical)
368
+ raise(NotImplementedError, "Property value object does not support to_ical method!")
369
+ end
370
+
371
+ unless params.nil?
372
+ # Extend with the parameter methods only if we have to...
373
+ unless val.respond_to?(:ical_params)
374
+ val.class.class_eval { attr_accessor :ical_params }
375
+ end
376
+ val.ical_params = params
377
+ end
378
+
379
+ if @properties.has_key?("#{property}")
380
+ @properties["#{property}"] << val
381
+ else
382
+ @properties["#{property}"] = [val]
383
+ end
384
+ end
385
+ code
386
+
387
+ class_eval code, "component.rb", 289
388
+ alias_method("add_#{property.downcase}", "#{adder}")
389
+ end
390
+ end
391
+
392
+ def Component.generate_multi_remover(property, singular)
393
+ # Remove an item from this properties array
394
+ unless instance_methods.include? "remove_#{singular}"
395
+ code = <<-code
396
+ def remove_#{singular}(a)
397
+ if @properties.has_key?("#{property}")
398
+ @properties["#{property}"].delete(a)
399
+ end
400
+ end
401
+ code
402
+
403
+ class_eval code, "component.rb", 303
404
+ alias_method("remove_#{property.downcase}", "remove_#{singular}")
405
+ end
406
+ end
407
+
408
+ def method_missing(method_name, *args)
409
+ @@logger.debug("Inside method_missing...")
410
+ method_name = method_name.to_s.downcase
411
+
412
+ unless method_name =~ /x_.*/
413
+ raise NoMethodError, "Method Name: #{method_name}"
414
+ end
415
+
416
+ # x-properties are accessed with underscore but stored with a dash so
417
+ # they output correctly and we don't have to special case the
418
+ # output code, which would require checking every property.
419
+ if args.size > 0 # Its a setter
420
+ # Pull off the possible equals
421
+ @properties[method_name[/x_[^=]*/].gsub('x_', 'x-')] = args.first
422
+ else # Or its a getter
423
+ return @properties[method_name.gsub('x_', 'x-')]
424
+ end
425
+ end
426
+
427
+ public
428
+
429
+ def respond_to?(method_name)
430
+ unless method_name.to_s.downcase =~ /x_.*/
431
+ super
432
+ end
433
+
434
+ true
435
+ end
436
+
437
+ end # class Component
438
+ end