curzonj-icalendar 1.0.2

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 (44) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +266 -0
  4. data/Rakefile +110 -0
  5. data/docs/rfcs/itip_notes.txt +69 -0
  6. data/docs/rfcs/rfc2425.pdf +0 -0
  7. data/docs/rfcs/rfc2426.pdf +0 -0
  8. data/docs/rfcs/rfc2445.pdf +0 -0
  9. data/docs/rfcs/rfc2446.pdf +0 -0
  10. data/docs/rfcs/rfc2447.pdf +0 -0
  11. data/docs/rfcs/rfc3283.txt +738 -0
  12. data/examples/create_cal.rb +45 -0
  13. data/examples/parse_cal.rb +20 -0
  14. data/examples/single_event.ics +18 -0
  15. data/lib/hash_attrs.rb +34 -0
  16. data/lib/icalendar.rb +39 -0
  17. data/lib/icalendar/base.rb +43 -0
  18. data/lib/icalendar/calendar.rb +113 -0
  19. data/lib/icalendar/component.rb +442 -0
  20. data/lib/icalendar/component/alarm.rb +44 -0
  21. data/lib/icalendar/component/event.rb +129 -0
  22. data/lib/icalendar/component/freebusy.rb +38 -0
  23. data/lib/icalendar/component/journal.rb +61 -0
  24. data/lib/icalendar/component/timezone.rb +105 -0
  25. data/lib/icalendar/component/todo.rb +64 -0
  26. data/lib/icalendar/conversions.rb +150 -0
  27. data/lib/icalendar/helpers.rb +109 -0
  28. data/lib/icalendar/parameter.rb +33 -0
  29. data/lib/icalendar/parser.rb +396 -0
  30. data/lib/meta.rb +32 -0
  31. data/test/calendar_test.rb +71 -0
  32. data/test/component/event_test.rb +256 -0
  33. data/test/component/todo_test.rb +13 -0
  34. data/test/component_test.rb +76 -0
  35. data/test/conversions_test.rb +97 -0
  36. data/test/fixtures/folding.ics +23 -0
  37. data/test/fixtures/life.ics +46 -0
  38. data/test/fixtures/simplecal.ics +119 -0
  39. data/test/fixtures/single_event.ics +23 -0
  40. data/test/interactive.rb +17 -0
  41. data/test/parameter_test.rb +29 -0
  42. data/test/parser_test.rb +84 -0
  43. data/test/read_write.rb +23 -0
  44. metadata +105 -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,39 @@
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
+ require 'icalendar/rrule'
25
+
26
+ # Calendar and components
27
+ require 'icalendar/calendar'
28
+ require 'icalendar/component/event'
29
+ require 'icalendar/component/journal'
30
+ require 'icalendar/component/todo'
31
+ require 'icalendar/component/freebusy'
32
+ require 'icalendar/component/timezone'
33
+ require 'icalendar/component/alarm'
34
+
35
+ # Calendar parser
36
+ require 'icalendar/parser'
37
+
38
+ # TZINFO support
39
+ # require 'icalendar/tzinfo'
@@ -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,113 @@
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 if self.timezones.length > 0
32
+
33
+ self.add_component e
34
+
35
+ if block
36
+ e.instance_eval &block
37
+ if e.tzid
38
+ e.dtstart.ical_params = { "TZID" => e.tzid }
39
+ e.dtend.ical_params = { "TZID" => e.tzid }
40
+ end
41
+ end
42
+
43
+ e
44
+ end
45
+
46
+ def find_event(uid)
47
+ self.events.find {|e| e.uid == uid}
48
+ end
49
+
50
+ def todo(&block)
51
+ e = Todo.new
52
+ self.add_component e
53
+
54
+ e.instance_eval &block if block
55
+
56
+ e
57
+ end
58
+
59
+ def find_todo(uid)
60
+ self.todos.find {|t| t.uid == uid}
61
+ end
62
+
63
+ def journal(&block)
64
+ e = Journal.new
65
+ self.add_component e
66
+
67
+ e.instance_eval &block if block
68
+
69
+ e
70
+ end
71
+
72
+ def find_journal(uid)
73
+ self.journals.find {|j| j.uid == uid}
74
+ end
75
+
76
+ def freebusy(&block)
77
+ e = Freebusy.new
78
+ self.add_component e
79
+
80
+ e.instance_eval &block if block
81
+
82
+ e
83
+ end
84
+
85
+ def find_freebusy(uid)
86
+ self.freebusys.find {|f| f.uid == uid}
87
+ end
88
+
89
+ def timezone(&block)
90
+ e = Timezone.new
91
+ self.add_component e
92
+
93
+ e.instance_eval &block if block
94
+
95
+ e
96
+ end
97
+
98
+ # The "PUBLISH" method in a "VEVENT" calendar component is an
99
+ # unsolicited posting of an iCalendar object. Any CU may add published
100
+ # components to their calendar. The "Organizer" MUST be present in a
101
+ # published iCalendar component. "Attendees" MUST NOT be present. Its
102
+ # expected usage is for encapsulating an arbitrary event as an
103
+ # iCalendar object. The "Organizer" may subsequently update (with
104
+ # another "PUBLISH" method), add instances to (with an "ADD" method),
105
+ # or cancel (with a "CANCEL" method) a previously published "VEVENT"
106
+ # calendar component.
107
+ def publish
108
+ self.ip_method = "PUBLISH"
109
+ end
110
+
111
+ end # class Calendar
112
+
113
+ end # module Icalendar
@@ -0,0 +1,442 @@
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
+ add_sliced_text(s,prelude+escape_chars(value))
149
+ else
150
+ prelude = "#{key.gsub(/_/, '-').upcase}"
151
+ val.each do |v|
152
+ params = print_parameters(v)
153
+ value = ":#{v.to_ical}"
154
+ add_sliced_text(s,prelude + params + escape_chars(value))
155
+ end
156
+ end
157
+ end
158
+ s
159
+ end
160
+
161
+ def escape_chars(value)
162
+ value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
163
+ end
164
+
165
+ def add_sliced_text(add_to,escaped)
166
+ escaped = escaped.split('') # split is unicdoe-aware when `$KCODE = 'u'`
167
+ add_to << escaped.slice!(0,MAX_LINE_LENGTH).to_s << "\r\n " while escaped.length != 0 # shift(MAX_LINE_LENGTH) does not work with ruby 1.8.6
168
+ add_to.gsub!(/ *$/, '')
169
+ end
170
+
171
+ # Print the parameters for a specific property
172
+ def print_parameters(val)
173
+ s = ""
174
+ return s unless val.respond_to?(:ical_params) and not val.ical_params.nil?
175
+
176
+ val.ical_params.each do |key,val|
177
+ s << ";#{key}"
178
+ val = [ val ] unless val.is_a?(Array)
179
+
180
+ # Possible parameter values
181
+ unless val.empty?
182
+ s << "="
183
+ sep = "" # First entry comes after = sign, but then we need commas
184
+ val.each do |pval|
185
+ if pval.respond_to? :to_ical
186
+ s << sep << pval.to_ical
187
+ sep = ","
188
+ end
189
+ end
190
+ end
191
+ end
192
+ s
193
+ end
194
+
195
+ # TODO: Look into the x-property, x-param stuff...
196
+ # This would really only be needed for subclassing to add additional
197
+ # properties to an application using the API.
198
+ def custom_property(name, value)
199
+ @properties[name] = value
200
+ end
201
+
202
+ def multi_property?(name)
203
+ @@multi_properties.has_key?(name.downcase)
204
+ end
205
+
206
+ def multiline_property?(name)
207
+ @@multiline_properties.has_key?(name.downcase)
208
+ end
209
+
210
+ # Make it protected so we can monitor usage...
211
+ protected
212
+
213
+ def Component.ical_component(*syms)
214
+ hash_accessor :@components, *syms
215
+ end
216
+
217
+ # Define a set of methods supporting a new property
218
+ def Component.ical_property(property, alias_name = nil, prop_name = nil)
219
+ property = "#{property}".strip.downcase
220
+ alias_name = "#{alias_name}".strip.downcase unless alias_name.nil?
221
+ # If a prop_name was given then we use that for the actual storage
222
+ property = "#{prop_name}".strip.downcase unless prop_name.nil?
223
+
224
+ generate_getter(property, alias_name)
225
+ generate_setter(property, alias_name)
226
+ generate_query(property, alias_name)
227
+ end
228
+
229
+ # Define a set of methods defining a new property, which
230
+ # supports multiple values for the same property name.
231
+ def Component.ical_multi_property(property, singular, plural)
232
+ property = "#{property}".strip.downcase.gsub(/-/, '_')
233
+ plural = "#{plural}".strip.downcase
234
+
235
+ # Set this key so the parser knows to use an array for
236
+ # storing this property type.
237
+ @@multi_properties["#{property}"] = true
238
+
239
+ generate_multi_getter(property, plural)
240
+ generate_multi_setter(property, plural)
241
+ generate_multi_query(property, plural)
242
+ generate_multi_adder(property, singular)
243
+ generate_multi_remover(property, singular)
244
+ end
245
+
246
+ # Define a set of methods defining a new property, which
247
+ # supports multiple values in multiple lines with same property name
248
+ def Component.ical_multiline_property(property, singular, plural)
249
+ @@multiline_properties["#{property}"] = true
250
+ ical_multi_property(property, singular, plural)
251
+ end
252
+
253
+
254
+ private
255
+
256
+ def Component.generate_getter(property, alias_name)
257
+ unless instance_methods.include? property
258
+ code = <<-code
259
+ def #{property}(val = nil, params = nil)
260
+ return @properties["#{property}"] if val.nil?
261
+
262
+ unless val.respond_to?(:to_ical)
263
+ raise(NotImplementedError, "Value of type (" + val.class.to_s + ") does not support to_ical method!")
264
+ end
265
+
266
+ unless params.nil?
267
+ # Extend with the parameter methods only if we have to...
268
+ unless val.respond_to?(:ical_params)
269
+ val.class.class_eval { attr_accessor :ical_params }
270
+ end
271
+ val.ical_params = params
272
+ end
273
+
274
+ @properties["#{property}"] = val
275
+ end
276
+ code
277
+
278
+ class_eval code, "component.rb", 219
279
+ alias_method("#{alias_name}", "#{property}") unless alias_name.nil?
280
+ end
281
+ end
282
+
283
+ def Component.generate_setter(property, alias_name)
284
+ setter = property + '='
285
+ unless instance_methods.include? setter
286
+ code = <<-code
287
+ def #{setter}(val)
288
+ #{property}(val)
289
+ end
290
+ code
291
+
292
+ class_eval code, "component.rb", 233
293
+ alias_method("#{alias_name}=", "#{property+'='}") unless alias_name.nil?
294
+ end
295
+ end
296
+
297
+ def Component.generate_query(property, alias_name)
298
+ query = "#{property}?"
299
+ unless instance_methods.include? query
300
+ code = <<-code
301
+ def #{query}
302
+ @properties.has_key?("#{property.downcase}")
303
+ end
304
+ code
305
+
306
+ class_eval code, "component.rb", 226
307
+
308
+ alias_method("#{alias_name}\?", "#{query}") unless alias_name.nil?
309
+ end
310
+ end
311
+
312
+ def Component.generate_multi_getter(property, plural)
313
+ # Getter for whole array
314
+ unless instance_methods.include? plural
315
+ code = <<-code
316
+ def #{plural}(a = nil)
317
+ if a.nil?
318
+ @properties["#{property}"] || []
319
+ else
320
+ self.#{plural}=(a)
321
+ end
322
+ end
323
+ code
324
+
325
+ class_eval code, "component.rb", 186
326
+ end
327
+ end
328
+
329
+ def Component.generate_multi_setter(property, plural)
330
+ # Setter for whole array
331
+ unless instance_methods.include? plural+'+'
332
+ code = <<-code
333
+ def #{plural}=(a)
334
+ if a.respond_to?(:to_ary)
335
+ a.to_ary.each do |val|
336
+ unless val.respond_to?(:to_ical)
337
+ raise(NotImplementedError, "Property values do not support to_ical method!")
338
+ end
339
+ end
340
+
341
+ @properties["#{property}"] = a.to_ary
342
+ else
343
+ raise ArgumentError, "#{plural} is a multi-property that must be an array! Use the add_[property] method to add single entries."
344
+ end
345
+ end
346
+ code
347
+
348
+ class_eval code, "component.rb", 198
349
+ end
350
+ end
351
+
352
+ def Component.generate_multi_query(property, plural)
353
+ # Query for any of these properties
354
+ unless instance_methods.include? plural+'?'
355
+ code = <<-code
356
+ def #{plural}?
357
+ @properties.has_key?("#{property}")
358
+ end
359
+ code
360
+
361
+ class_eval code, "component.rb", 210
362
+ end
363
+ end
364
+
365
+ def Component.generate_multi_adder(property, singular)
366
+ adder = "add_"+singular.to_s
367
+ # Add another item to this properties array
368
+ unless instance_methods.include? adder
369
+ code = <<-code
370
+ def #{adder}(val, params = {})
371
+ unless val.respond_to?(:to_ical)
372
+ raise(NotImplementedError, "Property value object does not support to_ical method!")
373
+ end
374
+
375
+ unless params.nil?
376
+ # Extend with the parameter methods only if we have to...
377
+ unless val.respond_to?(:ical_params)
378
+ val.class.class_eval { attr_accessor :ical_params }
379
+ end
380
+ val.ical_params = params
381
+ end
382
+
383
+ if @properties.has_key?("#{property}")
384
+ @properties["#{property}"] << val
385
+ else
386
+ @properties["#{property}"] = [val]
387
+ end
388
+ end
389
+ code
390
+
391
+ class_eval code, "component.rb", 289
392
+ alias_method("add_#{property.downcase}", "#{adder}")
393
+ end
394
+ end
395
+
396
+ def Component.generate_multi_remover(property, singular)
397
+ # Remove an item from this properties array
398
+ unless instance_methods.include? "remove_#{singular}"
399
+ code = <<-code
400
+ def remove_#{singular}(a)
401
+ if @properties.has_key?("#{property}")
402
+ @properties["#{property}"].delete(a)
403
+ end
404
+ end
405
+ code
406
+
407
+ class_eval code, "component.rb", 303
408
+ alias_method("remove_#{property.downcase}", "remove_#{singular}")
409
+ end
410
+ end
411
+
412
+ def method_missing(method_name, *args)
413
+ @@logger.debug("Inside method_missing...")
414
+ method_name = method_name.to_s.downcase
415
+
416
+ unless method_name =~ /x_.*/
417
+ raise NoMethodError, "Method Name: #{method_name}"
418
+ end
419
+
420
+ # x-properties are accessed with underscore but stored with a dash so
421
+ # they output correctly and we don't have to special case the
422
+ # output code, which would require checking every property.
423
+ if args.size > 0 # Its a setter
424
+ # Pull off the possible equals
425
+ @properties[method_name[/x_[^=]*/].gsub('x_', 'x-')] = args.first
426
+ else # Or its a getter
427
+ return @properties[method_name.gsub('x_', 'x-')]
428
+ end
429
+ end
430
+
431
+ public
432
+
433
+ def respond_to?(method_name)
434
+ if method_name.to_s.downcase =~ /x_.*/
435
+ true
436
+ else
437
+ super
438
+ end
439
+ end
440
+
441
+ end # class Component
442
+ end