paulsm-icalendar 1.1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +266 -0
  4. data/Rakefile +109 -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/icalendar/rrule.rb +126 -0
  31. data/lib/icalendar/tzinfo.rb +121 -0
  32. data/lib/meta.rb +32 -0
  33. data/test/calendar_test.rb +71 -0
  34. data/test/component/event_test.rb +256 -0
  35. data/test/component/timezone_test.rb +67 -0
  36. data/test/component/todo_test.rb +13 -0
  37. data/test/component_test.rb +76 -0
  38. data/test/conversions_test.rb +97 -0
  39. data/test/coverage/STUB +0 -0
  40. data/test/fixtures/folding.ics +23 -0
  41. data/test/fixtures/life.ics +46 -0
  42. data/test/fixtures/simplecal.ics +119 -0
  43. data/test/fixtures/single_event.ics +23 -0
  44. data/test/interactive.rb +17 -0
  45. data/test/parameter_test.rb +29 -0
  46. data/test/parser_test.rb +84 -0
  47. data/test/read_write.rb +23 -0
  48. 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
@@ -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
+
@@ -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