package org.dbxml.core.indexer;

/*
 * 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: IndexManager.java,v 1.50 2001/08/06 18:41:17 tom Exp $
 */

import org.dbxml.core.*;
import org.dbxml.core.Collection;
import org.dbxml.core.data.*;
import org.dbxml.core.query.*;
import org.dbxml.xml.*;
import org.dbxml.xml.sax.*;
import org.dbxml.xml.dom.*;
import org.dbxml.server.*;
import org.dbxml.server.util.*;

import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

import java.io.*;
import java.util.*;

import org.w3c.dom.*;

/**
 * IndexManager is a class that manages Indexes.  Good description, eh?
 * I should win a Pulitzer Prize for that one.
 */

public final class IndexManager extends SimpleConfigurable {
   private static final String[] EmptyStrings = new String[0];
   private static final IndexerInfo[] EmptyIndexerInfo = new IndexerInfo[0];

   private static final String INDEX = "index";
   private static final String NAME = "name";
   private static final String CLASS = "class";

   private static final int STATUS_READY = 0;
   private static final int STATUS_BUSY = 1;
   private static final int STATUS_DELETED = 2;

   private static final int ACTION_CREATE = 0;
   private static final int ACTION_UPDATE = 1;
   private static final int ACTION_DELETE = 2;

   private static final AutoIndexer autoIndexer = new AutoIndexer();

   private Map patternMap = new HashMap();   // IndexPattern to IndexerInfo
   private Map indexes = new HashMap();      // String to IndexerInfo
   private Map bestIndexers = new HashMap(); // String to SoftRefTab
                                             // of IndexPattern to Indexer

   private IndexerInfo[] idxList = EmptyIndexerInfo;

   private Collection collection;
   private SymbolTable symbols;
   private List newIndexers = new ArrayList(); // of IndexerInfo

   public IndexManager(Collection collection) {
      this.collection = collection;
      try {
         symbols = collection.getSymbols();
      }
      catch ( Exception e ) {
         org.dbxml.Debug.printStackTrace(e);
      }
   }

   public void setConfig(Configuration config) throws dbXMLException {
      super.setConfig(config);

      try {
         config.processChildren(INDEX,
            new ConfigurationCallback() {
               public void process(Configuration cfg) {
                  String className = cfg.getAttribute(CLASS);
                  try {
                     register(Class.forName(className), cfg);
                  }
                  catch ( Exception e ) {
                     org.dbxml.Debug.printStackTrace(e);
                  }
               }
         });
      }
      catch ( Exception e ) {
         //org.dbxml.Debug.println(e.getMessage());
      }
   }

   /**
    * list returns a list of the Indexers that this IndexerManager has
    * registered.
    *
    * @return An array containing the Indexer names
    */
   public String[] list() {
      return (String[])indexes.keySet().toArray(EmptyStrings);
   }

   /**
    * drop physically removes the specified Indexer and any
    * associated system resources that the Indexer uses.
    *
    * @param name The Indexer to drop
    * @return Whether or not the Indexer was dropped
    */
   public boolean drop(final String name) {
      Indexer idx = get(name);
      unregister(name);
      config.processChildren(INDEX,
         new ConfigurationCallback() {
            public void process(Configuration cfg) {
               try {
                  if ( cfg.getAttribute(NAME).equals(name) )
                     cfg.delete();
               }
               catch ( Exception e ) {
                  org.dbxml.Debug.printStackTrace(e);
               }
            }
      });

      boolean res = false;
      try {
         res = idx.drop();
      }
      catch ( Exception e ) {
         org.dbxml.Debug.printStackTrace(e);
      }
      return res;
   }

