File Annotation
Not logged in
02d1ed6ad2 2008-02-02   stephan: /*
02d1ed6ad2 2008-02-02   stephan: ** Copyright (c) 2007 D. Richard Hipp
2ab3a2f603 2008-02-03   stephan: ** Copyright (c) 2008 Stephan Beal
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: ** This program is free software; you can redistribute it and/or
02d1ed6ad2 2008-02-02   stephan: ** modify it under the terms of the GNU General Public
02d1ed6ad2 2008-02-02   stephan: ** License as published by the Free Software Foundation; either
02d1ed6ad2 2008-02-02   stephan: ** version 2 of the License, or (at your option) any later version.
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: ** This program is distributed in the hope that it will be useful,
02d1ed6ad2 2008-02-02   stephan: ** but WITHOUT ANY WARRANTY; without even the implied warranty of
02d1ed6ad2 2008-02-02   stephan: ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
02d1ed6ad2 2008-02-02   stephan: ** General Public License for more details.
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: ** You should have received a copy of the GNU General Public
02d1ed6ad2 2008-02-02   stephan: ** License along with this library; if not, write to the
02d1ed6ad2 2008-02-02   stephan: ** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
02d1ed6ad2 2008-02-02   stephan: ** Boston, MA  02111-1307, USA.
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: ** Author contact information:
02d1ed6ad2 2008-02-02   stephan: **   drh@hwaci.com
02d1ed6ad2 2008-02-02   stephan: **   http://www.hwaci.com/drh/
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: *******************************************************************************
02d1ed6ad2 2008-02-02   stephan: **
02d1ed6ad2 2008-02-02   stephan: ** Implementation of the Tag View page
02d1ed6ad2 2008-02-02   stephan: */
02d1ed6ad2 2008-02-02   stephan: #include <assert.h>
02d1ed6ad2 2008-02-02   stephan: #include "config.h"
02d1ed6ad2 2008-02-02   stephan: #include "tagview.h"
02d1ed6ad2 2008-02-02   stephan: 
02a7c850b4 2008-02-03   stephan: /**
b81e93f576 2008-02-03   stephan: tagview_strxform_f is a typedef for funcs with the following policy:
b81e93f576 2008-02-03   stephan: 
b81e93f576 2008-02-03   stephan: They accept a const string which they then transform into some other
b81e93f576 2008-02-03   stephan: form. They return a transformed copy, which the caller is responsible
b81e93f576 2008-02-03   stephan: for freeing.
b81e93f576 2008-02-03   stephan: 
b81e93f576 2008-02-03   stephan: The intention of this is to provide a way for a generic query routine
b81e93f576 2008-02-03   stephan: to format specific column data (e.g. transform an object ID into a
b81e93f576 2008-02-03   stephan: link to that object).
02d1ed6ad2 2008-02-02   stephan: */
02a7c850b4 2008-02-03   stephan: typedef char * (*tagview_strxform_f)( char const * );
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: #if 0
02a7c850b4 2008-02-03   stephan: /** A no-op transformer which can be used as a placeholder. */
02a7c850b4 2008-02-03   stephan: static char * tagview_xf_copy( char const * uuid )
02a7c850b4 2008-02-03   stephan: {
02a7c850b4 2008-02-03   stephan:   int len = strlen(uuid) + 1;
02a7c850b4 2008-02-03   stephan:   char * ret = (char *) malloc( len );
02a7c850b4 2008-02-03   stephan:   ret[len] = '\0';
02a7c850b4 2008-02-03   stephan:   strncpy( ret, uuid, len-1 );
02a7c850b4 2008-02-03   stephan:   return ret;
02a7c850b4 2008-02-03   stephan: }
02a7c850b4 2008-02-03   stephan: #endif
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /** Returns a hyperlink to uuid. */
02a7c850b4 2008-02-03   stephan: static char * tagview_xf_link_to_uuid( char const * uuid )
02a7c850b4 2008-02-03   stephan: {
b81e93f576 2008-02-03   stephan:   const int offset = 10;
b81e93f576 2008-02-03   stephan:   char shortname[offset+1];
b81e93f576 2008-02-03   stephan:   shortname[offset] = '\0';
b81e93f576 2008-02-03   stephan:   memcpy( shortname, uuid, offset );
b81e93f576 2008-02-03   stephan:   return mprintf( "<tt><a href='%s/vinfo/%s'><strong>%s</strong>%s</a></tt>",
b81e93f576 2008-02-03   stephan:                   g.zBaseURL, uuid, shortname, uuid+offset );
02a7c850b4 2008-02-03   stephan: }
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /** Returns a hyperlink to the given tag. */
02a7c850b4 2008-02-03   stephan: static char * tagview_xf_link_to_tagid( char const * tagid )
02a7c850b4 2008-02-03   stephan: {
02a7c850b4 2008-02-03   stephan:   return mprintf( "<a href='%s/tagview?tagid=%s'>%s</a>",
b81e93f576 2008-02-03   stephan:                   g.zBaseURL, tagid, tagid );
02a7c850b4 2008-02-03   stephan: }
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /** Returns a hyperlink to the named tag. */
02a7c850b4 2008-02-03   stephan: static char * tagview_xf_link_to_tagname( char const * tagid )
02a7c850b4 2008-02-03   stephan: {
02a7c850b4 2008-02-03   stephan:   return mprintf( "<a href='%s/tagview/%s'>%s</a>",
b81e93f576 2008-02-03   stephan:                   g.zBaseURL, tagid, tagid );
02d1ed6ad2 2008-02-02   stephan: }
02d1ed6ad2 2008-02-02   stephan: 
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /**
02a7c850b4 2008-02-03   stephan: * tagview_run_query():
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: * A very primitive helper to run an SQL query and table-ize the
02a7c850b4 2008-02-03   stephan: * results.
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: * The sql parameter should be a single, complete SQL statement.
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: * The coln parameter is optional (it may be 0). If it is 0 then the
b81e93f576 2008-02-03   stephan: * column names used in the output will be taken directly from the
02a7c850b4 2008-02-03   stephan: * SQL. If it is not null then it must have as many entries as the SQL
02a7c850b4 2008-02-03   stephan: * result has columns. Each entry is a column name for the SQL result
02a7c850b4 2008-02-03   stephan: * column of the same index. Any given entry may be 0, in which case
02a7c850b4 2008-02-03   stephan: * the column name from the SQL is used.
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: * The xform argument is an array of transformation functions (type
02a7c850b4 2008-02-03   stephan: * tagview_strxform_f). The array, or any single entry, may be 0, but
02a7c850b4 2008-02-03   stephan: * if the array is non-0 then it must have at least as many entries as
02a7c850b4 2008-02-03   stephan: * colnames does. Each index corresponds directly to an entry in
b81e93f576 2008-02-03   stephan: * colnames and the SQL results.  Any given entry may be 0. If it has
02a7c850b4 2008-02-03   stephan: * fewer, undefined behaviour results.  If a column has an entry in
02a7c850b4 2008-02-03   stephan: * xform, then the xform function will be called to transform the
02a7c850b4 2008-02-03   stephan: * column data before rendering it. This function takes care of freeing
02a7c850b4 2008-02-03   stephan: * the strings created by the xform functions.
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: * Example:
02a7c850b4 2008-02-03   stephan: *
02a7c850b4 2008-02-03   stephan: *  char const * const colnames[] = {
02a7c850b4 2008-02-03   stephan: *   "Tag ID", "Tag Name", "Something Else", "UUID"
02a7c850b4 2008-02-03   stephan: *  };
02a7c850b4 2008-02-03   stephan: *  tagview_strxform_f xf[] = {
02a7c850b4 2008-02-03   stephan: *    tagview_xf_link_to_tagid,
02a7c850b4 2008-02-03   stephan: *    tagview_xf_link_to_tagname,
02a7c850b4 2008-02-03   stephan: *    0,
02a7c850b4 2008-02-03   stephan: *    tagview_xf_link_to_uuid
02a7c850b4 2008-02-03   stephan: *  };
02a7c850b4 2008-02-03   stephan: *  tagview_run_query( "select a,b,c,d from foo", colnames, xf );
02a7c850b4 2008-02-03   stephan: *
02d1ed6ad2 2008-02-02   stephan: */
02a7c850b4 2008-02-03   stephan: static void tagview_run_query(
02a7c850b4 2008-02-03   stephan:   char const * sql,
02a7c850b4 2008-02-03   stephan:   char const * const * coln,
02a7c850b4 2008-02-03   stephan:   tagview_strxform_f * xform )
2ab3a2f603 2008-02-03   stephan: {
02d1ed6ad2 2008-02-02   stephan: 
2ab3a2f603 2008-02-03   stephan:   Stmt st;
02d1ed6ad2 2008-02-02   stephan:   @ <table cellpadding='4px' border='1'><tbody>
02a7c850b4 2008-02-03   stephan:   int i = 0;
02a7c850b4 2008-02-03   stephan:   int rc = db_prepare( &st, sql );
02a7c850b4 2008-02-03   stephan:   /**
02a7c850b4 2008-02-03   stephan:     Achtung: makeheaders apparently can't pull the function
02a7c850b4 2008-02-03   stephan:     name from this:
02a7c850b4 2008-02-03   stephan:    if( SQLITE_OK != db_prepare( &st, sql ) )
02a7c850b4 2008-02-03   stephan:   */
02a7c850b4 2008-02-03   stephan:   if( SQLITE_OK != rc )
02d1ed6ad2 2008-02-02   stephan:   {
02a7c850b4 2008-02-03   stephan:     @ tagview_run_query(): Error processing SQL: [%s(sql)]
02a7c850b4 2008-02-03   stephan:     return;
02a7c850b4 2008-02-03   stephan:   }
02a7c850b4 2008-02-03   stephan:   int colc = db_column_count(&st);
02a7c850b4 2008-02-03   stephan:   @ <tr>
02a7c850b4 2008-02-03   stephan:   for( i = 0; i < colc; ++i ) {
02a7c850b4 2008-02-03   stephan:     if( coln )
02a7c850b4 2008-02-03   stephan:     {
02a7c850b4 2008-02-03   stephan:       @ <th>%s(coln[i] ? coln[i] : db_column_name(&st,i))</th>
02a7c850b4 2008-02-03   stephan:     }
02a7c850b4 2008-02-03   stephan:     else
02a7c850b4 2008-02-03   stephan:     {
02a7c850b4 2008-02-03   stephan:       @ <td>%s(db_column_name(&st,i))</td>
02a7c850b4 2008-02-03   stephan:     }
02a7c850b4 2008-02-03   stephan:   }
02a7c850b4 2008-02-03   stephan:   @ </tr>
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan:   while( SQLITE_ROW == db_step(&st) ){
10437374a7 2008-02-02       drh:     @ <tr>
02a7c850b4 2008-02-03   stephan:       for( i = 0; i < colc; ++i ) {
02a7c850b4 2008-02-03   stephan:         char * xf = 0;
02a7c850b4 2008-02-03   stephan:         char const * xcf = 0;
02a7c850b4 2008-02-03   stephan:         xcf = (xform && xform[i])
02a7c850b4 2008-02-03   stephan:           ? (xf=(xform[i])(db_column_text(&st,i)))
02a7c850b4 2008-02-03   stephan:           : db_column_text(&st,i);
02a7c850b4 2008-02-03   stephan:         @ <td>%s(xcf)</td>
02a7c850b4 2008-02-03   stephan:         if( xf ) free( xf );
02a7c850b4 2008-02-03   stephan:       }
02a7c850b4 2008-02-03   stephan:     @ </tr>
02d1ed6ad2 2008-02-02   stephan:   }
02d1ed6ad2 2008-02-02   stephan:   db_finalize( &st );
02d1ed6ad2 2008-02-02   stephan:   @ </tbody></table>
02a7c850b4 2008-02-03   stephan: }
02a7c850b4 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /**
02a7c850b4 2008-02-03   stephan:   Lists all tags matching the given LIKE clause (which
02a7c850b4 2008-02-03   stephan: may be 0).
02a7c850b4 2008-02-03   stephan: */
02a7c850b4 2008-02-03   stephan: static void tagview_page_list_tags( char const * like )
02a7c850b4 2008-02-03   stephan: {
2ab3a2f603 2008-02-03   stephan:   char * likeclause = 0;
2ab3a2f603 2008-02-03   stephan:   const int limit = 10;
2ab3a2f603 2008-02-03   stephan:   char * limitstr = 0;
2ab3a2f603 2008-02-03   stephan:   if( like && strlen(like) )
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     likeclause = mprintf( "AND t.tagname LIKE '%%%%%q%%%%'", like );
2ab3a2f603 2008-02-03   stephan:     @ <h2>Tags matching [%s(likeclause)]:</h2>
2ab3a2f603 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   else
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     limitstr = mprintf( "LIMIT %d", limit );
b81e93f576 2008-02-03   stephan:     @ <h2>%d(limit) most recent non-wiki tags:</h2>
02a7c850b4 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   char * sql = mprintf(
2ab3a2f603 2008-02-03   stephan:     "SELECT t.tagid, t.tagname, DATETIME(tx.mtime), b.uuid "
2ab3a2f603 2008-02-03   stephan:     "FROM tag t, tagxref tx, blob b "
2ab3a2f603 2008-02-03   stephan:     "WHERE (t.tagid=tx.tagid) and (tx.srcid=b.rid) "
2ab3a2f603 2008-02-03   stephan:     "AND (tx.tagtype != 0) %s "
b81e93f576 2008-02-03   stephan:     "AND t.tagname NOT GLOB 'wiki-*' "
2ab3a2f603 2008-02-03   stephan:     "ORDER BY tx.mtime DESC %s",
2ab3a2f603 2008-02-03   stephan:     likeclause ? likeclause : " ",
2ab3a2f603 2008-02-03   stephan:     limitstr ? limitstr : " "
2ab3a2f603 2008-02-03   stephan:     );
02a7c850b4 2008-02-03   stephan:    /* "   AND t.tagname NOT GLOB 'wiki-*'" // Do we want this?? */
2ab3a2f603 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan:   char const * const colnames[] = {
02a7c850b4 2008-02-03   stephan:     "Tag ID", "Name", "Timestamp", "Version"
02a7c850b4 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_strxform_f xf[] = {
02a7c850b4 2008-02-03   stephan:     tagview_xf_link_to_tagid,
02a7c850b4 2008-02-03   stephan:     tagview_xf_link_to_tagname,
02a7c850b4 2008-02-03   stephan:     0,
02a7c850b4 2008-02-03   stephan:     tagview_xf_link_to_uuid
02a7c850b4 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_run_query( sql, colnames, xf );
02a7c850b4 2008-02-03   stephan:   free( sql );
2ab3a2f603 2008-02-03   stephan: }
2ab3a2f603 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /**
02a7c850b4 2008-02-03   stephan: A small search form which forwards to ?like=SEARCH_STRING
02a7c850b4 2008-02-03   stephan: */
2ab3a2f603 2008-02-03   stephan: static void tagview_page_search_miniform(void){
2ab3a2f603 2008-02-03   stephan:   char const * like = P("like");
2ab3a2f603 2008-02-03   stephan:   @ <div style='font-size:smaller'>
2ab3a2f603 2008-02-03   stephan:   @ <form action='/tagview' method='post'>
2ab3a2f603 2008-02-03   stephan:   @ Search for tags:
2ab3a2f603 2008-02-03   stephan:   @ <input type='text' name='like' value='%s((like?like:""))' size='10'/>
2ab3a2f603 2008-02-03   stephan:   @ <input type='submit'/>
2ab3a2f603 2008-02-03   stephan:   @ </form>
2ab3a2f603 2008-02-03   stephan:   @ </div>
2ab3a2f603 2008-02-03   stephan: }
2ab3a2f603 2008-02-03   stephan: 
b81e93f576 2008-02-03   stephan: /**
b81e93f576 2008-02-03   stephan:  tagview_page_default() renders the default page for tagview_page().
b81e93f576 2008-02-03   stephan: */
2ab3a2f603 2008-02-03   stephan: static void tagview_page_default(void){
2ab3a2f603 2008-02-03   stephan:   tagview_page_list_tags( 0 );
2ab3a2f603 2008-02-03   stephan: }
2ab3a2f603 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /**
02a7c850b4 2008-02-03   stephan:   Lists all tags matching the given tagid.
02a7c850b4 2008-02-03   stephan: */
2ab3a2f603 2008-02-03   stephan: static void tagview_page_tag_by_id( int tagid )
2ab3a2f603 2008-02-03   stephan: {
02a7c850b4 2008-02-03   stephan:   @ <h2>Tag #%d(tagid):</h2>
2ab3a2f603 2008-02-03   stephan:   char * sql = mprintf(
2ab3a2f603 2008-02-03   stephan:     "SELECT DISTINCT (t.tagname), DATETIME(tx.mtime), b.uuid "
2ab3a2f603 2008-02-03   stephan:     "FROM tag t, tagxref tx, blob b "
2ab3a2f603 2008-02-03   stephan:     "WHERE (t.tagid=%d) AND (t.tagid=tx.tagid) AND (tx.srcid=b.rid) "
b81e93f576 2008-02-03   stephan:     "AND t.tagname NOT GLOB 'wiki-*' "
2ab3a2f603 2008-02-03   stephan:     "ORDER BY tx.mtime DESC",
2ab3a2f603 2008-02-03   stephan:   tagid);
02a7c850b4 2008-02-03   stephan:   char const * const colnames[] = {
02a7c850b4 2008-02-03   stephan:       "Tag Name", "Timestamp", "Version"
b81e93f576 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_strxform_f xf[] = {
02a7c850b4 2008-02-03   stephan:       tagview_xf_link_to_tagname,
02a7c850b4 2008-02-03   stephan:       0,
02a7c850b4 2008-02-03   stephan:       tagview_xf_link_to_uuid
b81e93f576 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_run_query( sql, colnames, xf );
02a7c850b4 2008-02-03   stephan:   free(sql);
2ab3a2f603 2008-02-03   stephan: }
2ab3a2f603 2008-02-03   stephan: 
02a7c850b4 2008-02-03   stephan: /**
02a7c850b4 2008-02-03   stephan:   Lists all tags matching the given tag name.
02a7c850b4 2008-02-03   stephan: */
2ab3a2f603 2008-02-03   stephan: static void tagview_page_tag_by_name( char const * tagname )
2ab3a2f603 2008-02-03   stephan: {
02a7c850b4 2008-02-03   stephan:   @ <h2>Tag '%s(tagname)':</h2>
2ab3a2f603 2008-02-03   stephan:   char * sql = mprintf(
2ab3a2f603 2008-02-03   stephan:     "SELECT DISTINCT t.tagid, DATETIME(tx.mtime), b.uuid "
2ab3a2f603 2008-02-03   stephan:     "FROM tag t, tagxref tx, blob b "
2ab3a2f603 2008-02-03   stephan:     "WHERE (t.tagname='%q') AND (t.tagid=tx.tagid) AND (tx.srcid=b.rid) "
b81e93f576 2008-02-03   stephan:     "AND t.tagname NOT GLOB 'wiki-*' "
2ab3a2f603 2008-02-03   stephan:     "ORDER BY tx.mtime DESC",
2ab3a2f603 2008-02-03   stephan:     tagname);
02a7c850b4 2008-02-03   stephan:   char const * const colnames[] = {
02a7c850b4 2008-02-03   stephan:       "Tag ID", "Timestamp", "Version"
02a7c850b4 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_strxform_f xf[] = {
02a7c850b4 2008-02-03   stephan:       tagview_xf_link_to_tagid,
02a7c850b4 2008-02-03   stephan:       0,
02a7c850b4 2008-02-03   stephan:       tagview_xf_link_to_uuid
02a7c850b4 2008-02-03   stephan:   };
02a7c850b4 2008-02-03   stephan:   tagview_run_query( sql, colnames, xf );
02a7c850b4 2008-02-03   stephan:   free( sql );
02d1ed6ad2 2008-02-02   stephan: }
02d1ed6ad2 2008-02-02   stephan: 
2ab3a2f603 2008-02-03   stephan: 
2ab3a2f603 2008-02-03   stephan: /*
2ab3a2f603 2008-02-03   stephan: ** WEBPAGE: /tagview
2ab3a2f603 2008-02-03   stephan: */
2ab3a2f603 2008-02-03   stephan: void tagview_page(void){
2ab3a2f603 2008-02-03   stephan: 
2ab3a2f603 2008-02-03   stephan:   login_check_credentials();
b81e93f576 2008-02-03   stephan:   if( !g.okRdWiki ){
2ab3a2f603 2008-02-03   stephan:     login_needed();
2ab3a2f603 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   style_header("Tags");
2ab3a2f603 2008-02-03   stephan:   tagview_page_search_miniform();
2ab3a2f603 2008-02-03   stephan:   @ <hr/>
2ab3a2f603 2008-02-03   stephan:   char const * check = 0;
2ab3a2f603 2008-02-03   stephan:   if( 0 != (check = P("tagid")) )
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     tagview_page_tag_by_id( atoi(check) );
2ab3a2f603 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   else if( 0 != (check = P("like")) )
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     tagview_page_list_tags( check );
2ab3a2f603 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   else if( 0 != (check = P("name")) )
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     tagview_page_tag_by_name( check );
2ab3a2f603 2008-02-03   stephan:   }
2ab3a2f603 2008-02-03   stephan:   else
2ab3a2f603 2008-02-03   stephan:   {
2ab3a2f603 2008-02-03   stephan:     tagview_page_default();
2ab3a2f603 2008-02-03   stephan:   }
10437374a7 2008-02-02       drh:   style_footer();
10437374a7 2008-02-02       drh: }