| 1 | ;;+----------------------------------------------------------------------------- |
|---|
| 2 | ;;+ Isidorus |
|---|
| 3 | ;;+ (c) 2008-2010 Marc Kuester, Christoph Ludwig, Lukas Georgieff |
|---|
| 4 | ;;+ |
|---|
| 5 | ;;+ Isidorus is freely distributable under the LLGPL license. |
|---|
| 6 | ;;+ You can find a detailed description in trunk/docs/LLGPL-LICENSE.txt and |
|---|
| 7 | ;;+ trunk/docs/LGPL-LICENSE.txt. |
|---|
| 8 | ;;+----------------------------------------------------------------------------- |
|---|
| 9 | |
|---|
| 10 | (defpackage :atom |
|---|
| 11 | (:use :cl :cxml :constants :xml-tools :datamodel :drakma :isidorus-threading) |
|---|
| 12 | (:export :collection-feed |
|---|
| 13 | :defsite |
|---|
| 14 | :dependency |
|---|
| 15 | :entries |
|---|
| 16 | :feed |
|---|
| 17 | :feed-to-elem |
|---|
| 18 | :feed-to-string |
|---|
| 19 | :fragments-feed |
|---|
| 20 | :id |
|---|
| 21 | :link |
|---|
| 22 | :parse-fragments-feed |
|---|
| 23 | :parse-snapshots-feed |
|---|
| 24 | :path |
|---|
| 25 | :psi |
|---|
| 26 | :snapshots-feed |
|---|
| 27 | :source-locator-prefix |
|---|
| 28 | :subfeeds |
|---|
| 29 | :tm-id |
|---|
| 30 | :tm-id-p |
|---|
| 31 | :updated |
|---|
| 32 | :*base-url* |
|---|
| 33 | :*tm-feed*)) |
|---|
| 34 | |
|---|
| 35 | (in-package :atom) |
|---|
| 36 | |
|---|
| 37 | ;;General configuration options |
|---|
| 38 | (defparameter *base-url* "") ;*base-url* is set by hunchentoot |
|---|
| 39 | (defvar *author*) |
|---|
| 40 | (defvar *source-locator*) |
|---|
| 41 | (defparameter *tm-feed* nil) |
|---|
| 42 | (defvar *testtm-snapshotfeed*) |
|---|
| 43 | (defvar *testtm-fragmentfeed*) |
|---|
| 44 | (defvar *testtm-feed*) |
|---|
| 45 | (defvar *testtm-toplevel*) |
|---|
| 46 | (defvar *testtm-snapshots-feed*) |
|---|
| 47 | (defvar *testtm-fragments-feed*) |
|---|
| 48 | |
|---|
| 49 | (defun datetime-in-iso-format (&optional (seconds-since-epoch (get-universal-time))) |
|---|
| 50 | "Formats a time (seconds since epoch) in ISO format. If no parameter |
|---|
| 51 | is given, return the current time in ISO format" |
|---|
| 52 | (multiple-value-bind |
|---|
| 53 | (second minute hour date month year day daylight-p zone) |
|---|
| 54 | (decode-universal-time seconds-since-epoch) |
|---|
| 55 | (declare (ignore day)) |
|---|
| 56 | (let* |
|---|
| 57 | ((offset |
|---|
| 58 | (if daylight-p 0 -1)) |
|---|
| 59 | (timezone |
|---|
| 60 | (if (>= (+ zone offset) 0) |
|---|
| 61 | (format nil "+~2,'0d:00" zone) |
|---|
| 62 | (format nil "-~2,'0d:00" (abs zone))))) |
|---|
| 63 | (format nil "~d-~2,'0d-~2,'0dT~2,'0d:~2,'0d:~2,'0d~@d" |
|---|
| 64 | year month date hour minute second timezone)))) |
|---|
| 65 | |
|---|
| 66 | (defun get-most-recent-datetime () |
|---|
| 67 | "Gets the most datetime of the most recent revision" |
|---|
| 68 | (datetime-in-iso-format (apply #'max (get-all-revisions)))) |
|---|
| 69 | |
|---|
| 70 | (defun get-most-recent-datetime-for-tm (tm-id) |
|---|
| 71 | "Gets the most datetime of the most recent revision for a given TM" |
|---|
| 72 | (datetime-in-iso-format (apply #'max (get-all-revisions-for-tm tm-id)))) |
|---|
| 73 | |
|---|
| 74 | (defun to-link (href rel &optional application-type) |
|---|
| 75 | "Generates an Atom link element. The function is expected to be used |
|---|
| 76 | in the in-feed macro" |
|---|
| 77 | (cxml:with-element "a:link" |
|---|
| 78 | (cxml:attribute "rel" rel) |
|---|
| 79 | ;goes around a stupid IE bug |
|---|
| 80 | (when application-type |
|---|
| 81 | (cxml:attribute "type" application-type)) |
|---|
| 82 | (cxml:attribute "href" href))) |
|---|
| 83 | |
|---|
| 84 | (defun to-elem (qname text) |
|---|
| 85 | (cxml:with-element qname (if (typep text 'string) (cxml:text text) (format nil "~a" text)))) |
|---|
| 86 | |
|---|
| 87 | (defmacro in-feed ((title subtitle author link) &body body) |
|---|
| 88 | "Builds the element structure for the outer elements of an Atom feed. Expects a list of entry elements in the body" |
|---|
| 89 | `(cxml:with-namespace ("a" *atom-ns*) |
|---|
| 90 | (cxml:with-namespace ("e" *egovpt-ns*) |
|---|
| 91 | (cxml:with-element "a:feed" |
|---|
| 92 | (to-elem "a:title" ,title) |
|---|
| 93 | (when ,subtitle |
|---|
| 94 | (to-elem "a:subtitle" ,subtitle)) |
|---|
| 95 | (to-elem "a:id" ,link) |
|---|
| 96 | (cxml:with-element "a:author" |
|---|
| 97 | (to-elem "a:name" ,author)) |
|---|
| 98 | (to-link ,link "self") |
|---|
| 99 | ,@body)))) |
|---|
| 100 | |
|---|
| 101 | (defmacro feed-to-string (&body body) |
|---|
| 102 | "Serialize a feed as a string" |
|---|
| 103 | `(cxml:with-xml-output (cxml:make-string-sink :canonical t) |
|---|
| 104 | ,@body)) |
|---|
| 105 | |
|---|
| 106 | (defclass atom-element () |
|---|
| 107 | ((title :accessor title :initarg :title) |
|---|
| 108 | (updated :accessor updated :initarg :updated :initform nil) |
|---|
| 109 | (id :accessor id :initarg :id :initform (error "An atom element must always have an id")) |
|---|
| 110 | (path :accessor path :initarg :path) |
|---|
| 111 | (link :initarg :link))) |
|---|
| 112 | |
|---|
| 113 | (defgeneric link (ae) |
|---|
| 114 | (:documentation "calculate the link of an atom element (entry or feed). The link can be realized as a self link (for feeds) or an alternate link (for entries)")) |
|---|
| 115 | |
|---|
| 116 | (defmethod link ((ae atom-element)) |
|---|
| 117 | (if (slot-boundp ae 'link) |
|---|
| 118 | (slot-value ae 'link) |
|---|
| 119 | (format nil "~a/~a" *base-url* (path ae)))) |
|---|
| 120 | |
|---|
| 121 | (defclass feed (atom-element) |
|---|
| 122 | ((subtitle :accessor subtitle :initarg :subtitle :initform nil) |
|---|
| 123 | (author :accessor author :initarg :author) |
|---|
| 124 | (entries :accessor entries :initarg :entries :initform nil |
|---|
| 125 | :type list) |
|---|
| 126 | (subfeeds :accessor subfeeds :initarg :subfeeds :initform nil |
|---|
| 127 | :type list)) |
|---|
| 128 | (:documentation "abstract class for atom feeds")) |
|---|
| 129 | |
|---|
| 130 | |
|---|
| 131 | (defclass entry (atom-element) |
|---|
| 132 | () |
|---|
| 133 | (:documentation "Class that represents a minimalistic entry in an |
|---|
| 134 | Atom feed. Concrete classes implement the correct behaviour for the |
|---|
| 135 | individual entry types in the Atom protocol")) |
|---|
| 136 | |
|---|
| 137 | (defgeneric register-entry (feed entry) |
|---|
| 138 | (:documentation "Register an entry for a given feed")) |
|---|
| 139 | |
|---|
| 140 | (defmethod register-entry ((feed feed) (entry entry)) |
|---|
| 141 | (push entry (slot-value feed 'entries))) |
|---|
| 142 | |
|---|
| 143 | (defgeneric register-subfeed (feed subfeed) |
|---|
| 144 | (:documentation "Register a subfeed for a given feed")) |
|---|
| 145 | |
|---|
| 146 | (defmethod register-subfeed ((feed feed) (subfeed feed)) |
|---|
| 147 | (push subfeed (subfeeds feed))) |
|---|
| 148 | |
|---|
| 149 | (defgeneric entry-to-elem (entry) |
|---|
| 150 | (:documentation "build an entry element")) |
|---|
| 151 | |
|---|
| 152 | (defmethod entry-to-elem :around ((entry entry)) |
|---|
| 153 | (cxml:with-namespace ("a" *atom-ns*) |
|---|
| 154 | (cxml:with-namespace ("e" *egovpt-ns*) |
|---|
| 155 | (cxml:with-element "a:entry" |
|---|
| 156 | (to-elem "a:title" (title entry)) |
|---|
| 157 | (to-elem "a:id" (id entry)) |
|---|
| 158 | (to-link (link entry) "alternate") ;this version of the alternate link works around an IE bug |
|---|
| 159 | (call-next-method) |
|---|
| 160 | (to-elem "a:updated" (updated entry)))))) |
|---|
| 161 | |
|---|
| 162 | (defmethod entry-to-elem ((entry entry)) |
|---|
| 163 | ;do nothing |
|---|
| 164 | ) |
|---|
| 165 | |
|---|
| 166 | (defmethod id ((entry entry)) |
|---|
| 167 | ;TODO: consider to revisit that convention |
|---|
| 168 | (format nil "~a/~a" (slot-value entry 'id) "entry")) |
|---|
| 169 | |
|---|
| 170 | (defclass overview-entry (entry) |
|---|
| 171 | ((author :accessor author :initarg :author)) |
|---|
| 172 | (:documentation "Class that represents an entry for a collection in an overview feed")) |
|---|
| 173 | |
|---|
| 174 | (defmethod entry-to-elem ((entry overview-entry)) |
|---|
| 175 | (to-link (link entry) "alternate" "application/atom+xml") |
|---|
| 176 | (cxml:with-element "a:author" |
|---|
| 177 | (to-elem "a:name" (author entry))) |
|---|
| 178 | (setf (updated entry) (get-most-recent-datetime-for-tm (slot-value entry 'id))) |
|---|
| 179 | (to-link (link entry) "http://www.egovpt.org/sdshare/collectionfeed" "application/atom+xml")) |
|---|
| 180 | |
|---|
| 181 | (defgeneric feed-to-elem (feed) |
|---|
| 182 | (:documentation "Render an Atom feed to XML")) |
|---|
| 183 | |
|---|
| 184 | (defmethod feed-to-elem :around ((feed feed)) |
|---|
| 185 | (in-feed |
|---|
| 186 | ((title feed) (subtitle feed) (author feed) (link feed)) |
|---|
| 187 | (call-next-method) |
|---|
| 188 | (to-elem "a:updated" (updated feed)) |
|---|
| 189 | (map 'list #'entry-to-elem (entries feed)))) |
|---|
| 190 | |
|---|
| 191 | (defmethod feed-to-elem ((feed feed)) |
|---|
| 192 | (setf (updated feed) (get-most-recent-datetime))) |
|---|
| 193 | |
|---|
| 194 | ;; (defun build-overview-feed () |
|---|
| 195 | ;; (feed-to-elem *tm-feed*)) |
|---|