#!/usr/bin/env python3
"""
    birchdocdbkit.py - add, delete or change objects in a birchdocdb database

    birchdocdbkit.py is a command line client designed to be run by BioLegato
    to make changes in the database. Based on objects and commands in inputfile,
    birchdocdbkit.py generates SQL statements to make the corresponding changes
    in the database. 

    inputfile - TSV input file with queries in the form of database objects.
    databasefile - sqlite database file
    --commit - By default, will only produce a report on sanity checking.
    If --commit is specified, the change is made provided that sanity checking succeeded.

    Synopsis: 
    birchdocdbkit.py  inputfile databasefile [--commit]

@modified: June 6, 2026
@author: Brian Fristensky
@contact: brian.fristensky@umanitoba.ca
"""

import argparse
import os
import re
import shutil
import sqlite3
import sys

blib = os.environ.get("BIRCHLIB")
sys.path.append(blib)

from birchlib import Birchmod
from birchlib import Htmlutils
from birchlib import HTMLWriter

PROGRAM = "birchdocdbkit.py: "
USAGE = "\n\t USAGE: birchdocdbkit.py inputfile databasefile [--commit]"
BM = Birchmod(PROGRAM, USAGE)
PROGS = []

CATDICT = {}
PROGDICT = {}
PKGDICT = {}
DOCDICT = {}
HT = Htmlutils(CATDICT, PROGDICT)
PLATFORMS = {"L":"linux-x86_64","l":"linux-arm64","M":"osx-x86_64","m":"macos-arm64"}
COMTYPE = ["command","interactive","gui","birch","birchadmin","bldna",
           "blnalign","blprotein","blpalign","bldata","blmarker","blnfetch",
           "blpfetch","blreads","blncbi","bltable","bltree"]

# Column headers for different types of records           
ProgramCols = ["Name","Description","Installation"]
PackageCols = ["Name","Description","Installation"]
FileCols = ["Name","Description","Action","PathOrURL"]
CategoryCols = ["CatName"]
PkgProgCols = ["Package","Program"]
PkgCatCols = ["Package","Category"]
PkgPlatCols = ["Package","Platform"]
PkgDatCols = ["Package","Data"]
PkgDocCols = ["Package","Documentation"]
ProgCatCols = ["Program","Category"]
ProgDocCols = ["Program","Documentation"]
ProgDatCols = ["Program","Data"]
ProgPlatCols = ["Program","Platform"]
ProgLaunchCols = ["Program","ComType","Command"]
ProgSampleInpCols = ["Program","SampleInput"]
ProgSampleOutCols = ["Program","SampleOutput"]

# Constants
NL = "\n"
TAB = "\t"
SEMI = ";"
QUOTE = '"'
DEBUG = True
FOURBLANKS = "    "
OBJECTSEPARATOR = "----------------------------------------" + NL

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Parameters:
    "Wrapper class for global parameters"

    def __init__(self):
        """
          Get the location of the BIRCH home directory, BIRCHPATH
          Preferrably, we get it from the BIRCH.properties file,
          but if that isn't found, we get it from the BIRCH
          environment variable. The one catch is that since
          we don't already know the location of BIRCHPATH,
          the former will only work if the current working
          directory is install-scripts. Normally, this would
          only be used when installing or updating BIRCH
          Otherwise, the environment variable will be read.
          """
        FN = '../local/admin/BIRCH.properties'
        if  os.path.exists(FN):
            print("birchdocdbkit.py: Reading $BIRCH from BIRCH.properties")
            self.BIRCHPATH = ""
            self.BIRCH_PLATFORM = ""
            FILE = open(FN, 'r')

            for LINE in FILE:
                TOKENS = LINE.split("=")
                if TOKENS[0] == 'BirchProps.homedir':
                    self.BIRCHPATH = TOKENS[1].strip()
                if TOKENS[0] == 'BirchProps.platform':
                    self.BIRCH_PLATFORM = TOKENS[1].strip()
            FILE.close()
        else:
            print("birchdocdbkit.py: Reading $BIRCH and $BIRCH_PLATFORM environment variables.")
            self.BIRCHPATH = os.environ['BIRCH']      
            
        # If BIRCH_PLATFORM is set in the environment, we want that
        # to supersede the value in BIRCH.properties
        plat = ""
        try:
            plat = os.environ['BIRCH_PLATFORM']
        except:
            pass
        if plat != "":
            self.BIRCH_PLATFORM = plat

        # Everything else depends on BIRCHPATH
        print("birchdocdbkit.py: BIRCHPATH set to: " + self.BIRCHPATH)
        print("birchdocdbkit.py: BIRCH_PLATFORM set to: " + self.BIRCH_PLATFORM)
        self.BIRCHWEBPATH = self.BIRCHPATH + '/public_html'
        self.BIRCHDOCDBPATH = self.BIRCHPATH + '/public_html/birchdocdb'
        self.BIRCHLOCALDOCDBPATH = self.BIRCHPATH + '/local/public_html/birchdocdb'
        
        # After all that, now we get the command line arguments
        self.COMMIT = False
        self.IFN = ""
        self.OFN = ""
        self.DBFN = ""
        self.read_args()
        
    def read_args(self):
        """
                Read command line arguments into a Parameter object
        """

        parser = argparse.ArgumentParser()        
        parser.add_argument("--commit", dest="commit", action="store_true", default=False, help="commit the change in the database")
        parser.add_argument("--outfile", dest="ofn", action="store", default="", help="output file for SHOW transaction")
        parser.add_argument("remainder", nargs="*", action="store", default=[], help="remaining command line arguments")
        args = parser.parse_args()
  
        self.COMMIT = args.commit 
        self.OFN = args.ofn

        print(args.remainder)
        if len(args.remainder) == 2 :
            self.IFN = args.remainder[0]
            self.DBFN = args.remainder[1]   
        else:
            print(USAGE)
            exit()     

        if DEBUG :
            print("IFN: " + self.IFN)
            print("OFN: " + self.OFN)
            print("DBFN: " + self.DBFN)
            print("COMMIT: " + str(self.COMMIT))   

