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: cjones $'
12
 *     '$Date: 2009-10-26 10:20:51 -0700 (Mon, 26 Oct 2009) $'
13
 * '$Revision: 5093 $'
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.shared.MetacatUtilException;
36
import edu.ucsb.nceas.metacat.util.MetacatUtil;
37
import edu.ucsb.nceas.metacat.util.SystemUtil;
38

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

    
44
    private boolean casesensitive = false;
45

    
46
    private String searchmode = null;
47

    
48
    private String value = null;
49

    
50
    private String pathexpr = null;
51

    
52
    private boolean percentageSymbol = false;
53

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

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

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

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

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

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

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

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

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

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

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

    
223

    
224
        // to check xml_path_index can be used
225
        boolean usePathIndex = false;
226

    
227
        // if pathexpr has been specified in metacat.properties for indexing
228
        if(pathexpr != null){
229
        	try {
230
				if (SystemUtil.getPathsForIndexing().contains(pathexpr)) {
231
					usePathIndex = true;
232
				}
233
			} catch (MetacatUtilException ue) {
234
				log.warn("Could not get index paths: " + ue.getMessage());
235
			}
236
        }
237
        
238
        if(usePathIndex){
239
            // using xml_path_index table.....
240
        	if(notEqual == true ){
241
        		if (!inUnionGroup)
242
        		{
243
        			self.append("SELECT DISTINCT docid from xml_path_index WHERE");
244
        			self.append(" docid NOT IN (Select docid FROM xml_path_index WHERE ");
245
        			self.append(searchexpr);
246
        			self.append("AND path LIKE '" + pathexpr + "') ");
247
        		}
248
        		else
249
        		{
250
        			//if this is in union group we need to use "OR" to modify query
251
        			self.append("("+searchexpr);
252
        			self.append("AND path LIKE '" + pathexpr + "') ");
253
        		}
254
        	} else {
255
        		if (!inUnionGroup)
256
        		{
257
        			self.append("SELECT DISTINCT docid FROM xml_path_index WHERE ");
258
        			self.append(searchexpr);
259
        			self.append("AND path LIKE '" + pathexpr + "' ");
260
        		}
261
        		else
262
        		{
263
        			//if this is in union group we need to use "OR" to modify query
264
        			self.append("("+searchexpr);
265
        			self.append("AND path LIKE '" + pathexpr + "') ");
266
        		}
267
        	}
268

    
269
        } else {
270
            // using xml_nodes and xml_index tables
271

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

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

    
328
            }
329
            self.append(") ");
330
        }
331

    
332
        return self.toString();
333
    }
334

    
335
    /** A method to judge if a path have attribute */
336
    private boolean hasAttributeInPath(String path)
337
    {
338
        if (path.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1) {
339
            return true;
340
        } else {
341
            return false;
342
        }
343
    }
344

    
345
   
346
    public static String useNestedStatements(String pathexpr)
