public String buildQuery(int minimum)
   {
      String query;
      boolean validQuery = false;
      
      // the QName for the well known "name" attribute
      String nameAttr = Repository.escapeQName(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, ELEMENT_NAME));
      
      // match against content text
      String text = this.text.trim();
      
      StringBuilder fullTextBuf = new StringBuilder(64);
      StringBuilder nameAttrBuf = new StringBuilder(128);
      StringBuilder additionalAttrsBuf = new StringBuilder(128);
      
      if (text.length() != 0 && text.length() >= minimum)
      {
         if (text.indexOf(' ') == -1 && text.charAt(0) != '"')
         {
            // check for existance of a special operator
            boolean operatorAND = (text.charAt(0) == OP_AND);
            boolean operatorNOT = (text.charAt(0) == OP_NOT);
            // strip operator from term if one was found
            if (operatorAND || operatorNOT)
            {
               text = text.substring(1);
            }
            
            if (text.length() != 0)
            {
               // prepend NOT operator if supplied
               if (operatorNOT)
               {
                  fullTextBuf.append(OP_NOT);
                  nameAttrBuf.append(OP_NOT);
               }
               
               processSearchTextAttribute(nameAttr, text, nameAttrBuf, fullTextBuf);
               for (QName qname : this.simpleSearchAdditionalAttrs)
               {
                  processSearchAttribute(qname, text, additionalAttrsBuf, false, operatorNOT);
               }
            }
         }
         else
         {
            // multiple word search
            if (text.charAt(0) == '"' && text.charAt(text.length() - 1) == '"')
            {
               // as a single quoted phrase
               String quotedSafeText = '"' + text.substring(1, text.length() - 1) + '"';
               fullTextBuf.append("TEXT:").append(quotedSafeText);
               nameAttrBuf.append("@").append(nameAttr).append(":").append(quotedSafeText);
               for (QName qname : this.simpleSearchAdditionalAttrs)
               {
                  additionalAttrsBuf.append(" @").append(
                        Repository.escapeQName(qname)).append(":").append(quotedSafeText);
               }
            }
            else
            {
               // as individual search terms
               StringTokenizer t = new StringTokenizer(text, " ");
               
               fullTextBuf.append('(');
               nameAttrBuf.append('(');
               additionalAttrsBuf.append('(');
               
               int termCount = 0;
               int tokenCount = t.countTokens();
               for (int i=0; i
               {
                  String term = t.nextToken();
                  
                  // check for existance of a special operator
                  boolean operatorAND = (term.charAt(0) == OP_AND);
                  boolean operatorNOT = (term.charAt(0) == OP_NOT);
                  // strip operator from term if one was found
                  if (operatorAND || operatorNOT)
                  {
                     term = term.substring(1);
                  }
                  
                  // special case for AND all terms if set (apply after operator character removed)
                  // note that we can't force AND if NOT operator has been set
                  if (operatorNOT == false)
                  {
                     operatorAND = operatorAND | this.forceAndTerms;
                  }
                  
                  if (term.length() != 0)
                  {
                     // prepend NOT operator if supplied
                     if (operatorNOT)
                     {
                        fullTextBuf.append(OP_NOT);
                        nameAttrBuf.append(OP_NOT);
                     }
                     
                     // prepend AND operator if supplied
                     if (operatorAND)
                     {
                        fullTextBuf.append(OP_AND);
                        nameAttrBuf.append(OP_AND);
                     }
                     
                     processSearchTextAttribute(nameAttr, term, nameAttrBuf, fullTextBuf);
                     for (QName qname : this.simpleSearchAdditionalAttrs)
                     {
                        processSearchAttribute(qname, term, additionalAttrsBuf, operatorAND, operatorNOT);
                     }
                     
                     fullTextBuf.append(' ');
                     nameAttrBuf.append(' ');
                     additionalAttrsBuf.append(' ');
                     
                     termCount++;
                  }
               }
               fullTextBuf.append(')');
               nameAttrBuf.append(')');
               additionalAttrsBuf.append(')');
            }
         }
         
         validQuery = true;
      }
      
      // match a specific PATH for space location or categories
      StringBuilder pathQuery = null;
      if (location != null || (categories != null && categories.length !=0))
      {
         pathQuery = new StringBuilder(128);
         if (location != null)
         {
            pathQuery.append(" PATH:\"").append(location).append("\" ");
            if (categories != null && categories.length != 0)
            {
               pathQuery.append("AND (");
            }
         }
         if (categories != null && categories.length != 0)
         {
            for (int i=0; i
            {
               pathQuery.append(" PATH:\"").append(categories[i]).append("\" ");
            }
            if (location != null)
            {
               pathQuery.append(") ");
            }
         }
      }
      
      // match any extra query attribute values specified
      StringBuilder attributeQuery = null;
      if (queryAttributes.size() != 0)
      {
         attributeQuery = new StringBuilder(queryAttributes.size() << 6);
         for (QName qname : queryAttributes.keySet())
         {
            String value = queryAttributes.get(qname).trim();
            if (value.length() >= minimum)
            {
               processSearchAttribute(qname, value, attributeQuery);
            }
         }
         
         // handle the case where we did not add any attributes due to minimum length restrictions
         if (attributeQuery.length() == 0)
         {
            attributeQuery = null;
         }
      }
      
      // match any extra fixed value attributes specified
      if (queryFixedValues.size() != 0)
      {
         if (attributeQuery == null)
         {
            attributeQuery = new StringBuilder(queryFixedValues.size() << 6);
         }
         for (QName qname : queryFixedValues.keySet())
         {
            String escapedName = Repository.escapeQName(qname);
            String value = queryFixedValues.get(qname);
            attributeQuery.append(" +@").append(escapedName)
                          .append(":\"").append(value).append('"');
         }
      }
      
      // range attributes are a special case also
      if (rangeAttributes.size() != 0)
      {
         if (attributeQuery == null)
         {
            attributeQuery = new StringBuilder(rangeAttributes.size() << 6);
         }
         for (QName qname : rangeAttributes.keySet())
         {
            String escapedName = Repository.escapeQName(qname);
            RangeProperties rp = rangeAttributes.get(qname);
            String value1 = LuceneQueryParser.escape(rp.lower);
            String value2 = LuceneQueryParser.escape(rp.upper);
            attributeQuery.append(" +@").append(escapedName)
                          .append(":").append(rp.inclusive ? "[" : "{").append(value1)
                          .append(" TO ").append(value2).append(rp.inclusive ? "]" : "}");
         }
      }
      
      // mimetype is a special case - it is indexed as a special attribute it comes from the combined
      // ContentData attribute of cm:content - ContentData string cannot be searched directly
      if (mimeType != null && mimeType.length() != 0)
      {
         if (attributeQuery == null)
         {
            attributeQuery = new StringBuilder(64);
         }
         String escapedName = Repository.escapeQName(QName.createQName(ContentModel.PROP_CONTENT + ".mimetype"));
         attributeQuery.append(" +@").append(escapedName)
                       .append(":").append(mimeType);
      }
      
      // match against appropriate content type
      String fileTypeQuery;
      if (contentType != null)
      {
         fileTypeQuery = " TYPE:\"" + contentType + "\" ";
      }
      else
      {
         // default to cm:content
         fileTypeQuery = " TYPE:\"{" + NamespaceService.CONTENT_MODEL_1_0_URI + "}content\" ";
      }
      
      // match against appropriate folder type
      String folderTypeQuery;
      if (folderType != null)
      {
         folderTypeQuery = " TYPE:\"" + folderType + "\" ";
      }
      else
      {
         folderTypeQuery = " TYPE:\"{" + NamespaceService.CONTENT_MODEL_1_0_URI + "}folder\" ";
      }
      
      String fullTextQuery = fullTextBuf.toString();
      String nameAttrQuery = nameAttrBuf.toString();
      String additionalAttrsQuery =
         (this.simpleSearchAdditionalAttrs.size() != 0) ? additionalAttrsBuf.toString() : "";
      
      if (text.length() != 0 && text.length() >= minimum)
      {
         // text query for name and/or full text specified
         switch (mode)
         {
            case SearchContext.SEARCH_ALL:
               query = '(' + fileTypeQuery + " AND " + '(' + nameAttrQuery + ' ' + additionalAttrsQuery + ' ' + fullTextQuery + ')' + ')' +
                       ' ' +
                       '(' + folderTypeQuery + " AND " + '(' + nameAttrQuery + ' ' + additionalAttrsQuery + "))";
               break;
            
            case SearchContext.SEARCH_FILE_NAMES:
               query = fileTypeQuery + " AND " + nameAttrQuery;
               break;
            
            case SearchContext.SEARCH_FILE_NAMES_CONTENTS:
               query = fileTypeQuery + " AND " + '(' + nameAttrQuery + ' ' + fullTextQuery + ')';
               break;
            
            case SearchContext.SEARCH_SPACE_NAMES:
               query = folderTypeQuery + " AND " + nameAttrQuery;
               break;
            
            default:
               throw new IllegalStateException("Unknown search mode specified: " + mode);
         }
      }
      else
      {
         // no text query specified - must be an attribute/value query only
         switch (mode)
         {
            case SearchContext.SEARCH_ALL:
               query = '(' + fileTypeQuery + ' ' + folderTypeQuery + ')';
               break;
            
            case SearchContext.SEARCH_FILE_NAMES:
            case SearchContext.SEARCH_FILE_NAMES_CONTENTS:
               query = fileTypeQuery;
               break;
            
            case SearchContext.SEARCH_SPACE_NAMES:
               query = folderTypeQuery;
               break;
            
            default:
              throw new IllegalStateException("Unknown search mode specified: " + mode);
         }
      }
      
      // match entire query against any additional attributes specified
      if (attributeQuery != null)
      {
         query = attributeQuery + " AND (" + query + ')';
      }
      
      // match entire query against any specified paths
      if (pathQuery != null)
      {
         query = "(" + pathQuery + ") AND (" + query + ')';
      }
      
      // check that we have a query worth executing - if we have no attributes, paths or text/name search
      // then we'll only have a search against files/type TYPE which does nothing by itself!
      validQuery = validQuery | (attributeQuery != null) | (pathQuery != null);
      if (validQuery == false)
      {
         query = null;
      }
      
      if (logger.isDebugEnabled())
         logger.debug("Query:\r\n" + query);
      
      return query;
   }