package org.dbxml.core.query;

/*
 * dbXML License, Version 1.0
 *
 *
 * Copyright (c) 1999-2001 The dbXML Group, L.L.C.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        The dbXML Group (http://www.dbxml.com/)."
 *    Alternately, this acknowledgment may appear in the software
 *    itself, if and wherever such third-party acknowledgments normally
 *    appear.
 *
 * 4. The names "dbXML" and "The dbXML Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    info@dbxml.com.
 *
 * 5. Products derived from this software may not be called "dbXML",
 *    nor may "dbXML" appear in their name, without prior written
 *    permission of The dbXML Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE DBXML GROUP OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: QueryEngine.java,v 1.28 2001/08/31 01:31:52 tom Exp $
 */

import org.dbxml.server.*;
import org.dbxml.core.*;
import org.dbxml.core.Collection;
import org.dbxml.core.data.*;
import org.dbxml.core.indexer.*;
import org.dbxml.xml.*;

import org.w3c.dom.*;
import java.util.*;

/**
 * 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 db;
   private Map resolvers = new HashMap();

   public QueryEngine(Database db) {
      this.db = 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)Class.forName(className).newInstance();
                  res.setConfig(cfg);
                  res.setQueryEngine(QueryEngine.this);
                  resolvers.put(res.getQueryStyle(), res);
               }
               catch ( Exception e ) {
                  org.dbxml.Debug.printStackTrace(e);
               }
            }
      });
   }

   public Database getDatabase() {
      return db;
   }

   /**
    * 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 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 NodeSet with the query results
    */
   public NodeSet query(Collection col, String style, String query, NamespaceMap nsMap, Key[] keys) throws DBException, QueryException {
      QueryResolver res = getResolver(style);
      return res.query(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) {
      if ( keySets.length == 0 )
         return EmptyKeys;
      else if ( keySets.length == 1 )
         return keySets[0];
      else if ( keySets.length == 2 ) {
         if ( keySets[1].length == 0 )
            return keySets[0];
         else if ( keySets[0].length == 0 )
            return keySets[1];
      }

      SortedSet set = new TreeSet();

      int[] ptrs = new int[keySets.length];
      for ( int i = 0; i < ptrs.length; i++ )
         ptrs[i] = 0;

      boolean done = false;
      List highs = new ArrayList();
      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 ) {
         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();
   }

}

