Automated commit for upstream build of version 0.9.1.1
[cl-rss.git] / main.lisp
1 ;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
2 ;;;; *************************************************************************
3 ;;;; FILE IDENTIFICATION
4 ;;;;
5 ;;;; Name:          main.lisp
6 ;;;; Purpose:       Main RSS functions
7 ;;;; Programmer:    Kevin M. Rosenberg
8 ;;;; Date Started:  Sep 2003
9 ;;;;
10 ;;;; $Id$
11 ;;;; *************************************************************************
12
13 (in-package #:rss)
14
15 (defclass rss-channel ()
16   ((title :reader title)
17    (link :reader link)
18    (description :reader description :initform nil)
19    (language :reader language :initform nil)
20    (image :reader image :initform nil)
21    (pub-date :reader pub-date :initform nil)
22    (last-build-date :reader last-build-date :initform nil)
23
24    (items :accessor items :initform nil)))
25
26 (defclass rss-item ()
27   ((title :reader title)
28    (link :reader link)
29    (description :reader description :initform nil)
30    (pub-date :reader pub-date :initform nil)))
31
32 (defclass rss-image ()
33   ((url :reader url)
34    (title :reader title)
35    (link :reader link)
36    (width :reader width :initform nil)
37    (height :reader height :initform nil)
38    (description :reader description :initform nil)))
39
40 (defvar *sites*
41     '("http://www.cliki.net/recent-changes.rdf"))
42
43 (defun show-sites (&optional (sites *sites*))
44   (dolist (site (mklist sites))
45     (awhen (rss-site site)
46            (display-site it))))
47
48 (defun display-site (site &key (stream *standard-output*))
49   (format stream "Site: ~A~%" (title site))
50   (dolist (item (items site))
51     (format stream "  ~A~%" (title item))))
52
53 (defun rss-site (uri)
54   (multiple-value-bind (body response headers true-uri)
55       (net.aserve.client:do-http-request uri)
56     (declare (ignore true-uri headers))
57     (when (eql 200 response)
58       (with-input-from-string (strm body)
59         (parse-rss-stream strm)))))
60
61 (defun parse-rss-file (file)
62   (with-open-file (stream file :direction :input)
63     (parse-rss-stream stream)))
64
65 (defun is-rss-version-supported (version-string)
66   (and (member version-string '("0.91" "0.92" "2.0") :test #'string=) t))
67
68 (define-condition rss-parse-error (error)
69   ((msg :initarg :msg :reader msg))
70   (:documentation "Thrown when PARSE-RSS-STREAM encounters invalid RSS data.")
71   (:report
72    (lambda (condition stream)
73      (format stream "Parse error reading RSS~@[. ~A~]" (msg condition)))))
74
75 (define-condition rss-version-unsupported (rss-parse-error)
76   ((version :initarg :version :reader version))
77   (:documentation
78    "Thrown when PARSE-RSS-STREAM encounters RSS of a version it doesn't
79 recognise.")
80   (:report
81    (lambda (condition stream)
82      (format stream "Unexpected RSS version: ~S" (version condition)))))
83
84 (defmacro string=-case (keyform (&rest cases) &optional &body otherwise)
85   "A version of CASE that tests using string=."
86   (let ((key (gensym)) (expected))
87     `(let ((,key ,keyform))
88        (cond
89          ,@(mapcar
90             (lambda (form)
91               (destructuring-bind (string &body body) form
92                 (unless (stringp string)
93                   (error "Can only deal with strings as keys."))
94                 (push string expected)
95                 `((string= ,string ,key) ,@body)))
96             cases)
97          (t
98           ,@otherwise)))))
99
100 (defun setf-unique-slot (object name value)
101   "Set the slot with the given NAME in OBJECT to VALUE, throwing an error if it
102 was already set."
103   (when (and (slot-boundp object name) (slot-value object name))
104     (error 'rss-parse-error
105            :msg (format nil "<~A> should only be specified once in the node."
106                         name)))
107   (setf (slot-value object name) value))
108
109 (defun setf-unique-string (object name node)
110   "Set the slot with the given NAME in OBJECT to the string contents of NODE,
111 throwing an error if it was already set, or if they aren't a string. Used for
112 elements like <title> and <link>, which shouldn't crop up twice."
113   (let ((string (car (xmls:xmlrep-children node))))
114     ;; skip empty nodes
115     (when string
116         (progn
117           (unless (stringp string)
118             (error 'rss-parse-error
119                    :msg (format nil "Got ~A when expecting string for contents of <~A>"
120                                 string name)))
121           (setf-unique-slot object name string)))))
122
123 (defun ensure-string-slots-filled (object required-slots strict?)
124   "For each slot in REQUIRED-SLOTS, if it is unbound in OBJECT, set it to the
125 empty string if STRICT? is NIL, or throw an error if true."
126   (dolist (slot required-slots)
127     (unless (and (slot-boundp object slot) (slot-value object slot))
128       (if strict?
129           (error 'rss-parse-error
130                  :msg (format nil "Required field ~A not set for object." slot))
131           (setf (slot-value object slot) "")))
132     (when (and strict? (not (stringp (slot-value object slot))))
133       (error 'rss-parse-error
134              :msg (format nil "Slot ~A is set to ~A, which is not a string."
135                           slot (slot-value object slot)))))
136   (values))
137
138 (defun parse-type (class child-parser node strict? required-string-slots)
139   (let ((object (make-instance class)))
140     (map nil
141          (lambda (subnode) (funcall child-parser subnode object strict?))
142          (xmls:xmlrep-children node))
143     (ensure-string-slots-filled object required-string-slots strict?)
144     object))
145
146 (eval-when (:compile-toplevel :load-toplevel :execute)
147   (defun symbol-to-name (sym)
148     "Convert symbols in the form SOME-SYM to RSS-style camelCase (a lopsided
149 camel, it seems)."
150     (let ((parts (split-alphanumeric-string (symbol-name sym))))
151       (format nil "~A~{~A~}"
152               (string-downcase (first parts))
153               (mapcar #'string-capitalize (cdr parts))))))
154
155 (defmacro def-child-parser (name (&rest unique-strings)
156                             &rest complicated-forms)
157   "Define a parser that sets UNIQUE-STRINGS in the obvious
158 way. COMPLICATED-FORMS should be lists (KEY &body BODY) where KEY is a string
159 and BODY is performed with ITEM set to the item we're modifying and NODE set to
160 the XML node we just got."
161   `(defun ,name (node object strict?)
162      (declare (ignorable strict?))
163      ;; skip atom-related tags
164      (when (equalp "http://www.w3.org/2005/Atom" (xmls:node-ns node))
165        (return-from ,name nil))
166      (string=-case (xmls:xmlrep-tag node)
167          (,@(mapcar
168              (lambda (sym) `(,(symbol-to-name sym)
169                               (setf-unique-string object ',sym node)))
170              unique-strings)
171           ,@complicated-forms))))
172
173 (def-child-parser parse-item-child (title link description pub-date))
174 (defun parse-item (node strict?)
175   (parse-type 'rss-item 'parse-item-child node strict? '(title link)))
176
177 (def-child-parser parse-image-child (url title link width height description))
178 (defun parse-image (node strict?)
179   (parse-type 'rss-image 'parse-image-child node strict? '(url title link)))
180
181 (def-child-parser parse-channel-child
182     (title link description language pub-date last-build-date)
183   ("item" (push (parse-item node strict?) (items object)))
184   ("image" (setf-unique-slot object 'image (parse-image node strict?))))
185
186 (defun parse-rss-stream (str &key err strict?)
187   "Parse RSS data from STR, which can be either a stream or a string. If ERR,
188 then throw an error when something goes wrong, otherwise just return NIL. If
189 STRICT?, check more carefully for whether the document fails to follow the RSS
190 2.0 specification (which incorporates 0.9x)"
191   (handler-case
192       (let* ((*package* (find-package 'kmrcl))
193              (tree (xmls:parse str :compress-whitespace t))
194              (children (xmls:xmlrep-children tree))
195              (version (xmls:xmlrep-attrib-value "version" tree nil)))
196
197         (unless (string= "rss" (xmls:xmlrep-tag tree))
198           (error 'rss-parse-error :msg "Data doesn't claim to be RSS."))
199         (unless (is-rss-version-supported version)
200           (error 'rss-version-unsupported :version version))
201         (unless (and (= 1 (length children))
202                      (string= "channel" (xmls:xmlrep-tag (first children))))
203           (error 'rss-parse-error
204                  :msg "<rss> should have one child, a <channel>."))
205
206         (let ((channel (first children))
207               (rss (make-instance 'rss-channel)))
208
209           (map nil (lambda (child) (parse-channel-child child rss strict?))
210                (xmls:xmlrep-children channel))
211
212           (ensure-string-slots-filled
213            rss
214            `(title link description
215                    ,@(when (string= version "0.91") '(language)))
216            strict?)
217
218           (unless (or (not strict?) (string= version "2.0") (image rss))
219             (error 'rss-parse-error
220                    :msg "<rss> should have <image> specified for version <2.0"))
221
222           (setf (items rss) (nreverse (items rss)))
223           rss))
224
225     (rss-parse-error (e)
226       (when err (error e))
227       nil)))