/* $Id: PipelineManager.java 1865954 2019-08-26 20:27:25Z schuch $ */

/**
* 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 "pipelines" table, which contains the ordered transformation
* connections and their specification data.
* 
* <br><br>
* <b>jobpipelines</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>ownerid</td><td>BIGINT</td><td>Reference:jobs.id</td></tr>
* <tr><td>ordinal</td><td>BIGINT</td><td></td></tr>
* <tr><td>prerequisite</td><td>BIGINT</td><td></td></tr>
* <tr><td>outputname</td><td>VARCHAR(32)</td><td></td></tr>
* <tr><td>transformationname</td><td>VARCHAR(32)</td><td></td></tr>
* <tr><td>connectiondesc</td><td>VARCHAR(255)</td><td></td></tr>
* <tr><td>connectionspec</td><td>LONGTEXT</td><td></td></tr>
* </table>
* <br><br>
* 
*/
public class PipelineManager extends org.apache.manifoldcf.core.database.BaseTable
{
  public static final String _rcsid = "@(#)$Id: PipelineManager.java 1865954 2019-08-26 20:27:25Z schuch $";

  // Schema
  public final static String ownerIDField = "ownerid";
  public final static String ordinalField = "ordinal";
  public final static String prerequisiteField = "prerequisite";
  public final static String outputNameField = "outputname";
  public final static String transformationNameField = "transformationname";
  public final static String connectionDescriptionField = "connectiondesc";
  public final static String connectionSpecField = "connectionspec";

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

  /** 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,
    String outputTableName, String outputTableNameField,
    String transformationTableName, String transformationTableNameField)
    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(prerequisiteField,new ColumnDescription("BIGINT",false,true,null,null,false));
        map.put(outputNameField,new ColumnDescription("VARCHAR(32)",false,true,outputTableName,outputTableNameField,false));
        map.put(transformationNameField,new ColumnDescription("VARCHAR(32)",false,true,transformationTableName,transformationTableNameField,false));
        map.put(connectionDescriptionField,new ColumnDescription("VARCHAR(255)",false,true,null,null,false));
        map.put(connectionSpecField,new ColumnDescription("LONGTEXT",false,true,null,null,false));
        performCreate(map,null);
      }
      else
      {
        // Upgrade code goes here, if needed.
      }

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

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

      break;
    }
  }

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

  /** Build a query clause matching a set of transformation connection names.
  */
  public void buildTransformationQueryClause(StringBuilder query, ArrayList params,
    String parentIDField, List<String> connectionNames)
  {
    query.append("SELECT 'x' FROM ").append(getTableName()).append(" WHERE ");
    query.append(buildConjunctionClause(params,new ClauseDescription[]{
      new JoinClause(parentIDField,ownerIDField),
      new MultiClause(transformationNameField,connectionNames)}));
  }

  /** Build a query clause matching a set of output connection names.
  */
  public void buildOutputQueryClause(StringBuilder query, ArrayList params,
    String parentIDField, List<String> connectionNames)
  {
    query.append("SELECT 'x' FROM ").append(getTableName()).append(" WHERE ");
    query.append(buildConjunctionClause(params,new ClauseDescription[]{
      new JoinClause(parentIDField,ownerIDField),
      new MultiClause(outputNameField,connectionNames)}));
  }
  
  /** Get all the transformation connection names for a job.
  *@param ownerID is the job ID.
  *@return the set of connection names.
  */
  public String[] getTransformationConnectionNames(Long ownerID)
    throws ManifoldCFException
  {
    ArrayList newList = new ArrayList();
    StringBuilder query = new StringBuilder("SELECT ");
    query.append(transformationNameField).append(" FROM ").append(getTableName()).append(" WHERE ");
    query.append(buildConjunctionClause(newList,new ClauseDescription[]{
      new UnitaryClause(ownerIDField,ownerID),
      new NullCheckClause(transformationNameField,false)}));
    IResultSet set = performQuery(query.toString(),newList,null,null);
    String[] rval = new String[set.getRowCount()];
    for (int i = 0; i < set.getRowCount(); i++)
    {
      IResultRow row = set.getRow(i);
      rval[i] = (String)row.getValue(transformationNameField);
    }
    return rval;
  }

  /** Get all the output connection names for a job.
  *@param ownerID is the job ID.
  *@return the set of connection names.
  */
  public String[] getOutputConnectionNames(Long ownerID)
    throws ManifoldCFException
  {
    ArrayList newList = new ArrayList();
    StringBuilder query = new StringBuilder("SELECT ");
    query.append(outputNameField).append(" FROM ").append(getTableName()).append(" WHERE ");
    query.append(buildConjunctionClause(newList,new ClauseDescription[]{
      new UnitaryClause(ownerIDField,ownerID),
      new NullCheckClause(outputNameField,false)}));
    IResultSet set = performQuery(query.toString(),newList,null,null);
    String[] rval = new String[set.getRowCount()];
    for (int i = 0; i < set.getRowCount(); i++)
    {
      IResultRow row = set.getRow(i);
      rval[i] = (String)row.getValue(outputNameField);
    }
    return rval;
  }

