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
 *
11
 *   '$Author: daigle $'
12
 *     '$Date: 2008-07-06 21:25:34 -0700 (Sun, 06 Jul 2008) $'
13
 * '$Revision: 4080 $'
14
 *
15
 * This program is free software; you can redistribute it and/or modify
16
 * it under the terms of the GNU General Public License as published by
17
 * the Free Software Foundation; either version 2 of the License, or
18
 * (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 * GNU General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU General Public License
26
 * along with this program; if not, write to the Free Software
27
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
28
 */
29

    
30
package edu.ucsb.nceas.metacat;
31

    
32
import java.util.Vector;
33
import org.apache.log4j.Logger;
34

    
35
import edu.ucsb.nceas.metacat.util.MetaCatUtil;
36

    
37
/** a utility class that represents a single term in a query */
38
public class QueryTerm
39
{
40
    private static Logger log = Logger.getLogger(QueryTerm.class);
41

    
42
    private boolean casesensitive = false;
43

    
44
    private String searchmode = null;
45

    
46
    private String value = null;
47

    
48
    private String pathexpr = null;
49

    
50
    private boolean percentageSymbol = false;
51

    
52
    private int countPercentageSearchItem = 0;
53
    
54
    private boolean inUnionGroup = false;
55
    
56
    public static final String CONTAINS = "contains";
57
    
58
    public static final String EQUALS = "equals";
59

    
60
    /**
61
     * Construct a new instance of a query term for a free text search (using
62
     * the value only)
63
     *
64
     * @param casesensitive
65
     *            flag indicating whether case is used to match
66
     * @param searchmode
67
     *            determines what kind of substring match is performed (one of
68
     *            starts-with|ends-with|contains|matches-exactly)
69
     * @param value
70
     *            the text value to match
71
     */
72
    public QueryTerm(boolean casesensitive, String searchmode, String value)
73
    {
74
        this.casesensitive = casesensitive;
75
        this.searchmode = searchmode;
76
        this.value = value;
77
    }
78

    
79
    /**
80
     * Construct a new instance of a query term for a structured search
81
     * (matching the value only for those nodes in the pathexpr)
82
     *
83
     * @param casesensitive
84
     *            flag indicating whether case is used to match
85
     * @param searchmode
86
     *            determines what kind of substring match is performed (one of
87
     *            starts-with|ends-with|contains|matches-exactly)
88
     * @param value
89
     *            the text value to match
90
     * @param pathexpr
91
     *            the hierarchical path to the nodes to be searched
92
     */
93
    public QueryTerm(boolean casesensitive, String searchmode, String value,
94
            String pathexpr)
95
    {
96
        this(casesensitive, searchmode, value);
97
        this.pathexpr = pathexpr;
98
    }
99

    
100
    /** determine if the QueryTerm is case sensitive */
101
    public boolean isCaseSensitive()
102
    {
103
        return casesensitive;
104
    }
105

    
106
    /** get the searchmode parameter */
107
    public String getSearchMode()
108
    {
109
        return searchmode;
110
    }
111

    
112
    /** get the Value parameter */
113
    public String getValue()
114
    {
115
        return value;
116
    }
117

    
118
    /** get the path expression parameter */
119
    public String getPathExpression()
120
    {
121
        return pathexpr;
122
    }
123

    
124
    /** get the percentage count for one query term */
125
    public int getPercentageSymbolCount()
126
    {
127
        return countPercentageSearchItem;
128
    }
129
    
130
    /**
131
     * Set the query term in a union group
132
     * @param inUnionGroup
133
     */
134
    public void setInUnionGroup (boolean inUnionGroup)
135
    {
136
    	this.inUnionGroup = inUnionGroup;
137
    }
138
    
139
    /**
140
     * If this query group in Union group
141
     * @return
142
     */
143
    public boolean isInUnionGroup()
144
    {
145
    	return this.inUnionGroup;
146
    }
147

    
148
    /**
149
     * create a SQL serialization of the query that this instance represents
150
     */
151
    public String printSQL(boolean useXMLIndex)
152
    {
153
        StringBuffer self = new StringBuffer();
154

    
155
        // Uppercase the search string if case match is not important
156
        String casevalue = null;
157
        String nodedataterm = null;
158
        boolean notEqual = false;
159
        if (casesensitive) {
160
            nodedataterm = "nodedata";
161
            casevalue = value;
162
        } else {
163
            nodedataterm = "UPPER(nodedata)";
164
            casevalue = value.toUpperCase();
165
        }
166

    
167
        // Add appropriate wildcards to search string
168
        String searchexpr = null;
169
        if (searchmode.equals("starts-with")) {
170
            searchexpr = nodedataterm + " LIKE '" + casevalue + "%' ";
171
        } else if (searchmode.equals("ends-with")) {
172
            searchexpr = nodedataterm + " LIKE '%" + casevalue + "' ";
173
        } else if (searchmode.equals("contains")) {
174
            if (!casevalue.equals("%")) {
175
                searchexpr = nodedataterm + " LIKE '%" + casevalue + "%' ";
176
            } else {
177
                searchexpr = nodedataterm + " LIKE '" + casevalue + "' ";
178
                // find percentage symbol
179
                percentageSymbol = true;
180
            }
181
        } else if (searchmode.equals("not-contains")) {
182
        	notEqual = true;
183
            searchexpr = nodedataterm + " LIKE '%" + casevalue + "%' ";
184
        } else if (searchmode.equals("equals")) {
185
            searchexpr = nodedataterm + " = '" + casevalue + "' ";
186
        } else if (searchmode.equals("isnot-equal")) {
187
        	notEqual = true;
188
            searchexpr = nodedataterm + " = '" + casevalue + "' ";
189
        } else {
190
            String oper = null;
191
            if (searchmode.equals("greater-than")) {
192
                oper = ">";
193
                nodedataterm = "nodedatanumerical";
194
            } else if (searchmode.equals("greater-than-equals")) {
195
                oper = ">=";
196
                nodedataterm = "nodedatanumerical";
197
            } else if (searchmode.equals("less-than")) {
198
                oper = "<";
199
                nodedataterm = "nodedatanumerical";
200
            } else if (searchmode.equals("less-than-equals")) {
201
                oper = "<=";
202
                nodedataterm = "nodedatanumerical";
203
            } else {
204
                System.out
205
                        .println("NOT expected case. NOT recognized operator: "
206
                                + searchmode);
207
                return null;
208
            }
209
            try {
210
                // it is number; numeric comparison
211
                // but we need to make sure there is no string in node data
212
                searchexpr = nodedataterm + " " + oper + " "
213
                        + new Double(casevalue) + " ";
214
            } catch (NumberFormatException nfe) {
215
                // these are characters; character comparison
216
                searchexpr = nodedataterm + " " + oper + " '" + casevalue
217
                        + "' ";
218
            }
219
        }
220

    
221

    
222
        // to check xml_path_index can be used
223
        boolean usePathIndex = false;
224

    
225
        // if pathexpr has been specified in metacat.properties for indexing
226
        if(pathexpr != null){
227
            if(MetaCatUtil.pathsForIndexing.contains(pathexpr)){
228
                usePathIndex = true;
229
            }
230
        }
231
        
232
        
233
        if(usePathIndex){
234
            // using xml_path_index table.....
235
        	if(notEqual == true ){
236
        		if (!inUnionGroup)
237
        		{
238
        			self.append("SELECT DISTINCT docid from xml_path_index WHERE");
239
        			self.append(" docid NOT IN (Select docid FROM xml_path_index WHERE ");
240
        			self.append(searchexpr);
241
        			self.append("AND path LIKE '" + pathexpr + "') ");
242
        		}
243
        		else
244
        		{
245
        			//if this is in union group we need to use "OR" to modify query
246
        			self.append("("+searchexpr);
247
        			self.append("AND path LIKE '" + pathexpr + "') ");
248
        		}
249
        	} else {
250
        		if (!inUnionGroup)
251
        		{
252
        			self.append("SELECT DISTINCT docid FROM xml_path_index WHERE ");
253
        			self.append(searchexpr);
254
        			self.append("AND path LIKE '" + pathexpr + "' ");
255
        		}
256
        		else
257
        		{
258
        			//if this is in union group we need to use "OR" to modify query
259
        			self.append("("+searchexpr);
260
        			self.append("AND path LIKE '" + pathexpr + "') ");
261
        		}
262
        	}
263

    
264
        } else {
265
            // using xml_nodes and xml_index tables
266

    
267
        	if(notEqual == true){
268
        		self.append("SELECT DISTINCT docid from xml_nodes WHERE");
269
        		self.append(" docid NOT IN (Select docid FROM xml_nodes WHERE ");
270
        	} else {
271
        		self.append("(SELECT DISTINCT docid FROM xml_nodes WHERE ");
272
        	}
273
        	self.append(searchexpr);
274
        	
275
            if (pathexpr != null) {
276
             String path = pathexpr;
277
                // use XML Index
278
                if (useXMLIndex) {
279
                    if (!hasAttributeInPath(pathexpr)) {
280
                        // without attributes in path
281
                        self.append("AND parentnodeid IN ");
282
                    } else {
283
                        // has a attribute in path
284
                        String attributeName = QuerySpecification
285
                            .getAttributeName(pathexpr);
286
                        self.append(
287
                            "AND nodetype LIKE 'ATTRIBUTE' AND nodename LIKE '"
288
                            + attributeName + "' ");
289
                        // and the path expression includes element content other than
290
                        // just './' or '../'
291
                        if ( (!pathexpr.startsWith(QuerySpecification.
292
                            ATTRIBUTESYMBOL)) &&
293
                            (!pathexpr.startsWith("./" +
294
                                                  QuerySpecification.ATTRIBUTESYMBOL)) &&
295
                            (!pathexpr.startsWith("../" +
296
                                                  QuerySpecification.ATTRIBUTESYMBOL))) {
297

    
298
                            self.append("AND parentnodeid IN ");
299
                            path = QuerySpecification
300
                                .newPathExpressionWithOutAttribute(pathexpr);
301
                        }
302
                    }
303
                    self.append(
304
                        "(SELECT nodeid FROM xml_index WHERE path LIKE "
305
                        + "'" + path + "') ");
306
                }
307
                else {
308
                    // without using XML Index; using nested statements instead
309
                    //self.append("AND parentnodeid IN ");
310
                	self.append("AND ");
311
                    self.append(useNestedStatements(pathexpr));
312
                }
313
            }
314
            else if ( (value.trim()).equals("%")) {
315
                //if pathexpr is null and search value is %, is a
316
                // percentageSearchItem
317
                // the count number will be increase one
318
                countPercentageSearchItem++;
319

    
320
            }
321
            self.append(") ");
322
        }
323

    
324
        return self.toString();
325
    }
326

    
327
    /** A method to judge if a path have attribute */
328
    private boolean hasAttributeInPath(String path)
329
    {
330
        if (path.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1) {
331
            return true;
332
        } else {
333
            return false;
334
        }
335
    }
336

    
337
   
338
    public static String useNestedStatements(String pathexpr)
339
    {
340
        log.info("useNestedStatements()");
341
        log.info("pathexpr: " + pathexpr);
342
        String elementPrefix = " parentnodeid IN ";
343
        String attributePrefix  =  " nodeid IN ";
344
        boolean lastOneIsAttribute = false;
345
        StringBuffer nestedStmts = new StringBuffer();
346
        String path = pathexpr.trim();
347
        String sql = "";
348

    
349
        if (path.indexOf('/') == 0)
350
        {
351
            nestedStmts.append("AND parentnodeid = rootnodeid ");
352
            path = path.substring(1).trim();
353
        }
354

    
355
        do
356
        {
357
            int inx = path.indexOf('/');
358
            int predicateStart = -1;
359
            int predicateEnd;
360
            String node;
361
            Vector predicates = new Vector();
362

    
363
            // extract predicates
364
            predicateStart = path.indexOf(QuerySpecification.PREDICATE_START, predicateStart + 1);
365

    
366
            // any predicates in this node?
367
            if (inx != -1 && (predicateStart == -1 || predicateStart > inx))
368
            {
369
                // no
370
                node = path.substring(0, inx).trim();
371
                path = path.substring(inx + 1).trim();
372
            }
373
            else if (predicateStart == -1)
374
            {
375
                // no and it's the last node
376
                node = path;
377
                if (node != null && node.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1)
378
                {
379
                	lastOneIsAttribute = true;
380
                	node = removeAttributeSymbol(node);
381
                }
382
                path = "";
383
            }
384
            else
385
            {
386
                // yes
387
                node = path.substring(0, predicateStart).trim();
388
                path = path.substring(predicateStart);
389
                predicateStart = 0;
390

    
391
                while (predicateStart == 0)
392
                {
393
                    predicateEnd = path.indexOf(QuerySpecification.PREDICATE_END,
394
                            predicateStart);
395

    
396
                    if (predicateEnd == -1)
397
                    {
398
                        log.warn("useNestedStatements(): ");
399
                        log.warn("    Invalid path: " + pathexpr);
400
                        return "";
401
                    }
402

    
403
                    predicates.add(path.substring(1, predicateEnd).trim());
404
                    path = path.substring(predicateEnd + 1).trim();
405
                    inx = path.indexOf('/');
406
                    predicateStart = path.indexOf(QuerySpecification.PREDICATE_START);
407
                }
408

    
409
                if (inx == 0)
410
                    path = path.substring(1).trim();
411
                else if (!path.equals(""))
412
                {
413
                    log.warn("useNestedStatements(): ");
414
                    log.warn("    Invalid path: " + pathexpr);
415
                    return "";
416
                }
417
            }
418

    
419
            nestedStmts.insert(0, "' ").insert(0, node).insert(0,
420
                    "(SELECT nodeid FROM xml_nodes WHERE nodename LIKE '");
421

    
422
            // for the last statement: it is without " AND parentnodeid IN "
423
            if (!path.equals(""))
424
                nestedStmts.insert(0, "AND parentnodeid IN ");
425

    
426
            if (predicates.size() > 0)
427
            {
428
                for (int n = 0; n < predicates.size(); n++)
429
                {
430
                    String predSQL = predicate2SQL((String) predicates.get(n));
431

    
432
                    if (predSQL.equals(""))
433
                        return "";
434

    
435
                    nestedStmts.append(predSQL).append(' ');
436
                }
437
            }
438

    
439
            nestedStmts.append(") ");
440
        }
441
        while (!path.equals(""));
442
        if (lastOneIsAttribute)
443
        {
444
        	sql = attributePrefix+nestedStmts.toString();
445
        }
446
        else
447
        {
448
        	sql = elementPrefix+nestedStmts.toString();
449
        }
450
        return sql;
451
    }
452
    
453
    
454
    /*
455
     * Removes @ symbol from path. For example, if path is @entity, entity will be returned.
456
     * If path is entity, entity will be returned. 
457
     */
458
    private static String removeAttributeSymbol(String path)
459
    {
460
    	String newPath  ="";
461
    	log.debug("Original string before removing @ is " + path);
462
    	if (path != null)
463
    	{
464
    		
465
    		int attribute = path.indexOf(QuerySpecification.ATTRIBUTESYMBOL);
466
    		if (attribute != -1)
467
    		{
468
    			// has attribute symbol. Reomve it and return the remained part. 
469
    			try
470
    			{
471
    		         newPath = path.substring(attribute + 1).trim();
472
    			}
473
    			catch (Exception e)
474
    			{
475
    				newPath = path;
476
    			}
477
    		}
478
    		else
479
    		{
480
    			// doesn't have attribute symbol. Return original string
481
    			newPath = path;
482
    		}
483
    	}
484
    	else
485
    	{
486
    		// if is null, return null;
487
    		newPath = path;
488
    	}
489
    	log.debug("String after removing @ is " + newPath);
490
    	return newPath;
491
    	
492
    }
493

    
494
    /**
495
     * 
496
     */
497
    public static String predicate2SQL(String predicate)
498
    {
499
        String path = predicate.trim();
500
        int equals = path.indexOf('=');
501
        String literal = null;
502

    
503
        if (equals != -1)
504
        {
505
            literal = path.substring(equals + 1).trim();
506
            path = path.substring(0, equals).trim();
507
            int sQuote = literal.indexOf('\'');
508
            int dQuote = literal.indexOf('"');
509

    
510
            if (sQuote == -1 && dQuote == -1)
511
            {
512
                log.warn("predicate2SQL(): ");
513
                log.warn("    Invalid or unsupported predicate: " + predicate);
514
                return "";
515
            }
516

    
517
            if (sQuote == -1 &&
518
                (dQuote != 0 ||
519
                 literal.indexOf('"', dQuote + 1) != literal.length() - 1))
520
            {
521
                log.warn("predicate2SQL(): ");
522
                log.warn("    Invalid or unsupported predicate: " + predicate);
523
                return "";
524
            }
525

    
526
            if (sQuote != 0 ||
527
                literal.indexOf('\'', sQuote + 1) != literal.length() - 1)
528
            {
529
                log.warn("predicate2SQL(): ");
530
                log.warn("    Invalid or unsupported predicate: " + predicate);
531
                return "";
532
            }
533
        }
534

    
535
        StringBuffer sql = new StringBuffer();
536
        int attribute = path.indexOf('@');
537

    
538
        if (attribute == -1)
539
        {
540
            if (literal != null)
541
            {
542
                sql.append("AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'TEXT' AND nodedata LIKE ")
543
                    .append(literal).append(")");
544
            }
545
        }
546
        else
547
        {
548
            sql.append(
549
                    "AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'ATTRIBUTE' AND nodename LIKE '")
550
                    .append(path.substring(attribute + 1).trim()).append("' ");
551

    
552
            if (literal != null)
553
            {
554
                sql.append("AND nodedata LIKE ").append(literal);
555
            }
556

    
557
            sql.append(")");
558
            path = path.substring(0, attribute).trim();
559

    
560
            if (path.endsWith("/"))
561
                path = path.substring(0, path.length() - 1).trim();
562
            else
563
            {
564
                if (!path.equals(""))
565
                {
566
                    log.warn("predicate2SQL(): ");
567
                    log.warn("    Invalid or unsupported predicate: " + predicate);
568
                    return "";
569
                }
570
            }
571
        }
572

    
573
        while (!path.equals(""))
574
        {
575
            int ndx = path.lastIndexOf('/');
576
            int predicateEnd = -1;
577
            int predicateStart;
578
            String node;
579

    
580
            if (ndx != -1)
581
            {
582
                node = path.substring(ndx + 1).trim();
583
                path = path.substring(0, ndx).trim();
584
            }
585
            else
586
            {
587
                node = path;
588
                path = "";
589
            }
590

    
591
            if (!node.equals(""))
592
                sql.insert(0, "' ").insert(0, node)
593
                    .insert(0, "(SELECT parentnodeid FROM xml_nodes WHERE nodename LIKE '").append(") ");
594
            else if (!path.equals(""))
595
            {
596
                log.warn("predicate2SQL(): ");
597
                log.warn("    Invalid or unsupported predicate: " + predicate);
598
                return "";
599
            }
600

    
601
            if (path.equals(""))
602
            {
603
                sql.insert(0,
604
                        node.equals("") ? "AND rootnodeid IN " : "AND nodeid IN ");
605
            }
606
            else
607
            {
608
                sql.append("AND nodeid IN ");
609
            }
610
        }
611

    
612
        return sql.toString();
613
    }
614

    
615
    /**
616
     * create a String description of the query that this instance represents.
617
     * This should become a way to get the XML serialization of the query.
618
     */
619
    public String toString()
620
    {
621

    
622
        return this.printSQL(true);
623
    }
624
    
625
    /**
626
     * Compare two query terms to see if they have same search value.
627
     * @param term
628
     * @return
629
     */
630
    public boolean hasSameSearchValue(QueryTerm term)
631
    {
632
    	boolean same = false;
633
    	if (term != null)
634
    	{
635
    		String searchValue = term.getValue();
636
    		if (searchValue != null && this.value != null)
637
    		{
638
    			if (searchValue.equalsIgnoreCase(this.value))
639
    			{
640
    				same = true;
641
    			}
642
    		}
643
    	}
644
    	return same;
645
    }
646
}
(56-56/67)