From 579597e310dfac262d629aade17f86c3d6b80da4 Mon Sep 17 00:00:00 2001 From: drewc Date: Fri, 3 Jun 2005 15:53:45 -0700 Subject: [PATCH] Import to darcs darcs-hash:20050603225345-39164-a5bce4ed1fb531cc4d6a69bc1e0c0b1e50661d6e.gz --- lisp-on-lines.asd | 37 ++++++ lisp-on-lines.txt | 172 +++++++++++++++++++++++++++ src/backend/clsql.lisp | 228 +++++++++++++++++++++++++++++++++++ src/backend/ucw.lisp | 76 ++++++++++++ src/meta-model.lisp | 179 ++++++++++++++++++++++++++++ src/mewa.lisp | 263 +++++++++++++++++++++++++++++++++++++++++ src/packages.lisp | 34 ++++++ src/ucw.lisp | 151 +++++++++++++++++++++++ 8 files changed, 1140 insertions(+) create mode 100644 lisp-on-lines.asd create mode 100644 lisp-on-lines.txt create mode 100644 src/backend/clsql.lisp create mode 100644 src/backend/ucw.lisp create mode 100644 src/meta-model.lisp create mode 100644 src/mewa.lisp create mode 100644 src/packages.lisp create mode 100644 src/ucw.lisp diff --git a/lisp-on-lines.asd b/lisp-on-lines.asd new file mode 100644 index 0000000..031faa3 --- /dev/null +++ b/lisp-on-lines.asd @@ -0,0 +1,37 @@ +;;; -*- lisp -*- + +(eval-when (:compile-toplevel :load-toplevel :execute) + (unless (find-package :coop.tech.lisp-on-lines.system) + (defpackage :coop.tech.lisp-on-lines.system + (:documentation "ASDF System package for meta-model.") + (:use :common-lisp :asdf)))) + +(in-package :coop.tech.lisp-on-lines.system) + +(defsystem :meta-model + :components ( + (:module :src + :components ((:file "packages") + (:file "meta-model" :depends-on ("packages")))) + (:module :backend + :depends-on (:src) + :pathname "src/backend/" + :components ((:file "clsql")))) + :depends-on (:clsql)) + +;; this is no longer used +(defsystem :meta-model-clsql + :components () + :depends-on (:meta-model :clsql)) + +(defsystem :mewa + :components ((:module :src + :pathname "src/" + :components + ((:file "mewa") + (:file "ucw" :depends-on ("mewa"))))) + :depends-on (:ucw :meta-model)) + +(defsystem :lisp-on-lines + :components ((:static-file "lisp-on-lines.asd")) +:depends-on (:meta-model :mewa)) diff --git a/lisp-on-lines.txt b/lisp-on-lines.txt new file mode 100644 index 0000000..6518d61 --- /dev/null +++ b/lisp-on-lines.txt @@ -0,0 +1,172 @@ +LISP-ON-LINES 0.1 + + +Components: + +Meta Model Protocol - A Protocol for introspection on relational objects. +Mewa Presentations : A Mewa-like[1] layer for UncommonWeb[2] Presentations. + +Description: + +LISP-ON-LINES (LOL) is a framework for rapid development of complex data-driven web appilcations. +Introduction: + + +Example: + +First we start with the data model. The Meta Model Protocol (MMP) is used to provide information on the data objects and how they relate to one another. Its is currently implemented as a layer over CLSQL[3], although support is planned for other backends (CLOS,Elephant[4], whatever). + +The MMP shares its definition syntax with CLSQL's Object Oriented Data Definition Language (OODDL). The macro to define view-classes is named DEF-VIEW-CLASS/META, and takes the same arguments as DEF-VIEW-CLASS from CLSQL. For the purposes of this simple example, we will only need two functions from the MMP beyond what CLSQL provides : LIST-SLOTS and LIST-SLOT-TYPES[5]. + +We'll define a simple class to hold a user. +LISP-ON-LINES> (def-view-class/meta user () + ((userid :initarg :userid :accessor userid :type integer :db-kind :key) + (username :initarg :username :accessor username :type string :db-kind :base) + (password :initarg :password :accessor password :type string :db-kind :base))) +STYLE-WARNING: redefining META-MODEL.METADATA (USER) in DEFMETHOD +# +LISP-ON-LINES> (defparameter user (make-instance 'user :userid 1 :username "drewc" :password "p@ssw0rd")) +USER +LISP-ON-LINES> (list-slots user) +(USERID USERNAME PASSWORD) +LISP-ON-LINES> (list-slot-types user) +((USERID INTEGER) (USERNAME STRING) (PASSWORD STRING)) +; compiling file "/tmp/fileQQsHyN" (written 03 JUN 2005 03:20:06 PM): + +; /tmp/fileQQsHyN.fasl written +; compilation finished in 0:00:00 +LISP-ON-LINES> (default-attributes user) +((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)) +LISP-ON-LINES> (set-default-attributes user) +((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)) +LISP-ON-LINES> (find-class-attributes user) +(USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + COMMON-LISP:NIL) +LISP-ON-LINES> ;;;; note that the mewa functions (find-attribute, set-attribute etc) can take either an instance, or a class-name as a symbol , ie : +; No value +LISP-ON-LINES> (find-class-attributes 'user) +(USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + COMMON-LISP:NIL) +LISP-ON-LINES> (find-class-attributes (make-instance 'user)) +(USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + COMMON-LISP:NIL) +LISP-ON-LINES> + +Using that information, we have enough to create an interface to the object. UncommonWeb includes a powerful presentation system, but it is not quite dynamic enough for our needs. Mewa defines an approach to presentations that suits our purposes, but the paper is written from a smalltalk point of view. A mixture of the two , Mewa Presentations(MP), is described here. + +MP introduces to UCW the concept of attributes. an attribute is essentially a named version of the defpresentation slot-like arguments. for example in : + +(defpresentation person-editor (object-presentation) + ((string :label "First Name" :slot-name 'first-name :max-length 30))) + + the (string :label "First Name" ...) form is an attribute definiton. Attributes are accessed through FIND-ATTIRIBUTES, and are composed at run time (where the current system is done at compile time) to display the object. This allows a very flexible system of displaying objects which is reminiscent of CSS. I discovered this, rather than invent or design it, so there are some rough edges, but its a good start. + +Its much easier to show this then to tell. Lets present our user class. Currently in UCW, you'd define a presentation as such : + +(defpresentation user-presentation (object-presentation) +((INTEGER :LABEL "USERID" :SLOT-NAME USERID) + (STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD))) + +which could be presented using PRESENT-OBJECT : + +(present-object user :using 'user-presentation) + +The equiv approach using mewa presentations is actually longer and more verbose(!) but it serves to demonstrate how the MP system works. + +Mewa Presentations adds a set of attributes to a class, keyed off the class name. Attributes are inherited, so if you define an attribute on T, you can use it with any class. + +MP stores named attributes keyed on a class name. to achieve the same functionality as the above using mp would look like this : + +LISP-ON-LINES> (setf (find-attribute 'user :viewer) '(mewa-object-presentation :attributes (userid username password) :global-properties (:editablep nil))) +(:VIEWER MEWA-OBJECT-PRESENTATION + :ATTRIBUTES + (USERID USERNAME PASSWORD) + :GLOBAL-PROPERTIES + (:EDITABLEP NIL)) +LISP-ON-LINES> (setf (find-attribute 'user 'userid) '(INTEGER :LABEL "USERID" :SLOT-NAME USERID)) +(USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) +LISP-ON-LINES> (setf (find-attribute 'user 'username) '(STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)) +(USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) +LISP-ON-LINES> (setf (find-attribute 'user 'password) '(STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD)) +(PASSWORD STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD) + +LISP-ON-LINES> (find-class-attributes 'user) +(USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) + (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) + (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) + (:VIEWER MEWA-OBJECT-PRESENTATION + :ATTRIBUTES + (USERID USERNAME PASSWORD) + :GLOBAL-PROPERTIES + (:EDITABLEP NIL)) + COMMON-LISP:NIL) + + +this is all turned into a UCW presentation at runtime using MAKE-PRESENTATION : + +(defmethod render-on ((res response) (e presentations-index)) + " +As you'll see, nothing is exported from the LISP-ON-LINES package. +if you wish to use LOL in your own package (or in UCW-USER or whatever), +you simply need to use the MEWA and META-MODEL packages" + ( (set-attribute 'user 'password '(string :label "password: (must be at leat 8 chars)")) +(PASSWORD STRING + :LABEL + "password: (must be at leat 8 chars)" + :SLOT-NAME + PASSWORD) + + +Now we want to create a presentation with which to edit the username. we will use the existing attributes on a subclass of mewa-object-presetation : + +LISP-ON-LINES> (defcomponent user-editor (mewa-object-presentation) + () + (:default-initargs + :attributes '((username :label "Enter your New Username") password) + :global-properties '(:editablep t))) +USER-EDITOR +LISP-ON-LINES> (setf (find-attribute 'user :editor) '(user-editor)) +(:EDITOR USER-EDITOR) +LISP-ON-LINES> + + +which we then can display below our earlier example : + +(defmethod render-on ((res response) (e presentations-index)) + " +As you'll see, nothing is exported from the LISP-ON-LINES package. +if you wish to use LOL in your own package (or in UCW-USER or whatever), +you simply need to use the MEWA and META-MODEL packages" + (sym (name &optional (package nil)) + (flet ((xform (x) + (string-upcase (substitute #\- #\_ x)))) + (if package + (intern (xform (string name)) package) + (intern (xform (string name)))))) + +(defun table->slots (table pkey) + (mapcar + #'(lambda (col) + `(,(sql->sym col) + :accessor ,(sql->sym col) + :initarg ,(sql->sym col "KEYWORD") + :type ,(gen-type table col) + :db-kind + ,(if (equalp col pkey) + `:key + `:base))) + (list-attributes table))) + +(defun view-class-definition-list () + (mapcar #'(lambda (x) `(def-meta-model-from-table ,x)) + (list-tables))) + +(defmacro def-meta-models () + (let ((defs (view-class-definition-list))) + `(progn ,@defs))) + + + + + +(defun get-pkeys () + (let ((keys '())) + (dolist (row (get-pkeys-query)) + (setf keys (acons (car row) (list (cadr row)) keys))) + keys)) + +(defun get-pkeys-query() + (query + "SELECT pg_class.relname, pg_attribute.attname, pg_catalog.quote_ident(conname) AS constraint_n + , pg_catalog.pg_get_indexdef(d.objid) AS constraint_definition + , CASE + WHEN contype = 'p' THEN + 'PRIMARY KEY' + ELSE + 'UNIQUE' + END as constraint_type + FROM + pg_class, pg_attribute, + pg_catalog.pg_constraint AS c + JOIN pg_catalog.pg_depend AS d ON (d.refobjid = c.oid) + WHERE contype IN ('p', 'u') + AND deptype = 'i' + and conrelid = pg_class.oid + and pg_attribute.attnum = ANY (c.conkey) + and pg_attribute.attrelid = pg_class.oid")) + +;;here is how this works +;;from the postgres system tables we get +;;list of all the has-a relationships. +;;the inverse of a has-a is an implicit has-many +;;and any relation having more than one foreign key +;;is a join table hosting a many-to-many relationship + +(defun get-fkey-explosions () + (let ((key-table (get-fkey-explosions-query)) + (keys '())) + (dolist (row key-table) + (setf row (mapcar #'(lambda (x) + (sql->sym x)) + row)) + ;;this one does the has-a + (setf keys (acons (car row) (gen-has-a row) + keys)) + ;;the inverse of the previous represents a has-many. + (setf keys + (acons (fourth row) (gen-has-many row) + keys)) + + ;;many-to-many + (dolist (mrow + (remove-if #'(lambda (r) (or (not (equal (car row) (car r))) + (equal (last row) (last r)))) + (mapcar #'(lambda (x) + (mapcar #'sql->sym x)) + key-table))) + (setf keys (acons (fourth row) + (gen-many-to-many mrow (third row) (second row)) + keys)))) + keys )) + + +(defun get-fkey-explosions-query () +;;these query's are a mess, i don't even know how they work :) + (query " +SELECT pg_class.relname, + pg_attribute.attname, + fa.attname , + f.relname +FROM pg_class, + pg_constraint, + pg_attribute, + pg_class as f , + pg_attribute as fa +WHERE pg_class.relname in (select tablename from pg_tables where schemaname = 'public') +AND pg_class.oid = pg_constraint.conrelid +AND pg_attribute.attnum = ANY (pg_constraint.conkey) +AND pg_attribute.attrelid = pg_class.oid +AND f.oid = confrelid +AND fa.attrelid = f.oid +AND fa.attnum = ANY (pg_constraint.confkey)")) + + +;; i chose keyword args here so as to make the code more understandable. +;; it didn't really work. +(defun gen-join-slot (&key name home-key foreign-key join-class (set nil)) + `(,(intern name) + :accessor ,(intern name) + :db-kind :join + :db-info (:join-class ,join-class + :home-key ,home-key + :foreign-key ,foreign-key + :set ,set))) + +(defun gen-has-a (row) + (gen-join-slot + :name + (format nil "~A->~A" (string (car row))(string (second row))) + :home-key (second row) + :foreign-key (third row) + :join-class (fourth row))) + +(defun gen-has-many (row) + (gen-join-slot + :name + (format nil "~A->~A" (string (car row))(string (second row))) + :home-key (third row) + :foreign-key (second row) + :join-class (car row) + :set t)) + +(defun gen-many-to-many (row home-key foreign-key) + (let ((name (sql->sym (string-upcase (format nil "~A->~A" (string (car row)) (string (second row))))))) + (setf row (mapcar #'sql->sym row)) + `(,name + :accessor ,name + :db-kind :join + :db-info (:join-class ,(car row) + :home-key ,home-key + :foreign-key ,foreign-key + :target-slot ,name + :set t)))) + +(defmethod update-records-from-instance :before ((view clsql::standard-db-object) &key database) + (declare (ignorable database)) + (labels ((sym->sql (sym) (string-downcase (substitute #\_ #\- (string sym)))) + (get-def (slot) (caar (query + (format nil "SELECT DISTINCT adsrc from pg_attrdef join pg_attribute on attnum = adnum where adrelid = (select oid from pg_class where relname = '~A') and attname = '~A'" (sym->sql (class-name (class-of view))) (sym->sql slot))))) + (get-default-value (slot) (caar (query (format nil "SELECT ~A" (get-def slot)))))) + + (dolist (slot (list-slots view)) + (when (and (primary-key-p view slot) + (or (not (slot-boundp view slot)) + (equal (slot-value view slot) nil))) + (setf (slot-value view slot) (get-default-value slot)))))) + +;;;; + +(defmacro def-view-class/meta (name supers slots &rest args) + `(progn + (let* ((m (def-meta-model model-name ,supers ,slots ,args)) + (i (make-instance m))) + (prog1 (eval (def-base-class-expander i :clsql ',name ',args)) + (defmethod meta-model.metadata ((self ,name)) + (meta-model.metadata i)))))) + + +(defmacro def-view-class/table (table &optional name) + "takes the name of a table as a string and +creates a clsql view-class" + (let* ((pkey (cadr (assoc table (get-pkeys) :test #'equalp))) + (table-slots (table->slots table pkey)) + (join-slots + (let ((slots nil)) + (dolist (exp (get-fkey-explosions)) + (when (equalp (car exp) (sql->sym table)) + (setf slots (cons (cdr exp) slots)))) + slots))) + `(def-view-class/meta ,(if name name (sql->sym table)) + () + ,(append table-slots join-slots)))) + + + diff --git a/src/backend/ucw.lisp b/src/backend/ucw.lisp new file mode 100644 index 0000000..a7bab90 --- /dev/null +++ b/src/backend/ucw.lisp @@ -0,0 +1,76 @@ +(in-package :it.bese.ucw) + +(defslot-presentation clsql-wall-time-slot-presentation () + () + (:type-name clsql-sys:wall-time)) + +(defmethod present-slot ((slot clsql-wall-time-slot-presentation) instance) + (<:as-html (presentation-slot-value slot instance))) + +(defslot-presentation mewa-relation-slot-presentation () + ((slot-name :accessor slot-name :initarg :slot-name) + (foreign-instance :accessor foreign-instance) + (editablep :initarg :editablep :accessor editablep :initform :inherit)) + (:type-name relation)) + +(defmethod present-relation (( slot mewa-relation-slot-presentation) instance) + (when (foreign-instance slot) + (when (eql (editablep slot) :inherit) + (setf (editablep slot) (editablep (parent slot)))) + (flet ((render-slot () + ( " (foreign-instance slot) " from " instance ) + (let* ((e (getf (mewa::global-properties (parent slot)) :editablep)) + (i (foreign-instance slot)) + (pres (mewa::make-presentation + i + :type :one-line + :initargs (list + :global-properties + (list :editablep nil :linkedp (linkedp slot)))))) + (when (ucw::parent slot) (setf (component.place pres) (component.place (ucw::parent slot)))) + (flet ((render () (when i ( 0 (current slot)) + ;;what to do here is open to debate + (setf (current slot) (- (len slot)(number-to-display slot) )))) + + +(defmethod present-slot ((slot has-very-many-slot-presentation) instance) + ;;(<:as-html "isance: " instance) + (>")) + (call-next-method) + (<:as-html "total :" (len slot))) + +(defmethod get-foreign-instances :around ((slot has-very-many-slot-presentation) instance) + (let ((f (call-next-method))) + (setf (len slot) (length f)) + (setf (instances slot) f) + (loop for cons on (nthcdr (current slot) f) + for i from 0 upto (number-to-display slot) + collect (car cons)))) + +(defslot-presentation has-a-slot-presentation (one-of-presentation) + ((key :initarg :key :accessor key)) + (:type-name has-a)) + +(defmethod get-foreign-slot-value ((slot has-a-slot-presentation) (object t) (slot-name t)) + (slot-value object slot-name)) + +(defmethod present-slot ((slot has-a-slot-presentation) instance) + (<:as-html (presentation-slot-value slot instance)) + (if (editablep slot) + (