QueryParser.java
001 /*
002  *  QueryParser.java
003  *
004  *  Niraj Aswani, 19/March/07
005  *
006  *  $Id: QueryParser.html,v 1.0 2007/03/19 16:22:01 niraj Exp $
007  */
008 package gate.creole.annic.lucene;
009 
010 import gate.creole.annic.Constants;
011 import gate.creole.annic.apache.lucene.search.*;
012 import gate.creole.annic.apache.lucene.index.*;
013 import gate.creole.ir.SearchException;
014 
015 import java.util.*;
016 
017 /**
018  * QueryParser parses the provided ANNIC Query and converts it into the
019  * format understood to Lucene.
020  
021  @author niraj
022  
023  */
024 public class QueryParser {
025 
026   /**
027    * Queries generated as a result of normalizing the submitted query.
028    */
029   private List<String> queries = new ArrayList<String>();
030 
031   /**
032    * Name of the field that contains the index data.
033    */
034   private String field = "";
035 
036   /**
037    * Base token annotation type.
038    */
039   private String baseTokenAnnotationType = "Token";
040 
041   /**
042    * Indicates if we need to valid results returned by lucene.
043    */
044   private boolean needValidation = true;
045 
046   /**
047    * Constructor
048    */
049   public QueryParser() {
050     position = 0;
051   }
052 
053   public static void main(String[] args) {
054     System.out.println(isValidQuery(args[0]));
055   }
056 
057   /**
058    * Returns true if the submitted query is valid.
059    */
060   public static boolean isValidQuery(String query) {
061     QueryParser qp = new QueryParser();
062     try {
063       qp.parse("contents", query, "Token", null, null);
064     }
065     catch(SearchException se) {
066       return false;
067     }
068     return true;
069   }
070 
071   /**
072    * Given a query, this method parses it to convert it into one or more
073    * lucene queries.
074    @throws gate.creole.ir.SearchException
075    */
076   public Query[] parse(String field, String query,
077           String baseTokenAnnotationType, String corpusID, String annotationSetToSearchIn)
078           throws gate.creole.ir.SearchException {
079     this.field = field;
080     this.baseTokenAnnotationType = baseTokenAnnotationType;
081     this.position = 0;
082     // at the moment this supports only | operator
083     // it also support klene operators * and +
084     // implicit operator is &
085     // It supports simple String queries
086     // it supports eight kinds of tokens
087     // 1. String (without quotes)
088     // 2. "String" (with quotes)
089     // 3. {AnnotationType}
090     // 4. {AnnotationType==String}
091     // 5. {AnnotationType=="String"}
092     // 7. {AnnotationType.feature==string}
093     // 8. {AnnotationType.feature=="string"}
094 
095     // Steps
096     // The query would we searched from left to right order
097 
098     // returned arraylist contains queries where each query is required
099     // to
100     // be converted into the Phrase query
101     queries = SubQueryParser.parseQuery(query);
102     Query[] q = new Query[queries.size()];
103     for(int i = 0; i < queries.size(); i++) {
104       Query phraseQuery = createPhraseQuery(queries.get(i));
105       // if the corpusID is not provided we donot want to create a
106       // boolean query
107       if(corpusID == null && annotationSetToSearchIn == null) {
108         BooleanQuery booleanQuery = new BooleanQuery();
109         Term t = new Term(Constants.ANNOTATION_SET_ID, Constants.COMBINED_SET);
110         TermQuery tQuery = new TermQuery(t);
111         booleanQuery.add(tQuery, false, true);
112         booleanQuery.add(phraseQuery, true, false);
113         q[i= booleanQuery;
114       }
115       else {
116         BooleanQuery booleanQuery = new BooleanQuery();
117         booleanQuery.add(phraseQuery, true, false);
118         if(corpusID != null) {
119           Term t = new Term(Constants.CORPUS_ID, corpusID);
120           TermQuery tQuery = new TermQuery(t);
121           booleanQuery.add(tQuery, true, false);
122         }
123         
124         if(annotationSetToSearchIn != null) {
125           Term t = new Term(Constants.ANNOTATION_SET_ID, annotationSetToSearchIn);
126           TermQuery tQuery = new TermQuery(t);
127           booleanQuery.add(tQuery, true, false);
128         else {
129           Term t = new Term(Constants.ANNOTATION_SET_ID, Constants.COMBINED_SET);
130           TermQuery tQuery = new TermQuery(t);
131           booleanQuery.add(tQuery, false, true);
132         }
133         
134 
135         q[i= booleanQuery;
136       }
137     }
138     return q;
139   }
140 
141   /**
142    * When user submits an ANNIC query, one or more instances of lucene
143    * queries are created and returned. This method returns the string
144    * representation of the query at the given index.
145    */
146   public String getQueryString(int i) {
147     return queries.get(i);
148   }
149 
150   /**
151    * This method will create each normalized query into a Phrase or Term
152    * query If the query has only one term to search, it will be returned
153    * as a TermQuery otherwise, it will be returned as the PhraseQuery
154    */
155   private Query createPhraseQuery(String query)
156           throws gate.creole.ir.SearchException {
157     // Here we play the actual trick with lucene
158     // For a query like {Lookup}{Token}{Person.gender=="male"}
159     // internally this query is converted into the following PhraseQuery
160     // (Lookup Token Person male)
161     // these are the four terms which will be searched and they should
162     // occur
163     // in this order only
164     // but what we need is
165     // a pattern where
166     // Lookup -> the first annotation is of type Lookup
167     // Token -> the second annotation type is Token
168     // Person male -> and the third annotation must have a type person
169     // and a
170     // feature gender with male
171     // that means Person and male should be considered at the same
172     // location
173     // By default lucene doesn't do this and look for a position that is
174     // 1
175     // step more than the previous one
176     // so it will search for the first position of Lookup
177     // let say it is 19 (i.e. 19th annotation in the document)
178     // then it would consider 20th location for Token
179     // 21st for Person
180     // 22nd for male
181     // but we need, 19th for Lookup, 20th for Token and 21st for both
182     // Person
183     // and Male
184     // so from here itself we send our choice for the Location of
185     // annotations in this termPositions array :-).
186     // isn't it a great crack?
187     position = 0;
188 
189     PhraseQuery phQuery = new PhraseQuery();
190     // we will tokenize this query to convert it into different tokens
191     // query is like {Person}"said" "Hello" {Person.gender=="male"}
192     // we need to convert this into different tokens
193     // {Person}
194     // "said"
195     // "Hello"
196     // {Person.gender=="male"}
197     List<String> tokens = findTokens(query);
198 
199     // and then convert each token into separate terms
200     if(tokens.size() == 1) {
201       List<?>[] termsPos = createTerms(tokens.get(0));
202       
203       @SuppressWarnings("unchecked")
204       List<Term> terms = (List<Term>)termsPos[0];
205       
206       if(terms.size() == 1) {
207         if(areAllTermsTokens)
208           needValidation = false;
209         else needValidation = true;
210         return new TermQuery(terms.get(0));
211       }
212       else {
213         position = 0;
214       }
215     }
216 
217     int totalTerms = 0;
218     boolean hadPreviousTermsAToken = true;
219 
220     needValidation = false;
221 
222     // and now for each token we need to create Term(s)
223     outer: for(int i = 0; i < tokens.size(); i++) {
224       List<?>[] termpositions = createTerms(tokens.get(i));
225       
226       @SuppressWarnings("unchecked")
227       List<Term> terms = (List<Term>)termpositions[0];
228       
229       @SuppressWarnings("unchecked")
230       List<Integer> pos = (List<Integer>)termpositions[1];
231       
232       @SuppressWarnings("unchecked")
233       List<Boolean> consider = (List<Boolean>)termpositions[2];
234 
235       boolean allTermsTokens = true;
236       // lets first find out if there's any token in this terms
237       for(int k = 0; k < terms.size(); k++) {
238         Term t = terms.get(k);
239 
240         if(allTermsTokensallTermsTokens = isBaseTokenTerm(t);
241       }
242 
243       if(!hadPreviousTermsAToken) {
244         needValidation = true;
245         break;
246       }
247 
248       if(!allTermsTokens) {
249         // we want to break here
250         needValidation = true;
251         if(i > 0)
252           break outer;
253       }
254 
255       for(int k = 0; k < terms.size(); k++) {
256         Term t = terms.get(k);
257         boolean considerValue = consider.get(k).booleanValue();
258         phQuery.add(t, pos.get(k), considerValue);
259         if(considerValuetotalTerms++;
260       }
261 
262       hadPreviousTermsAToken = allTermsTokens;
263     }
264     phQuery.setTotalTerms(totalTerms);
265     return phQuery;
266   }
267 
268   /**
269    * Returns true if the provided Term is a based token term. To be a
270    * base token term it has to satisify the following terms: 1. If its
271    * text is baseTokenAnnotationType and the type is "*" or 2. If its
272    * type = "baseTokenAnnotationType.feature"
273    */
274   private boolean isBaseTokenTerm(Term t) {
275     // the term refers to the base token
276     // only if it satisfies the following conditions
277     // 1. If its text is baseTokenAnnotationType and the type is "*"
278     // or 2. If its type = "baseTokenAnnotationType.feature"
279 
280     // condition 1
281     if(t.text().equals(baseTokenAnnotationType&& t.type().equals("*"))
282       return true;
283 
284     // condition 2
285     if(t.type().startsWith(baseTokenAnnotationType + ".")) return true;
286 
287     return false;
288   }
289 
290   public int position = 0;
291 
292   /**
293    * Given a query this method returns tokens. Here token is an object
294    * of string.
295    @throws gate.creole.ir.SearchException
296    */
297   public List<String> findTokens(String query)
298           throws gate.creole.ir.SearchException {
299     List<String> tokens = new ArrayList<String>();
300     String token = "";
301     char ch = ' ';
302     char prev = ' ';
303     int balance = 0;
304     for(int i = 0; i < query.length(); i++) {
305       prev = ch;
306       ch = query.charAt(i);
307       if(isOpeneningBrace(ch, prev)) {
308         if(balance != 0) {
309           throw new SearchException("unbalanced braces",
310             "a closing brace (}) is missing before this opening brace", query, i);
311         }
312 
313         if(!token.trim().equals("")) {
314           tokens.add(token.trim());
315         }
316 
317         balance++;
318         token = "{";
319         continue;
320       }
321 
322       if(isClosingBrace(ch, prev)) {
323         balance--;
324         if(balance != 0) {
325           throw new SearchException("unbalanced braces",
326             "an opening brace ({) is missing before this closing brace", query, i);
327         }
328 
329         token += "}";
330         tokens.add(token.trim());
331         token = "";
332         continue;
333       }
334 
335       token += ch;
336     }
337 
338     if(balance != 0) {
339       if (balance > 0) {
340         throw new SearchException("unbalanced braces",
341                 "One closing brace (}) is missing in this expression", query);
342       else {
343         throw new SearchException("unbalanced braces",
344                 "One opening brace ({) is missing in this expression", query);
345       }
346     }
347 
348     if(!token.trim().equals("")) tokens.add(token);
349 
350     return tokens;
351   }
352 
353   private boolean isOpeneningBrace(char ch, char pre) {
354     if(ch == '{' && pre != '\\')
355       return true;
356     else return false;
357   }
358 
359   private boolean isClosingBrace(char ch, char pre) {
360     if(ch == '}' && pre != '\\')
361       return true;
362     else return false;
363   }
364 
365   boolean areAllTermsTokens = false;
366 
367   private boolean isEscapeSequence(String element, int index) {
368     if(index > 0) {
369       return element.charAt(index - 1== '\\';
370     }
371     return false;
372   }
373 
374   private ArrayList<String> splitString(String string, char with, boolean normalize) {
375     // here we want to split the string
376     // but also make sure the with character is not escaped
377     ArrayList<String> strings = new ArrayList<String>();
378     StringBuffer newString = new StringBuffer();
379     for(int i = 0; i < string.length(); i++) {
380       if(i == 0) {
381         newString.append(string.charAt(0));
382         continue;
383       }
384 
385       if(string.charAt(i== with) {
386         // need to check the previous character
387         if(string.charAt(i - 1== '\\') {
388           newString.append(with);
389           continue;
390         }
391         else {
392           if(normalize)
393             strings.add(norm(newString.toString()));
394           else strings.add(newString.toString());
395 
396           newString = new StringBuffer();
397           continue;
398         }
399       }
400 
401       newString.append(string.charAt(i));
402     }
403     if(newString.length() 0) {
404       if(normalize)
405         strings.add(norm(newString.toString()).trim());
406       else strings.add(newString.toString().trim());
407     }
408     return strings;
409   }
410 
411   private int findIndexOf(String element, char ch) {
412     int index1 = -1;
413     int start = -1;
414     while(true) {
415       index1 = element.indexOf(ch, start);
416       if(isEscapeSequence(element, index1)) {
417         start = index1 + 1;
418       }
419       else {
420         break;
421       }
422     }
423     return index1;
424   }
425 
426   private String norm(String string) {
427     StringBuffer sb = new StringBuffer();
428     for(int i = 0; i < string.length(); i++) {
429       if(string.charAt(i== '\\') {
430         if(i + <= string.length() 1) {
431           char ch = string.charAt(i + 1);
432           if(ch == ',' || ch == '.' || ch == '(' || ch == ')' || ch == '{'
433                   || ch == '}' || ch == '"' || ch == '\\'continue;
434         }
435       }
436       sb.append(string.charAt(i));
437     }
438     return sb.toString();
439   }
440 
441   public List<?>[] createTerms(String elem)
442           throws gate.creole.ir.SearchException {
443     areAllTermsTokens = true;
444     List<Term> terms = new ArrayList<Term>();
445     List<Integer> pos = new ArrayList<Integer>();
446     List<Boolean> consider = new ArrayList<Boolean>();
447 
448     elem = elem.trim();
449     if(elem.charAt(0== '{' && elem.charAt(elem.length() 1== '}') {
450       // possible
451       elem = elem.substring(1, elem.length() 1);
452       int index = elem.indexOf("==");
453       int index1 = findIndexOf(elem, '.');
454 
455       if(index == -&& index1 == -1) {
456         // 3. {AnnotationType}
457         // this can be {AnnotationType, AnnotationType...}
458         ArrayList<String> fields = splitString(elem, ','true);
459 
460         for(int p = 0; p < fields.size(); p++) {
461           if(areAllTermsTokens
462                   && !fields.get(p).equals(baseTokenAnnotationType))
463             areAllTermsTokens = false;
464 
465           terms.add(new Term(field, norm(fields.get(p))"*"));
466           pos.add(new Integer(position));
467           if(p == 0)
468             consider.add(new Boolean(true));
469           else consider.add(new Boolean(false));
470 
471         }
472         position++;
473       }
474       else if(index != -&& index1 == -1) {
475         // 4. {AnnotationType==String}
476         // 5. {AnnotationType=="String"}
477 
478         ArrayList<String> fields = splitString(elem, ','false);
479         for(int p = 0; p < fields.size(); p++) {
480           index = fields.get(p).indexOf("==");
481           // here this is also posible
482           // {AnnotationType, AnnotationType=="String"}
483           if(index != -1) {
484             String annotType = norm(fields.get(p).substring(0, index)
485                     .trim());
486             String annotText = norm(fields.get(p).substring(
487                     index + 2, fields.get(p).length()).trim());
488             if(annotText.length() && annotText.charAt(0== '\"'
489                     && annotText.charAt(annotText.length() 1== '\"') {
490               annotText = annotText.substring(1, annotText.length() 1);
491             }
492             if(!annotType.trim().equals(baseTokenAnnotationType))
493               areAllTermsTokens = false;
494             
495             terms.add(new Term(field, annotText, annotType + ".string"));
496             pos.add(new Integer(position));
497             if(p == 0)
498               consider.add(new Boolean(true));
499             else consider.add(new Boolean(false));
500 
501           }
502           else {
503             if(!(norm(fields.get(p))).equals(baseTokenAnnotationType))
504               areAllTermsTokens = false;
505             
506             terms.add(new Term(field, norm(fields.get(p))"*"));
507             pos.add(new Integer(position));
508             if(p == 0)
509               consider.add(new Boolean(true));
510             else consider.add(new Boolean(false));
511           }
512         }
513 
514         position++;
515 
516       }
517       else if(index == -&& index1 != -1) {
518         throw new SearchException("missing operator",
519                 "an equal operator (==) is missing",
520                 elem, (elem.indexOf("=", index1)!=-1)?
521                        elem.indexOf("=", index1):elem.length());
522       }
523       else if(index != -&& index1 != -1) {
524 
525         // it can be {AT, AT.f==S, AT=="S"}
526         int index2 = findIndexOf(elem, ',');
527         String[] subElems = null;
528         if(index2 == -1) {
529           subElems = new String[] {elem};
530         }
531         else {
532           ArrayList<String> list = splitString(elem, ','false);
533           subElems = new String[list.size()];
534           for(int k = 0; k < list.size(); k++) {
535             subElems[k= list.get(k);
536           }
537         }
538 
539         int lengthTravelledSoFar = 0;
540         for(int j = 0; j < subElems.length; j++) {
541           // 7. {AnnotationType.feature==string}
542           // 8. {AnnotationType.feature=="string"}
543           index = subElems[j].indexOf("==");
544           index1 = findIndexOf(subElems[j]'.');
545           if(index == -&& index1 == -1) {
546             // this is {AT}
547             if(!norm(subElems[j].trim()).equals(baseTokenAnnotationType))
548               areAllTermsTokens = false;
549             terms.add(new Term(field, norm(subElems[j].trim())"*"));
550             pos.add(new Integer(position));
551             if(j == 0)
552               consider.add(new Boolean(true));
553             else consider.add(new Boolean(false));
554 
555           }
556           else if(index != -&& index1 == -1) {
557             // this is {AT=="String"}
558             String annotType = norm(subElems[j].substring(0, index).trim());
559             String annotText = norm(subElems[j].substring(index + 2,
560                     subElems[j].length()).trim());
561             if(annotText.charAt(0== '\"'
562                     && annotText.charAt(annotText.length() 1== '\"') {
563               annotText = annotText.substring(1, annotText.length() 1);
564             }
565             if(!annotType.trim().equals(baseTokenAnnotationType))
566               areAllTermsTokens = false;
567             terms.add(new Term(field, annotText, annotType + ".string"));
568             pos.add(new Integer(position));
569             if(j == 0)
570               consider.add(new Boolean(true));
571             else consider.add(new Boolean(false));
572 
573           }
574           else if(index == -&& index1 != -1) {
575             throw new SearchException("missing operator",
576                     "an equal operator (==) is missing",
577                     elem, (elem.indexOf("=", lengthTravelledSoFar)!=-1)?
578                            elem.indexOf("=", lengthTravelledSoFar):elem.length());
579           }
580           else {
581             // this is {AT.f == "s"}
582             String annotType = norm(subElems[j].substring(0, index1).trim());
583             String featureType = norm(subElems[j].substring(index1 + 1, index)
584                     .trim());
585             String featureText = norm(subElems[j].substring(index + 2,
586                     subElems[j].length()).trim());
587             if(featureText.length() && featureText.charAt(0== '\"'
588                     && featureText.charAt(featureText.length() 1== '\"')
589               featureText = featureText.substring(1, featureText.length() 1);
590 
591             if(!annotType.trim().equals(baseTokenAnnotationType))
592               areAllTermsTokens = false;
593             terms.add(new Term(field, featureText, annotType + "."
594                     + featureType));
595             pos.add(new Integer(position));
596             if(j == 0)
597               consider.add(new Boolean(true));
598             else consider.add(new Boolean(false));
599           }
600           lengthTravelledSoFar += subElems[j].length() 1;
601         }
602         position++;
603       }
604     }
605     else {
606       // possible
607       // remove all the inverted commas
608       String newString = "";
609       char prev = ' ', ch = ' ';
610       for(int i = 0; i < elem.length(); i++) {
611         prev = ch;
612         ch = elem.charAt(i);
613         if(ch == '\"' && prev != '\\') {
614           continue;
615         }
616         else {
617           newString += ch;
618         }
619       }
620       // there can be many tokens
621       String[] subTokens = norm(newString).split("( )+");
622       for(int k = 0; k < subTokens.length; k++) {
623         if(subTokens[k].trim().length() 0) {
624           terms.add(new Term(field, norm(subTokens[k]), baseTokenAnnotationType
625                   ".string"));
626           pos.add(new Integer(position));
627           consider.add(new Boolean(true));
628           position++;
629         }
630       }
631     }
632     return new List<?>[] {terms, pos, consider};
633   }
634 
635   public boolean needValidation() {
636     return needValidation;
637   }
638 }