347
    {
348
        log.info("useNestedStatements()");
349
        log.info("pathexpr: " + pathexpr);
350
        String elementPrefix = " parentnodeid IN ";
351
        String attributePrefix  =  " nodeid IN ";
352
        boolean lastOneIsAttribute = false;
353
        StringBuffer nestedStmts = new StringBuffer();
354
        String path = pathexpr.trim();
355
        String sql = "";
356

    
357
        if (path.indexOf('/') == 0)
358
        {
359
            nestedStmts.append("AND parentnodeid = rootnodeid ");
360
            path = path.substring(1).trim();
361
        }
362

    
363
        do
364
        {
365
            int inx = path.indexOf('/');
366
            int predicateStart = -1;
367
            int predicateEnd;
368
            String node;
369
            Vector predicates = new Vector();
370

    
371
            // extract predicates
372
            predicateStart = path.indexOf(QuerySpecification.PREDICATE_START, predicateStart + 1);
373

    
374
            // any predicates in this node?
375
            if (inx != -1 && (predicateStart == -1 || predicateStart > inx))
376
            {
377
                // no
378
                node = path.substring(0, inx).trim();
379
                path = path.substring(inx + 1).trim();
380
            }
381
            else if (predicateStart == -1)
382
            {
383
                // no and it's the last node
384
                node = path;
385
                if (node != null && node.indexOf(QuerySpecification.ATTRIBUTESYMBOL) != -1)
386
                {
387
                	lastOneIsAttribute = true;
388
                	node = removeAttributeSymbol(node);
389
                }
390
                path = "";
391
            }
392
            else
393
            {
394
                // yes
395
                node = path.substring(0, predicateStart).trim();
396
                path = path.substring(predicateStart);
397
                predicateStart = 0;
398

    
399
                while (predicateStart == 0)
400
                {
401
                    predicateEnd = path.indexOf(QuerySpecification.PREDICATE_END,
402
                            predicateStart);
403

    
404
                    if (predicateEnd == -1)
405
                    {
406
                        log.warn("useNestedStatements(): ");
407
                        log.warn("    Invalid path: " + pathexpr);
408
                        return "";
409
                    }
410

    
411
                    predicates.add(path.substring(1, predicateEnd).trim());
412
                    path = path.substring(predicateEnd + 1).trim();
413
                    inx = path.indexOf('/');
414
                    predicateStart = path.indexOf(QuerySpecification.PREDICATE_START);
415
                }
416

    
417
                if (inx == 0)
418
                    path = path.substring(1).trim();
419
                else if (!path.equals(""))
420
                {
421
                    log.warn("useNestedStatements(): ");
422
                    log.warn("    Invalid path: " + pathexpr);
423
                    return "";
424
                }
425
            }
426

    
427
            nestedStmts.insert(0, "' ").insert(0, node).insert(0,
428
                    "(SELECT nodeid FROM xml_nodes WHERE nodename LIKE '");
429

    
430
            // for the last statement: it is without " AND parentnodeid IN "
431
            if (!path.equals(""))
432
                nestedStmts.insert(0, "AND parentnodeid IN ");
433

    
434
            if (predicates.size() > 0)
435
            {
436
                for (int n = 0; n < predicates.size(); n++)
437
                {
438
                    String predSQL = predicate2SQL((String) predicates.get(n));
439

    
440
                    if (predSQL.equals(""))
441
                        return "";
442

    
443
                    nestedStmts.append(predSQL).append(' ');
444
                }
445
            }
446

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

    
502
    /**
503
     * 
504
     */
505
    public static String predicate2SQL(String predicate)
506
    {
507
        String path = predicate.trim();
508
        int equals = path.indexOf('=');
509
        String literal = null;
510

    
511
        if (equals != -1)
512
        {
513
            literal = path.substring(equals + 1).trim();
514
            path = path.substring(0, equals).trim();
515
            int sQuote = literal.indexOf('\'');
516
            int dQuote = literal.indexOf('"');
517

    
518
            if (sQuote == -1 && dQuote == -1)
519
            {
520
                log.warn("predicate2SQL(): ");
521
                log.warn("    Invalid or unsupported predicate: " + predicate);
522
                return "";
523
            }
524

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

    
534
            if (sQuote != 0 ||
535
                literal.indexOf('\'', sQuote + 1) != literal.length() - 1)
536
            {
537
                log.warn("predicate2SQL(): ");
538
                log.warn("    Invalid or unsupported predicate: " + predicate);
539
                return "";
540
            }
541
        }
542

    
543
        StringBuffer sql = new StringBuffer();
544
        int attribute = path.indexOf('@');
545

    
546
        if (attribute == -1)
547
        {
548
            if (literal != null)
549
            {
550
                sql.append("AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'TEXT' AND nodedata LIKE ")
551
                    .append(literal).append(")");
552
            }
553
        }
554
        else
555
        {
556
            sql.append(
557
                    "AND nodeid IN (SELECT parentnodeid FROM xml_nodes WHERE nodetype = 'ATTRIBUTE' AND nodename LIKE '")
558
                    .append(path.substring(attribute + 1).trim()).append("' ");
559

    
560
            if (literal != null)
561
            {
562
                sql.append("AND nodedata LIKE ").append(literal);
563
            }
564

    
565
            sql.append(")");
566
            path = path.substring(0, attribute).trim();
567

    
568
            if (path.endsWith("/"))
569
                path = path.substring(0, path.length() - 1).trim();
570
            else
571
            {
572
                if (!path.equals(""))
573
                {
574
                    log.warn("predicate2SQL(): ");
575
                    log.warn("    Invalid or unsupported predicate: " + predicate);
576
                    return "";
577
                }
578
            }
579
        }
580

    
581
        while (!path.equals(""))
582
        {
583
            int ndx = path.lastIndexOf('/');
584
            int predicateEnd = -1;
585
            int predicateStart;
586
            String node;
587

    
588
            if (ndx != -1)
589
            {
590
                node = path.substring(ndx + 1).trim();
591
                path = path.substring(0, ndx).trim();
592
            }
593
            else
594
            {
595
                node = path;
596
                path = "";
597
            }
598

    
599
            if (!node.equals(""))
600
                sql.insert(0, "' ").insert(0, node)
601
                    .insert(0, "(SELECT parentnodeid FROM xml_nodes WHERE nodename LIKE '").append(") ");
602
            else if (!path.equals(""))
603
            {
604
                log.warn("predicate2SQL(): ");
605
                log.warn("    Invalid or unsupported predicate: " + predicate);
606
                return "";
607
            }
608

    
609
            if (path.equals(""))
610
            {
611
                sql.insert(0,
612
                        node.equals("") ? "AND rootnodeid IN " : "AND nodeid IN ");
613
            }
614
            else
615
            {
616
                sql.append("AND nodeid IN ");
617
            }
618
        }
619

    
620
        return sql.toString();
621
    }
622

    
623
    /**
624
     * create a String description of the query that this instance represents.
625
     * This should become a way to get the XML serialization of the query.
626
     */
627
    public String toString()
628
    {
629

    
630
        return this.printSQL(true);
631
    }
632
    
633
    /**
634
     * Compare two query terms to see if they have same search value.
635
     * @param term
636
     * @return
637
     */
638
    public boolean hasSameSearchValue(QueryTerm term)
639
    {
640
    	boolean same = false;
641
    	if (term != null)
642
    	{
643
    		String searchValue = term.getValue();
644
    		if (searchValue != null && this.value != null)
645
    		{
646
    			if (searchValue.equalsIgnoreCase(this.value))
647
    			{
648
    				same = true;
649
    			}
650
    		}
651
    	}
652
    	return same;
653
    }
654
}
(54-54/62)