;;;; XML-RPC library for Corman Lisp - Version 1.1 ;;;; ;;;; Copyright (C) 2000 Christopher Double. All Rights Reserved. ;;;; Adapted for CMUCL and CLISP, February 2001 by Eric Marsden. ;;;; ;;;; License ;;;; ======= ;;;; This software is provided 'as-is', without any express or implied ;;;; warranty. In no event will the author be held liable for any damages ;;;; arising from the use of this software. ;;;; ;;;; Permission is granted to anyone to use this software for any purpose, ;;;; including commercial applications, and to alter it and redistribute ;;;; it freely, subject to the following restrictions: ;;;; ;;;; 1. The origin of this software must not be misrepresented; you must ;;;; not claim that you wrote the original software. If you use this ;;;; software in a product, an acknowledgment in the product documentation ;;;; would be appreciated but is not required. ;;;; ;;;; 2. Altered source versions must be plainly marked as such, and must ;;;; not be misrepresented as being the original software. ;;;; ;;;; 3. This notice may not be removed or altered from any source ;;;; distribution. ;;;; ;;;; Notes ;;;; ===== ;;;; See the examples at the end of the file for useage. It has been tested ;;;; with at least version 1.41 of Corman Lisp available at ;;;; http://www.corman.net ;;;; ;;;; More recent versions of this software may be available at: ;;;; http://www.double.co.nz/cl ;;;; ;;;; Comments, suggestions and bug reports to the author, ;;;; Christopher Double, at: chris@double.co.nz ;;;; ;;;; 09/10/2000 - 1.0 ;;;; Initial release. ;;;; Examples at end of file. Still need to implement date/time ;;;; and base64. ;;;; (defpackage :XMLRPC (:use :common-lisp) (:export #:xml-rpc-send #:xmlrpc-fault #:xmlrpc-fault-code #:xmlrpc-fault-string #:define-xmlrpc-client-method)) (in-package :xmlrpc) ;; implementation-specific code #+cmu (defmacro with-client-socket ((socket &key host port) &body body) `(let ((,socket (ext:connect-to-inet-socket ,host ,port))) (unwind-protect (progn ,@body) (unix:unix-close ,socket)))) #+clisp (defmacro with-client-socket ((socket &key host port) &body body) `(let ((,socket (socket:socket-connect ,port ,host))) (unwind-protect (progn ,@body) (close ,socket)))) ;; this doesn't really work, because we need EOL conversion #+openmcl (defmacro with-client-socket ((socket &key host port) &body body) `(let ((,socket (ccl:make-socket :type :stream :format :text :remote-host ,host :remote-port ,port))) (unwind-protect (progn ,@body) (close ,socket)))) #-corman (defmacro with-socket-stream ((stream socket) &body body) `(let ((,stream (make-fd-stream/http ,socket))) (unwind-protect (progn ,@body) (close ,stream)))) #+cmu (defun make-fd-stream/http (socket) (let* ((stream (sys:make-fd-stream socket :input t :output t)) (orig-char-in (lisp::fd-stream-in stream)) (orig-char-out (lisp::fd-stream-out stream)) (orig-sout (lisp::fd-stream-sout stream))) ;; CR/LF translation, from Douglas Crosher (flet ((char-in (stream eof-error eof-value) (let ((char (funcall orig-char-in stream eof-error eof-value))) (cond ((and (eql char #\Return) (eql (peek-char nil stream) #\Linefeed)) (funcall orig-char-in stream nil nil) #\Linefeed) (t char)))) (char-out (stream char) (when (eql char #\Newline) (funcall orig-char-out stream #\Return)) (funcall orig-char-out stream char)) (sout (stream string start end) (declare (type (simple-base-string *) string)) (let ((start (or start 0)) (end (or end (length string)))) (declare (fixnum start end)) (loop (let ((break (position #\Newline string :start start :end end))) (unless break (return (funcall orig-sout stream string start end))) (funcall orig-sout stream string start break) (funcall orig-char-out stream #\Return) (funcall orig-char-out stream #\Newline) (setq start (1+ break))))))) (setf (lisp::fd-stream-in stream) #'char-in) (setf (lisp::fd-stream-out stream) #'char-out) (setf (lisp::fd-stream-sout stream) #'sout)) stream)) #+clisp (defun make-fd-stream/http (socket) socket) #+openmcl (defun make-fd-stream/http (socket) socket) ;; First define META routines to parse XML-RPC responses (defmacro match (x) (etypecase x (character `(when (and (< index end) (eql (char string index) ',x)) (incf index))) (string `(let ((old-index index)) ; 'old-index' is a lexical variable. (or (and ,@(map 'list #'(lambda (c) `(match ,c)) x)) (progn (setq index old-index) nil)))))) (defmacro match-type (x v) `(when (and (< index end) (typep (char string index) ',x)) (setq ,v (char string index)) (incf index))) (defstruct (meta (:print-function (lambda (m s d &aux (char (meta-char m)) (form (meta-form m))) (declare (ignore d)) (ecase char ((#\@ #\! #\$) (format s "~A~A" char form)) (#\[ (format s "[~{~A~^ ~}]" form)) (#\{ (format s "{~{~A~^ ~}}" form)))))) char form) (defun compileit (x) (typecase x (meta (ecase (meta-char x) (#\! (meta-form x)) (#\[ `(and ,@(mapcar #'compileit (meta-form x)))) (#\{ `(or ,@(mapcar #'compileit (meta-form x)))) (#\$ `(not (do ()((not ,(compileit (meta-form x))))))) (#\@ (let ((f (meta-form x))) `(match-type ,(car f) ,(cadr f)))))) (t `(match ,x)))) (defmacro matchit (x) (compileit x)) (defparameter *saved-readtable* (copy-readtable)) (defparameter *meta-readtable* (copy-readtable)) (defun meta-reader (s c) (make-meta :char c :form (read s))) (mapc #'(lambda (c) (set-macro-character c #'meta-reader nil *meta-readtable*)) '(#\@ #\$ #\!)) (set-macro-character #\{ #'(lambda (s c) (make-meta :char c :form (read-delimited-list #\} s t))) nil *meta-readtable*) (set-macro-character #\[ #'(lambda (s c) (make-meta :char c :form (read-delimited-list #\] s t))) nil *meta-readtable*) (mapc #'(lambda (c) (set-macro-character c (get-macro-character #\) nil) nil *meta-readtable*)) '(#\] #\})) (defmacro with-meta (&body body) `(progn (copy-readtable *meta-readtable* *readtable*) (unwind-protect (progn ,@body) (copy-readtable *saved-readtable* *readtable*)))) (defun enable-meta-syntax () (copy-readtable *meta-readtable* *readtable*)) (defun disable-meta-syntax() (copy-readtable *saved-readtable* *readtable*)) ;; Now onto the XML-RPC routines (defun is-valid-string-char (ch) "Return T if the given character is valid for an XML-RPC string." (and (characterp ch) (not (eql ch #\<)))) (deftype valid-string-char () '(satisfies is-valid-string-char)) (defun is-valid-method-name-char (ch) "Return T if the given character is valid for an XML-RPC method name." (member ch '(#\a #\b #\c #\d #\e #\f #\g #\h #\i #\j #\k #\l #\m #\n #\o #\p #\q #\r #\s #\t #\u #\v #\w #\x #\y #\z #\A #\B #\C #\D #\E #\F #\G #\H #\I #\J #\K #\L #\M #\N #\O #\P #\Q #\R #\S #\T #\U #\V #\W #\X #\Y #\Z #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\_ #\. #\: #\/))) (deftype valid-method-name-char () '(satisfies is-valid-method-name-char)) (defun is-valid-xml-header-char (ch) "Return T if the given character is valid for an XML header ()" (and (characterp ch) (not (eql ch #\<)) (not (eql ch #\>)))) (deftype valid-xml-header-char () '(satisfies is-valid-xml-header-char)) (deftype whitespace () '(member #\Tab #\Space #\Newline #\Return)) (deftype digit () '(member #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)) (define-condition xmlrpc-fault (error) ((fault-code :initarg :fault-code :initform nil :reader xmlrpc-fault-code) (fault-string :initarg :fault-string :initform nil :reader xmlrpc-fault-string)) (:report (lambda (condition stream) (format stream "XML-RPC fault code ~A (~A)." (xmlrpc-fault-code condition) (xmlrpc-fault-string condition))))) (enable-meta-syntax) (defun parse-response (string &aux (index 0) (end (length string)) last-call-result) "Parse the response from an XML-RPC call and return the value of that call." (labels ((ctoi (d) (- (char-code d) #.(char-code #\0))) (make-temp-array () (let ((array (make-array 10 :fill-pointer t :adjustable t))) (setf (fill-pointer array) 0) array)) (array-to-string (array) (concatenate 'string array)) (skip-ws (&aux (old-index index) ch) (or (matchit [$[@(whitespace ch)]]) (progn (setq index old-index) nil))) (check-integer (&aux (old-index index) d (n 0)) (or (matchit [!(skip-ws){"" ""} @(digit d) !(setq n (ctoi d)) $[@(digit d) !(setq n (+ (* n 10) (ctoi d)))] {"" ""} !(skip-ws) !(setq last-call-result n)]) (progn (setq index old-index) nil))) (check-string (&aux (old-index index) ch (string-result (make-temp-array))) (or (matchit [!(skip-ws) "" $[@(valid-string-char ch) !(vector-push-extend ch string-result)] "" !(skip-ws) !(setq last-call-result (array-to-string string-result))]) (progn (setq index old-index) nil))) (check-date (&aux (old-index index) ch (string-result (make-temp-array))) (or (matchit [!(skip-ws) "" $[@(valid-string-char ch) !(vector-push-extend ch string-result)] "" !(skip-ws) !(setq last-call-result (array-to-string string-result))]) (progn (setq index old-index) nil))) (check-double (&aux (old-index index) ch (string-result (make-temp-array))) (or (matchit [!(skip-ws) "" $[@(valid-string-char ch) !(vector-push-extend ch string-result)] "" !(skip-ws) !(setq last-call-result (array-to-string string-result))]) (progn (setq index old-index) nil))) (check-base64 (&aux (old-index index) ch (string-result (make-temp-array))) (or (matchit [!(skip-ws) "" $[@(valid-string-char ch) !(vector-push-extend ch string-result)] "" !(skip-ws) !(setq last-call-result (array-to-string string-result))]) (progn (setq index old-index) nil))) (check-name (&aux (old-index index) ch (result (make-temp-array))) (or (matchit [!(skip-ws) "" $[@(valid-string-char ch) !(vector-push-extend ch result)] "" !(skip-ws) !(setq last-call-result (array-to-string result))]) (progn (setq index old-index) nil))) (check-member (&aux (old-index index) name value) (or (matchit [!(skip-ws) "" !(skip-ws) [!(check-name) !(setq name last-call-result)] [!(check-value) !(setq value last-call-result)] !(skip-ws) "" !(skip-ws) !(setq last-call-result (list name value))]) (progn (setq index old-index) nil))) (check-array (&aux (old-index index) (result (make-temp-array))) (or (matchit [!(skip-ws) "" !(skip-ws) "" !(skip-ws) [!(check-value) !(vector-push-extend last-call-result result)] $[!(check-value) !(vector-push-extend last-call-result result)] !(skip-ws) "" !(skip-ws) "" !(skip-ws) !(setq last-call-result result)]) (progn (setq index old-index) nil))) (check-struct (&aux (old-index index) (result (make-hash-table :test #'equal))) (or (matchit [!(skip-ws) "" !(skip-ws) [!(check-member) !(setf (gethash (first last-call-result) result) (second last-call-result))] $[!(check-member) !(setf (gethash (first last-call-result) result) (second last-call-result))] !(skip-ws) "" !(skip-ws) !(setq last-call-result result)]) (progn (setq index old-index) nil))) (check-method-name (&aux (old-index index) ch (result (make-temp-array))) (or (matchit ["" $[@(valid-method-name-char ch) !(vector-push-extend ch result)] "" !(setq last-call-result (array-to-string result))]) (progn (setq index old-index) nil))) (check-value (&aux (old-index index) ch (string-result (make-temp-array))) (or (matchit [!(skip-ws) "" {!(check-integer) !(check-string) !(check-struct) !(check-array) !(check-date) !(check-double) !(check-base64) [$[@(valid-string-char ch) !(vector-push-extend ch string-result)] !(setq last-call-result (array-to-string string-result))]} "" !(skip-ws)]) (progn (setq index old-index) nil))) (check-param (&aux (old-index index)) (or (matchit [!(skip-ws) "" !(skip-ws) !(check-value) !(skip-ws) "" !(skip-ws)]) (progn (setq index old-index) nil))) (check-response-fault (&aux (old-index index)) (or (matchit [!(skip-ws) "" !(skip-ws) !(check-value) !(skip-ws) "" !(skip-ws) !(error 'xmlrpc-fault :fault-code (gethash "faultCode" last-call-result) :fault-string (gethash "faultString" last-call-result))]) (progn (setq index old-index) nil))) (check-response-params (&aux (old-index index)) (or (matchit [!(skip-ws) "" !(skip-ws) !(check-param) !(skip-ws) "" !(skip-ws)]) (progn (setq index old-index) nil))) (check-xml-header (&aux (old-index index)) (or (matchit [!(skip-ws) "" !(skip-ws)]) (progn (setq index old-index) nil))) (check-payload (&aux (old-index index)) (or (matchit [!(skip-ws) $[!(check-xml-header)] !(skip-ws) !(skip-ws) "" !(skip-ws) {!(check-response-params) !(check-response-fault)} !(skip-ws) "" !(skip-ws) !(setq last-call-result last-call-result)]) (progn (setq index old-index) nil)))) (values (matchit {!(check-payload)}) index last-call-result))) (disable-meta-syntax) #| (parse-response "123") (parse-response "test") |# (defun hashtable-to-xmlrpc (table) "Convert a hash-table to its XML-RPC representation." (let ((members "")) (maphash #'(lambda (key value) (setq members (concatenate 'string members "" key "" (coerce-to-xmlrpc-type value) ""))) table) (concatenate 'string "" members ""))) (defun array-to-xmlrpc (array) "Convert an array to its XML-RPC representation." (let ((members "")) (loop for value across array do (setq members (concatenate 'string members "" (coerce-to-xmlrpc-type value) ""))) (concatenate 'string "" members ""))) (defun coerce-to-xmlrpc-type (value) "Convert a lisp type to the XML-RPC string." (cond ((eq value t) "1") ((eq value nil) "0") (t (typecase value (string (format nil "~A" value)) (fixnum (format nil "~A" value)) (float (format nil "~A" value)) (hash-table (hashtable-to-xmlrpc value)) (array (array-to-xmlrpc value)) (t "Unknown"))))) (defun xml-rpc-send (host url method &rest args) "Send an XML-RPC request to HOST at URL calling METHOD with ARGS." (let ((request (format nil "~A" method))) (unless (zerop (length args)) (setq request (concatenate 'string request "")) (dolist (arg args) (setq request (concatenate 'string request (format nil "~A" (coerce-to-xmlrpc-type arg))))) (setq request (concatenate 'string request ""))) (setq request (concatenate 'string request "")) (with-client-socket (s :host host :port 80) (with-socket-stream (stream s) (write-line (format nil "POST ~A HTTP/1.0" url) stream) (write-line "User-Agent: CMUCL 18c" stream) (write-line (format nil "Host: ~A" host) stream) (write-line "Pragma: no-cache" stream) (write-line "Content-Type: text/xml" stream) (write-line (format nil "Content-Length: ~A" (length request)) stream) (write-line "" stream) (write-sequence request stream) (force-output stream) (loop as line = (read-line stream nil :eof) until (or (eq line :eof) (zerop (length line)))) (values (parse-response (apply #'concatenate 'string (loop as line = (read-line stream nil :eof) until (eq :eof line) collect line)))))))) (defmacro define-xmlrpc-client-method (name server url method-name (&rest args)) "Macro to create a function wrapper for an XML-RPC call." (let ((doc (format nil "XMLRPC client method calling ~A on ~A~A taking arguments ~{~A ~}." method-name server url args))) `(defun ,name (,@args) ,doc (xml-rpc-send ,server ,url ,method-name ,@args)))) (provide 'xmlrpc) #| (xml-rpc-send "xmlrpc.usefulinc.com" "/demo/server.php" "examples.getStateName" 32) (xml-rpc-send "xmlrpc.usefulinc.com" "/demo/server.php" "examples.echo" "foo") (xml-rpc-send "xmlrpc.usefulinc.com" "/demo/server.php" "system.listMethods") (sockets::start-sockets) (xml-rpc-send "betty.userland.com" "/RPC2" "examples.getStateName" 41) (xml-rpc-send "www.wc.cc.va.us" "/dtod/xmlrpcb2/code/server.asp" "helloWorld" "Chris") (setq x (make-hash-table :test #'equal)) (setf (gethash "state1" x) 10 (gethash "state2" x) 20 (gethash "state3" x) 41) (xml-rpc-send "betty.userland.com" "/RPC2" "examples.getStateStruct" x) (xml-rpc-send "betty.userland.com" "/RPC2" "examples.getStateList" (vector 12 28 33 39 46)) (xml-rpc-send "www.wc.cc.va.us" "/dtod/xmlrpcb2/code/server.asp" "superHello" (vector "Chris" "now")) (xml-rpc-send "betty.userland.com" "/RPC2" "examples.getStateName" (vector 51 22)) (xml-rpc-send "aggregator.userland.com" "/RPC2" "aggregator.getServiceInfo" 747) ;; with macro (define-xmlrpc-client-method get-state-name "betty.userland.com" "/RPC2" "examples.getStateName" (state-number)) (define-xmlrpc-client-method hello-world "www.wc.cc.va.us" "/dtod/xmlrpcb2/code/server.asp" "helloWorld" (name)) (define-xmlrpc-client-method get-state-struct "betty.userland.com" "/RPC2" "examples.getStateStruct" (state-structure)) (define-xmlrpc-client-method get-state-list "betty.userland.com" "/RPC2" "examples.getStateList" (state-list)) (define-xmlrpc-client-method super-hello "www.wc.cc.va.us" "/dtod/xmlrpcb2/code/server.asp" "superHello" (list)) (define-xmlrpc-client-method get-service-info "aggregator.userland.com" "/RPC2" "aggregator.getServiceInfo" (channel)) (get-state-name 41) (hello-world "Chris") (get-state-struct x) (get-state-list (vector 1 2 3 4)) (super-hello (vector "Chris" "now")) (get-service-info 747) ;; More tests (define-xmlrpc-client-method get-service-info "xmlrpc.usefulinc.com" "/demo/server.php" "system.listMethods" (v)) |#