View Javadoc

1   package uk.ac.ebi.intenz.webapp.controller;
2   
3   import java.io.IOException;
4   import java.sql.PreparedStatement;
5   import java.sql.ResultSet;
6   import java.sql.SQLException;
7   import java.util.ArrayList;
8   import java.util.Arrays;
9   import java.util.Iterator;
10  import java.util.List;
11  import java.util.SortedMap;
12  import java.util.StringTokenizer;
13  import java.util.TreeMap;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  import javax.servlet.ServletException;
18  
19  import org.apache.log4j.Logger;
20  
21  import uk.ac.ebi.biobabel.util.WebUtil;
22  import uk.ac.ebi.intenz.domain.enzyme.EnzymeCommissionNumber;
23  import uk.ac.ebi.intenz.domain.exceptions.EcException;
24  import uk.ac.ebi.intenz.webapp.IntEnzConfig;
25  import uk.ac.ebi.intenz.webapp.exceptions.QueryException;
26  import uk.ac.ebi.intenz.webapp.utilities.IntEnzMessenger;
27  import uk.ac.ebi.xchars.SpecialCharacters;
28  import uk.ac.ebi.xchars.domain.EncodingType;
29  import uk.ac.ebi.xchars.exceptions.InvalidUTF8OctetSequenceException;
30  
31  /**
32   * This class processes all full text queries.
33   * <p/>
34   * All queries are processed using the UTF-8 encoded (octet sequences) representation sent by browsers.
35   * Rather than using <code>request.getParameter(...)</code> this class decodes the query string manually, since
36   * <code>request.getParameter(...)</code> does not decode the octet sequences correctly, if special characters encoded
37   * as two or more octets are transmitted.<br/>
38   * See the <code>XChars</code> library documentation for more information regarding UTF-8 octet sequence decoding.
39   * <p/>
40   * <p/>
41   * TODO: Implement a query parser.
42   *
43   * @author Michael Darsow
44   * @version 2.0 - 13-July-2004
45   */
46  public class SearchCommand extends DatabaseCommand {
47  
48      public static final Logger LOGGER = Logger.getLogger(SearchCommand.class);
49  
50    private static final String COLUMNS =
51        "enzyme_id, ec, common_name, status, text, text_order";
52  
53    /**
54     * Returns the SQL statement for full text searching using a text index (ORACLE interMedia Text).
55     *
56     * @return the SQL statement.
57     */
58    private String fulltextQueryStatement() {
59      return "SELECT /*+ FIRST_ROWS */ score(1) score, " + COLUMNS +
60              " FROM enzyme.intenz_text" +
61              " WHERE CONTAINS (text, ?, 1) > 0" +
62              " ORDER BY score(1) DESC";
63    }
64  
65    /**
66     * Processes the full text query.
67     * <p/>
68     * The query string is parsed, decoded, checked and integrated into a SQL statement.
69     * After execution the result is being sent to the browser (if any).
70     * Exceptions regarding this process are caught and the user is informed accordingly.
71     *
72     * @throws ServletException ...
73     * @throws IOException      ...
74     */
75    public void process() throws ServletException, IOException {
76      int groupSize = Integer.parseInt(IntEnzConfig.getInstance().getPageSize());
77      String query = null;
78      StringBuffer userFriendlyQuery = null;
79      String userFriendlyQueryTF = null;
80  
81      // Decode the UTF-8 octets sent by the client.
82      try {
83        query = decodeQuery(request.getQueryString(), false);
84        userFriendlyQuery = new StringBuffer(typifyQuery(query, QueryType.valueOf(request.getParameter("t"))).trim());
85        userFriendlyQueryTF = new String(query);
86      } catch (InvalidUTF8OctetSequenceException e) {
87        request.setAttribute("query", "");
88        request.setAttribute("message", e.getMessage());
89        forward("/noResult.jsp"); // No result found.
90        return;
91      }
92  
93      // Catch empty queries.
94      if (query == null || query.equals("")) {
95        request.setAttribute("query", "");
96        request.setAttribute("message", "The query string was empty!");
97        forward("/noResult.jsp"); // No result found.
98        return;
99      }
100 
101     // Check query if it is valid and prepare it for the actual search process.
102     StringBuffer checkedQuery = null;
103     try {
104       // Parameter 't' stores the type of the search. This can be one of the following:
105       // ALL words (logical AND), ANY words (logical OR) or EXACT match (phrase search).
106       checkedQuery = new StringBuffer(checkQuery(query, QueryType.valueOf(request.getParameter("t"))).trim());
107 
108       // Check if the user entered words to be excluded.
109       String excludedWords = decodeQuery(request.getQueryString(), true);
110       if (excludedWords != null && !excludedWords.equals("")) {
111         checkedQuery.append(" ");
112         String extendedQuery = extendQuery(excludedWords, true);
113         checkedQuery.append(extendedQuery);
114         userFriendlyQuery.append(" ");
115         userFriendlyQuery.append(new String(extendQuery(excludedWords, false)));
116         request.setAttribute("excludedWords", excludedWords);
117       }
118 
119       // Check if the user chose a field to limit his/her search.
120       String field = request.getParameter("fields");
121       if (field != null && !field.equals("all")) {
122         checkedQuery.append(" ");
123         String withinQuery = addWithinClause(field);
124         checkedQuery.append(withinQuery);
125         userFriendlyQuery.append(" ");
126         userFriendlyQuery.append(new String(withinQuery));
127         request.setAttribute("field", field);
128       }
129     } catch (QueryException e) {
130       request.setAttribute("message", e.getMessage());
131       // Store (HTML-friendly) original query for feedback.
132       request.setAttribute("query", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQuery.toString())));
133       request.setAttribute("queryTF", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQueryTF)));
134       forward("/search.jsp");
135       return;
136     } catch (InvalidUTF8OctetSequenceException e) {
137       request.setAttribute("query", "");
138       request.setAttribute("message", e.getMessage());
139       forward("/noResult.jsp"); // No result found.
140       return;
141     }
142 
143     // Store (HTML-friendly) original query for feedback.
144     request.setAttribute("query", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQuery.toString())));
145     request.setAttribute("queryTF", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQueryTF)));
146 
147     PreparedStatement ps = null;
148     List<Result> results = new ArrayList<Result>();
149     Integer maxScore = new Integer(0);
150 
151     try {
152       ps = con.prepareStatement(fulltextQueryStatement());
153       ps.setString(1, checkedQuery.toString());
154 //      ps.setString(2, checkedQuery.toString());
155       ResultSet rs = ps.executeQuery();
156 
157       boolean maxScoreCounted = false;
158       queryResultsLoop: while (rs.next()) {
159         String id = rs.getString("enzyme_id");
160         // Check if we already have the entry within the results:
161         for (int i = 0; i < results.size(); i++){
162             Result previous = results.get(i);
163             if (id.equals(previous.id)){
164                 previous.addText(rs.getString("text"), rs.getInt("text_order"));
165                 previous.addScore(rs.getInt("score"));
166                 continue queryResultsLoop;
167             }
168         }
169 
170         Result res = new Result();
171         res.id = id;
172         res.ec = EnzymeCommissionNumber.valueOf(rs.getString("ec"));
173         String commonName = rs.getString("common_name");
174         if (commonName == null) commonName = "";
175         res.commonName = commonName;
176         res.status = rs.getString("status");
177         if (!maxScoreCounted) {
178             maxScore = new Integer(rs.getString("score"));
179             maxScoreCounted = true;
180         }
181         res.score = rs.getInt("score");
182         res.addText(rs.getString("text"), rs.getInt("text_order"));
183         if (res.ec.toString().equals(query)){
184         	results.add(0, res);
185         } else {
186         	results.add(res);
187         }
188 
189       }
190     } catch (IllegalArgumentException e) {
191       doErrorExceptionHandling(e);
192       return;
193     } catch (EcException e){
194         doErrorExceptionHandling(e);
195         return;
196     } catch (SQLException e) {
197        LOGGER.error("While searching", e);
198       IntEnzMessenger.sendError(this.getClass().toString(),
199               e.getMessage() + " query (checked query): " + query + "(" + checkedQuery + ")",
200               (String) request.getSession().getAttribute("user"));
201       if (e.getMessage().indexOf("DRG-51030") > -1) {
202         request.setAttribute("message", "Your query resulted in too many terms.\nPlease refine your query.");
203         forward("/search.jsp");
204         return;
205       }
206       if (e.getMessage().indexOf("DRG-50901") > -1) {
207         request.setAttribute("message",
208                 "The given query could not be processed.\nPlease check the usage of operators and special characters.");
209         forward("/search.jsp");
210         return;
211       }
212       if (e.getMessage().indexOf("DRG-10837") > -1) {
213         request.setAttribute("message",
214                 "The given section does not exist.\nPlease choose a section from the drop down list below.");
215         forward("/search.jsp");
216         return;
217       }
218       request.setAttribute("message", "The following database error occured:\n" + e.getMessage() +
219               this.databaseErrorMessage);
220       forward("/error.jsp");
221       return;
222 	} finally {
223       try {
224         ps.close();
225       } catch (SQLException e) {
226          doErrorExceptionHandling(e);
227          return;
228       }
229     }
230 
231     if (results.size() == 0) {
232       // Store (HTML-friendly) original query for feedback.
233       request.setAttribute("query", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQuery.toString())));
234       request.setAttribute("queryTF", WebUtil.escapeHTMLTag(escapeUTF8(userFriendlyQueryTF)));
235       request.setAttribute("message", "No results found for '" + query + "'");
236       forward("/noResult.jsp"); // No result found.
237       return;
238     }
239 
240     if (results.size() == 1){
241         // Go straight to the only one result:
242         String ec = results.get(0).ec.toString();
243         try {
244             switch (EnzymeCommissionNumber.valueOf(ec).getType()) {
245                 case ENZYME:
246                 case PRELIMINARY:
247                     String id = results.get(0).id;
248                     forward("/query?cmd=SearchID&id=" + id);
249                     return;
250                 default:
251                     forward("/query?cmd=SearchEC&ec=" + ec);
252                     return;
253             }
254         } catch (Exception e) {
255             // we shouldn't get here, that would mean there's something wrong in the DB!
256             request.setAttribute("message", ec + " is not a valid EC number.\nPlease try again.");
257             forward("/search.jsp");
258             return;
259         }
260     }
261 
262     List<Result> group = getGroup(results, 0, groupSize);
263     request.setAttribute("group", group);
264 
265     // Set maximum score.
266     request.getSession().setAttribute("max_score", maxScore);
267 
268     // Set current values (start and end index and result size).
269     request.setAttribute("st", "" + 0);
270     if (results.size() > groupSize)
271       request.setAttribute("end", "" + groupSize);
272     else
273       request.setAttribute("end", "" + results.size());
274     request.setAttribute("size", "" + results.size());
275 
276     // Set start index of the following group.
277     if (groupSize < results.size()) {
278       request.setAttribute("nst", "" + groupSize);
279     }
280 
281     // Set the group size.
282     request.getSession().setAttribute("gs", new Integer(groupSize));
283     request.getSession().setAttribute("qResult", escapeUTF8(userFriendlyQuery.toString()));
284     request.getSession().setAttribute("qResultTF", escapeUTF8(userFriendlyQueryTF));
285     request.getSession().setAttribute("result", results);
286     forward("/result.jsp");
287   }
288 
289    private void doErrorExceptionHandling (Exception e) throws ServletException, IOException {
290        LOGGER.error("Other error while searching", e);
291       IntEnzMessenger.sendError(this.getClass().toString(), e.getMessage(),
292               (String) request.getSession().getAttribute("user"));
293       request.setAttribute("message", "The following database error occured:\n" + e.getMessage() +
294               this.databaseErrorMessage);
295       forward("/error.jsp");
296    }
297 
298    /**
299    * Replaces '+' by a space.
300    * <p/>
301    * The plus character in UTF-8 URL encoding encodes the space character.
302    *
303    * @param queryString The query string to be checked. (The parameter cannot be <code>null</code> or empty, because it has been checked already.)
304    * @return query string with spaces.
305    */
306   private String replacePlus(String queryString) {
307     return queryString.replaceAll("\\+", " ");
308   }
309 
310   /**
311    * Decodes the UTF-8 query, i.e. the value of the <code>q</code> parameter.
312    * <p/>
313    * Does the same as <code>URLDecoder</code> except that the octets are decoded differently
314    * (see <code>XChars</code> library for more info).
315    *
316    * @param queryString    The query string to be decoded.
317    * @param isNotParameter
318    * @return the decoded value of the <code>q</code> parameter (i.e. the full text query).
319    * @throws InvalidUTF8OctetSequenceException
320    *          if the given octets are invalid (see <code>XChars</code> library for more info).
321    */
322   private String decodeQuery(String queryString, boolean isNotParameter) throws InvalidUTF8OctetSequenceException {
323     if (queryString == null || queryString.equals("")) return queryString;
324 
325     StringBuffer searchQuery = new StringBuffer(replacePlus(getSearchQuery(queryString, isNotParameter)));
326     Pattern utf8HexPattern = Pattern.compile("((%([a-fA-F0-9]){2}?)+)"); // Pattern for octet sequences.
327     Matcher utf8Matcher = utf8HexPattern.matcher(searchQuery);
328     int index = 0;
329     while (utf8Matcher.find(index)) {
330       String decodedString = SpecialCharacters.decodeUTF8(utf8Matcher.group(1));
331       index = utf8Matcher.start() + decodedString.length();
332       searchQuery.replace(utf8Matcher.start(), utf8Matcher.end(), decodedString);
333       if (index > searchQuery.length() - 1) break;
334       utf8Matcher.reset(searchQuery);
335     }
336 
337     return searchQuery.toString();
338   }
339 
340   private String getSearchQuery(String queryString, boolean isNotParameter) {
341     if (queryString == null || queryString.equals("")) return "";
342     int searchQueryStart = 0;
343     if (isNotParameter)
344       searchQueryStart = queryString.indexOf("not=");
345     else
346       searchQueryStart = queryString.indexOf("q=");
347 
348     if (searchQueryStart == -1) return "";
349     int searchQueryEnd;
350     String temp = null;
351     if (isNotParameter) {
352       temp = queryString.substring(searchQueryStart + 4);
353     } else {
354       temp = queryString.substring(searchQueryStart + 2);
355     }
356     StringBuffer searchQuery = null;
357     if (temp.indexOf('&') > -1) {
358       searchQueryEnd = temp.indexOf('&');
359       searchQuery = new StringBuffer(temp.substring(0, searchQueryEnd));
360     } else {
361       searchQuery = new StringBuffer(temp.substring(0));
362       searchQueryEnd = searchQueryStart + searchQuery.length();
363     }
364     return searchQuery.toString();
365   }
366 
367   private String addWithinClause(String field) {
368     StringBuffer withinClause = new StringBuffer();
369     withinClause.append("WITHIN ");
370     withinClause.append(field);
371     return withinClause.toString();
372   }
373 
374   private String extendQuery(String excludedWords, boolean transformQuery) throws QueryException {
375     if (transformQuery) excludedWords = transformQuery(excludedWords, null, true);
376 
377     StringBuffer extendedQuery = new StringBuffer();
378     for (StringTokenizer stringTokenizer = new StringTokenizer(excludedWords, ","); stringTokenizer.hasMoreTokens();) {
379       String token = stringTokenizer.nextToken().trim();
380       extendedQuery.append("NOT ");
381       extendedQuery.append(token);
382       extendedQuery.append(" ");
383     }
384 
385     return extendedQuery.toString().trim();
386   }
387 
388 
389 
390 
391   // ------------------- PRIVATE METHODS ------------------------
392 
393   private String escapeUTF8(String query) {
394     // Existing XChars elements will be transformed into escaped UTF-8 strings.
395     SpecialCharacters encoding = (SpecialCharacters) request.getSession().getServletContext().getAttribute("characters");
396     if (query.indexOf("<small>") > -1 || query.indexOf("</small>") > -1 || query.indexOf("<smallsup>") > -1 ||
397             query.indexOf("</smallsup>") > -1 || query.indexOf("</smallsub>") > -1 || query.indexOf("<smallsub>") > -1)
398       return encoding.xml2Display(query, EncodingType.SWISSPROT_CODE);
399     return encoding.xml2Display(query);
400   }
401 
402   /**
403    * Checks the query for various things.
404    * <p/>
405    *
406    * @param query
407    * @param type  The type of query. Can be <code>null</code>.
408    * @return
409    * @throws QueryException
410    */
411   private String checkQuery(String query, QueryType type) throws QueryException {
412     assert query != null && !query.equals("");
413     return transformQuery(query, type, false);
414   }
415 
416   public String transformQuery(String query, QueryType queryType, boolean exclusion) throws QueryException {
417     assert query != null && !query.equals("");
418 
419     // AND, OR and NOT operators will be removed, because manually added operators are not supported.
420     query = escapeBooleanOperators(operators2Uppercase(query));
421 
422     // XChars formmattings will be removed as they do not appear as tokens within the search engine.
423     query = removeFormattings(query);
424 
425     // Queries which are not phrase queries will be handled differently.
426     if (queryType == null || queryType != QueryType.EXACT) {
427       // The within operator will be removed, if the user entered it manually.
428       query = escapeWithinOperator(query);
429 
430       // Check for 'silly' queries.
431       String longQueryWord = getLongQueryWord(query);
432       if (!longQueryWord.equals(""))
433         throw new QueryException("\"" + longQueryWord.substring(0, 80) +
434                 "\"... is too long a word. Try using a shorter word.");
435 
436       if (countQueryWords(query) > 10)
437         throw new QueryException("The search query must not exceed 10 words.");
438 
439       if (!exclusion) {
440         // Use the type parameter to alter the query accordingly.
441         query = typifyQuery(query, queryType);
442       }
443     }
444 
445     // Existing XChars elements will be transformed into escaped UTF-8 strings.
446     query = escapeUTF8(query);
447 
448      query = removeFormattings(query);
449 
450     // Escape remaining tags.
451     if (Pattern.matches(".*?\\<.+?\\>.*?", query))
452       query = query.replaceAll("\\<", "\\\\<").replaceAll("\\>", "\\\\>").replaceAll("\\/", "\\\\/");
453 
454     // Escape/Remove unsupported operators.
455     query = escapeUnsupportedOperators(query);
456 
457     return query;
458   }
459 
460   /**
461    * Removes the within operator if it has been manually entered by the user.
462    *
463    * @param query
464    * @return
465    */
466   private String escapeWithinOperator(String query) {
467     assert query != null;
468     query = " " + query + " ";
469     return query.replaceAll("(\\sWITHIN\\s)", " ").trim();
470   }
471 
472   /**
473    * Handles the special case when the user entered <code>XChars</code> formattings.
474    * <p/>
475    * Currently only <code>&lt;smallsup&gt;</code> and <code>&lt;smallsub&gt;</code> elements represent
476    * <code>XChars</code> formattings.
477    *
478    * @param query The query to be checked.
479    * @return the checked query.
480    */
481   private String removeFormattings(String query) {
482     assert query != null;
483     query = query.replaceAll("\\<smallsu[pb]\\>", "").replaceAll("\\<\\/smallsu[pb]\\>", "");
484     query = query.replaceAll("\\<\\/?small\\>", "");
485     query = query.replaceAll("\\<\\/?sup\\>", "");
486     query = query.replaceAll("\\<\\/?sub\\>", "");
487     query = query.replaceAll("\\<\\/?b\\>", "");
488     query = query.replaceAll("\\<\\/?i\\>", "");
489     query = query.replaceAll("\\<\\/?p\\/?\\>", "");
490     return query.replaceAll("\\<activated\\>", "").replaceAll("\\<\\/activated\\>", "");
491   }
492 
493   /**
494    * Escapes unsupported characters or characters which are reserved characters of the search engine.
495    *
496    * @param query The query to be checked.
497    * @return The checked query.
498    */
499   private String escapeUnsupportedOperators(String query) {
500     assert query != null;
501     StringBuffer checkedQuery = new StringBuffer();
502     char[] chars = query.toCharArray();
503     char previous = '-';
504     char current = '-';
505     char next = '-';
506     for (int iii = 0; iii < chars.length; iii++) {
507       if (iii > 0) previous = current;
508       current = chars[iii];
509       if (iii < chars.length - 1)
510         next = chars[(iii + 1)];
511       else
512         next = '-';
513       switch (checkCharacter(previous, current, next)) {
514         case 0:
515           checkedQuery.append(current);
516           break;
517         case 1: // Escape character.
518           checkedQuery.append("\\");
519           checkedQuery.append(current);
520           break;
521         case 2: // Remove character and previous space.
522           checkedQuery = checkedQuery.deleteCharAt(checkedQuery.length() - 1);
523           break;
524       }
525     }
526 
527     return checkedQuery.toString();
528   }
529 
530   /**
531    * Checks whether the current character is an unsupported operator or reserved character of the search engine.
532    *
533    * @param preceeding The preceeding character.
534    * @param current    The current character.
535    * @param next       The nect character.
536    * @return a code fot the calling method (0 = do not escape, 1 = escape, 2 = remove character).
537    */
538   private int checkCharacter(char preceeding, char current, char next) {
539     char[] unsupportedOperators = {'[', ']', ',', '-', '_', '$', '!', '=', '*', ':', '?', '|', '>', '&', '#', ';', '(', ')', '.'};
540     Arrays.sort(unsupportedOperators);
541     int index = Arrays.binarySearch(unsupportedOperators, current);
542     if (index > -1) {
543       if (preceeding == ' ' && next == ' ') return 2; // remove character (should never happen, becuase it is checked earlier)
544       if (preceeding != '\\') return 1; // escape character
545     }
546     return 0; // do not escape
547   }
548 
549   /**
550    * Inserts logical operators in the query string according to the given type of search.
551    * <p/>
552    * Currently only <code>AND</code> and <code>OR</code> are supported.
553    *
554    * @param query The query to be extended.
555    * @param type  The type of the query.
556    * @return The extended query.
557    */
558   private String typifyQuery(String query, QueryType type) {
559     assert query != null;
560     StringBuffer adjustedQuery = new StringBuffer();
561     int iii = 0;
562     for (StringTokenizer stringTokenizer = new StringTokenizer(query); stringTokenizer.hasMoreTokens();) {
563       String token = stringTokenizer.nextToken();
564       if (iii > 0) {
565         if (type == QueryType.ANY)
566           adjustedQuery.append("OR ");
567         if (type == QueryType.ALL)
568           adjustedQuery.append("AND ");
569       }
570       adjustedQuery.append(token);
571       adjustedQuery.append(" ");
572       iii++;
573     }
574     return adjustedQuery.toString().trim();
575   }
576 
577   /**
578    * Removes supported boolean operators which have been entered manually.
579    * <p/>
580    * All supported boolean operators can only be used by filling the search form correctly (i.e. without entering
581    * them manually).
582    *
583    * @param query The query to be checked.
584    * @return The checked query.
585    */
586   private String escapeBooleanOperators(String query) {
587     assert query != null;
588     query = " " + query + " ";
589     query = query.replaceAll("(\\sAND\\s)", " {AND} ");
590     query = query.replaceAll("(\\sOR\\s)", " {OR} ");
591     query = query.replaceAll("(\\sNOT\\s)", " {NOT} ");
592     query = query.replaceAll("\\s\\&\\s", " {&} ");
593     query = query.replaceAll("\\s\\|\\s", " {|} ");
594     query = query.replaceAll("\\s\\~\\s", " {~} ");
595      if (query.indexOf(" BT ") != - 1)
596          query = query.replaceAll(" BT ", " {BT} ");
597       if (query.indexOf(" ABOUT ") != - 1)
598          query = query.replaceAll(" ABOUT ", " {ABOUT} ");
599       if (query.indexOf(" ACCUM ") != - 1)
600          query = query.replaceAll(" ACCUM ", " {ACCUM} ");
601       if (query.indexOf(" BTG ") != - 1)
602          query = query.replaceAll(" BTG ", " {BTG} ");
603       if (query.indexOf(" BTI ") != - 1)
604          query = query.replaceAll(" BTI ", " {BTI} ");
605       if (query.indexOf(" BTP ") != - 1)
606          query = query.replaceAll(" BTP ", " {BTP} ");
607       if (query.indexOf(" FUZZY ") != - 1)
608          query = query.replaceAll(" FUZZY ", " {FUZZY} ");
609       if (query.indexOf(" HASPATH ") != - 1)
610          query = query.replaceAll(" HASPATH ", " {HASPATH} ");
611       if (query.indexOf(" INPATH ") != - 1)
612          query = query.replaceAll(" INPATH ", " {INPATH} ");
613       if (query.indexOf(" MINUS ") != - 1)
614          query = query.replaceAll(" MINUS ", " {MINUS} ");
615       if (query.indexOf(" NEAR ") != - 1)
616          query = query.replaceAll(" NEAR ", " {NEAR} ");
617       if (query.indexOf(" NT ") != - 1)
618          query = query.replaceAll(" NT ", " {NT} ");
619       if (query.indexOf(" NTG ") != - 1)
620          query = query.replaceAll(" NTG ", " {NTG} ");
621       if (query.indexOf(" NTI ") != - 1)
622          query = query.replaceAll(" NTI ", " {NTI} ");
623       if (query.indexOf(" NTP ") != - 1)
624          query = query.replaceAll(" NTP ", " {NTP} ");
625       if (query.indexOf(" PT ") != - 1)
626          query = query.replaceAll(" PT ", " {PT} ");
627       if (query.indexOf(" RT ") != - 1)
628          query = query.replaceAll(" RT ", " {RT} ");
629       if (query.indexOf(" SQE ") != - 1)
630          query = query.replaceAll(" SQE ", " {SQE} ");
631       if (query.indexOf(" SYN ") != - 1)
632          query = query.replaceAll(" SYN ", " {SYN} ");
633       if (query.indexOf(" TR ") != - 1)
634          query = query.replaceAll(" TR ", " {TR} ");
635       if (query.indexOf(" TRSYN ") != - 1)
636          query = query.replaceAll(" TRSYN ", " {TRSYN} ");
637       if (query.indexOf(" TT ") != - 1)
638          query = query.replaceAll(" TT ", " {TT} ");
639     return query.trim();
640   }
641 
642   /**
643    * Checks if a query word is longer than 200 characters.
644    *
645    * @param query The query string.
646    * @return The word which length is greater than 200 characters.
647    */
648   private String getLongQueryWord(String query) {
649     assert query != null;
650     for (StringTokenizer stringTokenizer = new StringTokenizer(query); stringTokenizer.hasMoreTokens();) {
651       String word = stringTokenizer.nextToken();
652       if (word.length() > 200) return word;
653     }
654     return "";
655   }
656 
657   /**
658    * Counts the number of query words.
659    *
660    * @param detaggedQuery The query string.
661    * @return The number of query words (w/o operators).
662    */
663   private int countQueryWords(String detaggedQuery) {
664     int count = 0;
665     for (StringTokenizer stringTokenizer = new StringTokenizer(detaggedQuery); stringTokenizer.hasMoreTokens(); stringTokenizer.nextToken()) {
666       count++;
667     }
668     return count;
669   }
670 
671   /**
672    * Checks if the given string is a supported operator.
673    *
674    * @param word The search to be checked.
675    * @return <code>true</code>, if the given string is a supported operator.
676    */
677   private boolean isOperator(String word) {
678     assert word != null;
679     if (word.trim().toUpperCase().equals("AND") ||
680             word.toUpperCase().equals("OR") ||
681             word.toUpperCase().equals("NOT") ||
682             word.toUpperCase().equals("WITHIN"))
683       return true;
684 
685     return false;
686   }
687 
688   /**
689    * Writes all supported operators using uppercase letters.
690    *
691    * @param query The query to be checked.
692    * @return The formatted query.
693    */
694   private String operators2Uppercase(String query) {
695     assert query != null && !query.equals("");
696     StringBuffer sb = new StringBuffer();
697     for (StringTokenizer stringTokenizer = new StringTokenizer(query); stringTokenizer.hasMoreTokens();) {
698       String word = stringTokenizer.nextToken();
699       if (isOperator(word)) {
700         sb.append(word.toUpperCase() + " ");
701       } else {
702         sb.append(word + " ");
703       }
704     }
705 
706     return sb.toString().trim();
707   }
708 
709   /**
710    * Returns a result group to be shown in the browser.
711    *
712    * @param result The result.
713    * @param start  The start index of the group.
714    * @param end    The end index og the group.
715    * @return the selected group.
716    */
717   private List<Result> getGroup(List<Result> result, int start, int end) {
718     assert result != null;
719     List<Result> group = new ArrayList<Result>();
720 
721     if (end > result.size()) {
722       for (int iii = start; iii < result.size(); iii++) {
723         group.add(result.get(iii));
724       }
725     } else {
726       for (int iii = start; iii < end; iii++) {
727         group.add(result.get(iii));
728       }
729     }
730 
731     return group;
732   }
733 
734   private static class QueryType {
735     private String type;
736     public static final QueryType ANY = new QueryType("ANY");
737     public static final QueryType ALL = new QueryType("ALL");
738     public static final QueryType EXACT = new QueryType("EXACT");
739 
740     /**
741      * Returns the corresponding instance of the given query type.
742      * <p/>
743      * If the query type does not match any type an exception is thrown.
744      *
745      * @param type The query type.
746      * @return the class constant corresponding to the given type.
747      * @throws IllegalArgumentException if the type is invalid.
748      */
749     public static QueryType valueOf(String type) {
750       if (type == null || type.equals("") || type.toUpperCase().equals("UNDEF")) return EXACT;
751       type = type.toUpperCase();
752       if (type.equals(ANY.toString())) return ANY;
753       if (type.equals(ALL.toString())) return ALL;
754       if (type.equals(EXACT.toString())) return EXACT;
755       throw new IllegalArgumentException();
756     }
757 
758     /**
759      * Object cannot be created outside this class.
760      *
761      * @param type The query type.
762      */
763     private QueryType(String type) {
764       this.type = type;
765     }
766 
767     /**
768      * Standard equals method.
769      *
770      * @param o Object to be compared to this one.
771      * @return <code>true</code> if the objects are equal.
772      */
773     public boolean equals(Object o) {
774       if (this == o) return true;
775       if (!(o instanceof QueryType)) return false;
776 
777       final QueryType queryType = (QueryType) o;
778 
779       if (type != null ? !type.equals(queryType.type) : queryType.type != null) return false;
780 
781       return true;
782     }
783 
784     /**
785      * Returns the hash code of this object.
786      *
787      * @return the hash code of this object.
788      */
789     public int hashCode() {
790       return (type != null ? type.hashCode() : 0);
791     }
792 
793     /**
794      * Returns the query type's query.
795      *
796      * @return the query type's query.
797      */
798     public String toString() {
799       return type;
800     }
801   }
802 
803   public class Result {
804       private String id;
805       private EnzymeCommissionNumber ec;
806       private String commonName;
807       private String status;
808       private int score;
809       private SortedMap<Integer, String> xmlFragments;
810       private void addText(String text, int order){
811           if (xmlFragments == null) xmlFragments = new TreeMap<Integer, String>();
812           xmlFragments.put(new Integer(order), text);
813       }
814       private void addScore(int i) {
815           score += i;
816       }
817       private String getText(){
818           StringBuffer wholeXml = new StringBuffer();
819           for (Iterator<String> it = xmlFragments.values().iterator(); it.hasNext();){
820               wholeXml.append(it.next());
821           }
822           return wholeXml.toString();
823       }
824       public EnzymeCommissionNumber getEc() {
825           return ec;
826       }
827       public String getId() {
828           return id;
829       }
830       public String getCommonName() {
831           return commonName;
832       }
833       public String getStatus() {
834           return status;
835       }
836       public boolean isActive(){
837           return getText().indexOf("<active></active>") == -1;
838       }
839 
840   }
841 }