   /**
    * create creates a new Indexer object and any associated
    * system resources that the Indexer will need.
    *
    * @param cfg The Indexer's configuration
    * @return The Indexer that was created
    */
   public Indexer create(Configuration cfg) throws DBException {
      String className = cfg.getAttribute(CLASS);
      try {
         // Check for duplicates
         String name = cfg.getAttribute(NAME);
         Configuration[] cfgs = config.getChildren();
         for ( int i = 0; i < cfgs.length; i++ )
            if ( cfgs[i].getAttribute(NAME).equals(name) )
               throw new DuplicateIndexException("Duplicate Index '"+name+"'");

         Class c = Class.forName(className);
         Indexer idx = register(c, cfg);

         config.add(cfg);

         return idx;
      }
      catch ( DBException d ) {
         throw d;
      }
      catch ( Exception e ) {
         throw new CannotCreateException("Cannot create Index");
      }
   }

   public Indexer register(Class c, Configuration cfg) throws DBException {
      String name = null;
      try {
         Indexer idx = (Indexer)c.newInstance();
         initialize(idx, cfg);
         name = idx.getName();
         
         if ( name == null || name.trim().equals("") )
            throw new CannotCreateException("No name specified");

         IndexPattern pattern = new IndexPattern(symbols, idx.getPattern(), null);
         String style = idx.getIndexStyle();
         IndexerInfo info = new IndexerInfo(name, style, pattern, idx);

         if ( !idx.exists() ) {
            info.status = STATUS_BUSY;
            newIndexers.add(info);
            autoIndexer.populateIndexers(this);
         }
         else {
            info.status = STATUS_READY;
            idx.open();
         }

         indexes.put(name, info);
         patternMap.put(pattern, info);

         Map tbl = (Map)bestIndexers.get(style);
         if ( tbl == null ) {
            tbl = new WeakHashMap();
            bestIndexers.put(style, tbl);
         }
         tbl.clear();
         idxList = (IndexerInfo[])indexes.values().toArray(EmptyIndexerInfo);
         
         return idx;
      }
      catch ( DBException d ) {
         throw d;
      }
      catch ( Exception e ) {
         throw new CannotCreateException("Cannot create Index '"+name+"'");
      }
   }

   public void unregister(String name) {
      IndexerInfo idx = (IndexerInfo)indexes.remove(name);
      String style = idx.style;
      patternMap.remove(idx.pattern);

      Map tbl = (Map)bestIndexers.get(style);
      if ( tbl == null ) {
         tbl = new WeakHashMap();
         bestIndexers.put(style, tbl);
      }
      tbl.clear();

      idxList = (IndexerInfo[])indexes.values().toArray(EmptyIndexerInfo);
   }

   private void initialize(Indexer idx, Configuration cfg) throws dbXMLException {
      idx.setCollection(collection);
      idx.setConfig(cfg);
   }

   private void populateNewIndexers() throws DBException {
      if ( newIndexers.size() > 0 ) {
         IndexerInfo[] list = (IndexerInfo[])newIndexers.toArray(EmptyIndexerInfo);
         newIndexers.clear();

         if ( true ) {
            for ( int i = 0; i < list.length; i++ )
               org.dbxml.Debug.println("Index Creation: "+list[i].indexer.getName());
         }
         
org.dbxml.Stopwatch sw = new org.dbxml.Stopwatch("Populated Indexes", true);
         for ( int i = 0; i < list.length; i++ ) {
            try {
               if ( !list[i].indexer.exists() )
                  list[i].indexer.create();
            }
            catch ( Exception e ) {
               org.dbxml.Debug.printStackTrace(e);
            }
         }
         
         RecordSet rs = collection.getFiler().getRecordSet();
         while ( rs.hasMoreRecords() ) {
            Record rec = rs.getNextRecord();

            if ( rec == null )
               continue;
            
            Key key = rec.getKey();
            Document doc = new DocumentImpl(rec.getValue().getData(), symbols, new NodeSource(collection, key));
            try {
               new SAXHandler(key, doc, ACTION_CREATE, list);
            }
            catch ( Exception e ) {
               org.dbxml.Debug.printStackTrace(e);
            }
         }
sw.stop();
         
         if ( true ) {
            for ( int i = 0; i < list.length; i++ )
               org.dbxml.Debug.println("Index Complete: "+list[i].indexer.getName());
org.dbxml.Debug.println(sw.toString());
         }

         for ( int i = 0; i < list.length; i++ )
            list[i].status = STATUS_READY;
      }
   }

