package com.dbxml.db.core.query;

/*
 * dbXML - Native XML Database
 * Copyright (C) 1999-2004  The dbXML Group, L.L.C.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Id: QueryEngine.java,v 1.3 2004/02/12 00:17:57 bradford Exp $
 */

import java.util.*;

import com.dbxml.db.core.ClassResolver;
import com.dbxml.db.core.Collection;
import com.dbxml.db.core.DBException;
import com.dbxml.db.core.Database;
import com.dbxml.db.core.data.Key;
import com.dbxml.db.core.indexer.IndexMatch;
import com.dbxml.db.core.transaction.Transaction;
import com.dbxml.util.Configuration;
import com.dbxml.util.ConfigurationCallback;
import com.dbxml.util.SimpleConfigurable;
import com.dbxml.util.dbXMLException;
import com.dbxml.xml.NamespaceMap;

/**
 * QueryEngine is the dbXML Query Engine.  Its purpose is to orchestrate
 * query operations against the dbXML repository.  The QueryEngine
 * basically just manages a set of QueryResolvers that actually perform
 * the work.
 */

public class QueryEngine extends SimpleConfigurable {
   private static final String[] EmptyStrings = new String[0];
   private static final Key[] EmptyKeys = new Key[0];

   private static final String RESOLVER = "resolver";
   private static final String CLASS = "class";

   private Database database;
   private Map resolvers = Collections.synchronizedMap(new TreeMap());

   public QueryEngine(Database db) {
      this.database = db;
   }

   public void setConfig(Configuration config) throws dbXMLException {
      super.setConfig(config);
      config.processChildren(RESOLVER,
         new ConfigurationCallback() {
            public void process(Configuration cfg) {
               String className = cfg.getAttribute(CLASS);
               try {
                  QueryResolver res = (QueryResolver)ClassResolver.get(className).newInstance();
                  res.setConfig(cfg);
                  res.setQueryEngine(QueryEngine.this);
                  resolvers.put(res.getQueryStyle(), res);
               }
               catch ( Exception e ) {
                  e.printStackTrace(System.err);
               }
            }
         });
   }

   public Database getDatabase() {
      return database;
   }

   /**
    * listStyles returns a list of styles supported by the
    * QueryEngine (ex: XPath, XUpdate)
    *
    * @return The supported styles
    */
   public String[] listStyles() {
      return (String[])resolvers.keySet().toArray(EmptyStrings);
   }

   private QueryResolver getResolver(String style) throws QueryException {
      QueryResolver res = (QueryResolver)resolvers.get(style);
      if ( res == null )
         throw new StyleNotFoundException("No Resolver available for '" + style + "' queries");
      return res;
   }

   /**
    * query performs the specified query and returns a NodeSet with
    * any possible results from that query.  The query is performed
    * in the context of a Collection.
    *
    * @param tx The controlling Transaction
    * @param col The Collection context
    * @param style The query style (XPath, Fulltext, etc...)
    * @param query The Query
    * @param nsMap The namespace Map (if any)
    * @param keys The initial Key set to use (if any)
    * @return A ResultSet with the query results
    */
   public ResultSet query(Transaction tx, Collection col, String style, String query, NamespaceMap nsMap, Key[] keys) throws DBException, QueryException {
      QueryResolver res = getResolver(style);
      return res.query(tx, col, query, nsMap, keys);
   }

   /**
    * compileQuery compiles a Query against the specified Collection
    * context and returns the compiled Query.  This DOES NOT actually
    * run the query, merely just parses it and primes any possible
    * Indexers that the query might need.
    *
    * @param col The Collection context
    * @param style The query style (XPath, Fulltext, etc...)
    * @param query The Query
    * @param nsMap The namespace Map (if any)
    * @param keys The initial Key set to use (if any)
    * @return The compiled Query
    */
   public Query compileQuery(Collection col, String style, String query, NamespaceMap nsMap, Key[] keys) throws DBException, QueryException {
      QueryResolver res = getResolver(style);
      return res.compileQuery(col, query, nsMap, keys);
   }


   // Utility methods

   /**
    * getUniqueKeys takes a set of IndexMatch objects and extracts
    * all of its unique Keys in sorted order.
    *
    * @param matches The Match Set
    * @return The unique Keys in order
    */
   public static Key[] getUniqueKeys(IndexMatch[] matches) {
      SortedSet set = new TreeSet();
      for ( int i = 0; i < matches.length; i++ )
         set.add(matches[i].getKey());
      return (Key[])set.toArray(EmptyKeys);
   }