  /** Fill in a set of pipelines 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);
    for (int i = 0; i < set.getRowCount(); i++)
    {
      IResultRow row = set.getRow(i);
      Long ownerID = (Long)row.getValue(ownerIDField);
      Long prerequisite = (Long)row.getValue(prerequisiteField);
      String outputName = (String)row.getValue(outputNameField);
      String transformationName = (String)row.getValue(transformationNameField);
      String transformationDesc = (String)row.getValue(connectionDescriptionField);
      String transformationSpec = (String)row.getValue(connectionSpecField);
      boolean isOutput = outputName != null && outputName.length() > 0;
      int prerequisiteValue = (prerequisite == null)?-1:(int)prerequisite.longValue();
      JobDescription jd = returnValues.get(ownerID);
      jd.addPipelineStage(prerequisiteValue,isOutput,isOutput?outputName:transformationName,transformationDesc).fromXML(transformationSpec);
    }
  }

  /** Compare rows in job description with what's currently in the database.
  *@param ownerID is the owning identifier.
  *@param job is a job description.
  */
  public boolean compareRows(Long ownerID, IJobDescription job)
    throws ManifoldCFException
  {
    // Compute a set of the outputs
    Set<String> outputSet = new HashSet<String>();
    for (int i = 0; i < job.countPipelineStages(); i++)
    {
      if (job.getPipelineStageIsOutputConnection(i))
      {
        String outputName = job.getPipelineStageConnectionName(i);
        if (outputSet.contains(outputName))
          throw new ManifoldCFException("Output name '"+outputName+"' is duplicated within job; not allowed");
        outputSet.add(outputName);
      }
    }
    ArrayList params = new ArrayList();
    String query = buildConjunctionClause(params,new ClauseDescription[]{
      new UnitaryClause(ownerIDField,ownerID)});
    IResultSet set = performQuery("SELECT * FROM "+getTableName()+" WHERE "+
      query+" ORDER BY "+ordinalField+" ASC",params,null,null);
    if (set.getRowCount() != job.countPipelineStages())
      return false;
    for (int i = 0; i < set.getRowCount(); i++)
    {
      IResultRow row = set.getRow(i);
      String outputConnectionName = (String)row.getValue(outputNameField);
      boolean isOutputConnection = outputConnectionName != null && outputConnectionName.length() > 0;
      if (isOutputConnection)
      {
        if (!outputSet.contains(outputConnectionName))
          throw new ManifoldCFException("Output name '"+outputConnectionName+"' removed from job; not allowed");
      }
      String transformationConnectionName = (String)row.getValue(transformationNameField);
      Long prerequisite = (Long)row.getValue(prerequisiteField);
      String spec = (String)row.getValue(connectionSpecField);
      if (spec == null)
        spec = "";
      int prerequisiteValue = (prerequisite==null)?-1:(int)prerequisite.longValue();
      if (job.getPipelineStagePrerequisite(i) != prerequisiteValue)
        return false;
      if (job.getPipelineStageIsOutputConnection(i) != isOutputConnection)
        return false;
      if (!job.getPipelineStageConnectionName(i).equals(isOutputConnection?outputConnectionName:transformationConnectionName))
        return false;
      if (!job.getPipelineStageSpecification(i).toXML().equals(spec))
        return false;
    }
    return true;
  }
  
  /** Write an output stage (part of the upgrade code).
  */
  public void writeOutputStage(Long ownerID, String outputConnectionName, String outputSpecification)
    throws ManifoldCFException
  {
    HashMap map = new HashMap();
    map.put(ownerIDField,ownerID);
    map.put(ordinalField,new Long(0));
    map.put(outputNameField,outputConnectionName);
    map.put(connectionSpecField,outputSpecification);
    performInsert(map,null);
  }
  
  /** Write a pipeline list into the database.
  *@param ownerID is the owning identifier.
  *@param job is the job description that is the source of the pipeline.
  */
  public void writeRows(Long ownerID, IJobDescription job)
    throws ManifoldCFException
  {
    beginTransaction();
    try
    {
      HashMap map = new HashMap();
      for (int i = 0; i < job.countPipelineStages(); i++)
      {
        boolean isOutput = job.getPipelineStageIsOutputConnection(i);
        int prerequisite = job.getPipelineStagePrerequisite(i);
        String pipelineConnectionName = job.getPipelineStageConnectionName(i);
        String pipelineStageDescription = job.getPipelineStageDescription(i);
        Specification os = job.getPipelineStageSpecification(i);
        map.clear();
        map.put(ownerIDField,ownerID);
        map.put(ordinalField,new Long((long)i));
        if (prerequisite != -1)
          map.put(prerequisiteField,new Long(prerequisite));
        if (isOutput)
          map.put(outputNameField,pipelineConnectionName);
        else
          map.put(transformationNameField,pipelineConnectionName);
        if (pipelineStageDescription != null && pipelineStageDescription.length() > 0)
          map.put(connectionDescriptionField,pipelineStageDescription);
        map.put(connectionSpecField,os.toXML());
        performInsert(map,null);
      }
    }
    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);
  }

}
