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

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

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

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

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

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

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

    
493
      self.append("(");
494

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
635
      return self.toString();
636
    }
637

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

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

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

    
657
/**
658
 * '$Log$
659
 * 'Revision 1.16  2000/09/26 22:06:52  berkley
660
 * 'Added backtrack functionality.  Backtracking works by passing a returndoc parameter.  There can be more than one.  If a document that is hit by a query is not of type returndoc then it searches the database for a related file of type returndoc.  If one is found it is displayed, if no relation is found, the original is displayed.
661
 * '
662
 * 'Support was also added for an index of relations.  the table xml_relation handles the all of the relation indexing.
663
 * '
664
 * 'Revision 1.15  2000/09/15 19:52:12  berkley
665
 * '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.
666
 * '
667
 * 'Revision 1.14  2000/08/31 21:20:39  berkley
668
 * 'changed xslf for new returnfield scheme.  the returnfields are now returned as <param name="<returnfield>"> tags.
669
 * 'hThe sql for the returnfield query was redone to fix a previous problem with slow queries
670
 * '
671
 * 'Revision 1.13  2000/08/23 22:55:38  berkley
672
 * 'changed the field names to be case-sensitive in the returnfields
673
 * '
674
 * 'Revision 1.12  2000/08/23 17:29:05  berkley
675
 * 'added support for the returnfield parameter
676
 * '-QuerySpecification now sets a flag (containsExtendedSQL) when there are returnfield items in the pathquery document.
677
 * 'the accessor method containsExtendedSQL() can be called by other classes to check for extended return parameters
678
 * '-getReturnFields returns a Vector of the names of each specified return field.
679
 * '-printExtendedSQL returns a string of the extra SQL statements required for the query.
680
 * '
681
 * '-a calling class should first check containsExtendedSQL to make sure that there are extra fields being returned, then call printExtendedSQL to
682
 * 'insert the extra SQL into the query.  (Note that this is how DBQuery implements this.)
683
 * '
684
 * 'Revision 1.11  2000/08/14 20:53:34  jones
685
 * 'Added "release" keyword to all metacat source files so that the release
686
 * 'number will be evident in software distributions.
687
 * '
688
 * 'Revision 1.10  2000/06/26 10:35:05  jones
689
 * 'Merged in substantial changes to DBWriter and associated classes and to
690
 * 'the MetaCatServlet in order to accomodate the new UPDATE and DELETE
691
 * 'functions.  The command line tools and the parameters for the
692
 * 'servlet have changed substantially.
693
 * '
694
 * 'Revision 1.9.2.3  2000/06/25 23:38:17  jones
695
 * 'Added RCSfile keyword
696
 * '
697
 * 'Revision 1.9.2.2  2000/06/25 23:34:18  jones
698
 * 'Changed documentation formatting, added log entries at bottom of source files
699
 * ''
700
 */
(29-29/33)