   /**
    * andKeySets takes several sets of unique Keys and returns the
    * ANDed set (elements that exist in all sets).  The first dimension
    * of the array holds the individual sets, the second holds the
    * actual Keys.
    *
    * @param keySets 2-dimensional set of Keys
    * @return The ANDed set of Keys in order
    */
   public static Key[] andKeySets(Key[][] keySets) {
      int[] ptrs;

      if ( keySets.length == 0 )
         return EmptyKeys;
      else if ( keySets.length == 1 )
         return keySets[0];
      else {
         ptrs = new int[keySets.length];
         for ( int i = 0; i < keySets.length; i++ )
            if ( keySets[i].length == 0 )
               return EmptyKeys;
            else
               ptrs[i] = 0;
      }

      SortedSet set = new TreeSet();
      boolean done = false;
      List highs = new ArrayList(4);
      Key highest = null;
      while ( !done ) {
         boolean eq = true;

         for ( int i = 0; i < ptrs.length; i++ ) {
            Key comp = keySets[i][ptrs[i]];
            if ( highest == null ) {
               highest = comp;
               highs.add(new Integer(i));
            }
            else {
               int c = highest.compareTo(comp);
               if ( c != 0 )
                  eq = false;
               if ( c < 0 ) {
                  highest = comp;
                  highs.clear();
                  highs.add(new Integer(i));
               }
               else if ( c == 0 )
                  highs.add(new Integer(i));
            }
         }
         if ( eq ) {
            set.add(highest);
            highs.clear();
            highest = null;
         }
         for ( int i = 0; i < ptrs.length; i++ ) {
            if ( !highs.contains(new Integer(i)) )
               ptrs[i]++;
            if ( ptrs[i] >= keySets[i].length )
               done = true;
         }
         if ( !eq )
            highs.clear();
      }

      return (Key[])set.toArray(EmptyKeys);
   }

   /**
    * orKeySets takes several sets of unique Keys and returns the
    * ORed set (all unique elements).  The first dimension of the
    * array holds the individual sets, the second holds the actual
    * Keys.
    *
    * @param keySets 2-dimensional set of Keys
    * @return The ORed set of Keys in order
    */
   public static Key[] orKeySets(Key[][] keySets) {
      if ( keySets.length == 0 )
         return EmptyKeys;
      else if ( keySets.length == 1 )
         return keySets[0];
      else if ( keySets.length == 2 ) {
         // Optimization since most ORs will be 2 sets only
         if ( keySets[1].length == 0 )
            return keySets[0];
         else if ( keySets[0].length == 0 )
            return keySets[1];
      }

      SortedSet set = new TreeSet();

      for ( int i = 0; i < keySets.length; i++ )
         for ( int j = 0; j < keySets[i].length; j++ )
            set.add(keySets[i][j]);

      return (Key[])set.toArray(EmptyKeys);
   }

   /**
    * normalizeString normalizes the specific String by stripping
    * all leading, trailing, and continuous runs of white space.
    *
    * @param value The value to normalize
    * @return The result
    */
   public static String normalizeString(String value) {
      char[] c = value.toCharArray();
      char[] n = new char[c.length];
      boolean white = true;
      int pos = 0;
      for ( int i = 0; i < c.length; i++ ) {
         if ( " \t\n\r".indexOf(c[i]) != -1 ) {
            if ( !white ) {
               n[pos++] = ' ';
               white = true;
            }
         }
         else {
            n[pos++] = c[i];
            white = false;
         }
      }
      if ( white && pos > 0 )
         pos--;
      return new String(n, 0, pos);
   }

   /**
    * expandEntities expands the String's pre-defined XML entities
    * (&lt;, &gt;, etc...) into their actual character representations.
    *
    * @param value The value to expand entities for
    * @return The expanded String
    */
   public static String expandEntities(String value) {
      int idx = value.indexOf('&');
      if ( idx == -1 )
         return value;

      StringBuffer sb = new StringBuffer(value.length());
      int pos = 0;
      while ( pos < value.length() ) {
         if ( idx != -1 ) {
            if ( idx > pos )
               sb.append(value.substring(pos, idx));

            int end = value.indexOf(';', idx) + 1;
            if ( end == 0 )
               // Some sort of error
               return value;

            String token = value.substring(idx + 1, end - 1);
            if ( token.equals("apos") )
               sb.append("'");
            else if ( token.equals("quot") )
               sb.append("\"");
            else if ( token.equals("amp") )
               sb.append("&");
            else if ( token.equals("lt") )
               sb.append("<");
            else if ( token.equals("gt") )
               sb.append(">");
            else
               // Some sort of error
               return value;

            pos = end;
            idx = value.indexOf('&', pos);
         }
         else {
            sb.append(value.substring(pos));
            break;
         }
      }
      return sb.toString();
   }

}

