/* $Id: BinManager.java 1718072 2015-12-05 12:36:29Z kwright $ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.crawler.bins;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.CacheKeyFactory;
import org.apache.manifoldcf.crawler.system.ManifoldCF;
import org.apache.manifoldcf.crawler.system.Logging;
import java.util.*;

/** This class manages the docbins table.
* A row in this table represents a document bin.  The count that is kept is the
* number of documents in this particular bin that have been assigned a document priority.
* 
* <br><br>
* <b>docbins</b>
* <table border="1" cellpadding="3" cellspacing="0">
* <tr class="TableHeadingColor">
* <th>Field</th><th>Type</th><th>Description&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
* <tr><td>binname</td><td>VARCHAR(255)</td><td>Primary Key</td></tr>
* <tr><td>bincounter</td><td>BIGINT</td><td></td></tr>
* </table>
* <br><br>
* 
*/
public class BinManager extends org.apache.manifoldcf.core.database.BaseTable implements IBinManager
{
  public static final String _rcsid = "@(#)$Id: BinManager.java 1718072 2015-12-05 12:36:29Z kwright $";

  // Field names
  public final static String connectorClassField = "connectorclass";
  public final static String binNameField = "binname";
  public final static String binCounterField = "bincounter";
  
  /** Constructor.
  *@param database is the database handle.
  */
  public BinManager(IDBInterface database)
    throws ManifoldCFException
  {
    super(database,"docbins");
  }

  /** Install or upgrade this table.
  */
  @Override
  public void install()
    throws ManifoldCFException
  {
    // Standard practice: outer loop for installs
    while (true)
    {
      Map existing = getTableSchema(null,null);
      if (existing == null)
      {
        HashMap map = new HashMap();
        // HSQLDB does not like null primary keys!!
        map.put(connectorClassField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(binNameField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(binCounterField,new ColumnDescription("FLOAT",false,false,null,null,false));
        performCreate(map,null);
      }
      else
      {
        // Upgrade
        if (existing.get(connectorClassField) == null) {
          HashMap map = new HashMap();
          map.put(connectorClassField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
          performAlter(map,null,null,null);
        }
      }

      // Index management goes here
      IndexDescription binIndex = new IndexDescription(true,new String[]{connectorClassField,binNameField});

      // Get rid of indexes that shouldn't be there
      Map indexes = getTableIndexes(null,null);
      Iterator iter = indexes.keySet().iterator();
      while (iter.hasNext())
      {
        String indexName = (String)iter.next();
        IndexDescription id = (IndexDescription)indexes.get(indexName);

        if (binIndex != null && id.equals(binIndex))
          binIndex = null;
        else if (indexName.indexOf("_pkey") == -1)
          // This index shouldn't be here; drop it
          performRemoveIndex(indexName);
      }

      // Add the ones we didn't find
      if (binIndex != null)
        performAddIndex(null,binIndex);

      break;
    }
  }

  /** Uninstall.
  */
  @Override
  public void deinstall()
    throws ManifoldCFException
  {
    performDrop(null);
  }

  /** Reset all bins */
  @Override
  public void reset()
    throws ManifoldCFException
  {
    performDelete("", null, null);
  }

  /** Get N bin values (and set next one).  If the record does not yet exist, create it with a starting value.
  * We expect this to happen within a transaction!!.
  *@param connectorClass is the class name of the connector
  *@param binName is the name of the bin (256 char max)
  *@param newBinValue is the value to use if there is no such bin yet.  This is the value that will be
  * returned; what will be stored will be that value + 1.
  *@param count is the number of values desired.
  *@return the counter values.
  */
  @Override
  public double[] getIncrementBinValues(String connectorClass, String binName, double newBinValue, int count)
    throws ManifoldCFException
  {
    double[] returnValues = new double[count];
    // SELECT FOR UPDATE/MODIFY is the most common path
    ArrayList params = new ArrayList();
    String query = buildConjunctionClause(params,new ClauseDescription[]{
      new UnitaryClause(connectorClassField,connectorClass),
      new UnitaryClause(binNameField,binName)});
    IResultSet result = performQuery("SELECT "+binCounterField+" FROM "+getTableName()+" WHERE "+query+" FOR UPDATE",params,null,null);
    if (result.getRowCount() > 0)
    {
      IResultRow row = result.getRow(0);
      Double value = (Double)row.getValue(binCounterField);
      double rval = value.doubleValue();
      if (rval < newBinValue)
        rval = newBinValue;
      // rval is the starting value; compute the entire array based on it.
      for (int i = 0; i < count; i++)
      {
        returnValues[i] = rval;
        rval += 1.0;
      }
      HashMap map = new HashMap();
      map.put(binCounterField,new Double(rval));
      performUpdate(map," WHERE "+query,params,null);
    }
    else
    {
      for (int i = 0; i < count; i++)
      {
        returnValues[i] = newBinValue;
        newBinValue += 1.0;
      }
      HashMap map = new HashMap();
      map.put(connectorClassField,connectorClass);
      map.put(binNameField,binName);
      map.put(binCounterField,new Double(newBinValue));
      performInsert(map,null);
    }
    return returnValues;
  }

  /** Get N bin values (and set next one).  If the record does not yet exist, create it with a starting value.
  * This method invokes its own retry-able transaction.
  *@param connectorClass is the class name of the connector
  *@param binName is the name of the bin (256 char max)
  *@param newBinValue is the value to use if there is no such bin yet.  This is the value that will be
  * returned; what will be stored will be that value + 1.
  *@param count is the number of values desired.
  *@return the counter values.
  */
  @Override
  public double[] getIncrementBinValuesInTransaction(String connectorClass, String binName, double newBinValue, int count)
    throws ManifoldCFException
  {
    while (true)
    {
      long sleepAmt = 0L;
      beginTransaction();
      try
      {
        return getIncrementBinValues(connectorClass, binName, newBinValue, count);
      }
      catch (Error e)
      {
        signalRollback();
        throw e;
      }
      catch (RuntimeException e)
      {
        signalRollback();
        throw e;
      }
      catch (ManifoldCFException e)
      {
        signalRollback();
        if (e.getErrorCode() == e.DATABASE_TRANSACTION_ABORT)
        {
          if (Logging.perf.isDebugEnabled())
            Logging.perf.debug("Aborted transaction obtaining docpriorities: "+e.getMessage());
          sleepAmt = getSleepAmt();
          continue;
        }
        throw e;
      }
      finally
      {
        endTransaction();
        sleepFor(sleepAmt);
      }
    }
  }

}
