#!/usr/bin/python
#
# @package      hubzero-app
# @file         hubzero-app
# @author       Nicholas J. Kisseberth <nkissebe@purdue.edu>
# @copyright    Copyright (c) 2010-2014 HUBzero Foundation, LLC.
# @license      http://www.gnu.org/licenses/lgpl-3.0.html LGPLv3
#
# Copyright (c) 2010-2014 HUBzero Foundation, LLC.
#
# This file is part of: The HUBzero(R) Platform for Scientific Collaboration
#
# The HUBzero(R) Platform for Scientific Collaboration (HUBzero) 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.
#
# HUBzero 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/>.
#
# HUBzero is a registered trademark of HUBzero Foundation, LLC.
#

#
# 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

from optparse import OptionParser
from xml.dom.minidom import parse, parseString
import pwd, tarfile, os, sys, shutil, stat
import os, ConfigParser, re, MySQLdb, string
import hubzero.utilities.misc

class Hubzero_Database:
  def __init__(self, config = None):
    self.config(config)

  def config(self, config = None):
    if config:
      self.host = config.getJoomlaConfigParam('host')
      self.user = config.getJoomlaConfigParam('user')
      self.password = config.getJoomlaConfigParam('password')
      self.database = config.getJoomlaConfigParam('db')
      self.prefix = config.getJoomlaConfigParam('dbprefix')
    else:
      self.config = None
      self.host = None
      self.user = None
      self.password = None
      self.database = None
      self.prefix = None

  # connect() should handle all parameters legal to MySQLdb.connect and pass them along
  # but for now we keep it simple

  def connect(self):
    return MySQLdb.connect(host=self.host, user=self.user, passwd=self.password, db=self.database)

class Hubzero_Config:
  def __init__(self, name='default'):
    self.data = { 'component':{}, 'plugin':{}, 'module':{} }
    self.db = None

    try:
      if (name[0] == '/'):
        documentroot = name
      else:
        config = ConfigParser.RawConfigParser()
        config.read('/etc/hubzero.conf')
        site = config.get('DEFAULT','site')
        documentroot = config.get(site,'DocumentRoot')
      self.data['DocumentRoot'] = documentroot
    except:
      pass

  def __connect(self):
    try:
      if not self.db:
        hzdb = Hubzero_Database(self)
        self.db = hzdb.connect()
    except:
      pass

  def __readJoomlaConfig(self):
    self.data['joomla'] = {}

    try:
      jconfig = open(self.data['DocumentRoot'] + '/configuration.php')
      contents = jconfig.read()
      jconfig.close()
      for m in re.finditer("\s*(?:var|public)\s+\$([a-zA-Z-_0-9]+)\s*=\s*(.+)\s*;", contents):
        self.data['joomla'][m.group(1)] = m.group(2).strip(" \'\"\t")
    except:
      pass

  def __readHubzeroConfig(self):
    self.data['hubzero'] = {}

    try:
      hconfig = open(self.data['DocumentRoot'] + '/hubconfiguration.php')
      contents = hconfig.read()
      hconfig.close()
      for m in re.finditer("\s*(?:var|public)\s+\$([a-zA-Z-_0-9]+)\s*=\s*(.+)\s*;", contents):
        self.data['hubzero'][m.group(1)] = m.group(2).strip(" \'\"\t")
    except:
      pass

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

  def getHubzeroDatabase(self):
    self.__connect()
    return self.db

  def getJoomlaConfigParam(self,key):
    if ('joomla' not in self.data):
      self.__readJoomlaConfig()

    try:
      return self.data['joomla'][key]
    except:
      return None

  def getHubzeroConfigParam(self,key):
    if ('hubzero' not in self.data):
      self.__readHubzeroConfig()

    try:
      return self.data['hubzero'][key]
    except:
      return None

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"))

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,`fulltxt`,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 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()
      appid = self.db.string_literal(appid)
      cursor.execute("INSERT INTO jos_resources (id,title,type,logical_type,`introtext`,`fulltxt`,footertext,created,created_by,modified,modified_by,published,publish_up,publish_down,access,hits,path,checked_out,checked_out_time,standalone,group_owner,group_access,rating,times_rated,params,attribs,alias,ranking) VALUES (NULL," + appid + ",7,0," + appid + "," + appid + ",'',NOW(),62,NOW(),62,1,NOW(),'0000-00-00 00:00:00',0,0,'',0,'0000-00-00 00:00:00',1,'',NULL,'0.0',0,'',''," + appid + ",0);")
      result = cursor.execute("INSERT INTO jos_tool (toolname,title,registered,registered_by,state,state_changed) VALUE ("+ appid + "," + self.db.string_literal(apptitle) + ",NOW(),'admin','7',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 + '_r' + 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,`fulltxt`,`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_resources WHERE type='7' AND alias=" + self.db.string_literal( appid ) + ";")
      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_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)

  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

  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 + '/r' + apprevision)
    except:
      pass
    if (self.hza):
      self.hza.extractall(appsdir + '/' + appid + '/r' + 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 + '/r' + 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_Config()
    self.config = config

  def webGroup(self):
    if hubzero.utilities.misc.isRHEL():
        return "apache"
    elif hubzero.utilities.misc.isDebian():
        return "www-data"

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

      if (os.getuid() == 0):
        os.setregid(gid,gid)
        os.setgroups([gid1])
        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)

    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:])

  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 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()