   /**
    * get retrieves an Indexer by name.
    *
    * @name The Indexer name
    * @return The Indexer
    */
   public Indexer get(String name) {
      IndexerInfo info = (IndexerInfo)indexes.get(name);
      return info != null ? info.indexer
                          : null;
   }

   /**
    * getBestIndexer retrieves the best Indexer to use for the specified
    * IndexPattern.
    *
    * @param style The Indexer Style (ex: Node, Value)
    * @param pattern The IndexPattern to use
    * @return The best Indexer (or null)
    */
   public Indexer getBestIndexer(String style, IndexPattern pattern) {
      Map tbl = (Map)bestIndexers.get(style);
      if ( tbl == null ) {
         tbl = new WeakHashMap();
         bestIndexers.put(style, tbl);
      }
      Indexer idx = (Indexer)tbl.get(pattern);
      if ( idx == null ) {
         int highScore = 0;
         Iterator enum = indexes.values().iterator();
         while ( enum.hasNext() ) {
            IndexerInfo info = (IndexerInfo)enum.next();
            if ( info.status != STATUS_READY
              || !info.indexer.getIndexStyle().equals(style) )
               continue;
            int score = pattern.getMatchLevel(info.pattern);
            if ( score > highScore ) {
               idx = info.indexer;
               highScore = score;
            }
         }
         tbl.put(pattern, idx);
      }
      return idx;
   }
   
   public void addDocument(Key key, Document doc) {
      if ( idxList.length > 0 )
         new SAXHandler(key, doc, ACTION_CREATE);
   }

   public void removeDocument(Key key, Document doc) {
      if ( idxList.length > 0 )
         new SAXHandler(key, doc, ACTION_DELETE);
   }


   /**
    * IndexerInfo
    */

   private class IndexerInfo {
      public String name;
      public String style;
      public IndexPattern pattern;
      public Indexer indexer;
      public int status;

      public IndexerInfo(String name, String style, IndexPattern pattern, Indexer indexer) {
         this.name = name;
         this.style = style;
         this.pattern = pattern;
         this.indexer = indexer;
      }
   }


   /**
    * SAXHandler actually performs the work of adding and removing Indexer
    * entries.
    */

   private class SAXHandler implements ContentHandler, CompressionHandler {
      private ObjectStack stack = new ObjectStack();
      private IndexerInfo[] list;

      public Key key;
      public Document doc;
      public int action;

      public StackInfo info; // Current State

      public SAXHandler(Key key, Document doc, int action, IndexerInfo[] list) {
         this.list = list;
         this.key = key;
         this.doc = doc;
         this.action = action;

         try {
            SAXEventGenerator events = new SAXEventGenerator(doc);
            events.setContentHandler(this);
            events.setProperty(HANDLER, this);
            events.start();
            
            if ( action == ACTION_CREATE || action == ACTION_UPDATE ) {
               try {
                  collection.flushSymbolTable();
               }
               catch ( Exception e ) {
               }
            }
         }
         catch ( Exception e ) {
            org.dbxml.Debug.printStackTrace(e);
         }
      }
      
      public SAXHandler(Key key, Document doc, int action) {
         this(key, doc, action, idxList);
      }

      // These are all NO-OPs
      public void setDocumentLocator(Locator locator) {}
      public void startDocument() {}
      public void endDocument() {}
      public void startPrefixMapping(String prefix, String uri) {}
      public void endPrefixMapping(String prefix) {}
      public void ignorableWhitespace(char ch[], int start, int length) {}
      public void processingInstruction(String target, String data) {}
      public void skippedEntity(String name) {}
      public void symbols(SymbolTable symbols) {}
      public void dataBytes(byte[] data) {}

