mhc 1.0.0

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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. metadata +214 -0
@@ -0,0 +1,142 @@
1
+ module Mhc
2
+ module Sync
3
+ ##
4
+ # It wraps existing database to adds ability to manage etag cache for sync status tracking.
5
+ # The downstream database is supposed to respond to:
6
+ #
7
+ # 1. report_etags
8
+ # report_etags(uids = nil) # returns one of:
9
+ # => {uid_string => etag_object } # Hash
10
+ # => {uid_string => etag_string } # Hash
11
+ # => [uid_etag_object] # Array
12
+ #
13
+ # uid_etag_object is an object which respond to #etag and #uid method.
14
+ # etag_object is an object which respond to #etag method.
15
+ #
16
+ # 2. get_with_etag
17
+ # get_with_etag(uid)
18
+ # # => [RECORD, etag]
19
+ #
20
+ # Each RECORD has to respond to #to_ics_string or #body that
21
+ # returns iCalendar-conformed string.
22
+ #
23
+ # 3. put_if_match
24
+ # put_if_match(uid, ics_string, expected_etag)
25
+ # # => put ics_string if "ETAG" equals to expected_etag
26
+ #
27
+ # 4. delete_if_match
28
+ # delete_if_match(uid, expected_etag)
29
+ # # => delete uid if "ETAG" equals to expected_etag
30
+ #
31
+ # expected_etag is a string for sync. if expected_etag is omitted
32
+ # (or nil), put_if_match and delete_if_match will ignore
33
+ # conflictions.
34
+ #
35
+ class StatusManager
36
+ def initialize(real_db, etag_db)
37
+ @db, @etag_db = real_db, etag_db
38
+ refresh_status
39
+ end
40
+
41
+ def syncinfo(uid)
42
+ Status.new(uid, self)
43
+ end
44
+
45
+ def uid_list
46
+ (@db_status.keys + @etag_status.keys).sort.uniq
47
+ end
48
+
49
+ ##
50
+ # delegation to original DB with sync-status check.
51
+ #
52
+ # To make sure any kind of update is safe,
53
+ # you may want to check current_record.ex_etag == current_record.etag
54
+ # (means current_record.unmodified? is true) like:
55
+ #
56
+ # if current_record.unmodified?
57
+ # @db.put(new_record)
58
+ # end
59
+ #
60
+ # However, this will not work. Because this check-and-update
61
+ # must be an atomic operation.
62
+ # So, instead, we have to do:
63
+ #
64
+ # @db.put_if_match(new_record, current_record.ex_etag)
65
+ #
66
+ def get(uid)
67
+ res, etag = @db.get_with_etag(uid)
68
+ @db_status[uid] = etag if etag
69
+ return Status.new(uid, self, res)
70
+ end
71
+
72
+ def put(modified_record, overwrite = false)
73
+ current_record = syncinfo(modified_record.uid)
74
+ expected_etag = overwrite ? nil : current_record.ex_etag
75
+
76
+ if @db.put_if_match(current_record.uid, modified_record.to_ics_string, expected_etag)
77
+ ## XXX: put_if_match should return the new etag value, and we
78
+ ## have to use it as a new etag for the current record.
79
+ ## However, some CalDAV servers (Google Calendar) do not
80
+ ## return any etag on PUT.
81
+ ## So, we have to PROPFIND immediately after the
82
+ ## PUT. This is a small crack on atomicity
83
+ refresh_status(current_record.uid) # refresh propfind cache.
84
+ current_record.mark_synced
85
+ return Status.new(current_record.uid, self)
86
+ else
87
+ return nil # put failed.
88
+ end
89
+ end
90
+
91
+ def delete(uid)
92
+ current_record = syncinfo(uid)
93
+ if @db.delete_if_match(current_record.uid, current_record.ex_etag)
94
+ refresh_status(current_record.uid)
95
+ current_record.mark_synced
96
+ return Status.new(current_record.uid, self)
97
+ else
98
+ return nil
99
+ end
100
+ end
101
+
102
+ # propfind with cache
103
+ def refresh_status(uids = :all)
104
+ ### XXX: care fore UIDs for partial update.
105
+ @db_status = make_hash(@db.report_etags)
106
+ @etag_status = make_hash(@etag_db.report_etags)
107
+ end
108
+
109
+ def etag(uid)
110
+ if @db_status[uid].respond_to?(:etag)
111
+ return @db_status[uid].etag
112
+ else
113
+ return @db_status[uid]
114
+ end
115
+ end
116
+
117
+ def ex_etag(uid)
118
+ @etag_status[uid]
119
+ end
120
+
121
+ def mark_synced(uid, etag)
122
+ @etag_db.put(uid, etag)
123
+ return self
124
+ end
125
+
126
+ private
127
+ ## etag_report is one of:
128
+ ## + {uid_string => etag_object } style hash,
129
+ ## + {uid_string => etag_string } style hash,
130
+ ## + [uid_etag_object] style array,
131
+ ## uid_etag_object is an object which respond to #etag and #uid method.
132
+ ## etag_object is an object which respond to #etag method.
133
+ def make_hash(etag_report)
134
+ return etag_report if etag_report.respond_to?(:keys)
135
+ hash = {}
136
+ etag_report.map {|o| hash[o.uid] = o.etag} if etag_report
137
+ return hash
138
+ end
139
+
140
+ end # class StatusManager
141
+ end # module Sync
142
+ end # module Mhc
@@ -0,0 +1,233 @@
1
+ module Mhc
2
+ module Sync
3
+ module Strategy
4
+
5
+ class Factory
6
+ def self.create(strategy)
7
+ case strategy.to_sym
8
+ when :mirror
9
+ return Mirror.new
10
+ when :sync
11
+ return Sync.new
12
+ else
13
+ raise NotImplementedError, "#{strategy} #{strategy.class}"
14
+ end
15
+ end
16
+ end
17
+
18
+ # Our Sync mechanism is very simple, because
19
+ # we can assume every article is independent
20
+ # with eath other. It will work well with
21
+ # iCalendar basis articles.
22
+ #
23
+ # We simply follow the rule on the table below:
24
+ #
25
+ # Side 2
26
+ # |---+---------+------------+------------+-------|
27
+ # S | | M | U | N | D |
28
+ # i |---+---------+------------+------------+-------|
29
+ # d | M | CNF | OW 1->2 | CP 1->2 | CNF |
30
+ # e | U | OW 2->1 | - | ?? CP 1->2 | DEL 1 |
31
+ # 1 | N | CP 2->1 | ?? CP 2->1 | - | - |
32
+ # | D | CNF | DEL 2 | - | - |
33
+ # |---+---------+------------+------------+-------|
34
+ #
35
+ # M, U, N, and D indicate status changes on each article after
36
+ # the last sync:
37
+ #
38
+ # + M :: Modified (or Created)
39
+ # + U :: Unchanged
40
+ # + N :: No Record
41
+ # + D :: Deleted
42
+ #
43
+ # Each entry in the table means:
44
+ # + -- :: No operation (ignore)
45
+ # + ?? :: Not occurred in normal cases
46
+ # + OW :: Overwrite
47
+ # + CP :: Copy
48
+ # + DEL :: Delete
49
+ # + CNF :: Conflict
50
+ #
51
+ # Before applying the rule to our repository,
52
+ # we have to set the marks (M, U, N or D) to all articles
53
+ # in each side.
54
+ #
55
+ # strategy = Mhc::Sync::Strategy.create(strategy_name)
56
+ # strategy name is one of:
57
+ # * :empty ... ignore on every status
58
+ # * :mirror ... mirror from side1 to side2
59
+ # * :sync ... sync articles of side1 and side2
60
+ #
61
+ # and strategy.whatnow(side1, side2) returns a symbol one of:
62
+ # * :ignore :: Already synced, ignoreable
63
+ # * :conflict :: Conflicted
64
+ # * :delete1 :: Should delete the article of side1
65
+ # * :delete2 :: Should delete the article of side2
66
+ # * :copy1_to_2 :: Should copy the article of side1 to side2
67
+ # * :copy2_to_1 :: Should copy the article of side2 to side1
68
+ # * :overwrite1_to_2 :: Should overwrite the article of side1 to side2
69
+ # * :overwrite2_to_1 :: Should overwrite the article of side2 to side1
70
+ #
71
+ # side1 and side2 have to respond to:
72
+ # #nil?, # #modified?, #unmodified?, #norecord?, #deleted?
73
+ #
74
+ class Base
75
+ def whatnow(side1, side2)
76
+ # do nothing
77
+ actions = {
78
+ "MM" => :ignore,
79
+ "MU" => :ignore,
80
+ "MN" => :ignore,
81
+ "MD" => :ignore,
82
+
83
+ "UM" => :ignore,
84
+ "UU" => :ignore,
85
+ "UN" => :ignore,
86
+ "UD" => :ignore,
87
+
88
+ "NM" => :ignore,
89
+ "NU" => :ignore,
90
+ "NN" => :ignore,
91
+ "ND" => :ignore,
92
+
93
+ "DM" => :ignore,
94
+ "DU" => :ignore,
95
+ "DN" => :ignore,
96
+ "DD" => :ignore,
97
+ }
98
+ return actions[status_pair(side1, side2)]
99
+ end
100
+
101
+ private
102
+ # * Char (M,U,N,D) indicates status change on each article
103
+ # after the last sync:
104
+ #
105
+ # + M :: Modified
106
+ # + U :: Unchanged
107
+ # + N :: No Record
108
+ # + D :: Deleted
109
+ #
110
+ def status_signature(info)
111
+ return "N" if info.nil?
112
+
113
+ return "M" if info.modified?
114
+ return "U" if info.unmodified?
115
+ return "N" if info.norecord?
116
+ return "D" if info.deleted?
117
+
118
+ return "?" # NOTREACHED I hope
119
+ end
120
+
121
+ def status_pair(side1, side2)
122
+ return status_signature(side1) + status_signature(side2)
123
+ end
124
+ end # class Base
125
+
126
+ # * Sync side1 and side2
127
+ #
128
+ # simply follow the rule on the table below:
129
+ #
130
+ # Side 2
131
+ # |---+---------+------------+------------+-------|
132
+ # S | | M | U | N | D |
133
+ # i |---+---------+------------+------------+-------|
134
+ # d | M | CNF | CP 1->2 | CP 1->2 | CNF |
135
+ # e | U | CP 2->1 | - | ?? - | DEL 1 |
136
+ # 1 | N | CP 2->1 | ?? - | - | - |
137
+ # | D | CNF | DEL 2 | - | - |
138
+ # |---+---------+------------+------------+-------|
139
+ #
140
+ # + M :: Modified (or Created)
141
+ # + U :: Unchanged
142
+ # + N :: No Record
143
+ # + D :: Deleted
144
+ #
145
+ # + -- :: No operation (ignore)
146
+ # + ?? :: Not occurred in normal cases
147
+ # + OW :: Overwrite
148
+ # + CP :: Copy
149
+ # + DEL :: Delete
150
+ # + CNF :: Conflict
151
+ #
152
+ class Sync < Base
153
+ def whatnow(side1, side2)
154
+ actions = {
155
+ "MM" => :conflict,
156
+ "MU" => :copy1_to_2,
157
+ "MN" => :copy1_to_2,
158
+ "MD" => :conflict,
159
+
160
+ "UM" => :copy2_to_1,
161
+ "UU" => :ignore,
162
+ "UN" => :ignore,
163
+ "UD" => :delete1,
164
+
165
+ "NM" => :copy2_to_1,
166
+ "NU" => :ignore,
167
+ "NN" => :ignore,
168
+ "ND" => :ignore,
169
+
170
+ "DM" => :conflict,
171
+ "DU" => :delete2,
172
+ "DN" => :ignore,
173
+ "DD" => :ignore,
174
+ }
175
+ return actions[status_pair(side1, side2)]
176
+ end
177
+ end # class Sync
178
+
179
+ # * Mirror side1 to side2
180
+ #
181
+ # simply follow the rule on the table below:
182
+ #
183
+ # Side 2
184
+ # |---+---------+----------+------------+---------|
185
+ # S | | M | U | N | D |
186
+ # i |---+---------+----------+------------+---------|
187
+ # d | M | OW 1->2 | OW 1->2 | CP 1->2 | CP 1->2 |
188
+ # e | U | OW 1->2 | -- | ?? -- | CP 1->2 |
189
+ # 1 | N | DEL 2 | ?? -- | -- | -- |
190
+ # | D | DEL 2 | DEL 2 | -- | -- |
191
+ # |---+---------+----------+------------+---------|
192
+ #
193
+ # + M :: Modified (or Created)
194
+ # + U :: Unchanged
195
+ # + N :: No Record
196
+ # + D :: Deleted
197
+ #
198
+ # + -- :: No operation (ignore)
199
+ # + ?? :: Not occurred in normal cases
200
+ # + OW :: Overwrite
201
+ # + CP :: Copy
202
+ # + DEL :: Delete
203
+ #
204
+ class Mirror < Base
205
+ def whatnow(side1, side2)
206
+ actions = {
207
+ "MM" => :overwrite1_to_2,
208
+ "MU" => :overwrite1_to_2,
209
+ "MN" => :copy1_to_2,
210
+ "MD" => :copy1_to_2,
211
+
212
+ "UM" => :overwrite1_to_2,
213
+ "UU" => :ignore,
214
+ "UN" => :ignore,
215
+ "UD" => :copy1_to_2,
216
+
217
+ "NM" => :delete2,
218
+ "NU" => :ignore,
219
+ "NN" => :ignore,
220
+ "ND" => :ignore,
221
+
222
+ "DM" => :delete2,
223
+ "DU" => :delete2,
224
+ "DN" => :ignore,
225
+ "DD" => :ignore,
226
+ }
227
+ return actions[status_pair(side1, side2)]
228
+ end
229
+ end # class Mirror
230
+
231
+ end # module Strategy
232
+ end # module Sync
233
+ end # module Mhc
@@ -0,0 +1,98 @@
1
+ ################################################################
2
+ # Log maintenance functions.
3
+ #
4
+ # M 2000-04-25 00:06:08 <20.nom@.nomcom> ~nom/Mail/schedule/2000/04/1 Luncheon
5
+ # D 2000-04-25 00:06:08 <20.nom@.nomcom> ~nom/Mail/schedule/2000/04/1 Luncheon
6
+ # S 2000-04-25 00:06:08 user_id
7
+ #
8
+ module Mhc
9
+ class Log
10
+
11
+ def initialize(filename)
12
+ @filename = filename
13
+ end
14
+
15
+ def add_entry(entry)
16
+ file = File.open(@filename, "a+")
17
+ file.print "#{entry}\n"
18
+ file.fsync if file.respond_to?("fsync")
19
+ file.close
20
+ end
21
+
22
+ def each_entry
23
+ begin
24
+ file = File.open(@filename)
25
+ while line = file.gets
26
+ yield(MhcLogEntry.new(line.chomp))
27
+ end
28
+ file.close
29
+ rescue
30
+ end
31
+ end
32
+
33
+ def entries()
34
+ arry = []
35
+ each_entry{|e|
36
+ arry << e
37
+ }
38
+ return arry
39
+ end
40
+
41
+ def shrink_entries(user_id)
42
+ hash = {}
43
+ each_entry{|e|
44
+ if e.status == 'S' and e.rec_id == user_id
45
+ hash.clear
46
+ else
47
+ hash[e.rec_id] = e
48
+ end
49
+ }
50
+ return hash.values
51
+ end
52
+ end # class Log
53
+ end # module Mhc
54
+
55
+ ################
56
+ module Mhc
57
+ class LogEntry
58
+ attr :status
59
+ attr :mtime
60
+ attr :rec_id
61
+ attr :path
62
+ attr :subject
63
+
64
+ def initialize(status, mtime = nil, rec_id = nil, path = nil, subject = nil)
65
+ if mtime.nil?
66
+ init_from_string(status)
67
+ else
68
+ @status, @mtime, @rec_id, @path, @subject =
69
+ status, mtime, rec_id, path, subject
70
+ end
71
+ end
72
+
73
+ def to_s
74
+ return [
75
+ @status,
76
+ @mtime.strftime("%Y-%m-%d %H:%M:%S"),
77
+ @rec_id,
78
+ @path,
79
+ @subject
80
+ ].join(' ')
81
+ end
82
+
83
+ ################
84
+ private
85
+ ################
86
+ def init_from_string(line)
87
+ str = line.chomp
88
+ status, yymmdd, hhmmss, rec_id, path, subject = str.split
89
+ yy, mm, dd = yymmdd.split('-')
90
+ h, m, s = hhmmss.split(':')
91
+
92
+ mtime = ::Time.local(yy.to_i, mm.to_i, dd.to_i,
93
+ h .to_i, m .to_i, s .to_i)
94
+ @status, @mtime, @rec_id, @path, @subject =
95
+ status, mtime, rec_id, path, subject
96
+ end
97
+ end # class LogEntry
98
+ end # module Mhc