/* $Id: RepositoryConnectionManager.java 996524 2010-09-13 13:38:01Z 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.repository;

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


/** This class is the manager of the repository connection description.  Inside, multiple database tables are managed,
* with appropriate caching.
* Note well: The database handle is instantiated here using the DBInterfaceFactory.  This is acceptable because the
* actual database that this table is located in is fixed.
* 
* <br><br>
* <b>repoconnections</b>
* <table border="1" cellpadding="3" cellspacing="0" summary="">
* <tr class="TableHeadingColor">
* <th>Field</th><th>Type</th><th>Description&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
* <tr><td>connectionname</td><td>VARCHAR(32)</td><td>Primary Key</td></tr>
* <tr><td>description</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>classname</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>groupname</td><td>VARCHAR(32)</td><td></td></tr>
* <tr><td>maxcount</td><td>BIGINT</td><td></td></tr>
* <tr><td>configxml</td><td>LONGTEXT</td><td></td></tr>
* </table>
* <br><br>
*
*/
public class RepositoryConnectionManager extends org.apache.manifoldcf.core.database.BaseTable implements IRepositoryConnectionManager
{
  public static final String _rcsid = "@(#)$Id: RepositoryConnectionManager.java 996524 2010-09-13 13:38:01Z kwright $";

  // Special field suffix
  private final static String passwordSuffix = "password";

  // Database fields
  protected final static String nameField = "connectionname";     // Changed this to work around constraint bug in postgresql
  protected final static String descriptionField = "description";
  protected final static String classNameField = "classname";
  protected final static String maxCountField = "maxcount";
  protected final static String configField = "configxml";
  protected final static String groupNameField = "groupname";

  // Discontinued fields
  protected final static String authorityNameField = "authorityname";

  protected static Random random = new Random();

  // Handle for repository history manager
  protected final RepositoryHistoryManager historyManager;
  // Handle for throttle spec storage
  protected final ThrottleSpecManager throttleSpecManager;

  // Cache manager
  protected final ICacheManager cacheManager;
  // Thread context
  protected final IThreadContext threadContext;
  // Lock manager
  protected final ILockManager lockManager;
  
  protected final String repositoriesLock = "REPOSITORIES_LOCK";
  
  /** Constructor.
  *@param threadContext is the thread context.
  */
  public RepositoryConnectionManager(IThreadContext threadContext, IDBInterface database)
    throws ManifoldCFException
  {
    super(database,"repoconnections");

    historyManager = new RepositoryHistoryManager(threadContext,database);
    throttleSpecManager = new ThrottleSpecManager(database);
    cacheManager = CacheManagerFactory.make(threadContext);
    lockManager = LockManagerFactory.make(threadContext);
    this.threadContext = threadContext;
  }

