/* $Id: ScheduleManager.java 988245 2010-08-23 18:39:35Z 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.jobs;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.agents.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.*;
import java.util.*;

/** This class manages the "schedules" table, which contains the automatic execution schedule for each job.
* It's separated from the main jobs table because we will need multiple timeslots per job.
* 
* <br><br>
* <b>schedules</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>ownerid</td><td>BIGINT</td><td>Reference:jobs.id</td></tr>
* <tr><td>ordinal</td><td>BIGINT</td><td></td></tr>
* <tr><td>dayofweek</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>dayofmonth</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>monthofyear</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>yearlist</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>hourofday</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>minutesofhour</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>timezone</td><td>VARCHAR(32)</td><td></td></tr>
* <tr><td>windowlength</td><td>BIGINT</td><td></td></tr>
* <tr><td>requestminimum</td><td>CHAR(1)</td><td></td></tr>
* </table>
* <br><br>
* 
*/
public class ScheduleManager extends org.apache.manifoldcf.core.database.BaseTable
{
  public static final String _rcsid = "@(#)$Id: ScheduleManager.java 988245 2010-08-23 18:39:35Z kwright $";

  // Schema
  public final static String ownerIDField = "ownerid";
  public final static String ordinalField = "ordinal";
  public final static String dayOfWeekField = "dayofweek";
  public final static String dayOfMonthField = "dayofmonth";
  public final static String monthOfYearField = "monthofyear";
  public final static String yearField = "yearlist";
  public final static String hourOfDayField = "hourofday";
  public final static String minutesOfHourField = "minutesofhour";
  public final static String timezoneField = "timezone";
  public final static String windowDurationField = "windowlength";
  public final static String requestMinimumField = "requestminimum";

  /** Constructor.
  *@param threadContext is the thread context.
  *@param database is the database instance.
  */
  public ScheduleManager(IThreadContext threadContext, IDBInterface database)
    throws ManifoldCFException
  {
    super(database,"schedules");
  }

