Project

General

Profile

1
/**
2
 *  '$RCSfile$'
3
 *    Purpose: A Class that represents a structured query, and can be 
4
 *             constructed from an XML serialization conforming to 
5
 *             pathquery.dtd. The printSQL() method can be used to print 
6
 *             a SQL serialization of the query.
7
 *  Copyright: 2000 Regents of the University of California and the
8
 *             National Center for Ecological Analysis and Synthesis
9
 *    Authors: Matt Jones
10
 *    Release: @release@
11
 *
12
 *   '$Author: berkley $'
13
 *     '$Date: 2000-09-26 15:06:52 -0700 (Tue, 26 Sep 2000) $'
14
 * '$Revision: 465 $'
15
 */
16

    
17
package edu.ucsb.nceas.metacat;
18

    
19
import java.io.*;
20
import java.util.Stack;
21
import java.util.Vector;
22
import java.util.Enumeration;
23

    
24
import org.xml.sax.Attributes;
25
import org.xml.sax.InputSource;
26
import org.xml.sax.SAXException;
27
import org.xml.sax.SAXParseException;
28
import org.xml.sax.XMLReader;
29
import org.xml.sax.helpers.XMLReaderFactory;
30
import org.xml.sax.helpers.DefaultHandler;
31

    
32
/**
33
 * A Class that represents a structured query, and can be 
34
 * constructed from an XML serialization conforming to @see pathquery.dtd. 
35
 * The printSQL() method can be used to print a SQL serialization of the query.
36
 */
