;;; --------------------------------------------------------------------
;;; Lisp Server Pages (LSP) -- Implements something like Java Server
;;; Pages (JSP), but for Lisp.
;;;
;;; Copyright 2001, 2002 I/NET Inc. (http://www.inetmi.com/)
;;; John Wiseman (jjwiseman@yahoo.com)
;;; 2002-06-10
;;; Licensed under the MIT license--see the accompanying LICENSE.txt
;;; file.
;;;
;;;
;;; * Introduction
;;;
;;; Java Server Pages are a way to make web pages that are dynamic, by
;;; embedding Java code in HTML. Similarly, Lisp Server Pages allow
;;; you to make dynamic web pages that contain Lisp code.
;;;
;;; To publish an LSP page, call the PUBLISH-LSP function:
;;;
;;; PUBLISH-LSP (&key path file server) [function]
;;; Publishes the LSP file FILE at the URL prefix PATH, on SERVER
;;; (defaults to the default AllegroServe server, *wserver*). Example:
;;; (publish-lsp :path "/temp.html" :file "/Users/wiseman/src/temperature.lsp")
;;;
;;; An LSP file looks just like an HTML file, except for two new tags:
;;; <% ... %> and <%= ... %>.
;;;
;;; <% ... %> is a scriptlet tag (to use the JSP terminology), and
;;; wraps lisp code. For example, <% (dotimes (i 10) (beep)) %>. The
;;; code inside the tag is executed each time the page is requested.
;;;
;;; <%= ... %> is an expression tag, and the effect of this tag is to
;;; evaluate the contents as if they were wrapped with the Franz
;;; net.html.generator:html macro. For example,
;;;
;;; <%= (:h1 "header") "hi" (:princ-safe (generate-footer)) %>
;;;
;;; is equivalent to
;;;
;;; (net.html.generator:html
;;; (:h1 "header")
;;; "hi"
;;; (:princ-safe (generate-footer)))
;;;
;;; which will output something like the following HTML:
;;;
;;;
header
hi
2002-06-09
;;;
;;; During execution of LSP code, the following two variables will be
;;; bound:
;;;
;;; request -- The HTTP request object containing all the
;;; information about the request.
;;; entity -- The information passed to the publish-lsp function.
;;;
;;; (See the AllegroServe documentation for more information on these
;;; objects.)
;;;
;;;
;;; * Tips
;;;
;;; Expressions can be used inside HTML attributes, e.g.,
;;;
;;;
;;;
;;; Scriptlets do not need to be complete lisp forms, as long as the
;;; page as a whole is syntactically valid, e.g.,
;;;
;;; <% (dotimes (i 10) %>
;;;
;;; <% ) %>
;;;
;;;
;;; * Implementation Notes and Caveats
;;;
;;; LSP pages are converted to strings containing lisp code, which are
;;; then compiled and cached. If the source file containing the lsp
;;; code is modified, the next time a request is made for that page
;;; the code will be recompiled and recached.
;;;
;;; In my first attempt to do this, I tried to construct forms instead
;;; of strings. That just made it trickier to separate forms across
;;; <% ... %> tags (see the dotimes example above). Just because it's
;;; bad that other languages are often *limited* to manipulating code
;;; as strings doesn't mean there aren't times where it's appropriate.
;;;
;;; There's nothing like JSP's directives or declarations.
;;;
;;; LSP Requires Franz' AllegroServe
;;; (http://allegroserve.sourceforge.net/) or Portable AllegroServe
;;; (http://portableaserve.sourceforge.net/).
;;;
;;; See http://sourceforge.net/projects/lsp for a more serious attempt
;;; at doing this right, by Sunil Mishra and Tim Bradshaw.
(require :aserve)
(defpackage :com.lemonodor.lsp
(:use #:common-lisp)
(:export #:publish-lsp #:request #:entity))
(in-package :com.lemonodor.lsp)
(defun publish-lsp (&key path file (server net.aserve:*wserver*))
"Publishes an LSP file. PATH is a string containing the name part
of the URL at which to publish the file, e.g. \"/math/sum.lsp\";
FILE is a pathname that specifies the file containing the page
to publish."
(net.aserve:publish :path path
:server server
:function #'(lambda (request entity)
(do-lsp-request request entity file))))
(defun do-lsp-request (request entity file)
"Handles the request for an LSP URL."
(funcall (get-lsp-function file) request entity))
(defvar *lsp-functions* (make-hash-table :test #'equal)
"The table mapping LSP filespecs to function-time pairs.")
(defun get-lsp-function (file)
"Returns the function implementing a given LSP file. Builds and
compiles the function the first time it's requested, or if the file
has been modified."
(let ((func.time (gethash file *lsp-functions*)))
(if (or (null func.time)
(> (file-write-date file) (cdr func.time)))
(register-lsp-function file
(construct-lsp-function (contents-of-file file)))
(car func.time))))
(defun register-lsp-function (file function)
(setf (gethash file *lsp-functions*) (cons function (get-universal-time)))
function)
(defun construct-lsp-function (lsp-string)
"Builds and compiles the request-handling LSP function for the page
whose contents are in LSP-STRING."
(let ((form
`(lambda (request entity)
(net.aserve:with-http-response (request entity)
(net.aserve:with-http-body (request entity)
;; We punt hard on the issue of package.
,(read-from-string
(format nil "(progn ~A)"
(construct-lsp-body-string lsp-string))))))))
(compile nil form)))
(defun contents-of-file (pathname)
"Returns a string with the entire contents of the specified file."
;; This is excl:file-contents in ACL.
(with-output-to-string (contents)
(with-open-file (in pathname :direction :input)
(let* ((buffer-size 4096)
(buffer (make-string buffer-size)))
(labels ((read-chunks ()
(let ((size (read-sequence buffer in)))
(if (< size buffer-size)
(princ (subseq buffer 0 size) contents)
(progn
(princ buffer contents)
(read-chunks))))))
(read-chunks))))))
;; (i) Converts text outside <% ... %> tags (straight HTML) into calls
;; to net.html.generator.html, (ii) Text inside <% ... %>
;; ("scriptlets") is straight lisp code, (iii) Text inside <%= ... %>
;; ("expressions") becomes the body of the net.html.generator:html
;; macro.
(defun construct-lsp-body-string (lsp-string &optional (start 0))
"Takes a string containing an LSP page and returns a string
containing the lisp code that implements that page."
(multiple-value-bind (start-tag start-code tag-type)
(next-code lsp-string start)
(if (not start-tag)
(format nil "(net.html.generator:html ~S)" (subseq lsp-string start))
(let ((end-code (search "%>" lsp-string :start2 start-code)))
(if (not end-code)
(error "EOF reached in LSP inside open '<%' tag.")
(format nil "(net.html.generator:html ~S) ~A ~A"
(subseq lsp-string start start-tag)
(format nil (tag-template tag-type)
(subseq lsp-string start-code end-code))
(construct-lsp-body-string lsp-string (+ end-code 2))))))))
;; Finds the next scriptlet or expression tag in LSP source. Returns
;; nil if none are found, otherwise returns 3 values:
;; 1. The position of the opening bracket (<) of the tag.
;; 2. The position of the contents of the tag.
;; 3. The type of tag (:scriptlet or :expression).
(defun next-code (string start)
(let ((start-tag (search "<%" string :start2 start)))
(if (not start-tag)
nil
(if (and (> (length string) (+ start-tag 2))
(eql (char string (+ start-tag 2)) #\=))
(values start-tag (+ start-tag 3) :expression)
(values start-tag (+ start-tag 2) :scriptlet)))))
;; Given a tag type (:scriptlet or :expression), returns a format
;; string to be used to generate source code from the contents of the
;; tag.
(defun tag-template (tag-type)
(ecase tag-type
((:scriptlet) "~A")
((:expression) "(net.html.generator:html ~A)")))