r3627: *** empty log message ***
[hyperobject.git] / mop.lisp
1 ;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
2 ;;;; *************************************************************************
3 ;;;; FILE IDENTIFICATION
4 ;;;;
5 ;;;; Name:          mop.lisp
6 ;;;; Purpose:       Metaobject Protocol Interface
7 ;;;; Programmer:    Kevin M. Rosenberg
8 ;;;; Date Started:  Apr 2000
9 ;;;;
10 ;;;; This metaclass as functions to classes to allow display
11 ;;;; in Text, HTML, and XML formats. This includes hyperlinking
12 ;;;; capability and sub-objects.
13 ;;;;
14 ;;;; $Id: mop.lisp,v 1.14 2002/12/14 02:30:58 kevin Exp $
15 ;;;;
16 ;;;; This file is Copyright (c) 2000-2002 by Kevin M. Rosenberg
17 ;;;;
18 ;;;; *************************************************************************
19  
20 (in-package :hyperobject)
21
22 (eval-when (:compile-toplevel :execute)
23   (declaim (optimize (speed 2) (safety 2) (compilation-speed 0) (debug 2))))
24
25 ;; Main class
26
27 (defclass hyperobject-class (standard-class)
28   ( ;; slots initialized in defclass
29    (user-name :initarg :user-name :type string :initform nil
30               :accessor user-name
31               :documentation "User name for class")
32    (default-print-slots :initarg :default-print-slots :type list :initform nil
33                         :accessor default-print-slots
34                         :documentation "Defaults slots for a view")
35    (description :initarg :description :initform nil
36                 :accessor description
37                 :documentation "Class description")
38    (version :initarg :version :initform nil
39             :accessor version
40             :documentation "Version number for class")
41    (sql-name :initarg :sql-name :initform nil)
42
43    ;;; The remainder of these fields are calculated one time
44    ;;; in finalize-inheritence.
45    
46    (subobjects :initform nil :accessor subobjects
47                :documentation
48                "List of fields that contain a list of subobjects objects.")
49    (hyperlinks :type list :initform nil :accessor hyperlinks
50                :documentation "List of fields that have hyperlinks")
51    (direct-rules :type list :initform nil :initarg :direct-rules
52                  :accessor direct-rules
53                  :documentation "List of rules to fire on slot changes.")
54    (class-id :type integer :initform nil
55              :accessor class-id
56              :documentation "Unique ID for the class")
57    (default-view :initform nil :initarg :default-view :accessor default-view
58                  :documentation "The default view for a class")
59
60    ;; SQL commands
61    (create-table-cmd :initform nil :reader create-table-cmd)
62    (create-indices-cmds :initform nil :reader create-index-cmds)
63    (drop-table-cmd :initform nil :reader drop-table-cmd)
64
65    (views :type list :initform nil :initarg :views :accessor views
66           :documentation "List of views")
67    (rules :type list :initform nil :initarg :rules :accessor rules
68           :documentation "List of rules")
69    )
70   (:documentation "Metaclass for Markup Language classes."))
71
72 (defclass subobject ()
73   ((name-class :type symbol :initform nil :initarg :name-class :reader name-class)
74    (name-slot :type symbol :initform nil :initarg :name-slot :reader name-slot)
75    (lookup :type symbol :initform nil :initarg :lookup :reader lookup)
76    (lookup-keys :type list :initform nil :initarg :lookup-keys
77                 :reader lookup-keys))
78   (:documentation "Contains subobject information"))
79
80
81 (defmethod print-object ((obj subobject) (s stream))
82   (print-unreadable-object (obj s :type t :identity t)
83     (format s "~S" (name obj))))
84
85 (defclass hyperlink ()
86   ((name :type symbol :initform nil :initarg :name :reader name)
87    (lookup :type function :initform nil :initarg :lookup :reader lookup)
88    (link-parameters :type list :initform nil :initarg :link-parameters
89                     :reader link-parameters)))
90
91 (defmethod print-object ((obj hyperlink) (s stream))
92   (print-unreadable-object (obj s :type t :identity t)
93     (format s "~S" (name obj))))
94
95 #+(or cmu scl sbcl)
96 (defmethod validate-superclass ((class hyperobject-class) (superclass standard-class))
97   t)
98
99 (defmethod finalize-inheritance :after ((cl hyperobject-class))
100   (init-hyperobject-class cl)
101   )
102
103 ;; Slot definitions
104 (defmethod direct-slot-definition-class ((cl hyperobject-class) 
105                                          #+allegro &rest
106                                                    iargs)
107   (find-class 'hyperobject-dsd))
108
109
110 ; Slot definitions
111
112 (eval-when (:compile-toplevel :load-toplevel :execute)
113   (defmacro process-class-option (slot-name &optional required)
114     #+lispworks
115     `(defmethod clos:process-a-class-option ((class hyperobject-class)
116                                              (name (eql ,slot-name))
117                                              value)
118     (when (and ,required (null value))
119       (error "hyperobject class slot ~A must have a value" name))
120     (list name `',value))
121     #+(or allegro sbcl cmu scl)
122     (declare (ignore slot-name required))
123     )
124
125   (defmacro process-slot-option (slot-name)
126     #+lispworks
127     `(defmethod clos:process-a-slot-option ((class hyperobject-class)
128                                             (option (eql ,slot-name))
129                                             value
130                                           already-processed-other-options
131                                             slot)
132        (list option `',value))
133     #-lispworks
134     (declare (ignore slot-name))
135     )
136   
137   (dolist (option *class-options*)
138     (eval `(process-class-option ,option)))
139   (dolist (option *slot-options*)
140     (eval `(process-slot-option ,option)))
141
142   (eval
143    `(defclass hyperobject-dsd (standard-direct-slot-definition)
144       (,@(mapcar #'(lambda (x)
145                      `(,(intern (symbol-name x))
146                         :initform nil))
147                  *slot-options-no-initarg*)
148          ,@(mapcar #'(lambda (x)
149                        `(,(intern (symbol-name x))
150                           :initarg
151                           ,(intern (symbol-name x) (symbol-name :keyword))
152                           :initform nil
153                           :accessor
154                           ,(intern (concatenate 'string
155                                                 (symbol-name :dsd-)
156                                                 (symbol-name x)))))
157                    *slot-options*))))
158   (eval
159    `(defclass hyperobject-esd (standard-effective-slot-definition)
160       (,@(mapcar #'(lambda (x)
161                      `(,(intern (symbol-name x))
162                         :initarg
163                         ,(intern (symbol-name x) (symbol-name :keyword))
164                         :initform nil
165                         :accessor
166                         ,(intern (concatenate 'string
167                                               (symbol-name :esd-)
168                                               (symbol-name x)))))
169                  (append *slot-options* *slot-options-no-initarg*)))))
170   ) ;; eval-when
171
172 (defun intern-in-keyword (obj)
173   (cond
174     ((null obj)
175      nil)
176     ((eq t obj)
177      t)
178     ((atom obj)
179      (intern (symbol-name obj) (find-package 'keyword)))
180     ((consp obj)
181      (cons (intern-in-keyword (car obj) ) (intern-in-keyword (cdr obj))))
182     (t
183      obj)))
184
185 (defun canonicalize-value-type (vt)
186   (typecase vt
187     (atom
188      (ensure-keyword vt))
189     (cons
190      (cons (ensure-keyword (car vt)) (cdr vt)))
191     (t
192      t)))
193
194 (defmethod compute-effective-slot-definition :around ((cl hyperobject-class)
195                                                       #+(or allegro lispworks) name
196                                                       dsds)
197   #+allergo (declare (ignore name))
198   (let* ((dsd (car dsds))
199          (value-type (canonicalize-value-type (slot-value dsd 'value-type))))
200     (multiple-value-bind (sql-type length) (value-type-to-sql-type value-type)
201       (setf (slot-value dsd 'sql-type) sql-type)
202       (setf (slot-value dsd 'type) (value-type-to-lisp-type value-type))
203       (let ((ia (compute-effective-slot-definition-initargs
204                  cl #+lispworks name dsds)))
205         (apply
206          #'make-instance 'hyperobject-esd 
207          :value-type value-type
208          :sql-type sql-type
209          :length length
210          :print-formatter (slot-value dsd 'print-formatter)
211          :subobject (slot-value dsd 'subobject)
212          :hyperlink (slot-value dsd 'hyperlink)
213          :hyperlink-parameters (slot-value dsd 'hyperlink-parameters)
214          :description (slot-value dsd 'description)
215          :user-name (slot-value dsd 'user-name)
216          :index (slot-value dsd 'index)
217          :value-constraint (slot-value dsd 'value-constraint)
218          :null-allowed (slot-value dsd 'null-allowed)
219          ia)))))
220   
221 (defun value-type-to-lisp-type (value-type)
222   (case (if (atom value-type)
223             value-type
224             (car value-type))
225     ((:string :cdata :varchar :char)
226      'string)
227     (:character
228      'character)
229     (:fixnum
230      'fixnum)
231     (:boolean
232      'boolean)
233     (:integer
234      'integer)
235     ((:float :single-float)
236      'single-float)
237     (:double-float
238      'double-float)
239     (otherwise
240      t)))
241
242 (defun value-type-to-sql-type (value-type)
243   "Return two values, the sql type and field length."
244   (let ((type (if (atom value-type)
245                   value-type
246                   (car value-type)))
247         (length (when (consp value-type)
248                   (cadr value-type))))
249     (values
250      (case type
251        ((:string :cdata)
252         :string)
253        ((:fixnum :integer)
254         :integer)
255        (:boolean
256         :boolean)
257        ((:float :single-float)
258         :single-float)
259        (:double-float
260         :double-float)
261        (otherwise
262         :text))
263      length)))
264
265 ;;;; Class initialization function
266
267 ;; defines a slot-unbound method for class and slot-name, fills
268 ;; the slot by calling reader function with the slot values of
269 ;; the instance's reader-keys
270 (defmacro def-lazy-reader (class slot-name reader &rest reader-keys)
271   (let* ((the-slot-name (gensym))
272          (the-class (gensym))
273          (the-instance (gensym))
274          (keys '()))
275     (dolist (key reader-keys)
276       (push (list 'slot-value the-instance (list 'quote key)) keys))
277     (setq keys (nreverse keys))
278     `(defmethod slot-unbound (,the-class (,the-instance ,class)
279                                          (,the-slot-name (eql ',slot-name)))
280        (declare (ignore ,the-class))
281        (setf (slot-value ,the-instance ,the-slot-name)
282            (,reader ,@keys)))))
283
284 (defun finalize-subobjects (cl)
285   "Process class subobjects slot"
286   (setf (subobjects cl)
287     (let ((subobjects '()))
288       (dolist (slot (class-slots cl))
289         (let-when (subobj-def (esd-subobject slot))
290           (let ((subobject (make-instance 'subobject
291                                           :name-class (class-name cl)
292                                           :name-slot (slot-definition-name slot)
293                                           :lookup (if (atom subobj-def)
294                                                       subobj-def
295                                                       (car subobj-def))
296                                           :lookup-keys (if (atom subobj-def)
297                                                            nil
298                                                            (cdr subobj-def)))))
299             (unless (eq (lookup subobject) t)
300               (eval `(def-lazy-reader ,(name-class subobject)
301                          ,(name-slot subobject) ,(lookup subobject)
302                          ,@(lookup-keys subobject))))
303             (push subobject subobjects))))
304       subobjects)))
305
306 (defun finalize-documentation (cl)
307   "Calculate class documentation slot"
308   (awhen (slot-value cl 'user-name)
309          (setf (slot-value cl 'user-name)
310                (etypecase (slot-value cl 'user-name)
311                    (cons (car it))
312                    ((or string symbol) it))))
313   (awhen (slot-value cl 'description)
314          (setf (slot-value cl 'description)
315                (etypecase (slot-value cl 'description)
316                    (cons (car it))
317                    ((or string symbol) it))))
318
319   (let ((*print-circle* nil))
320     (setf (documentation (class-name cl) 'class)
321       (format nil "Hyperobject~A~A~A~A"
322               (aif (user-name cl)
323                    (format nil ": ~A" it ""))
324               (aif (description cl)
325                    (format nil "~%Class description: ~A" it) "")
326               (aif (subobjects cl)
327                    (format nil "~%Subobjects:~{ ~A~}" (mapcar #'name-slot it)) "")
328               (aif (default-print-slots cl)
329                    (format nil "~%Default print slots:~{ ~A~}" it) "")
330               ))))
331
332 (defun finalize-hyperlinks (cl)
333   (let ((hyperlinks '()))
334     (dolist (esd (class-slots cl))
335       (awhen (slot-value esd 'hyperlink)
336         (push
337          (make-instance 'hyperlink
338                         :name (slot-definition-name esd)
339                         :lookup it
340                         :link-parameters (slot-value esd 'hyperlink-parameters))
341          hyperlinks)))
342     (setf (slot-value cl 'hyperlinks) hyperlinks)))
343
344 (defun init-hyperobject-class (cl)
345   "Initialize a hyperobject class. Calculates all class slots"
346   (finalize-subobjects cl)
347   (finalize-views cl)
348   (finalize-hyperlinks cl)
349   (finalize-sql cl)
350   (finalize-rules cl)
351   (finalize-documentation cl))
352
353
354 ;;;; *************************************************************************
355 ;;;;  Metaclass Slot Accessors
356 ;;;; *************************************************************************
357
358 (defun find-slot-by-name (cl name)
359   (find name (class-slots cl) :key #'slot-definition-name))
360
361 (defun hyperobject-class-user-name (obj)
362   (awhen (user-name (class-of obj))
363          (if (consp it)
364              (car it)
365              it)))
366
367 (defun hyperobject-class-subobjects (obj)
368   (subobjects (class-of obj)))
369
370 (defun hyperobject-class-hyperlinks (obj)
371   (hyperlinks (class-of obj)))
372
373 (defun hyperobject-class-fields (obj)
374   (class-slots (class-of obj)))
375
376 ;;; Slot accessor and class rules
377
378 (defclass rule ()
379   ((name :initarg :name :initform nil :accessor name)
380    (dependants :initarg :dependants :initform nil :accessor dependants)
381    (volatile :initarg :volatile :initform nil :accessor volatile)
382    (source-code :initarg :source-code :initform nil :accessor source-code)
383    (func :initform nil :initarg :func :accessor func)))
384
385 (defun compile-rule (source-code dependants volatile cl)
386   #'(lambda (obj)
387       `(with-slots ,(append dependants volatile) obj
388         ,@source-code)))
389   
390 (defun finalize-rules (cl)
391   (let* ((direct-rules (direct-rules cl))
392          (rules '()))
393     (dolist (rule direct-rules)
394       (destructuring-bind (name (&key dependants volatile) &rest source-code)
395           rule
396         (setf dependants (mklist dependants)
397               volatile (mklist volatile))
398         (push
399          (make-instance 'rule :name name :dependants dependants
400                         :volatile volatile :source-code source-code
401                         :func (compile-rule
402                                source-code dependants volatile cl))
403          rules)))
404     (setf (rules cl) (nreverse rules))))
405
406
407 (defun fire-class-rules (cl obj slot)
408   "Fire all class rules. Called after a slot is modified."
409   (dolist (rule (direct-rules cl))
410     (cmsg "firing rule: ~a" rule)))
411
412 (defmethod (setf slot-value-using-class) 
413     :around (new-value (cl hyperobject-class) obj
414                        (slot standard-effective-slot-definition))
415     #+ignore
416     (cmsg-c :verbose "Setf slot value: class: ~s, obj: ~s, slot: ~s, value: ~s" cl (class-of obj) slot new-value)
417     
418     (let ((func (esd-value-constraint slot)))
419       (cond
420         ((and func (not (funcall func new-value)))
421          (warn "Rejected change to value of slot ~a of object ~a"
422                (slot-definition-name slot) obj)
423          (slot-value obj (slot-definition-name slot)))
424         (t
425          (call-next-method)
426          (fire-class-rules cl obj slot)
427          new-value))))
428
429 #+ignore
430 (defmethod slot-value-using-class :around ((cl hyperobject-class) obj
431                                            (slot standard-effective-slot-definition))
432   (let ((value (call-next-method)))
433     (cmsg-c :verbose "slot value: class: ~s, obj: ~s, slot: ~s" cl (class-of obj) slot)
434     value))