37
public class QuerySpecification extends DefaultHandler {
38
 
39
  private boolean containsExtendedSQL=false;
40
 
41
  // Query data structures
42
  private String meta_file_id;
43
  private String querytitle;
44
  private Vector doctypeList;
45
  private Vector returnFieldList;
46
  private QueryGroup query = null;
47

    
48
  private Stack elementStack;
49
  private Stack queryStack;
50
  private String currentValue;
51
  private String currentPathexpr;
52
  private String parserName = null;
53

    
54
  /**
55
   * construct an instance of the QuerySpecification class 
56
   *
57
   * @param queryspec the XML representation of the query (should conform
58
   *                  to pathquery.dtd) as a Reader
59
   * @param parserName the fully qualified name of a Java Class implementing
60
   *                  the org.xml.sax.XMLReader interface
61
   */
62
  public QuerySpecification( Reader queryspec, String parserName ) 
63
         throws IOException {
64
    super();
65
    
66
    // Initialize the class variables
67
    doctypeList = new Vector();
68
    elementStack = new Stack();
69
    queryStack   = new Stack();
70
    returnFieldList = new Vector();
71
    this.parserName = parserName;
72

    
73
    // Initialize the parser and read the queryspec
74
    XMLReader parser = initializeParser();
75
    if (parser == null) {
76
      System.err.println("SAX parser not instantiated properly.");
77
    }
78
    try {
79
      parser.parse(new InputSource(queryspec));
80
    } catch (SAXException e) {
81
      System.err.println("error parsing data");
82
      System.err.println(e.getMessage());
83
    }
84
  }
85

    
86
  /**
87
   * construct an instance of the QuerySpecification class 
88
   *
89
   * @param queryspec the XML representation of the query (should conform
90
   *                  to pathquery.dtd) as a String
91
   * @param parserName the fully qualified name of a Java Class implementing
92
   *                  the org.xml.sax.Parser interface
93
   */
94
  public QuerySpecification( String queryspec, String parserName ) 
95
         throws IOException {
96
    this(new StringReader(queryspec), parserName);
97
  }
98

    
99
  /** Main routine for testing */
100
  static public void main(String[] args) {
101

    
102
     if (args.length < 1) {
103
       System.err.println("Wrong number of arguments!!!");
104
       System.err.println("USAGE: java QuerySpecification <xmlfile>");
105
       return;
106
     } else {
107
       String xmlfile  = args[0];
108
        
109
       try {
110
         MetaCatUtil util = new MetaCatUtil();
111
         FileReader xml = new FileReader(new File(xmlfile));
112
         QuerySpecification qspec = 
113
                 new QuerySpecification(xml, util.getOption("saxparser"));
114
         System.out.println(qspec.printSQL());
115

    
116
       } catch (IOException e) {
117
         System.err.println(e.getMessage());
118
       }
119
         
120
     }
121
  }
122
  
123
  /**
124
   * Returns true if the parsed query contains and extended xml query 
125
   * (i.e. there is at least one &lt;returnfield&gt; in the pathquery document)
126
   */
127
  public boolean containsExtendedSQL()
128
  {
129
    if(containsExtendedSQL)
130
    {
131
      return true;
132
    }
133
    else
134
    {
135
      return false;
136
    }
137
  }
138
  
139
  /**
140
   * Accessor method to return a vector of the extended return fields as
141
   * defined in the &lt;returnfield&gt; tag in the pathquery dtd.
142
   */
143
  public Vector getReturnFieldList()
144
  {
145
    return this.returnFieldList; 
146
  }
147

    
148
  /**
149
   * Set up the SAX parser for reading the XML serialized query
150
   */
151
  private XMLReader initializeParser() {
152
    XMLReader parser = null;
153

    
154
    // Set up the SAX document handlers for parsing
155
    try {
156

    
157
      // Get an instance of the parser
158
      parser = XMLReaderFactory.createXMLReader(parserName);
159

    
160
      // Set the ContentHandler to this instance
161
      parser.setContentHandler(this);
162

    
163
      // Set the error Handler to this instance
164
      parser.setErrorHandler(this);
165

    
166
    } catch (Exception e) {
167
       System.err.println(e.toString());
168
    }
169

    
170
    return parser;
171
  }
172

    
173
  /**
174
   * callback method used by the SAX Parser when the start tag of an 
175
   * element is detected. Used in this context to parse and store
176
   * the query information in class variables.
177
   */
178
  public void startElement (String uri, String localName, 
179
                            String qName, Attributes atts) 
180
         throws SAXException {
181
    BasicNode currentNode = new BasicNode(localName);
182
    // add attributes to BasicNode here
183
    if (atts != null) {
184
      int len = atts.getLength();
185
      for (int i = 0; i < len; i++) {
186
        currentNode.setAttribute(atts.getLocalName(i), atts.getValue(i));
187
      }
188
    }
189

    
190
    elementStack.push(currentNode); 
191
    if (currentNode.getTagName().equals("querygroup")) {
192
      QueryGroup currentGroup = new QueryGroup(
193
                                currentNode.getAttribute("operator"));
194
      if (query == null) {
195
        query = currentGroup;
196
      } else {
197
        QueryGroup parentGroup = (QueryGroup)queryStack.peek();
198
        parentGroup.addChild(currentGroup);
199
      }
200
      queryStack.push(currentGroup);
201
    }
202
  }
203

    
204
  /**
205
   * callback method used by the SAX Parser when the end tag of an 
206
   * element is detected. Used in this context to parse and store
207
   * the query information in class variables.
208
   */
209
  public void endElement (String uri, String localName,
210
                          String qName) throws SAXException {
211
    BasicNode leaving = (BasicNode)elementStack.pop(); 
212
    if (leaving.getTagName().equals("queryterm")) {
213
      boolean isCaseSensitive = (new Boolean(
214
              leaving.getAttribute("casesensitive"))).booleanValue();
215
      QueryTerm currentTerm = null;
216
      if (currentPathexpr == null) {
217
        currentTerm = new QueryTerm(isCaseSensitive,
218
                      leaving.getAttribute("searchmode"),currentValue);
219
      } else {
220
        currentTerm = new QueryTerm(isCaseSensitive,
221
                      leaving.getAttribute("searchmode"),currentValue,
222
                      currentPathexpr);
223
      }
224
      QueryGroup currentGroup = (QueryGroup)queryStack.peek();
225
      currentGroup.addChild(currentTerm);
226
      currentValue = null;
227
      currentPathexpr = null;
228
    } else if (leaving.getTagName().equals("querygroup")) {
229
      QueryGroup leavingGroup = (QueryGroup)queryStack.pop();
230
    }
231
  }
232

    
233
  /**
234
   * callback method used by the SAX Parser when the text sequences of an 
235
   * xml stream are detected. Used in this context to parse and store
236
   * the query information in class variables.
237
   */
238
  public void characters(char ch[], int start, int length) {
239

    
240
    String inputString = new String(ch, start, length);
241
    BasicNode currentNode = (BasicNode)elementStack.peek(); 
242
    String currentTag = currentNode.getTagName();
243
    if (currentTag.equals("meta_file_id")) {
244
      meta_file_id = inputString;
245
    } else if (currentTag.equals("querytitle")) {
246
      querytitle = inputString;
247
    } else if (currentTag.equals("value")) {
248
      currentValue = inputString;
249
    } else if (currentTag.equals("pathexpr")) {
250
      currentPathexpr = inputString;
251
    } else if (currentTag.equals("returndoctype")) {
252
      doctypeList.add(inputString);
253
    } else if (currentTag.equals("returnfield")) {
254
      returnFieldList.add(inputString);
255
      containsExtendedSQL = true;
256
    }
257
  }
258

    
259

    
260
  /**
261
   * create a SQL serialization of the query that this instance represents
262
   */
263
  public String printSQL() {
264
    StringBuffer self = new StringBuffer();
265

    
266
    self.append("SELECT docid,docname,doctype,doctitle,");
267
    self.append("date_created, date_updated ");
268
    self.append("FROM xml_documents WHERE docid IN (");
269

    
270
    // This determines the documents that meet the query conditions
271
    self.append(query.printSQL());
272

    
273
    self.append(") ");
274
 
275
    // Add SQL to filter for doctypes requested in the query
276
    if (!doctypeList.isEmpty()) {
277
      boolean firstdoctype = true;
278
      self.append(" AND ("); 
279
      Enumeration en = doctypeList.elements();
280
      while (en.hasMoreElements()) {
281
        String currentDoctype = (String)en.nextElement();
282
        if (firstdoctype) {
283
           firstdoctype = false;
284
           self.append(" doctype = '" + currentDoctype + "'"); 
285
        } else {
286
          self.append(" OR doctype = '" + currentDoctype + "'"); 
287
        }
288
      }
289
      self.append(") ");
290
    }
291
    
292
    return self.toString();
293
  }
294
  
295
  /**
296
   * This method prints sql based upon the &lt;returnfield&gt; tag in the
297
   * pathquery document.  This allows for customization of the 
298
   * returned fields
299
   * @param doclist the list of document ids to search by
300
   */
301
  public String printExtendedSQL(String doclist)
302
  {  
303
    StringBuffer self = new StringBuffer();
304
    self.append("select xml_nodes.docid, xml_index.path, xml_nodes.nodedata ");
305
    self.append("from xml_index, xml_nodes where xml_index.nodeid=");
306
    self.append("xml_nodes.parentnodeid and (xml_index.path like '");
307
    boolean firstfield = true;
308
    //put the returnfields into the query
309
    //the for loop allows for multiple fields
310
    for(int i=0; i<returnFieldList.size(); i++)
311
    {
312
      if(firstfield)
313
      {
314
        firstfield = false;
315
        self.append((String)returnFieldList.elementAt(i));
316
        self.append("' ");
317
      }
318
      else
319
      {
320
        self.append("or xml_index.path like '");
321
        self.append((String)returnFieldList.elementAt(i));
322
        self.append("' ");
323
      }
324
    }
325
    self.append(") AND xml_nodes.docid in (");
326
    //self.append(query.printSQL());
327
    self.append(doclist);
328
    self.append(")");
329
    self.append(" AND xml_nodes.nodetype = 'TEXT'");
330

    
331
    //System.out.println(self.toString());
332
    return self.toString();
333
  }
334
  
335
  public static String printRelationSQL(String docid)
336
  {
337
    StringBuffer self = new StringBuffer();
338
    self.append("select subject, relationship, object from xml_relation ");
339
    self.append("where subject like '").append(docid).append("'");
340
    return self.toString();
341
  }
342
   
343
  /**
344
   * Prints sql that returns all relations in the database.
345
   */
346
  public static String printPackageSQL()
347
  {
348
    StringBuffer self = new StringBuffer();
349
    self.append("select z.nodedata, x.nodedata, y.nodedata from ");
350
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
351
    self.append("'package/relation/subject') s, (select nodeid, parentnodeid ");
352
    self.append("from xml_index where path like ");
353
    self.append("'package/relation/relationship') rel, ");
354
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
355
    self.append("'package/relation/object') o, ");
356
    self.append("xml_nodes x, xml_nodes y, xml_nodes z ");
357
    self.append("where s.parentnodeid = rel.parentnodeid ");
358
    self.append("and rel.parentnodeid = o.parentnodeid ");
359
    self.append("and x.parentnodeid in rel.nodeid ");
360
    self.append("and y.parentnodeid in o.nodeid ");
361
    self.append("and z.parentnodeid in s.nodeid ");
362
    //self.append("and z.nodedata like '%");
363
    //self.append(docid);
364
    //self.append("%'");
365
    return self.toString();
366
  }
367
  
368
  /**
369
   * Prints sql that returns all relations in the database that were input
370
   * under a specific docid
371
   * @param docid the docid to search for.
372
   */
373
  public static String printPackageSQL(String docid)
374
  {
375
    StringBuffer self = new StringBuffer();
376
    self.append("select z.nodedata, x.nodedata, y.nodedata from ");
377
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
378
    self.append("'package/relation/subject') s, (select nodeid, parentnodeid ");
379
    self.append("from xml_index where path like ");
380
    self.append("'package/relation/relationship') rel, ");
381
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
382
    self.append("'package/relation/object') o, ");
383
    self.append("xml_nodes x, xml_nodes y, xml_nodes z ");
384
    self.append("where s.parentnodeid = rel.parentnodeid ");
385
    self.append("and rel.parentnodeid = o.parentnodeid ");
386
    self.append("and x.parentnodeid in rel.nodeid ");
387
    self.append("and y.parentnodeid in o.nodeid ");
388
    self.append("and z.parentnodeid in s.nodeid ");
389
    self.append("and z.docid like '").append(docid).append("'");
390
    
391
    return self.toString();
392
  }
393
  
394
  /**
395
   * Returns all of the relations that has a certain docid in the subject
396
   * or the object.
397
   * 
398
   * @param docid the docid to search for
399
   */
400
  public static String printPackageSQL(String subDocidURL, String objDocidURL)
401
  {
402
    StringBuffer self = new StringBuffer();
403
    self.append("select z.nodedata, x.nodedata, y.nodedata from ");
404
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
405
    self.append("'package/relation/subject') s, (select nodeid, parentnodeid ");
406
    self.append("from xml_index where path like ");
407
    self.append("'package/relation/relationship') rel, ");
408
    self.append("(select nodeid, parentnodeid from xml_index where path like ");
409
    self.append("'package/relation/object') o, ");
410
    self.append("xml_nodes x, xml_nodes y, xml_nodes z ");
411
    self.append("where s.parentnodeid = rel.parentnodeid ");
412
    self.append("and rel.parentnodeid = o.parentnodeid ");
413
    self.append("and x.parentnodeid in rel.nodeid ");
414
    self.append("and y.parentnodeid in o.nodeid ");
415
    self.append("and z.parentnodeid in s.nodeid ");
416
    self.append("and (z.nodedata like '");
417
    self.append(subDocidURL);
418
    self.append("' or y.nodedata like '");
419
    self.append(objDocidURL);
420
    self.append("')");
421
    return self.toString();
422
  }
423
  
424
  public static String printGetDocByDoctypeSQL(String docid)
425
  {
426
    StringBuffer self = new StringBuffer();
427

    
428
    self.append("SELECT docid,docname,doctype,doctitle,");
429
    self.append("date_created, date_updated ");
430
    self.append("FROM xml_documents WHERE docid IN (");
431
    self.append(docid).append(")");
432
    return self.toString();
433
  }
434
  
435
  /**
436
   * create a String description of the query that this instance represents.
437
   * This should become a way to get the XML serialization of the query.
438
   */
439
  public String toString() {
440
    return "meta_file_id=" + meta_file_id + "\n" + 
441
           "querytitle=" + querytitle + "\n" + query;
442
  }
443

    
444
  /** a utility class that represents a group of terms in a query */
445
  private class QueryGroup {
446
    private String operator = null;  // indicates how query terms are combined
447
    private Vector children = null;  // the list of query terms and groups
448

    
449
    /** 
450
     * construct a new QueryGroup 
451
     *
452
     * @param operator the boolean conector used to connect query terms 
453
     *                    in this query group
454
     */
455
    public QueryGroup(String operator) {
456
      this.operator = operator;
457
      children = new Vector();
458
    }
459

    
460
    /** 
461
     * Add a child QueryGroup to this QueryGroup
462
     *
463
     * @param qgroup the query group to be added to the list of terms
464
     */
465
    public void addChild(QueryGroup qgroup) {
466
      children.add((Object)qgroup); 
467
    }
468

    
469
    /**
470
     * Add a child QueryTerm to this QueryGroup
471
     *
472
     * @param qterm the query term to be added to the list of terms
473
     */
474
    public void addChild(QueryTerm qterm) {
475
      children.add((Object)qterm); 
476
    }
477

    
478
    /**
479
     * Retrieve an Enumeration of query terms for this QueryGroup
480
     */
481
    public Enumeration getChildren() {
482
      return children.elements();
483
    }
484
   
485
    /**
486
     * create a SQL serialization of the query that this instance represents
487
     */
488
    public String printSQL() {
489
      StringBuffer self = new StringBuffer();
490
      boolean first = true;
491

    
492
      self.append("(");
493

    
494
      Enumeration en= getChildren();
495
      while (en.hasMoreElements()) {
496
        Object qobject = en.nextElement();
497
        if (first) {
498
          first = false;
499
        } else {
500
          self.append(" " + operator + " ");
501
        }
502
        if (qobject instanceof QueryGroup) {
503
          QueryGroup qg = (QueryGroup)qobject;
504
          self.append(qg.printSQL());
505
        } else if (qobject instanceof QueryTerm) {
506
          QueryTerm qt = (QueryTerm)qobject;
507
          self.append(qt.printSQL());
508
        } else {
509
          System.err.println("qobject wrong type: fatal error");
510
        }
511
      }
512
      self.append(") \n");
513
      return self.toString();
514
    }
515

    
516
    /**
517
     * create a String description of the query that this instance represents.
518
     * This should become a way to get the XML serialization of the query.
519
     */
520
    public String toString() {
521
      StringBuffer self = new StringBuffer();
522

    
523
      self.append("  (Query group operator=" + operator + "\n");
524
      Enumeration en= getChildren();
525
      while (en.hasMoreElements()) {
526
        Object qobject = en.nextElement();
527
        self.append(qobject);
528
      }
529
      self.append("  )\n");
530
      return self.toString();
531
    }
532
  }
533

    
534
  /** a utility class that represents a single term in a query */
535
  private class QueryTerm {
536
    private boolean casesensitive = false;
537
    private String searchmode = null;
538
    private String value = null;
539
    private String pathexpr = null;
540

    
541
    /**
542
     * Construct a new instance of a query term for a free text search
543
     * (using the value only)
544
     *
545
     * @param casesensitive flag indicating whether case is used to match
546
     * @param searchmode determines what kind of substring match is performed
547
     *        (one of starts-with|ends-with|contains|matches-exactly)
548
     * @param value the text value to match
549
     */
550
    public QueryTerm(boolean casesensitive, String searchmode, 
551
                     String value) {
552
      this.casesensitive = casesensitive;
553
      this.searchmode = searchmode;
554
      this.value = value;
555
    }
556

    
557
    /**
558
     * Construct a new instance of a query term for a structured search
559
     * (matching the value only for those nodes in the pathexpr)
560
     *
561
     * @param casesensitive flag indicating whether case is used to match
562
     * @param searchmode determines what kind of substring match is performed
563
     *        (one of starts-with|ends-with|contains|matches-exactly)
564
     * @param value the text value to match
565
     * @param pathexpr the hierarchical path to the nodes to be searched
566
     */
567
    public QueryTerm(boolean casesensitive, String searchmode, 
568
                     String value, String pathexpr) {
569
      this(casesensitive, searchmode, value);
570
      this.pathexpr = pathexpr;
571
    }
572

    
573
    /** determine if the QueryTerm is case sensitive */
574
    public boolean isCaseSensitive() {
575
      return casesensitive;
576
    }
577

    
578
    /** get the searchmode parameter */
579
    public String getSearchMode() {
580
      return searchmode;
581
    }
582
 
583
    /** get the Value parameter */
584
    public String getValue() {
585
      return value;
586
    }
587

    
588
    /** get the path expression parameter */
589
    public String getPathExpression() {
590
      return pathexpr;
591
    }
592

    
593
    /**
594
     * create a SQL serialization of the query that this instance represents
595
     */
596
    public String printSQL() {
597
      StringBuffer self = new StringBuffer();
598

    
599
      // Uppercase the search string if case match is not important
600
      String casevalue = null;
601
      String nodedataterm = null;
602

    
603
      if (casesensitive) {
604
        nodedataterm = "nodedata";
605
        casevalue = value;
606
      } else {
607
        nodedataterm = "UPPER(nodedata)";
608
        casevalue = value.toUpperCase();
609
      }
610

    
611
      // Add appropriate wildcards to search string
612
      String searchvalue = null;
613
      if (searchmode.equals("starts-with")) {
614
        searchvalue = casevalue + "%";
615
      } else if (searchmode.equals("ends-with")) {
616
        searchvalue = "%" + casevalue;
617
      } else if (searchmode.equals("contains")) {
618
        searchvalue = "%" + casevalue + "%";
619
      } else {
620
        searchvalue = casevalue;
621
      }
622

    
623
      self.append("SELECT DISTINCT docid FROM xml_nodes WHERE \n");
624

    
625
      if (pathexpr != null) {
626
        self.append(nodedataterm + " LIKE " + "'" + searchvalue + "' ");
627
        self.append("AND parentnodeid IN ");
628
        self.append("(SELECT nodeid FROM xml_index WHERE path LIKE " + 
629
                    "'" +  pathexpr + "') " );
630
      } else {
631
        self.append(nodedataterm + " LIKE " + "'" + searchvalue + "' ");
632
      }
633

    
634
      return self.toString();
635
    }
636

    
637
    /**
638
     * create a String description of the query that this instance represents.
639
     * This should become a way to get the XML serialization of the query.
640
     */
641
    public String toString() {
642
      StringBuffer self = new StringBuffer();
643

    
644
      self.append("    Query Term iscasesensitive=" + casesensitive + "\n");
645
      self.append("               searchmode=" + searchmode + "\n");
646
      self.append("               value=" + value + "\n");
647
      if (pathexpr != null) {
648
        self.append("               pathexpr=" + pathexpr + "\n");
649
      }
650

    
651
      return self.toString();
652
    }
653
  }
654
}
655

    
656
/**
657
 * '$Log$
658
 * 'Revision 1.15  2000/09/15 19:52:12  berkley
659
 * 'Added functionality for package specifications.  metacatservlet now contains a new action called getrelateddocument that handles retrieving related documents using the metacatURL specification (metacatURL.java).  DBQuery contains new code in runQuery that embeds relation tags in the returned hashtable describing the documents related to each docid.  querySpecification contains a new method which prints the sql that does the relation query.
660
 * '
661
 * 'Revision 1.14  2000/08/31 21:20:39  berkley
662
 * 'changed xslf for new returnfield scheme.  the returnfields are now returned as <param name="<returnfield>"> tags.
663
 * 'hThe sql for the returnfield query was redone to fix a previous problem with slow queries
664
 * '
665
 * 'Revision 1.13  2000/08/23 22:55:38  berkley
666
 * 'changed the field names to be case-sensitive in the returnfields
667
 * '
668
 * 'Revision 1.12  2000/08/23 17:29:05  berkley
669
 * 'added support for the returnfield parameter
670
 * '-QuerySpecification now sets a flag (containsExtendedSQL) when there are returnfield items in the pathquery document.
671
 * 'the accessor method containsExtendedSQL() can be called by other classes to check for extended return parameters
672
 * '-getReturnFields returns a Vector of the names of each specified return field.
673
 * '-printExtendedSQL returns a string of the extra SQL statements required for the query.
674
 * '
675
 * '-a calling class should first check containsExtendedSQL to make sure that there are extra fields being returned, then call printExtendedSQL to
676
 * 'insert the extra SQL into the query.  (Note that this is how DBQuery implements this.)
677
 * '
678
 * 'Revision 1.11  2000/08/14 20:53:34  jones
679
 * 'Added "release" keyword to all metacat source files so that the release
680
 * 'number will be evident in software distributions.
681
 * '
682
 * 'Revision 1.10  2000/06/26 10:35:05  jones
683
 * 'Merged in substantial changes to DBWriter and associated classes and to
684
 * 'the MetaCatServlet in order to accomodate the new UPDATE and DELETE
685
 * 'functions.  The command line tools and the parameters for the
686
 * 'servlet have changed substantially.
687
 * '
688
 * 'Revision 1.9.2.3  2000/06/25 23:38:17  jones
689
 * 'Added RCSfile keyword
690
 * '
691
 * 'Revision 1.9.2.2  2000/06/25 23:34:18  jones
692
 * 'Changed documentation formatting, added log entries at bottom of source files
693
 * ''
694
 */
(25-25/29)