#======================= DATABASE CLASSES =========================

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class File:

    def __init__(self):
        """
          Initializes arguments:
                name=""
                description=""
                command='\"$ACE_FILE_LAUNCHER\" '
                path=""
          """
        self.name = ""
        self.description = ""
        #self.command = '\"$ACE_FILE_LAUNCHER\" '
        self.command = '$ACE_FILE_LAUNCHER'
        self.path = ""

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def GetFileData(self,Cursor,ObjClass,ObjName):
        """
        From the database, get all data referencing the table.
        """
        row = GetRows(Cursor,ObjClass,"Name",ObjName)
        for r in row :
            self.name = r[0]
            self.description = r[1]
            self.command = r[2]
            self.path = r[3]

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def WriteFileData(self,ObjClass,ObjName,Outfile):
        """
        Write file data to output file.
        """
        SEP = TAB
        INDENT = FOURBLANKS
        Outfile.write(OBJECTSEPARATOR)
        Outfile.write(ObjClass + SEP + self.name + NL)
        Outfile.write(INDENT + "Description" + SEP + NotNull(self.description) + NL)
        Outfile.write(INDENT + "Command" + SEP + NotNull(self.command) + SEP + NotNull(self.path) + NL)
        Outfile.write(NL)
        Outfile.write(NL)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Category:

    def __init__(self):
        """
          Initializes arguments:
                name=""
                program=[]
                pkg=[]
          """
        self.catname = ""
        self.program = []
        self.PkgDict = {}

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def GetCategoryData(self,Cursor,ObjClass,ObjName):
        """
        From the database, get all data referencing the table.
        """

        # Get all programs listed in a category
        row = GetRows(Cursor,"ProgCat",ObjClass,ObjName) 
        for c in row :
            self.program.append(c[0])

        # Add each program to a dictionary keyed by the program's package 
        for p in self.program :
            row = GetRows(Cursor,"PkgProg","Program",p)
            for r in row :
                package = r[0]
            if package in self.PkgDict :
                self.PkgDict[package].append(p)
            else :
                self.PkgDict[package] = [p]

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def WriteCategoryData(self,ObjClass,ObjName,Outfile):
        """
        From the database, get all data referencing the table.
        """
        SEP = TAB
        INDENT = FOURBLANKS
        Outfile.write(OBJECTSEPARATOR)
        Outfile.write(ObjClass + SEP + ObjName + NL)
        for k in self.PkgDict.keys() :
            Outfile.write(INDENT + "Package" + SEP + NotNull(k) + NL)
            for p in self.PkgDict[k] :
                Outfile.write(INDENT + INDENT + "Program" + SEP + NotNull(p) + NL)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Program:

    def __init__(self):
        """
          Initializes arguments:
                name=""
                description=""
                category=[]
                interface=[]
                package=""
                doc=[]
                data=[]
                sampleinput=[]
                sampleoutput=[]
                platform=[]
          """
        self.name = ""
        self.description = ""
        self.category = []
        self.interface = []
        self.package = ""
        self.doc = []
        self.data = []
        self.sampleinput = []
        self.sampleoutput = []
        self.platforms = []
        self.installation = ""


    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def GetProgramData(self,Cursor,ObjClass,ObjName):
        """
        From the database, get all data referencing the table.
        """
        
        # Get Program table
        row = GetRows(Cursor,ObjClass,"Name",ObjName)
        for r in row :
            self.name = r[0]
            self.description = r[1]
            self.installation = r[2]
        
        # Get other tables that link to Program

        row = GetRows(Cursor,"PkgProg","Program",self.name)   
        for r in row :
            self.package = r[0]
        
        row = GetRows(Cursor,"ProgCat","Program",self.name) 
        for c in row :
            self.category.append(c[1]) 
                
        row = GetRows(Cursor,"ProgLaunch","Program",self.name) 
        for l in row :
            ComType = l[1]
            Command = l[2]
            self.interface.append([ComType,Command]) 
                
        row = GetRows(Cursor,"ProgDoc","Program",self.name) 
        for d in row :
            self.doc.append(d[1]) 
                    
        row = GetRows(Cursor,"ProgDat","Program",self.name) 
        for d in row :
            self.data.append(d[1])         

        row = GetRows(Cursor,"ProgSampleInp","Program",self.name) 
        for psi in row :
            self.sampleinput.append(psi[1])
            
        row = GetRows(Cursor,"ProgSampleOut","Program",self.name) 
        for pso in row :
            self.sampleoutput.append(pso[1])
            
        row = GetRows(Cursor,"ProgPlat","Program",self.name) 
        for plat in row :
            self.platforms.append(plat[1])            

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def WriteProgramData(self,ObjClass,ObjName,Outfile):
        """
        From the database, get all data referencing the table.
        """
        SEP = TAB
        INDENT = FOURBLANKS
        Outfile.write(OBJECTSEPARATOR)
        Outfile.write(ObjClass + SEP + self.name + NL)
        Outfile.write(INDENT + "Description" + SEP + NotNull(self.description) + NL)
        for c in self.category :
            Outfile.write(INDENT + "Category" + SEP + NotNull(c) + NL)
        Outfile.write(INDENT + "Package" + SEP + NotNull(self.package) + NL)
        for l in self.interface :
            # special characters in command string are in HTML, so we
            # have to replace the HTML with the actual characters.
            l1 = l[1].replace("&gt;",">")
            l1 = l1.replace("&lt;","<")
            Outfile.write(INDENT + "Launch" + SEP + NotNull(l[0]) + SEP + NotNull(l1) + NL)
        for d in self.doc :
            Outfile.write(INDENT + "Documentation" + SEP + NotNull(d) + NL)
        for d in self.data :
            Outfile.write(INDENT + "Data" + SEP + NotNull(d) + NL)
        for d in self.sampleinput :
            Outfile.write(INDENT + "Sample Input" + SEP + NotNull(d) + NL)
        for d in self.sampleoutput :
            Outfile.write(INDENT + "Sample Output" + SEP + NotNull(d) + NL)
        platforms = ""
        for plat in self.platforms :
            if not platforms == "" :
                platforms = platforms + ","
            platforms = platforms + plat
        Outfile.write(INDENT + "Platforms" + SEP + NotNull(platforms) + NL)
        Outfile.write(INDENT + "Installation" + SEP + NotNull(self.installation))
        Outfile.write(NL)
        Outfile.write(NL)
        

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Package:

    def __init__(self):
        """
          Initializes arguments:
                name=""
                description=""
                category=[]
                doc=[]
                program=[]
                data=[]
                platforms=[]
                installation=[]
          """
        self.name = ""
        self.description = ""
        self.category = []
        self.doc = []
        self.program = []
        self.data = []
        self.platforms = []
        self.installation = ""

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def GetPackageData(self,Cursor,ObjClass,ObjName):
        """
        From the database, get all data referencing the table.
        """
        
        # Get Package table
        rows = GetRows(Cursor,ObjClass,"Name",ObjName)
        for r in rows :
           self.name = r[0]
           self.description = r[1]
           self.installation = r[2]
        
        # Get other tables that link to Package
        rows = GetRows(Cursor,"PkgProg","Package",self.name)   
        for p in rows :
            self.program.append(p[1]) 
                    
        rows = GetRows(Cursor,"PkgCat","Package",self.name) 
        for c in rows :
            self.category.append(c[1]) 
                              
        rows = GetRows(Cursor,"PkgDoc","Package",self.name) 
        for d in rows :
            self.doc.append(d[1]) 
                    
        rows = GetRows(Cursor,"PkgDat","Package",self.name) 
        for d in rows :
            self.data.append(d[1])         
            
        rows = GetRows(Cursor,"PkgPlat","Package",self.name) 
        for plat in rows :
            self.platforms.append(plat[1])            

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def WritePackageData(self,ObjClass,ObjName,Outfile):
        """
        From the database, get all data referencing the table.
        """
        SEP = TAB
        INDENT = FOURBLANKS
        Outfile.write(OBJECTSEPARATOR)
        Outfile.write(ObjClass + SEP + self.name + NL)
        Outfile.write(INDENT + "Description" + SEP + NotNull(self.description) + NL)
        for c in self.category :
            Outfile.write(INDENT + "Category" + SEP + NotNull(c) + NL)
        for p in self.program :
            Outfile.write(INDENT + "Program" + SEP + NotNull(p) + NL)
        for d in self.doc :
            Outfile.write(INDENT + "Documentation" + SEP + NotNull(d) + NL)
        for d in self.data :
            Outfile.write(INDENT + "Data" + SEP + NotNull(d) + NL)
        platforms = ""
        for plat in self.platforms :
            if not platforms == "" :
                platforms = platforms + ","
            platforms = platforms + plat
        Outfile.write(INDENT + "Platforms" + SEP + NotNull(platforms) + NL)
        Outfile.write(INDENT + "Installation" + SEP + NotNull(self.installation))
        Outfile.write(NL)
        Outfile.write(NL)
        

