#!/usr/bin/python
#
# hubzero-app
# Nicholas J. Kisseberth <nkissebe@purdue.edu>
#
# Copyright 2010 by Purdue Research Foundation, West Lafayette, IN 47906
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License 
# along with this program. If not, see <http://www.gnu.org/licenses/>.

#
# Beta version, known to be incomplete and not to gracefully
# handle many error conditions
#

# hubzero-app --appsdir=DIR install FILE
# hubzero-app --appsdir=DIR install --publish FILE
# hubzero-app --appsdir=DIR uninstall APP
# hubzero-app --appsdir=DIR uninstall APP --all
# hubzero-app --appsdir=DIR uninstall APP --revision REVISION
# hubzero-app --appsdir=DIR publish APP
# hubzero-app --appsdir=DIR publish APP --all
# hubzero-app --appsdir=DIR publish APP --revision REVISION
# hubzero-app --appsdir=DIR unpublish APP
# hubzero-app --appsdir=DIR unpublish APP --revision REVISION
# hubzero-app --appsdir=DIR unpublish APP --all
# hubzero-app --appsdir=DIR list 
# hubzero-app --appsdir=DIR list --all
# hubzero-app --appsdir=DIR list --published
# hubzero-app --appsdir=DIR list --unpublished
# hubzero-app --appsdir=DIR list APP
# hubzero-app --appsdir=DIR list APP --all
# hubzero-app --appsdir=DIR list APP --published
# hubzero-app --appsdir=DIR list APP --unpublished
# hubzero-app --appsdir=DIR setup

from optparse import OptionParser
from xml.dom.minidom import parse, parseString
import hubzero, pwd, tarfile, os, sys, shutil, stat

class Hubzero_App_Manifest:

  def __init__(self):
    self.dom = None

  def __getdata(self,dom):
    try:
      data = dom[0].childNodes[0].data
    except:
      return ''

    return str(data)

  def read(self,manifest):
    self.dom = parse(manifest)

    app = self.dom.getElementsByTagName("app")[0]
    
    self.id = self.__getdata(app.getElementsByTagName("id"))
    self.title = self.__getdata(app.getElementsByTagName("title"))
    self.description = self.__getdata(app.getElementsByTagName("description"))
    self.about = self.__getdata(app.getElementsByTagName("about"))
    self.version = self.__getdata(app.getElementsByTagName("version"))
    self.revision = self.__getdata(app.getElementsByTagName("revision"))
    self.geometry = self.__getdata(app.getElementsByTagName("geometry"))
    self.timeout = self.__getdata(app.getElementsByTagName("timeout"))
    self.command = self.__getdata(app.getElementsByTagName("command"))
    self.middleware = self.__getdata(app.getElementsByTagName("middleware"))
    self.modified = self.__getdata(app.getElementsByTagName("modified"))
    self.author = self.__getdata(app.getElementsByTagName("author"))
    self.email = self.__getdata(app.getElementsByTagName("email"))

    if self.id == 'bin' or self.id == 'environ':
      raise Exception

