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: leinfelder $'
12
 *     '$Date: 2011-12-06 15:03:24 -0800 (Tue, 06 Dec 2011) $'
13
 * '$Revision: 6740 $'
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.ArrayList;
33
import java.util.Calendar;
34
import java.util.List;
35
import java.util.Vector;
36

    
37
import javax.xml.bind.DatatypeConverter;
38

    
39
import org.apache.log4j.Logger;
40

    
41
import edu.ucsb.nceas.metacat.shared.MetacatUtilException;
42
import edu.ucsb.nceas.metacat.util.SystemUtil;
43

    
44
/** a utility class that represents a single term in a query */
45
public class QueryTerm
46
{
47
    private static Logger log = Logger.getLogger(QueryTerm.class);
48

    
49
    private boolean casesensitive = false;
50

    
51
    private String searchmode = null;
52

    
53
    private String value = null;
54

    
55
    private String pathexpr = null;
56

    
57
    private boolean percentageSymbol = false;
58

    
59
    private int countPercentageSearchItem = 0;
60
    
61
    private boolean inUnionGroup = false;
62
    
63
    public static final String CONTAINS = "contains";
64
    
65
    public static final String EQUALS = "equals";
66

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

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

    
107
    /** determine if the QueryTerm is case sensitive */
108
    public boolean isCaseSensitive()
109
    {
110
        return casesensitive;
111
    }
112

    
113
    /** get the searchmode parameter */
114
    public String getSearchMode()
115
    {
116
        return searchmode;
117
    }
118

    
119
    /** get the Value parameter */
120
    public String getValue()
121
    {
122
        return value;
123
    }
124

    
125
    /** get the path expression parameter */
126
    public String getPathExpression()
127
    {
128
        return pathexpr;
129
    }
130

    
131
    /** get the percentage count for one query term */
132
    public int getPercentageSymbolCount()
133
    {
134
        return countPercentageSearchItem;
135
    }
136
    
137
    /**
138
     * Set the query term in a union group
139
     * @param inUnionGroup
140
     */
141
    public void setInUnionGroup (boolean inUnionGroup)
142
    {
143
    	this.inUnionGroup = inUnionGroup;
144
    }
145
    
146
    /**
147
     * If this query group in Union group
148
     * @return
149
     */
150
    public boolean isInUnionGroup()
151
    {
152
    	return this.inUnionGroup;
153
    }
154

    
155
    
156
    public String printSearchExprSQL(List<Object> parameterValues) {
157

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

    
170
        // Add appropriate wildcards to search string
171
        String searchexpr = null;
172
        if (searchmode.equals("starts-with")) {
173
            searchexpr = nodedataterm + " LIKE ? ";
174
            parameterValues.add(casevalue + "%");
175
        } else if (searchmode.equals("ends-with")) {
176
            searchexpr = nodedataterm + " LIKE ? ";
177
            parameterValues.add("%" + casevalue + "%");
178
        } else if (searchmode.equals("contains")) {
179
            if (!casevalue.equals("%")) {
180
                searchexpr = nodedataterm + " LIKE ? ";
181
                parameterValues.add("%" + casevalue + "%");
182
            } else {
183
                searchexpr = nodedataterm + " LIKE ? ";
184
                parameterValues.add(casevalue);
185
                // find percentage symbol
186
                percentageSymbol = true;
187
            }
188
        } else if (searchmode.equals("not-contains")) {
189
        	notEqual = true;
190
            searchexpr = nodedataterm + " LIKE ? ";
191
            parameterValues.add("%" + casevalue + "%");
192
        } else if (searchmode.equals("equals")) {
193
            searchexpr = nodedataterm + " = ? ";
194
            parameterValues.add(casevalue);
195
        } else if (searchmode.equals("isnot-equal")) {
196
        	notEqual = true;
197
            searchexpr = nodedataterm + " = ? ";
198
            parameterValues.add(casevalue);
199
        } else {
200
            String oper = null;
201
            if (searchmode.equals("greater-than")) {
202
                oper = ">";
203
                nodedataterm = "nodedatanumerical";
204
            } else if (searchmode.equals("greater-than-equals")) {
205
                oper = ">=";
206
                nodedataterm = "nodedatanumerical";
207
            } else if (searchmode.equals("less-than")) {
208
                oper = "<";
209
                nodedataterm = "nodedatanumerical";
210
            } else if (searchmode.equals("less-than-equals")) {
211
                oper = "<=";
212
                nodedataterm = "nodedatanumerical";
213
            } else {
214
                System.out
215
                        .println("NOT expected case. NOT recognized operator: "
216
                                + searchmode);
217
                return null;
218
            }
219
            
220
            
221
            try {
222
                // it is number; numeric comparison
223
            	Double doubleVal = new Double(casevalue);
224
                searchexpr = nodedataterm + " " + oper + " ? ";
225
                parameterValues.add(doubleVal);
226
            } catch (NumberFormatException nfe) {
227
            	// is it a date?
228
            	try {
229
                	// try ISO 8601 formats
230
                	Calendar dataDateValue = DatatypeConverter.parseDateTime(casevalue);
231
                	//String lexicalString = DatatypeConverter.printDateTime(dataDateValue);
232
                    nodedataterm = "nodedatadate";
233
            		searchexpr = 
234
            			nodedataterm + " " + oper + " ? ";
235
            		parameterValues.add(dataDateValue.getTime());
236
            	} catch (Exception pe) {
237
            		// these are characters; character comparison
238
                    searchexpr = nodedataterm + " " + oper + " ? ";
239
                    parameterValues.add(casevalue);
240
            	}
241
            }
242
        }
243
        return searchexpr;
244
    }
245
    
246
    public boolean isNotEqualTerm() {
247

    
248
        boolean notEqual = false;
249

    
250
        if (searchmode.equals("not-contains")) {
251
        	notEqual = true;
252
        } else if (searchmode.equals("isnot-equal")) {
253
        	notEqual = true;
254
        }
255
        return notEqual;
256
    }
257
    
258
    /**
259
     * create a SQL serialization of the query that this instance represents
260
     */
261
    public String printSQL(boolean useXMLIndex, List<Object> parameterValues)
262
    {
263
    	
264
        StringBuffer self = new StringBuffer();
265
        
266
        // does it contain a not equals?
267
        boolean notEqual = isNotEqualTerm();
268

    
269
        // keep track of parameter values
270
        List<Object> searchValues = new ArrayList<Object>();
271
        
272
        // get the general search expression
273
        String searchexpr = printSearchExprSQL(searchValues);
274
        
275
        // add our parameter values
276
        parameterValues.addAll(searchValues);
277
       
278
        // to check xml_path_index can be used
279
        boolean usePathIndex = false;
280

    
281
        // if pathexpr has been specified in metacat.properties for indexing
282
        if(pathexpr != null){
283
        	try {
284
				if (SystemUtil.getPathsForIndexing().contains(pathexpr)) {
285
					usePathIndex = true;
286
				}
287
			} catch (MetacatUtilException ue) {
288
				log.warn("Could not get index paths: " + ue.getMessage());
289
			}
290
        }
291
        
292
        if(usePathIndex){
293
            // using xml_path_index table.....
294
        	if(notEqual == true ){
295
        		if (!inUnionGroup)
296
        		{
297
        			self.append("SELECT DISTINCT docid from xml_path_index WHERE");
298
        			self.append(" docid NOT IN (Select docid FROM xml_path_index WHERE ");
299
        			self.append(searchexpr);
300
        			self.append("AND path LIKE ? ) ");
301
        			parameterValues.add(pathexpr);
302
        		}
303
        		else
304
        		{
305
        			//if this is in union group we need to use "OR" to modify query
306
        			self.append("("+searchexpr);
307
        			self.append("AND path LIKE ?) ");
308
        			parameterValues.add(pathexpr);
309
        		}
310
        	} else {
311
        		if (!inUnionGroup)
312
        		{
313
        			self.append("SELECT DISTINCT docid FROM xml_path_index WHERE ");
314
        			self.append(searchexpr);
315
        			self.append("AND path LIKE ? ");
316
        			parameterValues.add(pathexpr);
317
        		}
318
        		else
319
        		{
320
        			//if this is in union group we need to use "OR" to modify query
321
        			self.append("("+searchexpr);
322
        			self.append("AND path LIKE ?) ");
323
        			parameterValues.add(pathexpr);
324
        		}
325
        	}
326

    
327
        } else {
328
            // using xml_nodes and xml_index tables
329

    
330
        	if(notEqual == true){
331
        		self.append("SELECT DISTINCT docid from xml_nodes WHERE");
332
        		self.append(" docid NOT IN (Select docid FROM xml_nodes WHERE ");
333
        	} else {
334
        		self.append("(SELECT DISTINCT docid FROM xml_nodes WHERE ");
335
        	}
336
        	self.append(searchexpr);
337
        	
338
            if (pathexpr != null) {
339
             String path = pathexpr;
340
                // use XML Index
341
                if (useXMLIndex) {
342
                    if (!hasAttributeInPath(pathexpr)) {
343
                        // without attributes in path
344
                        self.append("AND parentnodeid IN ");
345
                        self.append(
346
                            "(SELECT nodeid FROM xml_index WHERE path LIKE ?) ");
347
                        parameterValues.add(path);
348
                    } else {
349
                        // has a attribute in path
350
                        String attributeName = QuerySpecification
351
                            .getAttributeName(pathexpr);
352
                        self.append(
353
                            "AND nodetype LIKE 'ATTRIBUTE' AND nodename LIKE ? ");
354
                        parameterValues.add(attributeName);
355
                        // and the path expression includes element content other than
356
                        // just './' or '../'
357
                        if ( (!pathexpr.startsWith(QuerySpecification.
358
                            ATTRIBUTESYMBOL)) &&
359
                            (!pathexpr.startsWith("./" +
360
                                                  QuerySpecification.ATTRIBUTESYMBOL)) &&
361
                            (!pathexpr.startsWith("../" +
362
                                                  QuerySpecification.ATTRIBUTESYMBOL))) {
363

    
364
                            self.append("AND parentnodeid IN ");
365
                            path = QuerySpecification
366
                                .newPathExpressionWithOutAttribute(pathexpr);
367
                            self.append(
368
                                "(SELECT nodeid FROM xml_index WHERE path LIKE ?) ");
369
                            parameterValues.add(path);
370
                        }
371
                    }
372
                }
373
                else {
374
                    // without using XML Index; using nested statements instead
375
                	// keep track of the values we add as prepared statement question marks (?)
376
                	List<Object> nestedParameterValues = new ArrayList<Object>();
377
                    //self.append("AND parentnodeid IN ");
378
                	self.append("AND ");
379
                	String nested = useNestedStatements(pathexpr, nestedParameterValues);		
380
                    self.append(nested);
381
                	// include them
382
                    parameterValues.addAll(nestedParameterValues);
383
                }
384
            }
385
            else if ( (value.trim()).equals("%")) {
386
                //if pathexpr is null and search value is %, is a
387
                // percentageSearchItem
388
                // the count number will be increase one
389
                countPercentageSearchItem++;
390

    
391
            }
392
            self.append(") ");
393
        }
394

    
395
        return self.toString();
396
    }
397

    
398
    /** A method to judge if a path have attribute */
399
    private boolean hasAttributeInPath(String path)
400
    {
401
        if (path.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1) {
402
            return true;
403
        } else {
404
            return false;
405
        }
406
    }
407

    
408
   
409
    public static String useNestedStatements(String pathexpr, List<Object> parameterValues)
410
    {
411
    	
412
        log.info("useNestedStatements()");
413
        log.info("pathexpr: " + pathexpr);
414
        String elementPrefix = " parentnodeid IN ";
415
        String attributePrefix  =  " nodeid IN ";
416
        boolean lastOneIsAttribute = false;
417
        StringBuffer nestedStmts = new StringBuffer();
418
        String path = pathexpr.trim();
419
        String sql = "";
420

    
421
        if (path.indexOf('/') == 0)
422
        {
423
            nestedStmts.append("AND parentnodeid = rootnodeid ");
424
            path = path.substring(1).trim();
425
        }
426

    
427
        do
428
        {
429
            int inx = path.indexOf('/');
430
            int predicateStart = -1;
431
            int predicateEnd;
432
            String node;
433
            Vector<String> predicates = new Vector<String>();
434

    
435
            // extract predicates
436
            predicateStart = path.indexOf(QuerySpecification.PREDICATE_START, predicateStart + 1);
437

    
438
            // any predicates in this node?
439
            if (inx != -1 && (predicateStart == -1 || predicateStart > inx))
440
            {
441
                // no
442
                node = path.substring(0, inx).trim();
443
                path = path.substring(inx + 1).trim();
444
            }
445
            else if (predicateStart == -1)
446
            {
447
                // no and it's the last node
448
                node = path;
449
                if (node != null && node.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1)
450
                {
451
                	lastOneIsAttribute = true;
452
                	node = removeAttributeSymbol(node);
453
                }
454
                path = "";
455
            }
456
            else
457
            {
458
                // yes
459
                node = path.substring(0, predicateStart).trim();
460
                path = path.substring(predicateStart);
461
                predicateStart = 0;
462

    
463
                while (predicateStart == 0)
464
                {
465
                    predicateEnd = path.indexOf(QuerySpecification.PREDICATE_END,
466
                            predicateStart);
467

    
468
                    if (predicateEnd == -1)
469
                    {
470
                        log.warn("useNestedStatements(): ");
471
                        log.warn("    Invalid path: " + pathexpr);
472
                        return "";
473
                    }
474

    
475
                    predicates.add(path.substring(1, predicateEnd).trim());
476
                    path = path.substring(predicateEnd + 1).trim();
477
                    inx = path.indexOf('/');
478
                    predicateStart = path.indexOf(QuerySpecification.PREDICATE_START);
479
                }
480

    
481
                if (inx == 0)
482
                    path = path.substring(1).trim();
483
                else if (!path.equals(""))
484
                {
485
                    log.warn("useNestedStatements(): ");
486
                    log.warn("    Invalid path: " + pathexpr);
487
                    return "";
488
                }
489
            }
490

    
491
            nestedStmts.insert(0, "(SELECT nodeid FROM xml_nodes WHERE nodename LIKE ? ");
492
            parameterValues.add(0, node);
493
            
494
            // for the last statement: it is without " AND parentnodeid IN "
495
            if (!path.equals("")) {
496
                nestedStmts.insert(0, "AND parentnodeid IN ");
497
            }
498
            
499
            if (predicates.size() > 0)
500
            {
501
                for (int n = 0; n < predicates.size(); n++)
502
                {
503
                	// keep track of the values we add as prepared statement question marks (?)
504
                	List<Object> predicateParameterValues = new ArrayList<Object>();
505
                    String predSQL = predicate2SQL(predicates.get(n), predicateParameterValues);
506

    
507
                    if (predSQL.equals("")) {
508
                        return "";
509
                    }
510
                    
511
                    // include the values
512
                    parameterValues.addAll(predicateParameterValues);
513
                    // include the sql
514
                    nestedStmts.append(predSQL).append(' ');
515
                }
516
            }
517

    
518
            nestedStmts.append(") ");
519
        }
520
        while (!path.equals(""));
521
        if (lastOneIsAttribute)
522
        {
523
        	sql = attributePrefix+nestedStmts.toString();
524
        }
525
        else
526
        {
527
        	sql = elementPrefix+nestedStmts.toString();
528
        }
529
        return sql;
530
    }
531
    
532
    
533
    /*
534
     * Removes @ symbol from path. For example, if path is @entity, entity will be returned.
535
     * If path is entity, entity will be returned. 
536
     */
537
    private static String removeAttributeSymbol(String path)
538
    {
539
    	String newPath  ="";
540
    	log.debug("Original string before removing @ is " + path);
541
    	if (path != null)
542
    	{
543
    		
544
    		int attribute = path.indexOf(QuerySpecification.ATTRIBUTESYMBOL);
545
    		if (attribute != -1)
546
    		{
547
    			// has attribute symbol. Reomve it and return the remained part. 
548
    			try
549
    			{
550
    		         newPath = path.substring(attribute + 1).trim();
551
    			}
552
    			catch (Exception e)
553
    			{
554
    				newPath = path;
555
    			}
556
    		}
557
    		else
558
    		{
559
    			// doesn't have attribute symbol. Return original string
560
    			newPath = path;
561
    		}
562
    	}
563
    	else
564
    	{
565
    		// if is null, return null;
566
    		newPath = path;
567
    	}
568
    	log.debug("String after removing @ is " + newPath);
569
    	return newPath;
570
    	
571
    }
572

    
573
    /**
574
     * 
575
     */
576
    public static String predicate2SQL(String predicate, List<Object> predicateParameterValues)
577
    {
578
        String path = predicate.trim();
579
        int equals = path.indexOf('=');
580
        String literal = null;
581

    
582
        if (equals != -1)
583
        {
584
            literal = path.substring(equals + 1).trim();
585
            path = path.substring(0, equals).trim();
586
            int sQuote = literal.indexOf('\'');
587
            int dQuote = literal.indexOf('"');
588

    
589
            if (sQuote == -1 && dQuote == -1)
590
            {
591
                log.warn("predicate2SQL(): ");
592
                log.warn("  1  Invalid or unsupported predicate: " + predicate);
593
                return "";
594
            }
595

    
596
            if (sQuote == -1 &&
597
                (dQuote != 0 ||
598
                 literal.indexOf('"', dQuote + 1) != literal.length() - 1))
599
            {
600
                log.warn("predicate2SQL(): ");
601
                log.warn("  2  Invalid or unsupported predicate: " + predicate);
602
                return "";
603
            }
604

    
605
            if (sQuote != 0 ||
606
                literal.indexOf('\'', sQuote + 1) != literal.length() - 1)
607
            {
608
                log.warn("predicate2SQL(): ");
609
                log.warn("  3  Invalid or unsupported predicate: " + predicate);
610
                return "";
611
            }
612
        }
613

    
614
        StringBuffer sql = new StringBuffer();
615
        int attribute = path.indexOf('@');
616

    
617
        if (attribute == -1)
618
        {
619
            if (literal != null)
620
            {
621
                sql.append("AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'TEXT' AND nodedata LIKE ")
622
                    .append("?").append(")");
623
                // remove single or double quotes
624
                literal = literal.substring(1, literal.length() - 1);
625
                // add to vlaues list
626
                predicateParameterValues.add(literal);
627
            }
628
        }
629
        else
630
        {
631
            sql.append(
632
                    "AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'ATTRIBUTE' AND nodename LIKE ? ");
633
            // add to values
634
            predicateParameterValues.add(path.substring(attribute + 1).trim());
635

    
636
            if (literal != null)
637
            {
638
                sql.append("AND nodedata LIKE ? ");
639
                // remove single or double quotes
640
                literal = literal.substring(1, literal.length() - 1);
641
                // add to values list
642
                predicateParameterValues.add(literal);
643
            }
644

    
645
            sql.append(")");
646
            path = path.substring(0, attribute).trim();
647

    
648
            if (path.endsWith("/"))
649
                path = path.substring(0, path.length() - 1).trim();
650
            else
651
            {
652
                if (!path.equals(""))
653
                {
654
                    log.warn("predicate2SQL(): ");
655
                    log.warn("    Invalid or unsupported predicate: " + predicate);
656
                    return "";
657
                }
658
            }
659
        }
660

    
661
        while (!path.equals(""))
662
        {
663
            int ndx = path.lastIndexOf('/');
664
            int predicateEnd = -1;
665
            int predicateStart;
666
            String node;
667

    
668
            if (ndx != -1)
669
            {
670
                node = path.substring(ndx + 1).trim();
671
                path = path.substring(0, ndx).trim();
672
            }
673
            else
674
            {
675
                node = path;
676
                path = "";
677
            }
678

    
679
            if (!node.equals("")) {
680
                sql.insert(0, "(SELECT parentnodeid FROM xml_nodes WHERE nodename LIKE ? ) ");
681
                predicateParameterValues.add(0, node);
682
            }
683
            else if (!path.equals(""))
684
            {
685
                log.warn("predicate2SQL(): ");
686
                log.warn("    Invalid or unsupported predicate: " + predicate);
687
                return "";
688
            }
689

    
690
            if (path.equals(""))
691
            {
692
                sql.insert(0,
693
                        node.equals("") ? "AND rootnodeid IN " : "AND nodeid IN ");
694
            }
695
            else
696
            {
697
                sql.append("AND nodeid IN ");
698
            }
699
        }
700

    
701
        return sql.toString();
702
    }
703

    
704
    /**
705
     * Show a string representation of the query
706
     * @deprecated This should not be used for performing the query
707
     * because the parameter values are not bound in the raw SQL
708
     */
709
    public String toString()
710
    {
711

    
712
    	// keep track of the values we add as prepared statement question marks (?)
713
    	List<Object> parameterValues = new ArrayList<Object>();
714
        return this.printSQL(true, parameterValues);
715
    }
716
    
717
    /**
718
     * Compare two query terms to see if they have same search value.
719
     * @param term
720
     * @return
721
     */
722
    public boolean hasSameSearchValue(QueryTerm term)
723
    {
724
    	boolean same = false;
725
    	if (term != null)
726
    	{
727
    		String searchValue = term.getValue();
728
    		if (searchValue != null && this.value != null)
729
    		{
730
    			if (searchValue.equalsIgnoreCase(this.value))
731
    			{
732
    				if (this.getSearchMode().equals(term.getSearchMode())) {
733
        				same = true;
734
    				}
735
    			}
736
    		}
737
    	}
738
    	return same;
739
    }
740
}
(56-56/64)