#================= GENERAL SQL AND RELATED METHODS ========================

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def NotNull(S):
    """
    Change a null value to an empty string.
    """
    if S == None :
        return ""
    else :
         return S

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def Quote(S,C):
    """
    Enclose string S in quotes using C as the quote character.
    """
    return C + S + C


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def QuoteList(L,C):
    """
    Quote each string in a list of strings.
    """
    QS = []
    for S in L :
        QS.append(Quote(S,C))
    return QS

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def ListContains(Source,Values):
    """
    If a list contains an item with ALL values in Values return True

    """

    if Values in Source :
        return True
    else :
        return False

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def AddToList(L, NAME):
    """
    If a list contains NAME, do nothing
    If it does not exist, append it to the list
    @param L: The list to check for name
    @type L: list
    @param NAME: The name to be checked for
    @type NAME: str
    """

    if NAME in L:
        pass
    else:
        L.append(NAME)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def ObjectExists(Cursor,TableName,ColName,ObjName) :
    """
    True : object exists; False : object doesn't exist
    """
    result = Cursor.execute("SELECT " + ColName + " from " + TableName + " WHERE " + ColName + "='" + ObjName + "';")
    if len(result.fetchall()) == 0 :
        return False        
    else :
        return True

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def GetRows(Cursor,TableName,Col,Value) :
    """
    From database, return all matching rows from a table
    """
    result = Cursor.execute("SELECT * from " + TableName + " WHERE " + Col + " ='" + Value + "';")
    return result

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def InsertRow(Cursor,TableName,Cols,Values) :
    """
    Write one or more rows of values to SQLFILE.
    """
    if len(Values) > 0 :
        COMMAND="INSERT INTO " + TableName + " ( " + Cols[0]
        for s in Cols[1:] :
            COMMAND += "," + s
        COMMAND += ") VALUES ("
        COMMAND += Values[0]
        for v in Values[1:] :
            COMMAND += "," + v
        COMMAND += ");"
        print("COMMAND: " + COMMAND)
        Cursor.execute(COMMAND)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def DeleteRow(Cursor,TableName,Cols,Values) :
    """
    Delete one or more rows containing Values in each field Cols.
    Database automatically takes care of removing any item
    that contains the TableName field eg. PkgProg, ProgCat etc.
    """
    if len(Values) > 0 :
        COMMAND="DELETE FROM " + TableName + " WHERE "
        i = 0
        COMMAND = COMMAND + Cols[i] + " = " + Quote(Values[i],'"')
        i =1
        while i < len(Cols) : 
            COMMAND = COMMAND + " AND " + Cols[i] + " = " + Quote(Values[i],'"')
            i += 1             
        COMMAND = COMMAND + ";"
        print("COMMAND: " + COMMAND)
        Cursor.execute(COMMAND)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                     
    def DeleteObject(self,Cursor,ObjClass,ColName,ObjName) :
        if ObjectExists(Cursor,ObjClass,ColName,ObjName) :
            DeleteRow(Cursor,ObjClass,[ColName],[Quote(ObjName,'"')])
        else:
            print(ObjClass + " " + ObjName + " does not exist. Ignoring transaction") 

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def SetField(Cursor,TableName,Key,ObjName,Col,Value) :
    """
    Set a field to a new value.
    """
    Cursor.execute("UPDATE " + TableName + " SET " + Col + " = " + Quote(Value,'"') +" WHERE " + Key + " = " + Quote(ObjName,'"') + ";")
   
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
def UnSetField(Cursor,TableName,Key,ObjName,Col) :
    """
    Set a field to NULL
    """
    Cursor.execute("UPDATE " + TableName + " SET " + Col + " = NULL" + " WHERE " + Key + " = " + Quote(ObjName,'"') + ";")


