On this page:

Common networking errors

Due 2004.02.23 11:59:59 PM: Source code and detailed notes on

- changes made
- problems encountered
- solutions tried/found
- outstanding problems
- plans for next step

I don’t expect everyone to have a fully-working game by tomorrow, so
most of your partial points will come from the notes. “It doesn’t
work.” isn’t enough; I need to know why it doesn’t work, if it used to
work before, what you’ve tried doing to get it to work, why you think
it doesn’t work, etc.

Along the way to your first milestone, you may have encountered the
following problems:

- ObjectInputStream seems to hang when you create it.

That happens when both sides of the connection try to create object
input streams at the same time, but the other side hasn’t opened an
object output stream yet. Fix: create the object output stream first
before you create the input stream.

ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

- Your program lags terribly.

How are you doing your networking and repainting code? If you’re doing
animation, repaint() should be in one thread and your networking code
should be in another. You shouldn’t do them in the same thread because
then repaint() will only happen whenever something is received from
the socket connection. Also, make sure you have delays somewhere!
while (true) { repaint(); } means your CPU will spend
most of its time repainting. Add a Thread.sleep in that loop so that
your computer has time to do other things.

- You can’t write certain objects over the stream.

Make sure the object is serializable and all of its attributes are
either transient or serializable as well. You really shouldn’t be
sending your entire application over the network – what is the other
side going to do with all those textfields? Mark many of your
attributes transient or create a small, simple object that
encapsulates the data you need to send.

- You’ve sent an object over the network connection and you’re calling methods on it on the other side, but the one on the first computer isn’t changing!

That’s the way it works. If you send an object from computer A to
computer B, computer B isn’t working on the original object but on a
copy of it. Any changes on A won’t be automatically sent to B and vice
versa. The correct thing to do is examine the object and perform
different actions locally.

If you really need to get something like this working and you have
plenty of spare time, read up on RMI (Remote Method Invocation). It’s
out of the scope of CS21B, though.

For tonight, I want lots and lots of notes.

TOMORROW: Data structures (stacks, queues and linked lists).

Congrats to teams who competed in Java Cup and Quizzardry!

Ealden wrote:

Congratulations to the teams who competed in the recently concluded
Java Cup and Quizzardry competitions! The Java Cup was held last 3
Mar 2005, while the Quizzardly was held the next day, 4 Mar 2005. The
teams competed from those sent by DLSU, iAcademy, UPD, AMA, FEU, Chang
Kai Shek, and UA&P.

The said events were part of the 20th Computer Science Week of the
University of the Philippines, Diliman, and were both organized by the
UP Association of Computer Science Majors (UP CURSOR).

JAVA CUP

Champion: Ateneo 3

  • Miguel Arguelles (4 CS)
  • Allan Espinosa (3 ECE)
  • Michael Tan (2 CS, alternate)

Second runner up: Ateneo 2

  • Joseph Noel (4 CS)
  • Paul Rivera (4 CS)

4th place: Ateneo 1

  • Erik Uy (4 CS)
  • Charles Yeung (4 MIS)
  • Ramon “Akie” Mejia (3 CS, alternate)

QUIZZARDRY

Champion: Ateneo 2

  • Mark Punzalan (3 CS)
  • Vanessa Gonzales (3 CS)
  • Randolph “Zippy” Espinosa (3 CS)

Second runner up: Ateneo 3

  • Erik Uy (4 CS)
  • Marc Lihan (3 CS)
  • Erwin Lee (3 MIS)

Ateneo 1

  • Ealden Escanan (3 MIS)
  • Gil San Pedro (3 CS)
  • Robin Yu (1 CS)

Pictures: http://www.engg.upd.edu.ph/%7Ejoyce/csweek/index.html ;)

Congrats! :D

Ealden Esto E. Escañan
Outgoing CompSAt President

E-Mail from Ealden Esto E. Escañan

On Technorati: , ,

Ruby: Turn bash.org quotes into a fortune file

The following code turns XML quotes from bash.org (a popular IRC quotes server) into a fortune-cookie file.
Handy for using with ../emacs/flashcard.el and my ../emacs/flashcard-config.el, which pops up a fortune
every time I get a correct answer.

bash-org-to-fortune.rb:

require 'rss/1.0'
require 'cgi'
require 'net/http'
host = Net::HTTP.new('bash.org', 80)
if ARGV[0] then
   resp, data = host.get('http://bash.org/xml/?top&below=' + ARGV[0], nil)
else
   resp, data = host.get('http://bash.org/xml/?top', nil)