  /** Install or upgrade.
  *@param ownerTable is the name of the table that owns this one.
  *@param owningTablePrimaryKey is the primary key of the owning table.
  */
  public void install(String ownerTable, String owningTablePrimaryKey)
    throws ManifoldCFException
  {
    // Standard practice: Outer loop to support upgrades
    while (true)
    {
      Map existing = getTableSchema(null,null);
      if (existing == null)
      {
        HashMap map = new HashMap();
        map.put(ownerIDField,new ColumnDescription("BIGINT",false,false,ownerTable,owningTablePrimaryKey,false));
        map.put(ordinalField,new ColumnDescription("BIGINT",false,false,null,null,false));
        map.put(dayOfWeekField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(dayOfMonthField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(monthOfYearField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(yearField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(hourOfDayField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(minutesOfHourField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(timezoneField,new ColumnDescription("VARCHAR(32)",false,true,null,null,false));
        map.put(windowDurationField,new ColumnDescription("BIGINT",false,true,null,null,false));
        map.put(requestMinimumField,new ColumnDescription("CHAR(1)",false,true,null,null,false));
        performCreate(map,null);
      }
      else
      {
        // Upgrade code goes here, if needed.
        if (existing.get(yearField) == null)
        {
          // Need to rename the "year" column as the "yearlist" column.
          HashMap map = new HashMap();
          map.put(yearField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
          performAlter(map,null,null,null);
          performModification("UPDATE "+getTableName()+" SET "+yearField+"=year",null,null);
          ArrayList list = new ArrayList();
          list.add("year");
          performAlter(null,null,list,null);

        }
        
        if (existing.get(requestMinimumField) == null)
        {
          HashMap map = new HashMap();
          map.put(requestMinimumField,new ColumnDescription("CHAR(1)",false,true,null,null,false));
          performAlter(map,null,null,null);
        }
        
      }

      // Index management
      IndexDescription ownerIndex = new IndexDescription(false,new String[]{ownerIDField});

      // 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 (ownerIndex != null && id.equals(ownerIndex))
          ownerIndex = 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 (ownerIndex != null)
        performAddIndex(null,ownerIndex);

      break;
    }
  }

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

  /** Fill in a set of schedules corresponding to a set of owner id's.
  *@param returnValues is a map keyed by ownerID, with value of JobDescription.
  *@param ownerIDList is the list of owner id's.
  *@param ownerIDParams is the corresponding set of owner id parameters.
  */
  public void getRows(Map<Long,JobDescription> returnValues, String ownerIDList, ArrayList ownerIDParams)
    throws ManifoldCFException
  {
    IResultSet set = performQuery("SELECT * FROM "+getTableName()+" WHERE "+ownerIDField+" IN ("+ownerIDList+") ORDER BY "+ordinalField+" ASC",ownerIDParams,
      null,null);
    int i = 0;
    while (i < set.getRowCount())
    {
      IResultRow row = set.getRow(i);
      Long ownerID = (Long)row.getValue(ownerIDField);
      ScheduleRecord sr = new ScheduleRecord(stringToEnumeratedValue((String)row.getValue(dayOfWeekField)),
        stringToEnumeratedValue((String)row.getValue(monthOfYearField)),
        stringToEnumeratedValue((String)row.getValue(dayOfMonthField)),
        stringToEnumeratedValue((String)row.getValue(yearField)),
        stringToEnumeratedValue((String)row.getValue(hourOfDayField)),
        stringToEnumeratedValue((String)row.getValue(minutesOfHourField)),
        (String)row.getValue(timezoneField),
        (Long)row.getValue(windowDurationField),
        stringToRequestMinimumValue((String)row.getValue(requestMinimumField)));
      returnValues.get(ownerID).addScheduleRecord(sr);
      i++;
    }
  }

  /** Get the max clauses that can be used with getRowsAlternate.
  */
  public int maxClauseGetRowsAlternate()
  {
    return findConjunctionClauseMax(new ClauseDescription[]{});
  }
    
  /** Fill in a set of schedules corresponding to a set of owner id's.
  *@param returnValues is a map keyed by ownerID, with a value that is an ArrayList of ScheduleRecord objects.
  *@param ownerIDParams is the corresponding set of owner id parameters.
  */
  public void getRowsAlternate(Map returnValues, ArrayList ownerIDParams)
    throws ManifoldCFException
  {
    ArrayList list = new ArrayList();
    String query = buildConjunctionClause(list,new ClauseDescription[]{
      new MultiClause(ownerIDField,ownerIDParams)});
      
    IResultSet set = performQuery("SELECT * FROM "+getTableName()+" WHERE "+query+" ORDER BY "+ordinalField+" ASC",list,
      null,null);
    int i = 0;
    while (i < set.getRowCount())
    {
      IResultRow row = set.getRow(i);
      Long ownerID = (Long)row.getValue(ownerIDField);
      ScheduleRecord sr = new ScheduleRecord(stringToEnumeratedValue((String)row.getValue(dayOfWeekField)),
        stringToEnumeratedValue((String)row.getValue(monthOfYearField)),
        stringToEnumeratedValue((String)row.getValue(dayOfMonthField)),
        stringToEnumeratedValue((String)row.getValue(yearField)),
        stringToEnumeratedValue((String)row.getValue(hourOfDayField)),
        stringToEnumeratedValue((String)row.getValue(minutesOfHourField)),
        (String)row.getValue(timezoneField),
        (Long)row.getValue(windowDurationField),
        stringToRequestMinimumValue((String)row.getValue(requestMinimumField)));
      ArrayList theList = (ArrayList)returnValues.get(ownerID);
      if (theList == null)
      {
        theList = new ArrayList();
        returnValues.put(ownerID,theList);
      }
      theList.add(sr);
      i++;
    }
  }

  /** Write a schedule list into the database.
  *@param ownerID is the owning identifier.
  *@param list is the job description that is the source of the schedule.
  */
  public void writeRows(Long ownerID, IJobDescription list)
    throws ManifoldCFException
  {
    beginTransaction();
    try
    {
      int i = 0;
      HashMap map = new HashMap();
      while (i < list.getScheduleRecordCount())
      {
        ScheduleRecord record = list.getScheduleRecord(i);
        map.clear();
        map.put(dayOfWeekField,enumeratedValueToString(record.getDayOfWeek()));
        map.put(monthOfYearField,enumeratedValueToString(record.getMonthOfYear()));
        map.put(dayOfMonthField,enumeratedValueToString(record.getDayOfMonth()));
        map.put(yearField,enumeratedValueToString(record.getYear()));
        map.put(hourOfDayField,enumeratedValueToString(record.getHourOfDay()));
        map.put(minutesOfHourField,enumeratedValueToString(record.getMinutesOfHour()));
        map.put(timezoneField,record.getTimezone());
        map.put(windowDurationField,record.getDuration());
        map.put(requestMinimumField,requestMinimumValueToString(record.getRequestMinimum()));
        map.put(ownerIDField,ownerID);
        map.put(ordinalField,new Long((long)i));
        performInsert(map,null);
        i++;
      }
    }
    catch (ManifoldCFException e)
    {
      signalRollback();
      throw e;
    }
    catch (Error e)
    {
      signalRollback();
      throw e;
    }
    finally
    {
      endTransaction();
    }
  }

  /** Delete rows.
  *@param ownerID is the owner whose rows to delete.
  */
  public void deleteRows(Long ownerID)
    throws ManifoldCFException
  {
    ArrayList list = new ArrayList();
    String query = buildConjunctionClause(list,new ClauseDescription[]{
      new UnitaryClause(ownerIDField,ownerID)});
      
    performDelete("WHERE "+query,list,null);
  }

  /** Go from string to enumerated value.
  *@param value is the input.
  *@return the enumerated value.
  */
  public static EnumeratedValues stringToEnumeratedValue(String value)
    throws ManifoldCFException
  {
    if (value == null)
      return null;
    try
    {
      ArrayList valStore = new ArrayList();
      if (!value.equals("*"))
      {
        int curpos = 0;
        while (true)
        {
          int newpos = value.indexOf(",",curpos);
          if (newpos == -1)
          {
            valStore.add(new Integer(value.substring(curpos)));
            break;
          }
          valStore.add(new Integer(value.substring(curpos,newpos)));
          curpos = newpos+1;
        }
      }
      return new EnumeratedValues(valStore);
    }
    catch (NumberFormatException e)
    {
      throw new ManifoldCFException("Bad number: '"+value+"'",e);
    }

  }

  /** Go from enumerated value to string.
  *@param values is the enumerated value.
  *@return the string value.
  */
  public static String enumeratedValueToString(EnumeratedValues values)
  {
    if (values == null)
      return null;
    if (values.size() == 0)
      return "*";
    StringBuilder rval = new StringBuilder();
    Iterator iter = values.getValues();
    boolean first = true;
    while (iter.hasNext())
    {
      if (first)
        first = false;
      else
        rval.append(',');
      rval.append(((Integer)iter.next()).toString());
    }
    return rval.toString();
  }

  public static String requestMinimumValueToString(boolean requestMinimum)
  {
    return requestMinimum?"T":"F";
  }
  
  public static boolean stringToRequestMinimumValue(String requestMinimum)
    throws ManifoldCFException
  {
    if (requestMinimum == null)
      return false;
    else if (requestMinimum.equals("T"))
      return true;
    else if (requestMinimum.equals("F"))
      return false;
    else
      throw new ManifoldCFException("Bad requestminimum value: '"+requestMinimum+"'");
  }
    
}