#======================= TRANSACTIONS =========================
    
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class InputTokens:
    """
       Class for lines read from inputfile.
       Lines are parsed into non-blank tokens, skipping
       comments.
    """

    def __init__(self):
        """

        """

        self.tlines = [] # a list of tokenized lines
        # start and finish are indices of a slice of the list
        self.start = 0
        self.finish = 0 

        
    def read_lines(self,IFN):
        """
                Read lines from infile and parse them into tokens.
        """   

        infile = open(IFN, "r")       
        line = infile.readline()
        while not line == "" :
            line = line.strip()
            # ignore comments and blank lines
            # other lines get split into non-blank tokens delimited
            # by tabs
            if not line.startswith("--") and not line == "" : 
                self.tlines.append(line.split(TAB))
            line = infile.readline()      
        infile.close()            

        #if DEBUG :
        #    for t in self.tlines :
        #        print(str(t)) 
        
    def get_next_transaction(self):
        """
            Read lines of tokens for each transaction.
        """   
        self.start = self.finish
        i = self.finish
        while not self.tlines[i][-1].endswith(SEMI) and i < len(self.tlines) :
            i+=1 
        self.finish = i + 1  
        t = Transaction()
        t.lines = self.tlines[self.start:i] 
        return t      
                

#----------------------------------------------------------------
class Transaction():
    """
       Class for input tokens, broken up into individual
       transactions. Each transaction is set of lines terminated by a 
       semicolon (;)
    """

    def __init__(self):
        """

        """
        self.lines = [] # a list of lines 

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
    def InsertFile(self,Cursor,ObjClass,ObjName) :
        if ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " already exists. Canceling transaction")
        else:
            F = File()
            F.name = ObjName
            for line in self.lines :
                if line[0] == "Description" :
                    F.description = line[1]
                elif line[0] == "Command" :
                    F.command = line[1]
                    F.path = line[2]
            Values = [F.name,F.description,F.command,F.path]        
            InsertRow(Cursor,'File',FileCols,QuoteList(Values,QUOTE))

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def InsertCategory(self,Cursor,ObjClass,ObjName) :
        if ObjectExists(Cursor,ObjClass,"CatName",ObjName) :
            print(ObjClass + " " + ObjName + " already exists. Canceling transaction")
        else:
            C = Category()
            C.catname = ObjName
            Values = [C.catname]        
            InsertRow(Cursor,'Category',CategoryCols,QuoteList(Values,QUOTE))

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def InsertProgram(self,Cursor,ObjClass,ObjName) :
        if ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " already exists. Canceling transaction")
        else:
            P = Program()
            P.name = ObjName

            # Populate Program object with data from transaction lines
            for line in self.lines[1:] :
                key = line[0]
                if key == "Description" :
                    P.description = line[1]                   
                elif key == "Launch" :
                    P.interface.append([line[1],line[2]]) 
                elif key == "Documentation" :
                    P.doc.append(line[1])      
                elif key == "Data" :
                    P.data.append(line[1])  
                elif key == "SampleInput" :
                    P.sampleinput.append(line[1])  
                elif key == "SampleOutput" :
                    P.sampleoutput.append(line[1]) 
                elif key == "Package" :
                    P.package = line[1]  
                elif key == "Category" :
                    P.category = line[1] 
                elif key == "Platforms" :
                    plats = line[1].split(":")
                    for p in plats :
                        P.platforms.append(PLATFORMS[p]) 
                elif key == "Installation" :
                    P.installation = line[1]  
                else :
                    pass                   

            # Generate SQL to add the object to the database
            Values = [P.name,P.description,P.installation]
            InsertRow(Cursor,'Program',ProgramCols,QuoteList(Values,QUOTE)) 

            for i in P.interface :
                Values = [P.name,i[0],i[1]]
                InsertRow(Cursor,'ProgLaunch',ProgLaunchCols,QuoteList(Values,QUOTE))
            
            for doc in P.doc :
                Values = [P.name,doc]
                InsertRow(Cursor,'ProgDoc',ProgDocCols,QuoteList(Values,QUOTE))

            for dat in P.data :
                Values = [P.name,dat]
                InsertRow(Cursor,'ProgDat',ProgDatCols,QuoteList(Values,QUOTE))

            for psi in P.sampleinput :
                Values = [P.name,psi]
                InsertRow(Cursor,'ProgSampleInp',ProgSampleInpCols,QuoteList(Values,QUOTE))

            for pso in P.sampleoutput :
                Values = [P.name,pso]
                InsertRow(Cursor,'ProgSampleOut',ProgSampleOutCols,QuoteList(Values,QUOTE))

            Values = [P.package,P.name]
            InsertRow(Cursor,'PkgProg',PkgProgCols,QuoteList(Values,QUOTE))                 

            Values = [P.name,P.category]
            InsertRow(Cursor,'ProgCat',ProgCatCols,QuoteList(Values,QUOTE)) 

            for plat in P.platforms :
                Values = [P.name,plat]
                InsertRow(Cursor,'ProgPlat',ProgPlatCols,QuoteList(Values,QUOTE))                                               


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def InsertPackage(self,Cursor,ObjClass,ObjName) :
        if ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " already exists. Canceling transaction")
        else:
            P = Package()
            P.name = ObjName

            # Populate Program object with data from transaction lines
            for line in self.lines[1:] :
                key = line[0]
                if key == "Description" :
                    P.description = line[1]                   
                elif key == "Documentation" :
                    P.doc.append(line[1])      
                elif key == "Data" :
                    P.data.append(line[1])  
                elif key == "Program" :
                    P.package = line[1]  
                elif key == "Category" :
                    P.category = line[1] 
                elif key == "Platforms" :
                    plats = line[1].split(":")
                    for p in plats :
                        P.platforms.append(PLATFORMS[p]) 
                elif key == "Installation" :
                    P.installation = line[1]  
                else :
                    pass                   

            # Generate SQL to add the object to the database
            Values = [P.name,P.description,P.installation]
            InsertRow(Cursor,'Package',PackageCols,QuoteList(Values,QUOTE)) 
            
            for doc in P.doc :
                Values = [P.name,doc]
                InsertRow(Cursor,'PkgDoc',PkgDocCols,QuoteList(Values,QUOTE))

            for dat in P.data :
                Values = [P.name,dat]
                InsertRow(Cursor,'PkgDat',PkgDatCols,QuoteList(Values,QUOTE))               

            Values = [P.name,P.category]
            InsertRow(Cursor,'PkgCat',PkgCatCols,QuoteList(Values,QUOTE)) 

            for plat in P.platforms :
                Values = [P.name,plat]
                InsertRow(Cursor,'PkgPlat',PkgPlatCols,QuoteList(Values,QUOTE))                                               
                                     

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -    
    def UpdateFile(self,Cursor,ObjName) :
        ObjClass = "File"
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " does not exist. Canceling transaction")
        else:
            F = File()
            F.GetFileData(Cursor,ObjClass,ObjName)
            for line in self.lines :
                if line[0] == "SET" :
                    if line[1] == "Description" :
                        F.description = line[2]
                        SetField(Cursor,"File","Name",ObjName,"Description",F.description)
                    elif line[1] == "Action" :
                        F.command = line[2]
                        SetField(Cursor,"File","Name",ObjName,"Action",F.command)                        
                    elif line[1] == "PathOrUrl" :
                        F.path = line[3]
                        SetField(Cursor,"File","Name",ObjName,"PathOrURL",F.path) 
                elif line[0] == "UNSET" :
                    if line[1] == "Description" :
                        UnSetField(Cursor,"File","Name",ObjName,"Description")
                    elif line[1] == "Action" :
                        UnSetField(Cursor,"File","Name",ObjName,"Action")                        
                    elif line[1] == "PathOrUrl" :
                        UnSetField(Cursor,"File","Name",ObjName,"PathOrURL") 
                else :
                    pass                          

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def UpdateProgram(self,Cursor,ObjName) :
        ObjClass = "Program"
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " does not exist. Canceling transaction")
        else:
            P = Program()
            P.GetProgramData(Cursor,ObjClass,ObjName)
            for line in self.lines :
                if line[0] == "SET" :
                    Field = line[1]
                    if Field == "Description" :
                        P.description = line[2]
                        SetField(Cursor,"Program","Name",ObjName,"Description",P.description)
                    elif Field == "Package" :
                        P.package = line[2]
                        if ObjectExists(Cursor,"Package","Name",P.package) :
                            progs = GetRows(Cursor,"PkgProg","Program",ObjName)
                            if progs == None :                         
                                Values = [P.package,ObjName]
                                InsertRow(Cursor,"PkgProg",PkgProgCols,Values)                                                       
                            else :   
                                SetField(Cursor,"PkgProg","Program",ObjName,"Package",P.package) 
                        else :
                            print("Package " + P.package + " does not exist. Aborting transaction.")                                               
                    elif Field == "Launch" :
                        ComType = line[2]
                        Command = line[3]
                        if [ComType,Command] in P.interface :
                            print(ComType + "," + Command + " already set. Doing nothing.")
                        else :                        
                            Values = [ObjName,ComType,Command]
                            InsertRow(Cursor,"ProgLaunch",ProgLaunchCols,QuoteList(Values,'"'))                                                        
                    elif Field == "Documentation" :
                        FileName = line[2]
                        if FileName in P.doc :
                            print(FileName + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"ProgDoc",ProgDocCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "Data" :
                        FileName = line[2]
                        if FileName in P.data :
                            print(FileName + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"ProgDat",ProgDatCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "SampleInput" :
                        FileName = line[2]
                        if FileName in P.sampleinput :
                            print(FileName + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"ProgSampleInp",ProgSampleInpCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "SampleOutput" :
                        FileName = line[2]
                        if FileName in P.sampleoutput :
                            print(FileName + " alredy set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"ProgSampleOut",ProgSampleOutCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "Category" :
                        Cat = line[2]
                        if Cat in P.category :
                            print(Cat + " alredy set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"Category","CatName",Cat) :                       
                                Values = [ObjName,Cat]
                                InsertRow(Cursor,"ProgCat",ProgCatCols,QuoteList(Values,'"'))  
                            else:
                                print("Category " + Cat + " does not exist. Transaction aborted.")
                    elif Field == "Platform" :
                        # Delete from the database all platforms for this program
                        for p in PLATFORMS.keys() :
                            Values = [ObjName,PLATFORMS[p]]
                            DeleteRow(Cursor,"ProgPlat",ProgPlatCols,Values)                             
                        # Add only those platforms specified using the abbreviations from the transaction file
                        plat = line[2]
                        abbreviations = plat.strip().split(":")
                        plat = []
                        for a in abbreviations :
                            if a in PLATFORMS.keys() :
                                plat.append(PLATFORMS[a])
                        for p in plat :
                            Values = [ObjName,p]
                            InsertRow(Cursor,"ProgPlat",ProgPlatCols,QuoteList(Values,'"'))
                    elif Field == "Installation" :
                        P.installation = line[2]
                        if P.installation in ["BIRCH","local"] :
                            SetField(Cursor,"Program","Name",ObjName,"Installation",P.installation) 
                        else :
                            print("Illegal value for Program.installation: " + P.installation)
                    else :
                        pass
                elif line[0] == "UNSET" :
                    Field = line[1]
                    if Field == "Description" :
                        UnSetField(Cursor,"Program","Name",ObjName,"Description")
                    elif Field == "Package" :
                        DeleteRow(Cursor,"PkgProg",["Program"],[ObjName])                        
                    elif Field == "Launch" :
                        ComType = line[2]
                        Command = line[3]
                        Values = [ObjName,ComType,Command]
                        if [ComType,Command] in P.interface :
                            DeleteRow(Cursor,"ProgLaunch",ProgLaunchCols,Values) 
                        else :                        
                            print(ComType + "," + Command + " not found. Doing nothing.")
                    elif Field == "Documentation" :
                        DeleteRow(Cursor,"ProgDoc",["Documentation"],[line[2]]) 
                    elif Field == "Data" :
                        DeleteRow(Cursor,"ProgDat",["Data"],[line[2]]) 
                    elif Field == "SampleInput" :
                        DeleteRow(Cursor,"ProgSampleInp",["SampleInput"],[line[2]]) 
                    elif Field == "SampleOutput" :
                        DeleteRow(Cursor,"ProgSampleOut",["SampleOutput"],[line[2]]) 
                    elif Field == "Platform" :
                        # Delete from the database all platforms for this program
                        for p in PLATFORMS.keys() :
                            Values = [ObjName,PLATFORMS[p]]
                            DeleteRow(Cursor,"ProgPlat",ProgPlatCols,Values)
                    elif Field == "Category" :
                        DeleteRow(Cursor,"ProgCat",["Category"],[line[2]])   
                    elif Field == "Installation" :
                        print("Program.Installation cannot be NULL. Must be BIRCH or local.")

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    def UpdatePackage(self,Cursor,ObjName) :
        ObjClass = "Package"
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " does not exist. Canceling transaction")
        else:
            P = Package()
            P.GetPackageData(Cursor,ObjClass,ObjName)
            for line in self.lines :
                if line[0] == "SET" :
                    Field = line[1]
                    if Field == "Description" :
                        P.description = line[2]
                        SetField(Cursor,"Package","Name",ObjName,"Description",P.description)
                    elif Field == "Program" :
                        Prog = line[2]
                        if Prog in P.program :
                            print(Prog + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"Program","Name",Prog) :                       
                                Values = [ObjName,Prog]
                                InsertRow(Cursor,"PkgProg",PkgProgCols,QuoteList(Values,'"'))  
                            else:
                                print("Program " + Prog + " does not exist. Transaction aborted.")                                                                                                      
                    elif Field == "Documentation" :
                        FileName = line[2]
                        if FileName in P.doc :
                            print(FileName + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"PkgDoc",PkgDocCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "Data" :
                        FileName = line[2]
                        if FileName in P.data :
                            print(FileName + " already set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"File","Name",FileName) :                       
                                Values = [ObjName,FileName]
                                InsertRow(Cursor,"PkgDat",PkgDatCols,QuoteList(Values,'"'))  
                            else:
                                print("File " + FileName + " does not exist. Transaction aborted.")
                    elif Field == "Category" :
                        Cat = line[2]
                        if Cat in P.category :
                            print(Cat + " alredy set. Doing nothing.")
                        else :
                            if ObjectExists(Cursor,"Category","CatName",Cat) :                       
                                Values = [ObjName,Cat]
                                InsertRow(Cursor,"PkgCat",PkgCatCols,QuoteList(Values,'"'))  
                            else:
                                print("Category " + Cat + " does not exist. Transaction aborted.")
                    elif Field == "Platforms" :
                        # Delete from the database all platforms for this program
                        for p in PLATFORMS.keys() :
                            Values = [ObjName,PLATFORMS[p]]
                            DeleteRow(Cursor,"PkgPlat",PkgPlatCols,Values)                             
                        # Add only those platforms specified using the abbreviations from the transaction file
                        plat = line[2]
                        abbreviations = plat.strip().split(":")
                        plat = []
                        for a in abbreviations :
                            if a in PLATFORMS.keys() :
                                plat.append(PLATFORMS[a])
                        for p in plat :
                            Values = [ObjName,p]
                            InsertRow(Cursor,"PkgPlat",PkgPlatCols,QuoteList(Values,'"'))
                    elif Field == "Installation" :
                        P.installation = line[2]
                        if P.installation in ["BIRCH","local"] :
                            SetField(Cursor,"Package","Name",ObjName,"Installation",P.installation) 
                        else :
                            print("Illegal value for Package.installation: " + P.installation)
                    else :
                        pass
                elif line[0] == "UNSET" :
                    Field = line[1]
                    if Field == "Description" :
                        UnSetField(Cursor,"Package","Name",ObjName,"Description")
                    elif Field == "Package" :
                        DeleteRow(Cursor,"PkgProg",["Package"],[ObjName])                        
                    elif Field == "Documentation" :
                        DeleteRow(Cursor,"PkgDoc",["Documentation"],[line[2]]) 
                    elif Field == "Data" :
                        DeleteRow(Cursor,"PkgDat",["Data"],[line[2]]) 
                    elif Field == "Platform" :
                        # Delete from the database all platforms for this package
                        for p in PLATFORMS.keys() :
                            Values = [ObjName,PLATFORMS[p]]
                            DeleteRow(Cursor,"PkgPlat",PkgPlatCols,Values)

                    elif Field == "Category" :
                        DeleteRow(Cursor,"PkgCat",["Category"],[line[2]])   
                    elif Field == "Installation" :
                        print("Program.Installation cannot be NULL. Must be BIRCH or local.")

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
    def ShowFile(self,Cursor,outfile,ObjClass,ObjName) :
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " doesn't exist. Canceling transaction")
        else:
            F = File()
            F.GetFileData(Cursor,ObjClass,ObjName)
            F.WriteFileData(ObjClass,ObjName,outfile)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
    def ShowCategory(self,Cursor,outfile,ObjClass,ObjName) :
        if not ObjectExists(Cursor,ObjClass,"CatName",ObjName) :
            print(ObjClass + " " + ObjName + " doesn't exist. Canceling transaction")
        else:
            C = Category()
            C.GetCategoryData(Cursor,ObjClass,ObjName)
            C.WriteCategoryData(ObjClass,ObjName,outfile)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
    def ShowPackage(self,Cursor,outfile,ObjClass,ObjName) :
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " doesn't exist. Canceling transaction")
        else:
            P = Package()
            P.GetPackageData(Cursor,ObjClass,ObjName)
            P.WritePackageData(ObjClass,ObjName,outfile)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
    def ShowProgram(self,Cursor,outfile,ObjClass,ObjName) :
        if not ObjectExists(Cursor,ObjClass,"Name",ObjName) :
            print(ObjClass + " " + ObjName + " doesn't exist. Canceling transaction")
        else:
            P = Program()
            P.GetProgramData(Cursor,ObjClass,ObjName)
            P.WriteProgramData(ObjClass,ObjName,outfile)

#======================== MAIN PROCEDURE ==========================

def main():
    """
    Called when not in documentation mode.
    """
    print ("***** Obtain parameters from environment and command line.")
    P = Parameters()

    # Open output file, if specified.
    if not P.OFN == "" :
        outfile = open(P.OFN,"w")

    # parse input lines into tokens
    Inp = InputTokens()
    Inp.read_lines(P.IFN)
    
    # read transactions as a groups of lines from Inp
    T = []
    while Inp.finish < len(Inp.tlines) :
        t = Inp.get_next_transaction()              
        # We'll probably do some sanity checking here before
        # adding the next transaction object
        T.append(t)
    if DEBUG :
        for t in T :
            print("---------- Transaction ----------")
            for l in t.lines :
                print(str(l))              
    
    # Open connections to database
    Connection = sqlite3.connect(P.DBFN)
    Cursor = Connection.cursor()
    Cursor.execute("PRAGMA foreign_keys = ON;") # req'd for CASCADE

    # Create SQL for each transaction
    for t in T :
        print(str(t.lines[0]))
        ObjAction = t.lines[0][0]
        ObjClass = t.lines[0][1]
        ObjName = t.lines[0][2]
        if ObjClass == "PROGRAM" :  
            if ObjAction == "INSERT_INTO" :
                t.InsertProgram(Cursor,"Program",ObjName)
            elif ObjAction == "DELETE" :
                t.DeleteObject(Cursor,"Program","Name",ObjName)
            elif ObjAction == "UPDATE" :
                t.UpdateProgram(Cursor,ObjName)  
            elif ObjAction == "SHOW" :
                t.ShowProgram(Cursor,outfile,"Program",ObjName)
            else :
                pass      
        elif ObjClass == "PACKAGE" :
            if ObjAction == "INSERT_INTO" :
                t.InsertPackage(Cursor,"Package",ObjName)
            elif ObjAction == "DELETE" :
                t.DeleteObject(Cursor,"Package","Name",ObjName)
            elif ObjAction == "UPDATE" :
                t.UpdatePackage(Cursor,ObjName)
            elif ObjAction == "SHOW" :
                t.ShowPackage(Cursor,outfile,"Package",ObjName)
            else :
                pass
        elif ObjClass == "CATEGORY" :
            if ObjAction == "INSERT_INTO" :
                t.InsertCategory(Cursor,"Category",ObjName)
            elif ObjAction == "DELETE" :
                t.DeleteObject(Cursor,"Category","CatName",ObjName)
            elif ObjAction == "SHOW" :
                t.ShowCategory(Cursor,outfile,"Category",ObjName)
            else :
                pass
        elif ObjClass == "FILE" :
            if ObjAction == "INSERT_INTO" :
                t.InsertFile(Cursor,"File",ObjName)
            elif ObjAction == "DELETE" :
                t.DeleteObject(Cursor,"File","Name",ObjName)
            elif ObjAction == "UPDATE" :
                t.UpdateFile(Cursor,ObjName)                
            elif ObjAction == "SHOW" :
                t.ShowFile(Cursor,outfile,"File",ObjName)  
            else :
                pass
        else :
            pass 

    # execute the SQL to change the database
    if P.COMMIT :
        Connection.commit()

    Connection.close()

    # Open output file, if specified.
    if not P.OFN == "" :
        outfile.close()

if __name__=="__main__":
    if (BM.documentor() or "-test" in sys.argv):
        pass
    else:
        main()
        BM.exit_success()