      public void processEntry(IndexPattern pattern, String value, int pos, int len) {
         for ( int i = 0; i < list.length; i++ ) {
            if ( pattern.getMatchLevel(list[i].pattern) > 0 ) {
               try {
                  switch ( action ) {
                     case ACTION_CREATE:
                     case ACTION_UPDATE:
                        list[i].indexer.add(value, key, pos, len, pattern.getElementID(), pattern.getAttributeID());
                        break;
                     case ACTION_DELETE:
                        list[i].indexer.remove(value, key, pos, len, pattern.getElementID(), pattern.getAttributeID());
                        break;
                  }
               }
               catch ( Exception e ) {
                  org.dbxml.Debug.printStackTrace(e);
               }
            }
         }
      }

      public void startElement(String namespaceURI, String localName, String qName, Attributes atts) {
         // Modify the stack info to normalize the symbolID
         if ( namespaceURI != null && namespaceURI.length() > 0 ) {
            String elemNSID = SymbolTable.getNormalizedLocalName(localName, namespaceURI);
            info.symbolID = symbols.getSymbol(elemNSID, namespaceURI, true);
         }
         
         int size = atts.getLength();
         for ( int i = 0; i < size; i++ ) {
            String nsURI = atts.getURI(i);
            if ( nsURI != null && nsURI.length() > 0 ) {
               String attrNSID = "ns"+Integer.toString(nsURI.hashCode())+":"+atts.getLocalName(i);
               short id = symbols.getSymbol(attrNSID, nsURI, true);
               processEntry(new IndexPattern(symbols, info.symbolID, id), atts.getValue(i), info.pos, info.len);
            }
            else {
               short id = symbols.getSymbol(atts.getQName(i));
               processEntry(new IndexPattern(symbols, info.symbolID, id), atts.getValue(i), info.pos, info.len);
            }
         }
      }

      public void endElement(String namespaceURI, String localName, String qName) {
         if ( info.sb != null )
            processEntry(new IndexPattern(symbols, info.symbolID), info.sb.toString(), info.pos, info.len);
         info = (StackInfo)stack.pop();
      }

      public void characters(char ch[], int start, int length) {
         String val = new String(ch).trim();
         if ( info.sb == null )
            info.sb = new StringBuffer(ch.length);
         else if ( info.sb.length() > 0 )
            info.sb.append(' ');
         info.sb.append(val);
      }

      public void symbolID(short symbolID) {
         if ( info != null )
            stack.push(info);
         info = new StackInfo(symbolID);
      }

      public void dataLocation(int pos, int len) {
         info.pos = pos;
         info.len = len;
      }
   }


   /**
    * StackInfo
    */

   private class StackInfo {
      public short symbolID;
      public StringBuffer sb = null;
      public int pos = -1;
      public int len = -1;

      public StackInfo(short symbolID) {
         this.symbolID = symbolID;
      }
   }


   /**
    * AutoIndexer
    */
   private static class AutoIndexer extends Thread {
      public Set mgrSet = new HashSet();

      public AutoIndexer() {
         setName("Automatic Indexer");
         start();
      }

      public void run() {
         while ( true ) {
            try {
               sleep(5000);
            }
            catch ( Exception e ) {
            }

            if ( !mgrSet.isEmpty() ) {
               // Use a copy of the IndexManager Set
               Set set;
               synchronized(this) {
                  set = new HashSet(mgrSet);
               }

               Iterator iter = set.iterator();
               while ( iter.hasNext() ) {
                  IndexManager mgr = (IndexManager)iter.next();

                  // Remove from the Original IndexManager Set
                  synchronized(this) {
                     mgrSet.remove(mgr);
                  }

                  try {
                     mgr.populateNewIndexers();
                  }
                  catch ( Exception e ) {
                     org.dbxml.Debug.printStackTrace(e);
                  }
               }
            }
         }
      }

      public synchronized void populateIndexers(IndexManager mgr) {
         mgrSet.add(mgr);
      }
   }
}