class Hubzero_App_Tables:

  def __init__(self, db):
    self.db = db

  def readToolVersion(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("SELECT toolname,title,description,`fulltext`,version,revision,vnc_geometry,vnc_timeout,vnc_command,mw FROM jos_tool_version WHERE toolname=" + self.db.string_literal( appid ) + " AND revision=" + self.db.string_literal(apprevision) + ";")
      
      if result == 0:
        return None

      print result
      toolname,title,description,fulltext,version,revision,vnc_geometry,vnc_timeout,vnc_command,mw = cursor.fetchone()
      
      manifest = Hubzero_App_Manifest()
      manifest.id = str(toolname)
      manifest.title = title
      manifest.description = description
      manifest.about = fulltext
      manifest.version = version
      manifest.revision = str(revision)
      manifest.geometry = vnc_geometry
      manifest.timeout = vnc_timeout
      manifest.command = vnc_command
      manifest.middleware = mw
      return manifest
    except:
      raise
  
  def getToolRevisionList(self,appid):
    try:
      cursor = self.db.cursor()
      cursor.execute("SELECT revision FROM jos_tool_version WHERE toolname=" + self.db.string_literal( appid ) + ";")
      results = cursor.fetchall()
      resultList = []
      for result in results:
        resultList.append(str(result[0]))
      return resultList
    except:
      raise
      return None

  def getToolId(self,appid):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("SELECT id FROM jos_tool WHERE toolname=" + self.db.string_literal( appid ) + ";")
      if (result == 0):
        return None
      result, = cursor.fetchone()
      return result
    except:
      raise

  def reserveAppName(self,name):
    toolid = self.getToolId(name)

    if toolid != None:
      return toolid

    try:
      cursor = self.db.cursor()
      result = cursor.execute("INSERT INTO jos_tool (toolname,registered,registered_by,state_changed) VALUE ("+ self.db.string_literal(name) + "," + "NOW(),'admin',NOW());")
      return self.db.insert_id()
    except:
      raise

  def getToolVersionId(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("SELECT id FROM jos_tool_version WHERE toolname=" + self.db.string_literal( appid ) + " AND revision=" + self.db.string_literal( apprevision ) + ";")
      if (result == 0):
        return None
      result, = cursor.fetchone()
      return result
    except:
      raise

  def installTool(self,appid,apptitle):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("INSERT INTO jos_tool (toolname,title,registered,registered_by,state_changed) VALUE ("+ self.db.string_literal(appid) + "," + self.db.string_literal(apptitle) + ",NOW(),'admin',NOW());")
      return self.db.insert_id()
    except:
      raise

  def installToolVersion(self,manifest):
    toolversionid = self.getToolVersionId(manifest.id,manifest.revision)
    if toolversionid != None:
      if self.updateToolVersion(manifest):
        return toolversionid
      else:
        return None

    toolid = self.getToolId(manifest.id)
    if toolid == None:
      toolid = self.installTool(manifest.id,manifest.title)
      if toolid == None:
        return None

    cursor = self.db.cursor()
    appid = self.db.string_literal( manifest.id )
    apptitle = self.db.string_literal( manifest.title )
    instance = self.db.string_literal( manifest.id + '_' + manifest.revision )
    apprevision = self.db.string_literal( manifest.revision )
    appdescription = self.db.string_literal( manifest.description )
    appabout = self.db.string_literal( manifest.about )
    appversion = self.db.string_literal( manifest.version )
    appgeom = self.db.string_literal( manifest.geometry )
    apptimeout = self.db.string_literal( manifest.timeout )
    appcommand = self.db.string_literal( manifest.command )
    appmw = self.db.string_literal( manifest.middleware )
    toolid = str(toolid)
    if (apptimeout == ''):
      apptimeout='NULL'
    if (appcommand == ''):
      appcommand='NULL'
    if (appmw == ''):
      appmw = 'NULL'

    cursor.execute("INSERT INTO jos_tool_version (toolname,instance,title,description,`fulltext`,`version`,revision,vnc_geometry,vnc_timeout,vnc_command,mw,toolid) VALUE (" + appid + "," + instance + "," + apptitle + "," + appdescription + "," + appabout + "," + appversion + "," + apprevision  + "," + appgeom + "," + apptimeout + "," + appcommand + "," + appmw + "," + toolid + ");")
    return self.db.insert_id()


  def updateToolVersion(self, manifest):
    print "tool version already exists, updating manifest data"
    sys.exit(0)
    try:
      cursor = self.db.cursor()
      toolid = self.getToolId(appid)
      toolid = self.db.string_literal( str(toolid) )
      appid = self.db.string_literal(appid)
      apprevision = self.db.string_literal(apprevision)
      result = cursor.execute("INSERT INTO jos_tool_version (toolname,revision,toolid) VALUE ("+ appid + "," + apptitle + "," + toolid + ");")
      return result
    except:
      return None


  def deleteToolVersion(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("DELETE FROM jos_tool_version WHERE toolname=" + self.db.string_literal( appid ) + " AND revision=" + self.db.string_literal( apprevision ) + ";")
      return result
    except:
      raise

  def deleteTool(self,appid):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("DELETE FROM jos_tool WHERE toolname=" + self.db.string_literal( appid ) + ";")
      return result
    except:
      raise

  def publishToolVersion(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("UPDATE jos_tool_version SET released_by='admin', released=NOW(), unpublished=NULL, state='1' WHERE toolname=" + self.db.string_literal( appid ) + " AND revision=" + self.db.string_literal(apprevision) + ";")
      return result
    except:
      raise

  def publishDevToolVersion(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("UPDATE jos_tool_version SET released_by='admin', released=NOW(), unpublished=NULL, state='3' WHERE toolname=" + db.string_literal( appid ) + " AND revision=" + db.string_literal(apprevision) + ";")
      return result
    except:
      return None

  def unpublishToolVersion(self,appid,apprevision):
    try:
      cursor = self.db.cursor()
      result = cursor.execute("UPDATE jos_tool_version SET released_by='admin', released=NOW(), unpublished=NOW(), state='0' WHERE toolname=" + self.db.string_literal( appid ) + " AND revision=" + self.db.string_literal(apprevision) + ";")
      return result
    except:
      raise

  def getNumToolVersions(self,appid):
    try:
      cursor = self.db.cursor()
      cursor.execute("SELECT COUNT(*) FROM jos_tool_version WHERE toolname=" + self.db.string_literal(appid) + ";")
      result, = cursor.fetchone()
      return result
    except:
      raise

class Hubzero_App_Package:

  def __init__(self, config = None):
    self.hza = None
    self.manifest = None
    self.hzat = None
    self.config = config

  def __config(self):
    if self.config == None:
      self.config = hubzero.Hubzero_Config()

  def open(self,package):
    self.hza = tarfile.open(package)
    try:
      flo = self.hza.extractfile('manifest.xml')
    except:
      flo = self.hza.extractfile('./manifest.xml')

    self.manifest = Hubzero_App_Manifest()
    self.manifest.read(flo)
    self.__config()
    self.db = self.config.getHubzeroDatabase()
    self.hzat = Hubzero_App_Tables(self.db)
    if self.manifest.id == 'bin' or self.manifest.id == 'environ':
      raise Exception

  def load(self,appid,appversion):
    self.hza = None
    self.__config()
    self.db = self.config.getHubzeroDatabase()
    self.hzat = Hubzero_App_Tables(self.db)
    self.manifest = self.hzat.readToolVersion(appid,appversion)
    if self.manifest == None:
      self.manifest = Hubzero_App_Manifest()
      self.manifest.id = appid
      self.manifest.revision = appversion
    if self.manifest.id == 'bin' or self.manifest.id == 'environ':
      raise Exception

  def install(self, appsdir = "/apps"):
    if not self.hzat:
      return None

    appid = self.manifest.id
    apprevision = self.manifest.revision
    try:
      os.mkdir(appsdir + '/' + appid)
      os.mkdir(appsdir + '/' + appid + '/' + apprevision)
    except:
      pass
    if (self.hza):
      self.hza.extractall(appsdir + '/' + appid + '/' + apprevision)
    self.hzat.installToolVersion(self.manifest)

  def uninstall(self, appsdir = "/apps"):
    appid = self.manifest.id
    apprevision = self.manifest.revision
    print appid
    appdir = appsdir +'/' + appid
    revdir = appdir + '/' + self.manifest.revision
    if os.path.isdir(revdir):
      shutil.rmtree(revdir)
    self.hzat.deleteToolVersion(self.manifest.id,self.manifest.revision)
    numVersions = self.hzat.getNumToolVersions(self.manifest.id)
    print numVersions
    if (numVersions == 0):
      self.hzat.deleteTool(self.manifest.id)

    if (os.path.isdir(appdir)):
      files = os.listdir(appdir)
      numfiles = len(files)
      if (numfiles == 0):
        os.rmdir(appdir)

  def publish(self, appsdir = "/apps"):
    if not self.hzat:
      return None

    apprevision = self.manifest.revision
    appid = self.manifest.id
    try:
      wd = os.getcwd()
      os.chdir(appsdir  + '/' + appid)
      os.unlink('current')
      os.symlink(apprevision,'current')
      os.chdir(wd)
    except:
      pass
    self.hzat.publishToolVersion(self.manifest.id,self.manifest.revision)

  def unpublish(self, appsdir = "/apps"):
    if not self.hzat:
      return None

    apprevision = self.manifest.revision
    appid = self.manifest.id
    try:
      wd = os.getcwd()
      os.chdir(appsdir  + '/' + appid)
      #os.unlink('current')
      #os.symlink(apprevision,'current')
      os.chdir(wd)
    except:
      pass
    self.hzat.unpublishToolVersion(self.manifest.id,self.manifest.revision)

class Hubzero_App:
  def __init__(self,config = None):
    self.appsdir = '/apps'
    if config == None:
      config = hubzero.Hubzero_Config()
    self.config = config

  def changeuid(self):
    try:
      _,_,uid,gid,_,_,_ = pwd.getpwnam("apps")

      if (os.getuid() == 0):
        os.setregid(gid,gid)
        os.setreuid(uid,uid)

      if (os.getuid() != uid):
        raise Exception
    except:
      raise

  def dispatch(self,argv = None):
    parser = OptionParser()
    parser.disable_interspersed_args()
    parser.add_option("-d","--appsdir",dest="appsdir",default=self.appsdir)
    (options, args) = parser.parse_args(argv)
    self.appsdir = os.path.abspath(options.appsdir)

    if args[0] != "setup":
      self.changeuid()

    if (args[0] == 'install'):
      self.install(args[1:])
    elif (args[0] == 'uninstall'):
      self.uninstall(args[1:])
    elif (args[0] == 'publish'):
      self.publish(args[1:])
    elif (args[0] == 'unpublish'):
      self.unpublish(args[1:])
    elif (args[0] == 'list'):
      self.list(args[1:])
    elif (args[0] == 'setup'):
      self.setup(args[1:])

  def install(self,argv):
    parser = OptionParser()
    parser.disable_interspersed_args()
    parser.add_option("-p","--publish",action="store_true", dest="publish",default=False)
    (options, args) = parser.parse_args(argv)

    hzap = Hubzero_App_Package(self.config)
    hzap.open(args[0])
    hzap.install(self.appsdir)

    if (options.publish):
      hzap.publish(self.appsdir)

  def uninstall(self,argv):
    parser = OptionParser()
    parser.disable_interspersed_args()
    parser.add_option("-a","--all",action="store_true", dest="all",default=False)
    parser.add_option("-r","--revision",dest="revision")
    (options, args) = parser.parse_args(argv)

    appid = args[0]
    appdir = os.path.abspath(self.appsdir + '/' + appid)

    hzap = Hubzero_App_Package(self.config)

    versions = []

    if options.all:
      print "comput all possible versions from directory and table"
    elif options.revision:
      versions.append(options.revision)
    else:
      if os.path.isdir(appdir):
        files = os.listdir(appdir)
        numfiles = len(files)
        if (numfiles == 1):
          versions.append(files[0])
        else:
          print "multiple versions available. use --all to uninstall all versions or --version VERSION to uninstall a specific version"
          sys.exit(0)
      else:
        hzat = Hubzero_App_Tables(self.config.getHubzeroDatabase())
        versions = hzat.getToolRevisionList(appid)

    if versions:
      for version in versions:
        hzap.load(appid,version)
        hzap.uninstall(self.appsdir)
    else:
      print "No apps were found to uninstall"   

  def publish(self,argv):
    parser = OptionParser()
    parser.disable_interspersed_args()
    parser.add_option("-a","--all",action="store_true", dest="all",default=False)
    parser.add_option("-r","--revision",dest="revision")
    (options, args) = parser.parse_args(argv)

    appid = args[0]
    appdir = os.path.abspath(self.appsdir + '/' + appid)

    hzap = Hubzero_App_Package(self.config)

    versions = []

    if options.all:
      print "comput all possible versions from directory and table"
    elif options.revision:
      versions.append(options.revision)
    else:
      if os.path.isdir(appdir):
        files = os.listdir(appdir)
        numfiles = len(files)
        if (numfiles == 1):
          versions.append(files[0])
        else:
          print "multiple versions available. use --all to uninstall all versions or --version VERSION to uninstall a specific version"
          sys.exit(0)
      else:
        hzat = Hubzero_App_Tables(self.config.getHubzeroDatabase())
        versions = hzat.getToolRevisionList(appid)

    if versions:
      for version in versions:
        hzap.load(appid,version)
        hzap.publish()
    else:
      print "No apps were found to publish"   


  def unpublish(self,argv):
    parser = OptionParser()
    parser.disable_interspersed_args()
    parser.add_option("-a","--all",action="store_true", dest="all",default=False)
    parser.add_option("-r","--revision",dest="revision")
    (options, args) = parser.parse_args(argv)

    appid = args[0]
    appdir = os.path.abspath(self.appsdir + '/' + appid)

    hzap = Hubzero_App_Package(self.config)

    versions = []

    if options.all:
      print "comput all possible versions from directory and table"
    elif options.revision:
      versions.append(options.revision)
    else:
      if os.path.isdir(appdir):
        files = os.listdir(appdir)
        numfiles = len(files)
        if (numfiles == 1):
          versions.append(files[0])
        else:
          print "multiple versions available. use --all to uninstall all versions or --version VERSION to uninstall a specific version"
          sys.exit(0)
      else:
        hzat = Hubzero_App_Tables(self.config.getHubzeroDatabase())
        versions = hzat.getToolRevisionList(appid)

    if versions:
      for version in versions:
        hzap.load(appid,version)
        hzap.unpublish()
    else:
      print "No apps were found to unpublish"   

  def setup(self,argv):
    parser = OptionParser()
    parser.enable_interspersed_args()
    (options, args) = parser.parse_args(argv)
    
    apps_uid = -1
    apps_gid = -1

    try:
      _,_,apps_uid,_,_,_,_ = pwd.getpwnam("apps")  
      _,_,apps_gid,_ = grp.getgrnam("apps")
    except:
      pass

    if not os.path.isdir(self.appsdir):
      if os.path.exists(self.appsdir):
        print "The path " + self.appsdir + " exists, but is not a directory. Skipping..."
      else:
        try:
          os.mkdir(self.appsdir)
        except:
          pass

    if os.path.isdir(self.appsdir):
      try:
        os.chmod(self.appsdir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
        os.chown(self.appsdir, apps_uid, apps_gid)
      except:
        pass

    bindir = self.appsdir + '/' + 'bin'

    if not os.path.isdir(bindir):
      if os.path.exists(bindir):
        print "The path " + bindir + " exists, but is not a directory. Skipping..."
      else:
        try:
          os.mkdir(bindir)
        except:
          pass

    if os.path.isdir(bindir):
      try:
        os.chmod(bindir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
        os.chown(bindir, apps_uid, apps_gid)
      except:
        pass

    envdir = self.appsdir + '/' + 'environ'

    if not os.path.isdir(envdir):
      if os.path.exists(envdir):
        print "The path " + envdir + " exists, but is not a directory. Skipping..."
      else:
        try:
          os.mkdir(envdir)
        except:
          pass

    if os.path.isdir(envdir):
      try:
        os.chmod(envdir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
        os.chown(envdir, apps_uid, apps_gid)
      except:
        pass

    hzat = Hubzero_App_Tables(self.config.getHubzeroDatabase())
    hzat.reserveAppName('bin')
    hzat.reserveAppName('environ')
    
  def list(self,argv):
    parser = OptionParser()
    parser.enable_interspersed_args()
    parser.add_option("-a","--all",action="store_true", dest="all",default=False)
    parser.add_option("-p","--published",action="store_true", dest="publish",default=False)
    parser.add_option("-u","--unpublished",action="store_true", dest="publish",default=False)
    parser.add_option("-r","--revision",dest="revision")
    (options, args) = parser.parse_args(argv)

    appid = args[0]
    appdir = os.path.abspath(self.appsdir + '/' + appid)

    hzap = Hubzero_App_Package(self.config)

    fs_versions = []
    known_versions = []
    db_versions = []

    if os.path.isdir(appdir):
      files = os.listdir(appdir)
      for file in files:
        if os.path.isdir(appdir + '/' + file):
          fs_versions.append(file)
    hzat = Hubzero_App_Tables(self.config.getHubzeroDatabase())
    db_versions = hzat.getToolRevisionList(appid)
    known_versions = set(db_versions)
    for fs_version in fs_versions:
      known_versions.add(fs_version)

    if options.all:
      for version in known_versions:
        if version in db_versions:
          #hzap.load(appid,str(version))
          print appid + " " + str(version) 
        else:
          print appid + " " + str(version) + " in " + appdir + " only, not installed"
    else:
      if (str(options.revision) in known_versions):
        if options.revision in db_versions:
          #hzap.load(appid,str(version))
          print appid + " " + str(options.revision) 
        else:
          print appid + " " + str(options.revision) + " in " + appdir + " only, not installed"

Hubzero_App().dispatch()