  /** Install the manager.
  */
  public void install()
    throws ManifoldCFException
  {
    // First, get the authority manager table name and name column
    IAuthorityGroupManager authMgr = AuthorityGroupManagerFactory.make(threadContext);

    // Always use a loop, and no transaction, as we may need to retry due to upgrade
    while (true)
    {
      Map existing = getTableSchema(null,null);
      if (existing == null)
      {
        // Install the "objects" table.
        HashMap map = new HashMap();
        map.put(nameField,new ColumnDescription("VARCHAR(32)",true,false,null,null,false));
        map.put(descriptionField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(classNameField,new ColumnDescription("VARCHAR(255)",false,false,null,null,false));
        map.put(groupNameField,new ColumnDescription("VARCHAR(32)",false,true,
          authMgr.getTableName(),authMgr.getGroupNameColumn(),false));
        map.put(maxCountField,new ColumnDescription("BIGINT",false,false,null,null,false));
        map.put(configField,new ColumnDescription("LONGTEXT",false,true,null,null,false));
        performCreate(map,null);
      }
      else
      {
        // Upgrade code
      }

      // Install dependent tables.
      historyManager.install(getTableName(),nameField);
      throttleSpecManager.install(getTableName(),nameField);

      // Index management
      IndexDescription authorityIndex = new IndexDescription(false,new String[]{groupNameField});
      IndexDescription classIndex = new IndexDescription(false,new String[]{classNameField});
      
      // 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 (authorityIndex != null && id.equals(authorityIndex))
          authorityIndex = null;
        else if (classIndex != null && id.equals(classIndex))
          classIndex = 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 (authorityIndex != null)
        performAddIndex(null,authorityIndex);

      if (classIndex != null)
        performAddIndex(null,classIndex);
      
      break;
    }
  }

  /** Uninstall the manager.
  */
  public void deinstall()
    throws ManifoldCFException
  {
    beginTransaction();
    try
    {
      throttleSpecManager.deinstall();
      historyManager.deinstall();
      performDrop(null);
    }
    catch (ManifoldCFException e)
    {
      signalRollback();
      throw e;
    }
    catch (Error e)
    {
      signalRollback();
      throw e;
    }
    finally
    {
      endTransaction();
    }
  }

  /** Export configuration */
  public void exportConfiguration(java.io.OutputStream os)
    throws java.io.IOException, ManifoldCFException
  {
    // Write a version indicator
    ManifoldCF.writeDword(os,1);
    // Get the authority list
    IRepositoryConnection[] list = getAllConnections();
    // Write the number of authorities
    ManifoldCF.writeDword(os,list.length);
    // Loop through the list and write the individual repository connection info
    int i = 0;
    while (i < list.length)
    {
      IRepositoryConnection conn = list[i++];
      ManifoldCF.writeString(os,conn.getName());
      ManifoldCF.writeString(os,conn.getDescription());
      ManifoldCF.writeString(os,conn.getClassName());
      ManifoldCF.writeString(os,conn.getConfigParams().toXML());
      ManifoldCF.writeString(os,conn.getACLAuthority());
      ManifoldCF.writeDword(os,conn.getMaxConnections());
      String[] throttles = conn.getThrottles();
      ManifoldCF.writeDword(os,throttles.length);
      int j = 0;
      while (j < throttles.length)
      {
        String throttleName = throttles[j++];
        ManifoldCF.writeString(os,throttleName);
        ManifoldCF.writeString(os,conn.getThrottleDescription(throttleName));
        ManifoldCF.writefloat(os,conn.getThrottleValue(throttleName));
      }
    }
  }

  /** Import configuration */
  public void importConfiguration(java.io.InputStream is)
    throws java.io.IOException, ManifoldCFException
  {
    int version = ManifoldCF.readDword(is);
    if (version != 1)
      throw new java.io.IOException("Unknown repository connection configuration version: "+Integer.toString(version));
    int count = ManifoldCF.readDword(is);
    int i = 0;
    while (i < count)
    {
      IRepositoryConnection conn = create();
      conn.setName(ManifoldCF.readString(is));
      conn.setDescription(ManifoldCF.readString(is));
      conn.setClassName(ManifoldCF.readString(is));
      conn.getConfigParams().fromXML(ManifoldCF.readString(is));
      conn.setACLAuthority(ManifoldCF.readString(is));
      conn.setMaxConnections(ManifoldCF.readDword(is));
      int throttleCount = ManifoldCF.readDword(is);
      int j = 0;
      while (j < throttleCount)
      {
        String throttleName = ManifoldCF.readString(is);
        conn.addThrottleValue(throttleName,ManifoldCF.readString(is),ManifoldCF.readfloat(is));
        j++;
      }
      // Attempt to save this connection
      save(conn);
      i++;
    }
  }

  /** Obtain a list of the repository connections, ordered by name.
  *@return an array of connection objects.
  */
  public IRepositoryConnection[] getAllConnections()
    throws ManifoldCFException
  {
    lockManager.enterReadLock(repositoriesLock);
    try
    {
      // Read all the tools
      StringSetBuffer ssb = new StringSetBuffer();
      ssb.add(getRepositoryConnectionsKey());
      StringSet localCacheKeys = new StringSet(ssb);
      IResultSet set = performQuery("SELECT "+nameField+",lower("+nameField+") AS sortfield FROM "+getTableName()+" ORDER BY sortfield ASC",null,
        localCacheKeys,null);
      String[] names = new String[set.getRowCount()];
      for (int i = 0; i < names.length; i++)
      {
        IResultRow row = set.getRow(i);
        names[i] = row.getValue(nameField).toString();
      }
      return loadMultiple(names);
    }
    finally
    {
      lockManager.leaveReadLock(repositoriesLock);
    }
  }

  /** Load a repository connection by name.
  *@param name is the name of the repository connection.
  *@return the loaded connection object, or null if not found.
  */
  public IRepositoryConnection load(String name)
    throws ManifoldCFException
  {
    return loadMultiple(new String[]{name})[0];
  }

  protected final static int FETCH_MAX = 200;
  
  /** Load multiple repository connections by name.
  *@param names are the names to load.
  *@return the loaded connection objects.
  */
  public IRepositoryConnection[] loadMultiple(String[] names)
    throws ManifoldCFException
  {
    IRepositoryConnection[] rval = new IRepositoryConnection[names.length];
    if (names.length == 0)
      return rval;
    int inputIndex = 0;
    int outputIndex = 0;
    while (names.length - inputIndex > FETCH_MAX)
    {
      outputIndex = loadMultipleInternal(rval,outputIndex,names,inputIndex,FETCH_MAX);
      inputIndex += FETCH_MAX;
    }
    loadMultipleInternal(rval,outputIndex,names,inputIndex,names.length-inputIndex);
    return rval;
  }
  
  protected int loadMultipleInternal(IRepositoryConnection[] rval, int outputIndex, String[] fetchNames, int inputIndex, int length)
    throws ManifoldCFException
  {
    // Build description objects
    RepositoryConnectionDescription[] objectDescriptions = new RepositoryConnectionDescription[length];
    StringSetBuffer ssb = new StringSetBuffer();
    for (int i = 0; i < length; i++)
    {
      ssb.clear();
      String name = fetchNames[inputIndex + i];
      ssb.add(getRepositoryConnectionKey(name));
      objectDescriptions[i] = new RepositoryConnectionDescription(name,new StringSet(ssb));
    }

    RepositoryConnectionExecutor exec = new RepositoryConnectionExecutor(this,objectDescriptions);
    cacheManager.findObjectsAndExecute(objectDescriptions,null,exec,getTransactionID());
    IRepositoryConnection[] results = exec.getResults();
    for (IRepositoryConnection result : results)
    {
      rval[outputIndex++] = result;
    }
    return outputIndex;
  }

  /** Create a new repository connection object.
  *@return the new object.
  */
  public IRepositoryConnection create()
    throws ManifoldCFException
  {
    RepositoryConnection rval = new RepositoryConnection();
    return rval;
  }

  /** Save a repository connection object.
  *@param object is the object to save.
  *@return true if the object was created, false otherwise.
  */
  public boolean save(IRepositoryConnection object)
    throws ManifoldCFException
  {
    StringSetBuffer ssb = new StringSetBuffer();
    ssb.add(getRepositoryConnectionsKey());
    ssb.add(getRepositoryConnectionKey(object.getName()));
    StringSet cacheKeys = new StringSet(ssb);
    while (true)
    {
      // Catch deadlock condition
      long sleepAmt = 0L;
      try
      {
        lockManager.enterNonExWriteLock(repositoriesLock);
        try
        {
          ICacheHandle ch = cacheManager.enterCache(null,cacheKeys,getTransactionID());
          try
          {
            beginTransaction();
            try
            {
              //performLock();
              // Notify of a change to the configuration
              ManifoldCF.noteConfigurationChange();
              boolean isNew = object.getIsNew();
              // See whether the instance exists
              ArrayList params = new ArrayList();
              String query = buildConjunctionClause(params,new ClauseDescription[]{
                new UnitaryClause(nameField,object.getName())});
              IResultSet set = performQuery("SELECT * FROM "+getTableName()+" WHERE "+
                query+" FOR UPDATE",params,null,null);
              HashMap values = new HashMap();
              values.put(descriptionField,object.getDescription());
              values.put(classNameField,object.getClassName());
              values.put(groupNameField,object.getACLAuthority());
              values.put(maxCountField,new Long((long)object.getMaxConnections()));
              String configXML = object.getConfigParams().toXML();
              values.put(configField,configXML);
              boolean notificationNeeded = false;
              boolean isCreated;
              
              if (set.getRowCount() > 0)
              {
                // If the object is supposedly new, it is bad that we found one that already exists.
                if (isNew)
                  throw new ManifoldCFException("Repository connection '"+object.getName()+"' already exists");
                isCreated = false;
                IResultRow row = set.getRow(0);
                String oldXML = (String)row.getValue(configField);
                if (oldXML == null || !oldXML.equals(configXML))
                  notificationNeeded = true;
                
                // Update
                params.clear();
                query = buildConjunctionClause(params,new ClauseDescription[]{
                  new UnitaryClause(nameField,object.getName())});
                performUpdate(values," WHERE "+query,params,null);
                throttleSpecManager.deleteRows(object.getName());
              }
              else
              {
                // If the object is not supposed to be new, it is bad that we did not find one.
                if (!isNew)
                  throw new ManifoldCFException("Repository connection '"+object.getName()+"' no longer exists");
                isCreated = true;
                // Insert
                values.put(nameField,object.getName());
                // We only need the general key because this is new.
                performInsert(values,null);
              }

              // Write secondary table stuff
              throttleSpecManager.writeRows(object.getName(),object);

              // If notification required, do it.
              if (notificationNeeded)
              {
                IJobManager jobManager = JobManagerFactory.make(threadContext);
                jobManager.noteConnectionChange(object.getName());
              }

              cacheManager.invalidateKeys(ch);
              return isCreated;
            }
            catch (ManifoldCFException e)
            {
              signalRollback();
              throw e;
            }
            catch (Error e)
            {
              signalRollback();
              throw e;
            }
            finally
            {
              endTransaction();
            }
          }
          finally
          {
            cacheManager.leaveCache(ch);
          }
        }
        finally
        {
          lockManager.leaveNonExWriteLock(repositoriesLock);
        }
      }
      catch (ManifoldCFException e)
      {
        // Is this a deadlock exception?  If so, we want to try again.
        if (e.getErrorCode() != ManifoldCFException.DATABASE_TRANSACTION_ABORT)
          throw e;
        sleepAmt = getSleepAmt();
      }
      finally
      {
        sleepFor(sleepAmt);
      }
    }
  }

  /** Delete a repository connection.
  *@param name is the name of the connection to delete.  If the
  * name does not exist, no error is returned.
  */
  public void delete(String name)
    throws ManifoldCFException
  {
    // Grab a job manager handle.  We will need to check if any jobs refer to this connection.
    IJobManager jobManager = JobManagerFactory.make(threadContext);
    StringSetBuffer ssb = new StringSetBuffer();
    ssb.add(getRepositoryConnectionsKey());
    ssb.add(getRepositoryConnectionKey(name));
    StringSet cacheKeys = new StringSet(ssb);
    lockManager.enterNonExWriteLock(repositoriesLock);
    try
    {
      ICacheHandle ch = cacheManager.enterCache(null,cacheKeys,getTransactionID());
      try
      {
        beginTransaction();
        try
        {
          // Check if any jobs refer to this connection name
          if (jobManager.checkIfReference(name))
            throw new ManifoldCFException("Can't delete repository connection '"+name+"': existing jobs refer to it");
          ManifoldCF.noteConfigurationChange();
          throttleSpecManager.deleteRows(name);
          historyManager.deleteOwner(name);
          ArrayList params = new ArrayList();
          String query = buildConjunctionClause(params,new ClauseDescription[]{
            new UnitaryClause(nameField,name)});
          performDelete("WHERE "+query,params,null);
          cacheManager.invalidateKeys(ch);
        }
        catch (ManifoldCFException e)
        {
          signalRollback();
          throw e;
        }
        catch (Error e)
        {
          signalRollback();
          throw e;
        }
        finally
        {
          endTransaction();
        }
      }
      finally
      {
        cacheManager.leaveCache(ch);
      }
    }
    finally
    {
      lockManager.leaveNonExWriteLock(repositoriesLock);
    }
  }

  /** Return true if the specified authority name is referenced.
  *@param groupName is the group name.
  *@return true if referenced, false otherwise.
  */
  @Override
  public boolean isGroupReferenced(String groupName)
    throws ManifoldCFException
  {
    StringSetBuffer ssb = new StringSetBuffer();
    ssb.add(getRepositoryConnectionsKey());
    StringSet localCacheKeys = new StringSet(ssb);
    ArrayList params = new ArrayList();
    String query = buildConjunctionClause(params,new ClauseDescription[]{
      new UnitaryClause(groupNameField,groupName)});
    IResultSet set = performQuery("SELECT "+nameField+" FROM "+getTableName()+" WHERE "+query,params,
      localCacheKeys,null);
    return set.getRowCount() > 0;
  }

  /** Get a list of repository connections that share the same connector.
  *@param className is the class name of the connector.
  *@return the repository connections that use that connector.
  */
  @Override
  public String[] findConnectionsForConnector(String className)
    throws ManifoldCFException
  {
    StringSetBuffer ssb = new StringSetBuffer();
    ssb.add(getRepositoryConnectionsKey());
    StringSet localCacheKeys = new StringSet(ssb);
    ArrayList params = new ArrayList();
    String query = buildConjunctionClause(params,new ClauseDescription[]{
      new UnitaryClause(classNameField,className)});
    IResultSet set = performQuery("SELECT "+nameField+" FROM "+getTableName()+" WHERE "+query,params,
      localCacheKeys,null);
    String[] rval = new String[set.getRowCount()];
    int i = 0;
    while (i < rval.length)
    {
      IResultRow row = set.getRow(i);
      rval[i] = (String)row.getValue(nameField);
      i++;
    }
    java.util.Arrays.sort(rval);
    return rval;
  }

  /** Check if underlying connector exists.
  *@param name is the name of the connection to check.
  *@return true if the underlying connector is registered.
  */
  @Override
  public boolean checkConnectorExists(String name)
    throws ManifoldCFException
  {
    beginTransaction();
    try
    {
      StringSetBuffer ssb = new StringSetBuffer();
      ssb.add(getRepositoryConnectionKey(name));
      StringSet localCacheKeys = new StringSet(ssb);
      ArrayList params = new ArrayList();
      String query = buildConjunctionClause(params,new ClauseDescription[]{
        new UnitaryClause(nameField,name)});
      IResultSet set = performQuery("SELECT "+classNameField+" FROM "+getTableName()+" WHERE "+query,params,
        localCacheKeys,null);
      if (set.getRowCount() == 0)
        throw new ManifoldCFException("No such connection: '"+name+"'");
      IResultRow row = set.getRow(0);
      String className = (String)row.getValue(classNameField);
      IConnectorManager cm = ConnectorManagerFactory.make(threadContext);
      return cm.isInstalled(className);
    }
    catch (ManifoldCFException e)
    {
      signalRollback();
      throw e;
    }
    catch (Error e)
    {
      signalRollback();
      throw e;
    }
    finally
    {
      endTransaction();
    }
  }

  // Schema related
  /** Return the name column.
  *@return the name column.
  */
  @Override
  public String getConnectionNameColumn()
  {
    return nameField;
  }


  // Reporting and analysis related

  /** Delete history rows related to a specific connection, upon user request.
  *@param connectionName is the connection whose history records should be removed.
  */
  @Override
  public void cleanUpHistoryData(String connectionName)
    throws ManifoldCFException
  {
    historyManager.deleteOwner(connectionName);
  }
  
  /** Delete history rows older than a specified timestamp.
  *@param timeCutoff is the timestamp to delete older rows before.
  */
  @Override
  public void cleanUpHistoryData(long timeCutoff)
    throws ManifoldCFException
  {
    historyManager.deleteOldRows(timeCutoff);
  }
  
  /** Record time-stamped information about the activity of the connection.  This information can originate from
  * either the connector or from the framework.  The reason it is here is that it is viewed as 'belonging' to an
  * individual connection, and is segregated accordingly.
  *@param connectionName is the connection to which the record belongs.  If the connection is deleted, the
  * corresponding records will also be deleted.  Cannot be null.
  *@param startTime is either null or the time since the start of epoch in milliseconds (Jan 1, 1970).  Every
  *       activity has an associated time; the startTime field records when the activity began.  A null value
  *       indicates that the start time and the finishing time are the same.
  *@param activityType is a string which is fully interpretable only in the context of the connector involved, which is
  *       used to categorize what kind of activity is being recorded.  For example, a web connector might record a
  *       "fetch document" activity, while the framework might record "ingest document", "job start", "job finish",
  *       "job abort", etc.  Cannot be null.
  *@param dataSize is the number of bytes of data involved in the activity, or null if not applicable.
  *@param entityIdentifier is a (possibly long) string which identifies the object involved in the history record.
  *       The interpretation of this field will differ from connector to connector.  May be null.
  *@param resultCode contains a terse description of the result of the activity.  The description is limited in
  *       size to 255 characters, and can be interpreted only in the context of the current connector.  May be null.
  *@param resultDescription is a (possibly long) human-readable string which adds detail, if required, to the result
  *       described in the resultCode field.  This field is not meant to be queried on.  May be null.
  *@param childIdentifiers is a set of child entity identifiers associated with this activity.  May be null.
  */
  public void recordHistory(String connectionName, Long startTime, String activityType, Long dataSize,
    String entityIdentifier, String resultCode, String resultDescription, String[] childIdentifiers)
    throws ManifoldCFException
  {
    long endTimeValue = System.currentTimeMillis();
    long startTimeValue;
    if (startTime == null)
      startTimeValue = endTimeValue - 1L;
    else
    {
      startTimeValue = startTime.longValue();
      if (startTimeValue == endTimeValue)
        startTimeValue -= 1L;   // Zero-time events are not allowed.
    }

    long dataSizeValue;
    if (dataSize == null)
      dataSizeValue = 0L;
    else
      dataSizeValue = dataSize.longValue();

    Long rowID = historyManager.addRow(connectionName,startTimeValue,endTimeValue,dataSizeValue,activityType,
      entityIdentifier,resultCode,resultDescription);
    // child identifiers are not stored, for now.
    // MHL
  }

  /** Count the number of rows specified by a given set of criteria.  This can be used to make decisions
  * as to whether a query based on those rows will complete in an acceptable amount of time.
  *@param connectionName is the name of the connection.
  *@param criteria is the filtering criteria, which selects the records of interest.
  *@return the number of rows included by the criteria.
  */
  public long countHistoryRows(String connectionName, FilterCriteria criteria)
    throws ManifoldCFException
  {
    return historyManager.countHistoryRows(connectionName,criteria);
  }

  /** Get the maximum number of rows a window-based report can work with.
  *@return the maximum rows.
  */
  public long getMaxRows()
    throws ManifoldCFException
  {
    return historyManager.getMaxRows();
  }

  /** Generate a report, listing the start time, elapsed time, result code and description, number of bytes, and entity identifier.
  * The records selected for this report are based on the filtering criteria object passed into this method.
  * The record order is based on the sorting criteria object passed into this method.
  * The resultset returned should have the following columns: "starttime","elapsedtime","resultcode","resultdesc","bytes","identifier".
  *@param connectionName is the name of the connection.
  *@param criteria is the filtering criteria, which selects the records of interest.
  *@param sort is the sorting order, which can specify sort based on the result columns.
  *@param startRow is the first row to include (beginning with 0)
  *@param maxRowCount is the maximum number of rows to include.
  */
  public IResultSet genHistorySimple(String connectionName, FilterCriteria criteria, SortOrder sort, int startRow, int maxRowCount)
    throws ManifoldCFException
  {
    return historyManager.simpleReport(connectionName,criteria,sort,startRow,maxRowCount);
  }

  /** Generate a report, listing the start time, activity count, and identifier bucket, given
  * a time slice (interval) size.
  * The records selected for this report are based on the filtering criteria object passed into this method.
  * The record order is based on the sorting criteria object passed into this method.
  * The identifier bucket description is specified by the bucket description object.
  * The resultset returned should have the following columns: "starttime","endtime","activitycount","idbucket".
  *@param connectionName is the name of the connection.
  *@param criteria is the filtering criteria, which selects the records of interest.
  *@param sort is the sorting order, which can specify sort based on the result columns.
  *@param idBucket is the description of the bucket based on processed entity identifiers.
  *@param interval is the time interval, in milliseconds, to locate.  There will be one row in the resultset
  *       for each distinct idBucket value, and the returned activity count will the maximum found over the
  *       specified interval size.
  *@param startRow is the first row to include (beginning with 0)
  *@param maxRowCount is the maximum number of rows to include.
  */
  public IResultSet genHistoryActivityCount(String connectionName, FilterCriteria criteria, SortOrder sort, BucketDescription idBucket,
    long interval, int startRow, int maxRowCount)
    throws ManifoldCFException
  {
    return historyManager.maxActivityCountReport(connectionName,criteria,sort,idBucket,interval,startRow,maxRowCount);
  }

  /** Generate a report, listing the start time, bytes processed, and identifier bucket, given
  * a time slice (interval) size.
  * The records selected for this report are based on the filtering criteria object passed into this method.
  * The record order is based on the sorting criteria object passed into this method.
  * The identifier bucket description is specified by the bucket description object.
  * The resultset returned should have the following columns: "starttime","endtime","bytecount","idbucket".
  *@param connectionName is the name of the connection.
  *@param criteria is the filtering criteria, which selects the records of interest.
  *@param sort is the sorting order, which can specify sort based on the result columns.
  *@param idBucket is the description of the bucket based on processed entity identifiers.
  *@param interval is the time interval, in milliseconds, to locate.  There will be one row in the resultset
  *       for each distinct idBucket value, and the returned activity count will the maximum found over the
  *       specified interval size.
  *@param startRow is the first row to include (beginning with 0)
  *@param maxRowCount is the maximum number of rows to include.
  */
  public IResultSet genHistoryByteCount(String connectionName, FilterCriteria criteria, SortOrder sort, BucketDescription idBucket,
    long interval, int startRow, int maxRowCount)
    throws ManifoldCFException
  {
    return historyManager.maxByteCountReport(connectionName,criteria,sort,idBucket,interval,startRow,maxRowCount);
  }


  /** Generate a report, listing the result bucket and identifier bucket.
  * The records selected for this report are based on the filtering criteria object passed into this method.
  * The record order is based on the sorting criteria object passed into this method.
  * The result code bucket description is specified by a bucket description object.
  * The identifier bucket description is specified by a bucket description object.
  * The resultset returned should have the following columns: "resultcodebucket","idbucket".
  *@param connectionName is the name of the connection.
  *@param criteria is the filtering criteria, which selects the records of interest.
  *@param sort is the sorting order, which can specify sort based on the result columns.
  *@param resultCodeBucket is the description of the bucket based on processed result codes.
  *@param idBucket is the description of the bucket based on processed entity identifiers.
  *@param startRow is the first row to include (beginning with 0)
  *@param maxRowCount is the maximum number of rows to include.
  */
  public IResultSet genHistoryResultCodes(String connectionName, FilterCriteria criteria, SortOrder sort,
    BucketDescription resultCodeBucket, BucketDescription idBucket, int startRow, int maxRowCount)
    throws ManifoldCFException
  {
    return historyManager.resultCodesReport(connectionName,criteria,sort,resultCodeBucket,idBucket,startRow,maxRowCount);
  }

  // Caching strategy: Individual connection descriptions are cached, and there is a global cache key for the list of
  // repository connections.

  /** Construct a key which represents the general list of repository connectors.
  *@return the cache key.
  */
  protected static String getRepositoryConnectionsKey()
  {
    return CacheKeyFactory.makeRepositoryConnectionsKey();
  }

  /** Construct a key which represents an individual repository connection.
  *@param connectionName is the name of the connector.
  *@return the cache key.
  */
  protected static String getRepositoryConnectionKey(String connectionName)
  {
    return CacheKeyFactory.makeRepositoryConnectionKey(connectionName);
  }

  // Other utility methods.

  /** Fetch multiple repository connections at a single time.
  *@param connectionNames are a list of connection names.
  *@return the corresponding repository connection objects.
  */
  protected RepositoryConnection[] getRepositoryConnectionsMultiple(String[] connectionNames)
    throws ManifoldCFException
  {
    RepositoryConnection[] rval = new RepositoryConnection[connectionNames.length];
    HashMap returnIndex = new HashMap();
    int i = 0;
    while (i < connectionNames.length)
    {
      rval[i] = null;
      returnIndex.put(connectionNames[i],new Integer(i));
      i++;
    }
    beginTransaction();
    try
    {
      i = 0;
      ArrayList params = new ArrayList();
      int j = 0;
      int maxIn = maxClauseGetRepositoryConnectionsChunk();
      while (i < connectionNames.length)
      {
        if (j == maxIn)
        {
          getRepositoryConnectionsChunk(rval,returnIndex,params);
          params.clear();
          j = 0;
        }
        params.add(connectionNames[i]);
        i++;
        j++;
      }
      if (j > 0)
        getRepositoryConnectionsChunk(rval,returnIndex,params);
      return rval;
    }
    catch (Error e)
    {
      signalRollback();
      throw e;
    }
    catch (ManifoldCFException e)
    {
      signalRollback();
      throw e;
    }
    finally
    {
      endTransaction();
    }
  }

  /** Calculate how many repository connections to get at once.
  */
  protected int maxClauseGetRepositoryConnectionsChunk()
  {
    return Math.min(findConjunctionClauseMax(new ClauseDescription[]{}),
      throttleSpecManager.maxClauseGetRows());
  }
  
  /** Read a chunk of repository connections.
  *@param rval is the place to put the read policies.
  *@param returnIndex is a map from the object id (resource id) and the rval index.
  *@param params is the set of parameters.
  */
  protected void getRepositoryConnectionsChunk(RepositoryConnection[] rval, Map returnIndex, ArrayList params)
    throws ManifoldCFException
  {
    ArrayList list = new ArrayList();
    String query = buildConjunctionClause(list,new ClauseDescription[]{
      new MultiClause(nameField,params)});
    IResultSet set = performQuery("SELECT * FROM "+getTableName()+" WHERE "+
      query,list,null,null);
    int i = 0;
    while (i < set.getRowCount())
    {
      IResultRow row = set.getRow(i++);
      String name = row.getValue(nameField).toString();
      int index = ((Integer)returnIndex.get(name)).intValue();
      RepositoryConnection rc = new RepositoryConnection();
      rc.setIsNew(false);
      rc.setName(name);
      rc.setDescription((String)row.getValue(descriptionField));
      rc.setClassName((String)row.getValue(classNameField));
      rc.setACLAuthority((String)row.getValue(groupNameField));
      rc.setMaxConnections((int)((Long)row.getValue(maxCountField)).longValue());
      String xml = (String)row.getValue(configField);
      if (xml != null && xml.length() > 0)
        rc.getConfigParams().fromXML(xml);
      rval[index] = rc;
    }

    // Do throttle part
    throttleSpecManager.getRows(rval,returnIndex,params);
  }

  // The cached instance will be a RepositoryConnection.  The cached version will be duplicated when it is returned
  // from the cache.
  //
  // The description object is based completely on the name.

  /** This is the object description for a repository connection object.
  */
  protected static class RepositoryConnectionDescription extends org.apache.manifoldcf.core.cachemanager.BaseDescription
  {
    protected String connectionName;
    protected String criticalSectionName;
    protected StringSet cacheKeys;

    public RepositoryConnectionDescription(String connectionName, StringSet invKeys)
    {
      super("repositoryconnectioncache");
      this.connectionName = connectionName;
      criticalSectionName = getClass().getName()+"-"+connectionName;
      cacheKeys = invKeys;
    }

    public String getConnectionName()
    {
      return connectionName;
    }

    public int hashCode()
    {
      return connectionName.hashCode();
    }

    public boolean equals(Object o)
    {
      if (!(o instanceof RepositoryConnectionDescription))
        return false;
      RepositoryConnectionDescription d = (RepositoryConnectionDescription)o;
      return d.connectionName.equals(connectionName);
    }

    public String getCriticalSectionName()
    {
      return criticalSectionName;
    }

    /** Get the cache keys for an object (which may or may not exist yet in
    * the cache).  This method is called in order for cache manager to throw the correct locks.
    * @return the object's cache keys, or null if the object should not
    * be cached.
    */
    public StringSet getObjectKeys()
    {
      return cacheKeys;
    }

  }

  /** This is the executor object for locating repository connection objects.
  */
  protected static class RepositoryConnectionExecutor extends org.apache.manifoldcf.core.cachemanager.ExecutorBase
  {
    // Member variables
    protected RepositoryConnectionManager thisManager;
    protected RepositoryConnection[] returnValues;
    protected HashMap returnMap = new HashMap();

    /** Constructor.
    *@param manager is the ToolManager.
    *@param objectDescriptions are the object descriptions.
    */
    public RepositoryConnectionExecutor(RepositoryConnectionManager manager, RepositoryConnectionDescription[] objectDescriptions)
    {
      super();
      thisManager = manager;
      returnValues = new RepositoryConnection[objectDescriptions.length];
      int i = 0;
      while (i < objectDescriptions.length)
      {
        returnMap.put(objectDescriptions[i].getConnectionName(),new Integer(i));
        i++;
      }
    }

    /** Get the result.
    *@return the looked-up or read cached instances.
    */
    public RepositoryConnection[] getResults()
    {
      return returnValues;
    }

    /** Create a set of new objects to operate on and cache.  This method is called only
    * if the specified object(s) are NOT available in the cache.  The specified objects
    * should be created and returned; if they are not created, it means that the
    * execution cannot proceed, and the execute() method will not be called.
    * @param objectDescriptions is the set of unique identifier of the object.
    * @return the newly created objects to cache, or null, if any object cannot be created.
    *  The order of the returned objects must correspond to the order of the object descriptinos.
    */
    public Object[] create(ICacheDescription[] objectDescriptions) throws ManifoldCFException
    {
      // Turn the object descriptions into the parameters for the ToolInstance requests
      String[] connectionNames = new String[objectDescriptions.length];
      int i = 0;
      while (i < connectionNames.length)
      {
        RepositoryConnectionDescription desc = (RepositoryConnectionDescription)objectDescriptions[i];
        connectionNames[i] = desc.getConnectionName();
        i++;
      }

      return thisManager.getRepositoryConnectionsMultiple(connectionNames);
    }


    /** Notify the implementing class of the existence of a cached version of the
    * object.  The object is passed to this method so that the execute() method below
    * will have it available to operate on.  This method is also called for all objects
    * that are freshly created as well.
    * @param objectDescription is the unique identifier of the object.
    * @param cachedObject is the cached object.
    */
    public void exists(ICacheDescription objectDescription, Object cachedObject) throws ManifoldCFException
    {
      // Cast what came in as what it really is
      RepositoryConnectionDescription objectDesc = (RepositoryConnectionDescription)objectDescription;
      RepositoryConnection ci = (RepositoryConnection)cachedObject;

      // Duplicate it!
      if (ci != null)
        ci = ci.duplicate();

      // In order to make the indexes line up, we need to use the hashtable built by
      // the constructor.
      returnValues[((Integer)returnMap.get(objectDesc.getConnectionName())).intValue()] = ci;
    }

    /** Perform the desired operation.  This method is called after either createGetObject()
    * or exists() is called for every requested object.
    */
    public void execute() throws ManifoldCFException
    {
      // Does nothing; we only want to fetch objects in this cacher.
    }


  }

}