end
parsed = RSS::Parser.parse(data, false)
parsed.items.each { |x| puts CGI::unescapeHTML(x.description.gsub('
', "\n")); puts "%\n" }

Call like this:

ruby bash-org-to-fortune.rb > bash; strfile bash; fortune bash

# or to get the top quotes with score < 1000
ruby bash-org-to-fortune.rb 1000 > bash; strfile bash; fortune bash

On Technorati: , ,

Back in the groove

I had a totally awesome day at IBM today, crunching some data and
doing some nifty photocloud visualizations of all the people who had
blogged about a certain topic. I sorted it by domain in order to
collocate people from the same lab, and resized the photos according
to the number of posts they had made, linking each picture to the
respective blogs. I then ran the same analysis on their bookmarks.
Next week, I’m going to do a 2D plot… =)

It was _tons_ of fun hacking together a little Java program to do the
analysis. I had to look up a lot of things in order to do the first
program. It took me a while to dump SAX in favor of DOM, but once I’d
gotten the XML parsing code out of the way, I wrote the data analysis
quickly. My code wouldn’t win points for elegance or efficiency, but
it works. I wish I had either Ruby or Python on that system – heck, I
could do this stuff in Emacs Lisp. I think I’ll go install the Windows
versions of my favorite programming languages as soon as possible,
although I might have to clear it with IBM first.

Today put me firmly back in the programming/hacking camp. I couldn’t
pry my fingers off the keyboard. =) Mark had to call me to make sure I
didn’t miss the 3:55 bus. I just had so much fun writing glue code and
crunching all the data. In terms of geek stuff, this is where I
belong…

On Technorati: , ,

Random Japanese sentence: ペルシャ猫に関連した古いお話しがあります。
There is a classic story related about a Persian cat. Perusha-neko ni
kanren shita furui o-hanashi ga arimasu.

Even more awesome LotusScript mail merge for Lotus Notes + Microsoft Excel

UPDATE: May 20, 2011 – David Turner has shared his excellent improvements. Check them out!

UPDATE: May 28, 2010 – fixed errors caused by default Option Declare. Thanks to Lisa Harnett for the feedback!
UPDATE: Oct 19, 2010 – fixed Quit(). Thanks to Vance for the feedback! Also, clarified tokens.

Based on the feedback on my Lotus Notes mail merge from a Microsoft Excel spreadsheet (2009), I’ve refined my merge script to make it more awesome. How is it more awesome?

  • Prompts you for drafting or sending
  • Saves sent messages
  • Allows you to customize the subject
  • Uses [ ] instead of < and > for built-in tokens ([to], [cc], [subject]) for less HTML confusion in blog posts and replies
  • Displays number of sent messages and errors
  • Closes the Microsoft Excel spreadsheet afterwards

The search-and-replace tokens are defined in the first row of your Microsoft Excel spreadsheet. The script searches for them in the message body, replaces them with the appropriate values from the current row, and either saves the message as the draft or sends the message. There are a few built-in tokens for this script ([to], [cc], [subject]) – these are case-sensitive, so enter them exactly like that. All the other tokens are up to you, so you could use FOO and BAR as search-and-replace tokens if you want.

Tokens are replaced only in the message body. If you want a variable subject line, use a formula to calculate the subject in a column with the [subject] header.

As always, test your mail merges with a small list before using it for your entire list. Create an agent and call it something like “Mail merge”. Edit the agent and set the type to LotusScript. In the (Declarations) section, add

%Include "lsconst.lss"

In the “Initialize” section, put in:

Sub Initialize
	'Mail merge script by Sacha Chua ([email protected])

	Dim ws As NotesUIWorkspace
	Set ws = New NotesUIWorkspace
	Dim sendTypes(1) As String
	Dim sendValue As String
	Dim errorCount As Integer
	errorCount = 0
	sendTypes(0) = "Draft messages without sending"
	sendTypes(1) = "Send messages"
	sendValue = ws.Prompt(PROMPT_OKCANCELLIST, "Sending options", "What would you like to do?", "", sendTypes)
	If (sendValue = "") Then
		Exit Sub
	End If

	Dim fileName As String
	Dim strXLFilename As String
	'Prompt for the filename - should be a Microsoft Excel file with columns, where the first row of each column
	'is a token that will be used when replacing text in the body of the message
	'Special tokens: [to], [cc], [subject] set the appropriate fields
	'Make sure the first column does not have any blank cells
	fileName$ = ws.Prompt(12, "Select file", "3")
	If fileName$ = "" Then
		Exit Sub   'Cancel was pressed
	End If
	strXLFilename = fileName$
	Dim s As New NotesSession
	Dim uidoc As NotesUIDocument
	Dim partno As String
	Dim db As NotesDatabase
	Dim view As NotesView
	Dim doc As NotesDocument
	Dim collection As NotesDocumentCollection
	Dim memo As NotesDocument
	Dim body As NotesRichTextItem
	Dim newBody As NotesRichTextItem
	Dim range As NotesRichTextRange
	Dim count As Integer

	Set db = s.CurrentDatabase
	Set collection = db.UnprocessedDocuments
	Set memo = collection.getFirstDocument()

	Dim varXLFile As variant
	'Get data from the spreadsheet
	Set varXLFile = CreateObject("Excel.Application")
	varXLFile.Visible = False
	Dim varXLWorkbook As variant
	Set varXLWorkbook = Nothing
	varXLFile.Workbooks.Open strXLFilename
	Set varXLWorkbook = varXLFile.ActiveWorkbook
	Dim varXLSheet As variant
	Set varXLSheet = varXLWorkbook.ActiveSheet

	Dim lngRow As Integer
	Dim columnNo As Integer
	Dim token As String
	Dim value As string
	lngRow = 2
	Dim maildoc As NotesDocument
	While (Not (varXLSheet.Cells(lngRow, 1).Value = ""))
		'Fill in the template
		Dim subject As string
		subject = memo.Subject(0)
		Set body = memo.GetFirstItem("Body")

		'Compose message
		
		Set maildoc = New NotesDocument(db)
		Set maildoc= db.CreateDocument()
		maildoc.Form = "Memo"
		maildoc.Subject = subject
		Set newBody = maildoc.CreateRichTextItem("Body")
		Call newBody.appendRTItem(body)
		Set range = newBody.CreateRange			

		'Count the number of fields
		'Look up tokens from the column headings and replace them
		columnNo = 1
		While Not(varXLSheet.Cells(1, columnNo).Value = "")
			token = varXLSheet.Cells(1, columnNo).Value
			value = varXLSheet.Cells(lngRow, columnNo).Value
			count = range.FindAndReplace(token, value, 16)
			If (token = "[to]") Then
				maildoc.SendTo = value
			End If
			If (token = "[cc]") Then
				maildoc.CopyTo = value
			End If
			If (token = "[subject]") Then
				maildoc.Subject = value
			End If
			columnNo = columnNo + 1
		Wend
		On Error GoTo save
		If (sendValue = sendTypes(0)) Then
			Call maildoc.Save(True, False)
		Else
			maildoc.SaveMessageOnSend = True
			maildoc.PostedDate = Now()
			Call maildoc.Send(False)
			Call maildoc.Save(True, True)
		End If
		GoTo nextrow
save:
		MessageBox("Error processing " + maildoc.sendTo)
		errorCount = errorCount + 1
		Resume Next
nextrow:
		lngRow = lngRow + 1
	Wend
	If (sendValue = sendTypes(0)) Then
		MsgBox "Drafted " & (lngRow - errorCount - 2) & " message(s). Errors: " & errorCount
	Else
		MsgBox "Sent " & (lngRow - errorCount - 2) & " message(s). Errors: " & errorCount
	End If
	Call varXLFile.Quit()
End Sub

Org-toodledo

I finally got around to asking my manager for permission to contribute org-toodledo as open source. Here it is. Enjoy!

;;; org-toodledo.el - Toodledo integration for Emacs Org mode
;; (c) 2010 Sacha Chua ([email protected])
;;
;; This file is not part of GNU Emacs.

;; This is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 2, or (at your option) any later
;; version.
;;
;; This is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
;; for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
;; MA 02111-1307, USA.

;; How to use:
;; 1. Customize org-toodledo-userid and org-toodledo-password
;; 2. Open a blank org file.
;; 3. Call org-toodledo-initialize-org
;; Call org-toodledo-update to bring in new/updated tasks (skips locally modified tasks newer than updated)
;; Call org-toodledo-sync-task to create or update the current task
;; Call org-toodledo-delete-current-task to delete the current task
;;
;; Doesn't do lots of error trapping. Might be a good idea to version-control your Org file.
;;
;; TOODLEDO ATTRIBUTES and how they are bi-directionally handled
;; Context: Handled by tags (ex:   :@work:  :@errands:)
;;   - will create new contexts if necessary
;; Task status: Mapped to TODO state.
;;   See org-toodledo-status-to-string and org-toodledo-parse-current-task for the mapping
;;   You will probably want something like this in your ~/.emacs:
;; (setq org-todo-keywords
;;      '((sequence
;;         "TODO(t)"  ; next action
;;         "PLAN(-)"
;;         "STARTED(s)"
;;         "WAITING(w@/!)"
;;         "POSTPONED(p)" "SOMEDAY(s@/!)" "|" "DONE(x!)" "CANCELLED(c@)")
;;      (type "DELEGATED(d@!)" "DONE(x)")))
;; Length: Mapped to effort
;; Priority: Mapped to [#A], [#B], or [#C]. (TODO: Change this to five levels of priority to match Toodledo)
;; Start date: Mapped to "SCHEDULED"
;; Due date: Mapped to "DEADLINE"
;; Tags: Mapped to tags
;; Note: Mapped to todo text. May get confused by asterisks, so don't use any starting asterisks in your body text.
;;   (or anything that looks like an Org headline).
;; Completed: Mapped to DONE todo state.
;;
;; TODO:
;; - [ ] Double-check new/changed/deleted task updating, still seems buggy
;; - [ ] Test, test, test - maybe make test harness?
;; - [ ] Move status<->string mapping to a variable - lookups are better than logic
;; - [ ] Make sure sync timestamps aren't getting updated more often than needed
;; - [ ] Suggest some kind of hook to make it easier to mark a task as locally modified

(require 'org)
(require 'w3m)
(require 'xml)
(defcustom org-toodledo-userid ""
  "UserID from Toodledo: http://www.toodledo.com/info/api_doc.php"
  :group 'org-toodledo
  :type 'string)

(defcustom org-toodledo-password ""
  "Password for Toodledo."
  :group 'org-toodledo
  :type 'string)

(defvar org-toodledo-token-expiry nil "Expiry time for authentication token.")
(defvar org-toodledo-token nil "Authentication token.")
(defvar org-toodledo-key nil "Authentication key.")

(require 'url)
(require 'url-http)

(defun org-toodledo-initialize-org ()
  "Replace buffer contents with Toodledo tasks."
  (interactive)
  (delete-region (point-min) (point-max))
  (let ((account-info (org-toodledo-get-account-info))
        (server-info (org-toodledo-get-server-info))
        (tasks (org-toodledo-get-tasks '(("notcomp" . "1")))))
    (insert "* Toodledo\n"
            ":PROPERTIES:\n"
            ":Last-modified: " (cdr (assoc "lastaddedit" account-info)) "\n"
            ":Last-deleted: " (cdr (assoc "lastdelete" account-info)) "\n"
            ":Last-sync: " (cdr (assoc "unixtime" server-info)) "\n"
            ":END:\n")
    (insert (mapconcat 'org-toodledo-task-to-string tasks "\n"))))

(defun org-toodledo-get-token ()
  "Retrieve authentication token valid for four hours."
  (if (and org-toodledo-token
           org-toodledo-token-expiry
           (time-less-p (current-time) org-toodledo-token-expiry))
      org-toodledo-token
    ;; Else retrieve a new token
    (let ((response
            (with-current-buffer
                (url-retrieve-synchronously
                 (concat "http://api.toodledo.com/api.php?method=getToken;userid="
                         org-toodledo-userid))
              (xml-parse-region (point-min) (point-max)))))
      (if (equal (car (car response)) 'error)
          (progn
            (setq org-toodledo-token nil
                  org-toodledo-key nil
                  org-toodledo-token-expiry nil)
            (error "Could not log in to Toodledo: %s" (elt (car response) 2)))
        (setq org-toodledo-token
              (elt (car response) 2))
        (setq org-toodledo-key (org-toodledo-key)
              ;; Set the expiry time
              org-toodledo-token-expiry
              (seconds-to-time
               (+ (time-to-seconds (current-time))
                  (* 60 60 4)))))   ;; four hours
      org-toodledo-token)))

(defun org-toodledo-key ()
  "Return authentication key used for each request."
  (if (and org-toodledo-token
           org-toodledo-token-expiry
           (time-less-p (current-time) org-toodledo-token-expiry)
           org-toodledo-key)
      org-toodledo-key
    (setq org-toodledo-key
          (md5 (concat (md5 org-toodledo-password)
                       org-toodledo-token
                       org-toodledo-userid)))))

(defun org-toodledo-get-url (method-name &optional params)
  "Return URL for METHOD-NAME and PARAMS."
  (org-toodledo-get-token)
  (concat "http://api.toodledo.com/api.php?method="
          (w3m-url-encode-string method-name)
          ";key=" (org-toodledo-key)
          (if params
              (concat
               ";"
               (mapconcat (lambda (x)
                            (concat
                             (w3m-url-encode-string (car x)) "="
                             (w3m-url-encode-string (cdr x))))
                          params
                          ";"))
            "")))

(defun org-toodledo-call-method (method-name &optional params)
  "Call METHOD-NAME with PARAMS and return the parsed XML."
  (setq params (cons (cons "unix" "1") params))
  (with-current-buffer
      (url-retrieve-synchronously
       (org-toodledo-get-url method-name params))
    (xml-parse-region (point-min) (point-max))))

(defmacro org-toodledo-defun (function-name api-name description)
  `(defun ,function-name (params)
     ,description
     (org-toodledo-call-method ,api-name params)))

(defun org-toodledo-get-server-info ()
  "Return server information."
  (org-toodledo-convert-xml-result-to-alist
    (car (org-toodledo-call-method "getServerInfo"))))

(defun org-toodledo-get-account-info ()
  "Return server information."
  (org-toodledo-convert-xml-result-to-alist
   (car (org-toodledo-call-method "getAccountInfo"))))

(org-toodledo-defun org-toodledo-add-task "addTask" "Add task with PARAMS.")
(org-toodledo-defun org-toodledo-edit-task "editTask" "Edit task with PARAMS.")
(org-toodledo-defun org-toodledo-delete-task "deleteTask" "Delete task with PARAMS.")

;; (setq temp (org-toodledo-get-tasks '(("notcomp" . "1"))))
;; (setq server-info (org-toodledo-get-server-info))
;; (setq account-info (org-toodledo-get-account-info))
(defun org-toodledo-convert-xml-result-to-alist (info)
  "Convert INFO to an alist."
  (delq nil
        (mapcar
         (lambda (item)
           (if (listp item)
               (cons (symbol-name (car item)) (elt item 2))))
         (xml-node-children (delete "\n\t" info)))))

(defun org-toodledo-get-tasks (&optional params)
  "Retrieve tasks using PARAMS.
Return a list of task alists."
  (mapcar
   'org-toodledo-convert-xml-result-to-alist
   (xml-get-children
    (car (org-toodledo-call-method "getTasks" params))
    'task)))

(defun org-toodledo-get-deleted (&optional params)
  "Retrieve deleted tasks using PARAMS.
Return a list of task alists."
  (mapcar
   'org-toodledo-convert-xml-result-to-alist
   (xml-get-children
    (car (org-toodledo-call-method "getDeleted" params))
    'task)))

(defun org-toodledo-entry-note ()
  "Extract the note for this entry."
  (save-excursion
    (org-back-to-heading)
    (when (looking-at org-complex-heading-regexp)
      (goto-char (match-end 0))
      (let ((text (buffer-substring-no-properties
                   (point)
                   (if (re-search-forward org-complex-heading-regexp nil t)
                       (match-beginning 0)
                     (org-end-of-subtree)))))
        (with-temp-buffer
          (insert text)
          (goto-char (point-min))
          (when (re-search-forward
                 (concat "\\<"
                         (regexp-quote org-deadline-string) " +<[^>\n]+>[ \t]*") nil t)
            (replace-match ""))
          (goto-char (point-min))
          (when (re-search-forward
                 (concat "\\<"
                         (regexp-quote org-scheduled-string) " +<[^>\n]+>[ \t]*") nil t)
            (replace-match ""))
          (goto-char (point-min))
          (while (re-search-forward "\n\n+" nil t)
            (replace-match "\n"))
          (org-export-remove-or-extract-drawers org-drawers nil nil)
          (buffer-substring-no-properties (point-min)
                                          (point-max)))))))

(defun org-toodledo-parse-current-task ()
  "Extract the status and Toodledo ID of the current task."
  (save-excursion
    (org-back-to-heading t)
    (when (and (looking-at org-complex-heading-regexp)
               (match-string 2)) ;; TODO
      (let* (info
             (status (match-string-no-properties 2))
             (priority (match-string-no-properties 3))
             (title (match-string-no-properties 4))
             (tags (match-string-no-properties 5))
             (id (org-entry-get (point) "Toodledo-ID"))
             (contexts (org-toodledo-get-contexts))
             context)
        ;; (add-to-list 'info (cons "title" (match-string-no-properties 1)))
        (if id (add-to-list 'info (cons "id" id)))
        (when tags
          (setq tags
              (delq nil
                    (mapcar
                     (lambda (tag)
                       (if (> (length tag) 0)
                           (if (string-match (org-re "@\\([[:alnum:]_]+\\)") tag)
                               (progn
                                 ;; Not recognized context
                                 (if (null (assoc (match-string 1 tag) contexts))
                                     ;; Create it if it does not yet exist
                                     (let ((result
                                            (org-toodledo-call-method
                                             "addContext"
                                             (list (cons "title" (match-string 1 tag))))))
                                       (if (eq (caar result) 'added)
                                           (setq org-toodledo-contexts
                                                 (cons (cons (match-string 1 tag)
                                                             (elt (car result) 2))
                                                       org-toodledo-contexts)
                                                 contexts org-toodledo-contexts))))
                                   ;; Get the ID of the context
                                 (setq context
                                       (cdr (assoc (match-string 1 tag) contexts)))
                                 nil)
                             tag)))
                     (split-string tags ":")))))
        (setq info
              (list
               (cons "id" id)
               (cons "title" title)
               (cons "length" (org-entry-get (point) "Effort"))
               (cons "context" context) 
               (cons "tag" (mapconcat 'identity tags " "))
               (cons "completed" (if (equal status "DONE") "1" "0"))
               (cons "status"
                     (cond 
                      ((equal status "STARTED") "2")
                      ((equal status "DELEGATED") "4")
                      ((equal status "SOMEDAY") "8")
                      ((equal status "CANCELLED") "9")
                      ((equal status "PLAN") "3")
                      ((equal status "WAITING") "5")                      
                      ((equal status "TODO") "1")))
               (cons "priority"
                     (cond
                      ((equal priority "[#A]") "2")
                      ((equal priority "[#B]") "1")
                      ((equal priority "[#C]") "0")))
               (cons "note"
                     (org-toodledo-entry-note))))
        (when (org-entry-get nil "DEADLINE")
          (setq info (cons (cons "duedate"
                                 (substring (org-entry-get nil "DEADLINE")
                                            0 10)) info)))
        (when (org-entry-get nil "SCHEDULED")
          (setq info (cons (cons "startdate"
                                 (substring (org-entry-get nil "SCHEDULED")
                                            0 10)) info)))
        info))))

(defun org-toodledo-sync ()
  "Synchronize all tasks."
  ;; Retrieve all tasks
  ;; For each task in the current buffer
  ;;   Synchronize an existing task that has changed
   (let ((regexp (concat "^\\*+[ \t]+\\(" org-todo-regexp "\\)")))
    (goto-char (point-min))
    (while (re-search-forward regexp nil t)
      (org-toodledo-sync-task))))

(defun org-toodledo-update ()
  "Insert new tasks and update previous tasks."
  (interactive)
  (let* ((server-info (org-toodledo-get-server-info))
         (account-info (org-toodledo-get-account-info))
         (changed (org-toodledo-account-changed account-info))
         (last-deleted (string-to-number (or (org-entry-get-with-inheritance "Last-deleted") "0")))
         (last-modified (string-to-number (or (org-entry-get-with-inheritance "Last-modified") "0")))
         (last-update (string-to-number (or (org-entry-get-with-inheritance "Last-sync") "0")))
         processed)
    ;; If tasks have been deleted or modified, then the Toodledo API
    ;; will give us the timestamps. We need to find out which tasks
    ;; have been deleted or modified since the last time we retrieved
    ;; the list of tasks that have been deleted or modified. We store
    ;; the last times in the properties of the root element.
    
    (if (and (assoc "deleted" changed) ;; Tasks have been deleted
             (>= (string-to-number (cdr (assoc "deleted" changed))) last-deleted))
        (setq processed
              (append (org-toodledo-process-deleted-tasks
                       last-deleted)
                       processed)))
    (if (and (assoc "modified" changed) ;; Tasks have been added or edited
             (>= (string-to-number (cdr (assoc "modified" changed)))
                last-modified))
        ;; Retrieve added/modified tasks
        (setq processed (append
                         (org-toodledo-process-modified-tasks last-modified) processed)))
    ;; TODO Look for tasks that were modified locally since the last synchronization
    (org-toodledo-process-locally-modified-tasks last-update processed)
    ;; TODO Update timestamps here
    (goto-char (point-min))
    (when (re-search-forward (concat "^\\(" outline-regexp "\\)") nil t)
      (org-entry-put (point)
                     "Last-sync"
                     (cdr (assoc "unixtime" server-info)))
      (when (assoc "lastaddedit" account-info)
        (org-entry-put (point)
                       "Last-modified"
                       (cdr 
                        (assoc "lastaddedit" account-info))))
      (when (assoc "lastdelete" account-info)
        (org-entry-put (point)
                         "Last-deleted"
                         (cdr
                          (assoc "lastdelete" account-info)))))))

(defun org-toodledo-process-locally-modified-tasks (last-update processed)
  "Synchronize tasks that were locally modified after LAST-UPDATE.
Skip tasks with IDs in PROCESSED."
  (goto-char (point-min))
  (let ((start (float-time (current-time))))
    (while (re-search-forward org-complex-heading-regexp nil t)
      ;; Look for all tasks in this buffer
      (if (match-string 2)
          ;; Is it a new task, or has it been modified since the last update?
          (let ((id (org-entry-get (point) "Toodledo-ID"))
                (modified (string-to-number (or (org-entry-get (point) "Modified") "")))
                (last-sync (if (org-entry-get (point) "Sync")
                               (string-to-number (org-entry-get (point) "Sync"))
                             0)))
            (if (or (null id)
                    (and (> modified last-sync)
                         (< modified start)
                         (not (member id processed))))
                (save-excursion (org-toodledo-sync-task))))))))

(defun org-toodledo-touch ()
  "Update the current task."
  (interactive)
  (org-entry-put (point) "Modified" (format "%d" (float-time (current-time)))))

(defvar org-toodledo-actually-delete t)
(defun org-toodledo-process-deleted-tasks (timestamp)
  "Remove tasks deleted after TIMESTAMP."
  (delq nil
        (mapcar
         (lambda (task)
           (when (org-toodledo-find-task task)
             (if org-toodledo-actually-delete
                 (delete-region (org-back-to-heading)
                                (if (re-search-forward org-complex-heading-regexp nil t)
                                    (match-beginning 0)
                                  (org-end-of-subtree)))
               (org-entry-delete (point) "Toodledo-ID")
               (org-entry-put (point) "Toodledo-Deleted" (timestamp)))
             (org-toodledo-task-id task)))
         (org-toodledo-get-deleted
          (list (cons "after" (number-to-string timestamp)))))))
  
(defun org-toodledo-process-modified-tasks (modified)
  "Handle all the tasks that have been modified since MODIFIED."
  (delq nil
        (mapcar
         (lambda (task)
           (if (org-toodledo-find-task task)
               (if (null (org-toodledo-update-task task modified))
                   (org-toodledo-task-id task))
             (org-toodledo-create-task task)))
         (org-toodledo-get-tasks (list (cons "modafter" (number-to-string modified)))))))



(defun org-toodledo-create-task (task)
  "Create a task for TASK."
  (goto-char (point-max))
  (if (point-at-eol) (insert "\n"))
  (insert (org-toodledo-task-to-string task))
  (org-toodledo-task-id task))

(defun org-toodledo-find-task (task)
  "Find the task specified by TASK."
  (goto-char (point-min))
  (re-search-forward
   (concat "^[ \t]*:Toodledo-ID:[ \t]+" (org-toodledo-task-id task) "$")
   nil t))
  
(defun org-toodledo-account-changed (account-info)
  "Return non-nil if the account has changed since the last check.
The result will be an alist of (\"modified\" . \"timestamp\") if tasks have
been added/edited and (\"deleted\" . \"timestamp\") if tasks have been deleted."
  (let ((last-modified (org-entry-get-with-inheritance "Last-modified"))
        (last-deleted (org-entry-get-with-inheritance "Last-deleted"))
        result)
    (if (> (string-to-number (or (cdr (assoc "lastaddedit" account-info)) "0"))
           (string-to-number (or last-modified "0")))
        (add-to-list 'result (cons "modified" last-modified)))
    (if (> (string-to-number (or (cdr (assoc "lastdelete" account-info)) ""))
           (string-to-number (or last-deleted "0")))
        (add-to-list 'result (cons "deleted" last-deleted)))
    result))
  
(defun org-toodledo-sync-task (&optional force)
  "Update my Toodledo for the current task."
  (interactive "P")
  (save-excursion
    (let ((task (org-toodledo-parse-current-task)))
      (if (null (org-toodledo-task-id task))
          ;; New task, create it
          (let ((result (org-toodledo-add-task task)))
            (when (eq (elt (car result) 0) 'added)
              (org-entry-put (point) "Toodledo-ID" (elt (car result) 2))
              (org-entry-put (point) "Sync"
                             (format "%d" (float-time (current-time)) 1000))))
        ;; Old task, update
        (when (org-toodledo-success-p (org-toodledo-edit-task task))
          (if (equal (org-toodledo-task-completed task) "1")
              (org-entry-put (point) "Completed" "1")
            (org-entry-put (point) "Status" (org-toodledo-task-status task)))
          (org-entry-put (point) "Sync"
                         (format "%d" (float-time (current-time)) 1000)))))))

;; (assert (equal (org-toodledo-format-date "2003-08-12") "<2003-08-12 Tue>"))
(defun org-toodledo-format-date (date &optional repeat)
  "Return yyyy-mm-dd day for DATE."
  (concat
   "<"
   (format-time-string
    "%Y-%m-%d %a"
    (cond
     ((listp date) date)
     ((numberp date) (seconds-to-time date))
     ((and (stringp date)
           (string-match "^[0-9]+$" date))
      (seconds-to-time (string-to-number date)))
     (t (apply 'encode-time (org-parse-time-string date)))))
   (if repeat (concat " " repeat) "")
   ">"))

;; (mapconcat 'org-toodledo-task-to-string temp "\n")
;; (setq task (elt temp 2))
;; (org-toodledo-task-to-string task)
(defun org-toodledo-task-to-string (task &optional level)
  "Return an Org-formatted version of TASK."
  (let* ((repeat (string-to-number (org-toodledo-task-repeat task)))
         (rep-advanced (org-toodledo-task-repeat-advanced task))
         (repeat-string (org-toodledo-repeat-to-string repeat rep-advanced))
         (priority (org-toodledo-task-priority task)))
    (concat
     (make-string (or level 2) ?*) " "
     (org-toodledo-status-to-string task) " "
     (cond
      ((equal priority "-1") "")
      ((equal priority "0") "[#C] ")
      ((equal priority "1") "[#B] ")
      ((equal priority "2") "[#A] ")
      ((equal priority "3") "[#A] "))
     (org-toodledo-task-title task)
     (if (org-toodledo-task-context task)
         (concat " :@" (org-toodledo-task-context task) ":") 
       "")
     "\n"
     (if (and (org-toodledo-task-duedate task)
              (not (equal (org-toodledo-task-duedate task) ""))
              (not (< (string-to-number (org-toodledo-task-duedate task)) 0)))
         (concat org-deadline-string " "
                 (org-toodledo-format-date
                  (org-toodledo-task-duedate task)
                  repeat-string)
                 "\n")
       "")
     (or (org-toodledo-task-note task) "") "\n"
     ":PROPERTIES:\n"
     ":Toodledo-ID: " (org-toodledo-task-id task) "\n"
     ":Modified: " (org-toodledo-task-modified task) "\n"
     ":Sync: " (format "%d" (float-time (current-time))) "\n"
     ":Effort: " (org-toodledo-task-length task) "\n"
     ":END:\n"
     )))

;; (assert (equal (org-toodledo-repeat-to-string 0) ""))
;; (assert (equal (org-toodledo-repeat-to-string 1) "+1w"))
;; (assert (equal (org-toodledo-repeat-to-string 2) "+1m"))
;; (assert (equal (org-toodledo-repeat-to-string 3) "+1y"))
;; (assert (equal (org-toodledo-repeat-to-string 4) "+1d"))
;; (assert (equal (org-toodledo-repeat-to-string 5) "+2w"))
;; (assert (equal (org-toodledo-repeat-to-string 6) "+2m"))
;; (assert (equal (org-toodledo-repeat-to-string 7) "+6m"))
;; (assert (equal (org-toodledo-repeat-to-string 8) "+3m"))
;; (assert (equal (org-toodledo-repeat-to-string 108) ".+3m"))
;; (assert (equal (org-toodledo-repeat-to-string 101) ".+1w"))
;; (assert (equal (org-toodledo-repeat-to-string 0) ""))

(defconst org-toodledo-repeat-intervals '("" "+1w" "+1m" "+1y" "+1d" "+2w" "+2m" "+6m" "+3m"))
(defun org-toodledo-status-to-string (task)
  (let ((comp (org-toodledo-task-completed task))
        (status (string-to-number (org-toodledo-task-status task))))
    (cond
     ((not (or (null comp) (equal comp "") (equal comp "0"))) "DONE")
     ((= status 0) "TODO")
     ((= status 1) "TODO")
     ((= status 2) "STARTED")
     ((= status 3) "PLAN")
     ((= status 4) "DELEGATED")
     ((= status 5) "WAITING")
     ((= status 6) "PLAN")  ; hold
     ((= status 7) "SOMEDAY")  ; postponed
     ((= status 8) "SOMEDAY")
     ((= status 9) "CANCELLED")
     )))

(defun org-toodledo-repeat-to-string (repeat &optional rep-advanced)
  "Turn TASK into a repeat sequence."
  (cond
   ((= repeat 0) nil)
   ((> repeat 100) (concat "+" (org-toodledo-repeat-to-string (mod repeat 100) rep-advanced)))
   ((and (= repeat 50) rep-advanced)
    (cond
     ((string-match "Every \\([0-9]+\\) week" rep-advanced)
      (concat "+" (match-string 1 rep-advanced) "w"))
     ((string-match "Every \\([0-9]+\\) month" rep-advanced)
      (concat "+" (match-string 1 rep-advanced) "m"))
     ((string-match "Every \\([0-9]+\\) year" rep-advanced)
      (concat "+" (match-string 1 rep-advanced) "y"))
     ((string-match "Every \\([0-9]+\\) day" rep-advanced)
      (concat "+" (match-string 1 rep-advanced) "d"))
     (t rep-advanced)))
   (t (elt org-toodledo-repeat-intervals repeat))))

(defun org-toodledo-delete-current-task ()
  "Delete the current task."
  (interactive)
  (org-back-to-heading t)
  (let ((task (org-toodledo-parse-current-task)))
    (and (> (length (org-toodledo-task-id task)) 0)
         (org-toodledo-success-p (org-toodledo-delete-task task)))
    (delete-region
     (point)
     (if (and (end-of-line)
              (re-search-forward org-complex-heading-regexp nil t))
         (match-beginning 0)
       (org-end-of-subtree t t)
       (point)))))

  
(defun org-toodledo-task-get-prop (task prop) (cdr (assoc prop task)))
(defmacro org-toodledo-task-prop-defun (field)
  `(defun ,(intern (concat "org-toodledo-task-" field)) (task)
     (cdr (assoc ,field task))))

(defun org-toodledo-success-p (result)
  "Return non-nil if RESULT indicates success."
  (eq (car (car result)) 'success))
        
(org-toodledo-task-prop-defun "id")
(org-toodledo-task-prop-defun "title")
(org-toodledo-task-prop-defun "status")
(org-toodledo-task-prop-defun "completed")
(org-toodledo-task-prop-defun "repeat")
(org-toodledo-task-prop-defun "context")
(org-toodledo-task-prop-defun "duedate")
(org-toodledo-task-prop-defun "modified")
(org-toodledo-task-prop-defun "priority")
(org-toodledo-task-prop-defun "note")
(org-toodledo-task-prop-defun "length")
;; defun'd separately because of the change in name
(defun org-toodledo-task-repeat-advanced (task)
  (cdr (assoc "rep_advanced" task)))

(defvar org-toodledo-contexts nil "An alist of (context . id).")
(defun org-toodledo-get-contexts (&optional force)
  "Store an alist of (context . id) in `org-toodledo-contexts'.
Reload if FORCE is non-nil."
  (if (or force (null org-toodledo-contexts))
      (setq org-toodledo-contexts
            (mapcar
             (lambda (node)
               (cons
              (car (xml-node-children node))
              (xml-get-attribute node 'id)))
             (xml-get-children (car
                                (org-toodledo-call-method "getContexts")) 'context)))
    org-toodledo-contexts))

(defun org-toodledo-agenda-touch ()
  "Update the Modified timestamp for the current entry in the agenda."
  (org-agenda-check-type t 'agenda 'timeline)
  (org-agenda-check-no-diary)
  (let* ((marker (or (org-get-at-bol 'org-marker)
                     (org-agenda-error)))
         (buffer (marker-buffer marker))
         (pos (marker-position marker)))
    (org-with-remote-undo buffer
     (with-current-buffer buffer
       (widen)
       (goto-char pos)
       (if (org-entry-get (point) "Modified")
           (org-entry-put (point) "Modified" (format "%d" (float-time (current-time)))))))))


(defun org-toodledo-update-task (task &optional last-update)
  (let* ((modified (string-to-number (or (org-entry-get (point) "Modified") "")))
         (last-sync (if (org-entry-get (point) "Sync")
                        (string-to-number (org-entry-get (point) "Sync"))
                      0))
         (level (car (org-heading-components)))
         (locally-modified (> modified last-sync)))
    ;; Locally modified? keep
    (if locally-modified
        nil
      ;; Not locally modified? replace
      ;; Figure out what our level is
      (delete-region (org-back-to-heading)
                     (progn (goto-char (match-end 0))
                            (if (re-search-forward org-complex-heading-regexp nil t)
                                (goto-char (match-beginning 0))
                              (org-end-of-subtree))))
      (insert (org-toodledo-task-to-string task level))
      t)))

(provide 'org-toodledo)