########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Common/Install/InstallUtil.py,v 1.72 2005/04/07 00:18:47 jkloth Exp $
"""
Definitions and functions used in the installation process

Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import sys, os, sha, urllib, stat
from Ft.Server.Common import Schema, AclConstants
from Ft.Server.Common.ResourceTypes import ResourceType
from Ft.Lib import Time, ProgressIndicator
from distutils import file_util, dep_util

from Ft.Xml import XPath, Domlette, ReaderException, EMPTY_NAMESPACE
from Ft.Xml.Xslt import XSL_NAMESPACE
from Ft.Lib import Uri
from Ft.Lib import Resolvers
from Ft.Xml import InputSource


class StatusFlags:
    UNKNOWN = -1
    NO_CHANGE = 0
    NOT_PRESENT = 1
    CONTENT_CHANGE = 2
    IMT_CHANGE = 4
    ACL_CHANGE = 8
    OWNER_CHANGE = 16
    OTHER_CHANGE = 32


    ACTION_REQUIRED = NOT_PRESENT | CONTENT_CHANGE | IMT_CHANGE | ACL_CHANGE | OWNER_CHANGE | OTHER_CHANGE


g_safeAclIdents = [AclConstants.SUPER_USER_GROUP_NAME,
                   AclConstants.WORLD_GROUP_NAME,
                   AclConstants.USERS_GROUP_NAME,
                   AclConstants.ANONYMOUS_USER_NAME,
                   AclConstants.OWNER]


class IndicatorBase:
    indicator = None
    def _message(self, msg, quiet):
        if not quiet:
            if self.indicator:
                self.indicator.message(msg)
            else:
                print msg

    def _setIndicator(self, indicator):
        """
        Store information about where we are in the indicator list
        """
        self.indicator = indicator


class RawFile(IndicatorBase):
    resourceType = ResourceType.RAW_FILE

    contentUri = None
    stringContent = None
    stringLastModified = None  #specified in iso 8601
    lmds = None
    owners = {}
    imts = {}

    def __init__(self, path, acl, owner, imt):
        self.path = path
        self.absPath = path + ';no-traverse'

        #Cache any changes to ACL
        self._aclToUpdate = {}
        self._aclToRemove = []

        self.owner = owner
        self.imt = imt or 'text/plain'

        self.status = StatusFlags.UNKNOWN
        self.dependencies = []
        if acl is None:
            acl = []
        self._setAcl(acl)
        return

    def _setAcl(self, acl):
        """
        Helper function for test harnesses only
        """
        self.acl = acl
        self.aclDict = {}
        for access, ident, allowed in acl:
            if access not in self.aclDict:
                self.aclDict[access] = {}
            self.aclDict[access][ident] = allowed
        return

    def setPath(self, contentUri):
        self.contentUri = contentUri
        return

    def setSource(self, stringContent, stringLastModified):
        self.stringContent = stringContent
        self.stringLastModified = stringLastModified
        return

    def checkStatus(self, repo, force=0, checkAcl=1):
        """
        Get the status of this resource from against the supplied repo,
        Note, force here specifies that we force update to the status.  Not force install
        """

        if not force and self.status != StatusFlags.UNKNOWN:
            return self.status

        self._initRdfCaches(repo)

        if self.path not in self.lmds:
            self.status = StatusFlags.ACTION_REQUIRED

            #Force the ACL changes
            self._aclToUpdate = self.aclDict

            #Exit here
            return self.status
        self.status = StatusFlags.NO_CHANGE

        # Compare the file times.
        localTime = None
        if self.contentUri is not None:
            urlfile = Uri.UrlOpen(self.contentUri)
            try:
                mtime = os.fstat(urlfile.fileno())[stat.ST_MTIME]
            except OSError:
                # Window's socket streams cannot be stat'ed
                # Get the last modified date from the headers, if possible
                modified = urlfile.info().getheader('Last-Modified')
                if modified:
                    datetime = Time.FromRFC822(modified)
                else:
                    # Unable to determine last modified date, use current
                    # time to force an update
                    datetime = Time.FromPythonTime()
            else:
                datetime = Time.FromPythonTime(mtime)

            urlfile.close()
            localTime = datetime.asISO8601DateTime()
        elif self.stringLastModified is not None:
            localTime = self.stringLastModified

        if localTime is not None:
            # Compare it with the repo.  Some things (i.e., containers) don't
            # have a modified date check.
            if localTime > self.lmds[self.path]:
                self.status = self.status | StatusFlags.CONTENT_CHANGE

        # Check for other kinds of changes
        if checkAcl:
            self._checkAclChange(repo)

        self._checkImtChange(repo)
        self._checkOwnerChange(repo)
        self._checkOtherChange(repo)

        return self.status

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create RawFile: %s IMT=%s"%(self.path, self.imt), quiet)
        repo.createRawFile(self.path, self.imt, self._getContent())
        #Don't need to reset this on a create
        self.status &= ~StatusFlags.IMT_CHANGE
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        self._message("Modify %s content"%self.path, quiet)
        repo.fetchResource(self.path).setContent(self._getContent())
        return

    def updateImt(self, repo, quiet):
        """
        Called when we need to update our imt in the repo
        """
        self._message("Setting IMT of %s --> %s" % (self.path, self.imt), quiet)
        repo.fetchResource(self.absPath).setImt(self.imt)
        return

    def updateOwner(self, repo, quiet):
        """
        Called when we need to update our owner in the repo
        """
        self._message("Setting Owner of %s --> %s" % (self.path, self.owner), quiet)
        owner = repo.fetchUserOrGroupByName(self.owner)
        #Owner will be None if 'world'
        if owner:
            repo.fetchResource(self.absPath).setOwner(owner)
        return

    def updateAcl(self, repo, quiet):
        """
        Called when we need to update our acl in the repo
        """
        res = repo.fetchResource(self.absPath)
        for access in self._aclToRemove:
            self._message("Removing ACL: %s from %s" % (access, self.path), quiet)
            res.inheritAcl(access)

        for access, info in self._aclToUpdate.items():
            self._message("Setting ACL: %s for %s" % (access, self.path), quiet)
            l = info.items()
            for ident, allowed in l[:1]:
                res.setAcl(access, ident, allowed)
            for ident, allowed in l[1:]:
                res.addAcl(access, ident, allowed)
        return

    def updateOther(self, repo, quiet):
        """
        Called when we need to update our other stuff in the repo
        """
        #The resource type might be changing, in which case we need to abort
        #the update, delete the existing resource, and install the new.
        #Aborting at this point could get ugly, so for now, just raise an
        #exception. FIXME!
        doc = repo.fetchResource(self.absPath)
        if doc.resourceType != self.resourceType:
            raise RuntimeError("Resource type change for %s not supported; please delete it first." % self.path)
        return

    def _getContent(self):
        if self.stringContent:
            return self.stringContent
        elif self.contentUri:
            urlfile = Uri.UrlOpen(self.contentUri)
            content = urlfile.read()
            urlfile.close()
            return content
        else:
            raise RuntimeError("Attempting to update %s with no content" % self.path)
        return

    def _checkAclChange(self, repo):
        """
        Look for changes in the ACL
        """
        res = repo.fetchResource(self.absPath)

        cur = res.getAcl()

        for access, info in self.aclDict.items():
            if access not in cur:
                #Add the entire access
                self._aclToUpdate[access] = info
            else:
                #Verify all of the keys are the same
                for ident, allowed in info.items():
                    if cur[access].get(ident, -1) != allowed:
                        #The access is the same, but the ident/allowed is different
                        #Add the whole thing
                        self._aclToUpdate[access] = info
                        break
                else:
                    #Make sure that cur doesn't define any extra idents
                    for ident, allowed in cur[access].items():
                        if ident not in self.aclDict[access]:
                            #It does so rewrite the entire access
                            self._aclToUpdate[access] = info
                            break

        #Lastly, make sure that cur does not define any other access
        for access in cur:
            if access not in self.aclDict:
                self._aclToRemove.append(access)

        if len(self._aclToUpdate) or len(self._aclToRemove):
            self.status |= StatusFlags.ACL_CHANGE

        return

    def _checkImtChange(self, repo):
        """
        Look for changes in the IMT
        """
        if self.imt and self.imt != self.imts[self.path]:
            self.status |= StatusFlags.IMT_CHANGE
        return

    def _checkOwnerChange(self, repo):
        """
        Look for changes in the Owner
        """
        if self.owner and self.owner != self.owners[self.path]:
            self.status |= StatusFlags.OWNER_CHANGE
        return

    def _checkOtherChange(self, repo):
        """
        Look for other changes
        """
        pass

    def _setBase(self, basePath):
        """
        Adjust for a relative installation
        """
        self.path = Uri.BaseJoin(basePath, self.path)
        self.absPath = self.path + ';no-traverse'
        return

    def _gatherDependencies(self, basePath):
        """
        Get a list of dependencies for this resource.
        At the RawFile level, it is dependencies based on path
        """
        self.dependencies = []
        base = os.path.dirname(self.path)
        if not (base == basePath or base + '/' == basePath):
            self.dependencies.append(base)
        return

    def _initRdfCaches(self, repo):
        """
        We use RDF completes to get a lot of the information about the resources.  This speeds up installation a lot.
        """
        if self.lmds is not None: return
        RawFile.lmds = {}
        for lmd in repo.getModel().complete(None, Schema.MODIFIED_DATE, None):
            self.lmds[lmd.subject] = lmd.object

        for imt in repo.getModel().complete(None, Schema.IMT, None):
            self.imts[imt.subject] = imt.object

        for owner in repo.getModel().complete(None, Schema.OWNER, None):
            self.owners[owner.subject] = owner.object
        return

    def __repr__(self):
        return "<%s to be installed at %s id=%s>" % (self.__class__.__name__,
                                                     self.path,
                                                     str(hex(id(self))))
class UriReferenceFile(RawFile):
    resourceType = ResourceType.URI_REFERENCE_FILE

    _refs = None

    def __init__(self, path, acl, owner, imt, href):
        self.href = href
        RawFile.__init__(self, path, acl, owner, imt)
        return

    def _checkOtherChange(self, repo):
        """
        See if the doc def has changed
        """
        RawFile._checkOtherChange(self, repo)
        if self.path not in self._refs or self.href != self._refs[self.path]:
            self.status |= StatusFlags.OTHER_CHANGE
        return

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create UriReferenceFile: %s IMT=%s reference=%s"%(self.path, self.imt, self.href), quiet)
        repo.createUriReferenceFile(self.path, self.imt, self.href)
        #Don't need to reset this on a create
        self.status &= ~StatusFlags.IMT_CHANGE
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        raise RuntimeError("Unable to update content for %s" % self.path)

    def _initRdfCaches(self, repo):
        RawFile._initRdfCaches(self, repo)

        if self._refs is not None: return
        UriReferenceFile._refs = {}
        for ref in repo.getModel().complete(None, Schema.URI_REFERENCE_LOCATION, None):
            self._refs[ref.subject] = ref.object
        return


class XmlDocument(RawFile):
    resourceType = ResourceType.XML_DOCUMENT

    _dds = None

    docName='XmlDocument'

    def __init__(self, path, acl, owner, imt, docDef):
        RawFile.__init__(self, path, acl, owner, imt)
        self.docDef = docDef
        if imt is None:  self.imt = 'text/xml'
        return

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create %s: %s IMT=%s%s" % (self.docName,
                       self.path, self.imt,
                       " docDef=" * (not not self.docDef) + (self.docDef or "")),
                      quiet)

        cnt = self._getContent()
        repo.createDocument(self.path, cnt, docDef = self.docDef,
                            imt=self.imt, forcedType = self.resourceType)

        #Don't need to reset this on a create
        self.status &= ~StatusFlags.IMT_CHANGE

        #Note, we need to be smart in updateOther cause we already set the doc def
        return

    def updateOther(self, repo, quiet):
        RawFile.updateOther(self, repo, quiet)
        doc = repo.fetchResource(self.absPath)
        curDD = doc.getDocumentDefinition()
        if (curDD is None and self.docDef is not None) or (curDD is not None and curDD.getAbsolutePath() != self.docDef):

            self._message("Setting %s document definition to %s" % (self.path, self.docDef), quiet)
            dd = None
            if self.docDef is not None:
                dd = doc.fetchResource(self.docDef)
            doc.setDocumentDefinition(dd)
        return

    def _setBase(self, basePath):
        """
        Adjust for a relative installation
        """
        RawFile._setBase(self, basePath)
        if self.docDef is not None:
            self.docDef = Uri.BaseJoin(basePath, self.docDef)
        return

    def _checkOtherChange(self, repo):
        """
        See if the doc def has changed
        """
        RawFile._checkOtherChange(self, repo)
        if self.path not in self._dds or self.docDef != self._dds[self.path]:
                self.status = self.status | StatusFlags.OTHER_CHANGE
        return

    def _getContent(self):
        # note this might be overridden by a descendant class
        if self.stringContent:
            return self.stringContent
        elif self.contentUri:
            urlfile = Uri.UrlOpen(self.contentUri)
            content = urlfile.read()
            urlfile.close()
            return content
        return

    def _gatherDependencies(self, basePath):
        RawFile._gatherDependencies(self, basePath)
        #Find dependencies from resolution needed during parsing
        #FIXME: Only works for one resolution dependency at a time
        #
        # the general idea here is to attempt to parse the XML.
        # if one of the document's external entities or xincludes
        # aren't readable, then the XML reader will report this as
        # a UriException from which we can usually get the URI of
        # whatever couldn't be read.
        self.cachedDoc = None
        resolve_requests = []
        #Use a closure to log all requests to the resolver
        def resolve_hook(uri, base):
            resolve_requests.append(uri)
        resolver = Resolvers.FacadeResolver(observer=resolve_hook)
        factory = InputSource.InputSourceFactory(resolver=resolver)
        #Using NoExtDtdReaderBase is a cop out, basically, for the difficulty
        #Of determining how to dummy out
        reader = Domlette.NonvalidatingReaderBase(inputSourceFactory=factory)
        doc = self._getContent()
        if not doc:
            # nothing to parse, which is probably a problem,
            # but as far as dependency checking goes, is OK
            return
        if self.contentUri:
            uri = self.contentUri
        else:
            # if we don't have a real base URI for the content
            # then we won't be able to properly resolve any
            # relative URI refs of external entities / xincludes.
            # but we should try to parse anyway in order to detect
            # unreadable ext. entities / xincludes that are
            # referenced by absolute URIs.
            uri = 'urn:dummy:xml-string-content-for-%s' % self.path
        #Until parse is successful...
        while not self.cachedDoc:
            try:
                doc = reader.parseString(doc, uri)
                self.cachedDoc = doc
            except Uri.UriException, e:
                #print >> sys.stderr, 'dependency found:', e.params[0]['uri'], '\n', e.params
                code = e.errorCode
                if code == Uri.UriException.RESOURCE_ERROR:
                    # 1st arg to the err msg is assumed to be a dict
                    # in which the 'loc' item is the URI
                    # of the unreadable entity or xinclude
                    self.dependencies.append(e.params[0]['loc'])
                    #Can't be '' in case it's to be used as stand-in for an
                    #XInclude
                    resolver.cache[e.params[0]['uri']] = '<dummy/>'
                elif code in (Uri.UriException.INVALID_BASE_URI,
                              Uri.UriException.RELATIVE_BASE_URI,
                              Uri.UriException.SCHEME_REQUIRED):
                    # 1st arg to the err msg is assumed to be a dict
                    # in which the 'ref' item is the URI ref of the
                    # entity or xinclude that we couldn't attempt to
                    # read, possibly due to a bad base URI
                    self.dependencies.append(
                        '%s - possibly bad a base URI %s' % (e.params[0]['ref'],
                                                             e.params[0]['base']))
                else:
                    self.dependencies.append('an external entity or XInclude (URI unknown)')
            except ReaderException, e:
                if not resolver.cache.get(e.params[0]):
                    #Either this didn't come from cached resolution (get() -> None) or
                    #We've already fallen back to trying the empty/dummy resource (value == '')
                    #So just abort
                    raise
                else:
                    resolver.cache[e.params[0]] = ''
            except:
                raise
        if self.docDef is not None:
            self.dependencies.append(self.docDef)
        #Everything encountered in the facade resolver is also a dependency
        #Comment out for now because the blasted dependency sorting logic is broken
        #self.dependencies.extend(resolve_requests)
        return

    def _initRdfCaches(self, repo):
        RawFile._initRdfCaches(self, repo)

        if self._dds is not None: return
        XmlDocument._dds = {}
        for dd in repo.getModel().complete(None, Schema.DOCDEF, None):
            if dd.object == Schema.NULL_DOCDEF:
                self._dds[dd.subject] = None
            else:
                self._dds[dd.subject] = dd.object
        return


class Container(XmlDocument):
    resourceType = ResourceType.CONTAINER

    def __init__(self, path, acl, owner, imt, docDef):
        if path[-1] == '/' and len(path) > 1: path = path[:-1]
        XmlDocument.__init__(self, path, acl, owner, imt, docDef)
        return

    def createContent(self, repo, quiet):
        self._message("Create Container: %s" % self.path, quiet)
        repo.createContainer(self.path, 1)
        if self.imt == 'text/xml':
            self.status &= ~StatusFlags.IMT_CHANGE
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        raise RuntimeError("Unable to update content for %s" % self.path)


from Ft.Share import ExtensionModules
_EXT_NAME = ExtensionModules.__name__
_EXT_DIR = os.path.dirname(ExtensionModules.__file__)
del ExtensionModules

class ExtensionModule(IndicatorBase):
    resourceType = None
    owner=-1
    def __init__(self, file):
        if not file:
            raise Exception('File attribute not given')
        self._file = Uri.UriToOsPath(file, attemptAbsolute=False)
        self.path = ''

        basename = os.path.basename(self._file)
        self._dest = os.path.join(_EXT_DIR, basename)
        self.moduleName = _EXT_NAME + '.' + os.path.splitext(basename)[0]
        self.status = StatusFlags.UNKNOWN
        return

    def createContent(self, repo, quiet):
        #repo.copyFile(open(self._file, 'r').read(), )
        res = file_util.copy_file(self._file,
                                  self._dest,
                                  update=1,
                                  )
        if res[1]:
            self._message("Copy extension module: %s" % self._file, quiet)
        return

    def updateContent(self, repo, quiet):
        self.createContent(repo, quiet)
        repo.reloadModule(self.moduleName)

    def checkStatus(self, repo, force=0, checkAcl = 1):
        self.status = StatusFlags.NO_CHANGE
        if os.path.exists(self._dest):
            if force or dep_util.newer(self._file, self._dest):
                self.status = StatusFlags.CONTENT_CHANGE
        else:
            self.status = StatusFlags.NOT_PRESENT
        return self.status

    def _setBase(self, basePath): return

    def _gatherDependencies(self, basePath): self.dependencies = []

    def setPath(self, fName): pass

class XsltDocument(XmlDocument):
    resourceType = ResourceType.XSLT_DOCUMENT
    dependencyXPath = XPath.Compile('/xsl:*/xsl:import | /xsl:*/xsl:include')
    docName = 'XsltDocument'

    def _gatherDependencies(self, basePath):
        """
        The dependencies of a Xslt document are all xslt:include and xslt:import elements
        """
        XmlDocument._gatherDependencies(self, basePath)
        if not self.cachedDoc: return
        con = XPath.Context.Context(self.cachedDoc,
                                    processorNss={'xsl': XSL_NAMESPACE})

        for dep in self.dependencyXPath.evaluate(con):
            d = dep.getAttributeNS(EMPTY_NAMESPACE, 'href')
            if d:
                if ':' in d:
                    #External to the system (not a safe assumption, really)
                    continue
                if d[0] != '/':
                    d = Uri.BaseJoin(self.path, d)
                self.dependencies.append(d)
        return


class SchematronDocument(XmlDocument):
    resourceType = ResourceType.SCHEMATRON_DOCUMENT
    docName = 'SchematronDocument'

class RdfDocument(XmlDocument):
    resourceType = ResourceType.RDF_DOCUMENT
    docName = 'RdfDocument'

class XsltDocumentDefinition(XmlDocument):
    resourceType = ResourceType.XSLT_DOCUMENT_DEFINITION
    docName = 'XsltDocumentDefinition'

class XPathDocumentDefinition(XmlDocument):
    resourceType = ResourceType.XPATH_DOCUMENT_DEFINITION
    docName = 'XPathDocumentDefinition'

class Server(XmlDocument):
    resourceType = ResourceType.SERVER
    docName = 'Server'

class Command(XmlDocument):
    resourceType = ResourceType.COMMAND
    docName = 'Command'

class Alias(XmlDocument):
    resourceType = ResourceType.ALIAS
    _refs = None

    def __init__(self, path, acl, owner, imt, docDef, reference):
        self.reference = reference
        XmlDocument.__init__(self, path, acl, owner, imt, docDef)

        if self.reference[0] != '/':
            #Make it absolute
            self.reference = Uri.BaseJoin(self.path, self.reference)
        return

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create Alias: %s" % self.path, quiet)

        doc = repo.fetchResource(self.reference)
        doc.addAlias(self.path)

        if self.imt == 'text/xml':
            self.status &= ~StatusFlags.IMT_CHANGE

        #Note, we need to be smart in updateOther cause we already set the reference
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        raise RuntimeError("Unable to update content for %s" % self.path)

    def updateOther(self, repo, quiet):
        XmlDocument.updateOther(self, repo, quiet)

        #You cannot change an aliases reference, so raise exception for now, we could lose ACL, owner, etc
        if repo.fetchResource(self.absPath).getReference().getAbsolutePath() != self.reference:
            raise RuntimeError("Unable to change the alias reference for %s, please remove alias by hand.  You can do so on the command line using '4ss delete --no-traverse %s'" % (self.path, self.path))
        return

    def _checkOtherChange(self, repo):
        """
        See if the doc def has changed
        """
        XmlDocument._checkOtherChange(self, repo)
        if self.path not in self._refs or self.reference != self._refs[self.path]:
            self.status |= StatusFlags.OTHER_CHANGE
        return

    def _setBase(self, basePath):
        """
        Adjust for a relative installation
        """
        XmlDocument._setBase(self, basePath)
        self.reference = Uri.BaseJoin(self.path, self.reference)
        return

    def _gatherDependencies(self, basePath):
        XmlDocument._gatherDependencies(self, basePath)
        self.dependencies.append(self.reference)
        return

    def _initRdfCaches(self, repo):
        XmlDocument._initRdfCaches(self, repo)

        if self._refs is not None: return
        Alias._refs = {}
        for ref in repo.getModel().complete(None, Schema.ALIAS_REFERENCE, None):
            self._refs[ref.subject] = ref.object
        return


class User(XmlDocument):
    resourceType = ResourceType.USER

    def __init__(self, userName, acl, owner, imt, docDef, basePath,
                 password=None, data={}, passwdHash=None):
        XmlDocument.__init__(self, basePath + '/' + userName, acl, owner,
                             imt, docDef)
        self.userName = userName
        self.basePath = basePath

        if passwdHash:
            self.passwordHash = passwdHash
            self.password = password
        else:
            self.passwordHash = sha.new(password).hexdigest()
            self.password = password

        self.data = data
        return

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create User: %s%s" % (self.path,
                       " docDef=" * (not not self.docDef) + (self.docDef or "")),
                      quiet)

        newUser=repo.fetchResource(self.basePath).createUser(self.userName,
                                                             self.passwordHash,
                                                             self.docDef)
        newUser.setUserData(self.data)

        #Note, we need to be smart in updateOther cause we already set the password
        if self.imt == 'text/xml':
            self.status &= ~StatusFlags.IMT_CHANGE
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        raise RuntimeError("Unable to update content for %s" % self.path)

    def updateOther(self, repo, quiet):
        XmlDocument.updateOther(self, repo, quiet)

        if repo.fetchResource(self.path).getPassword() != self.passwordHash:
            self._message("Updating User %s password" % self.userName, quiet)
            repo.fetchResource(self.path).setPassword(self.passwordHash)

        if repo.fetchResource(self.path).getUserData() != self.data:
            self._message("Updating User %s's data" % self.userName, quiet)
            repo.fetchResource(self.path).setUserData(self.data)
        return

    def _checkAclChange(self, repo):

        #Users get thier ACL set automatically by the system.  We need to account for that here.
        #So, we need to update the ACL list before we do the check, unless an ACL list was specified

        if not self.acl:
            for key in [AclConstants.DELETE_ACCESS,
                        AclConstants.CHANGE_PERMISSIONS_ACCESS,
                        AclConstants.WRITE_USER_MODEL_ACCESS,
                        AclConstants.CHANGE_OWNER_ACCESS,
                        AclConstants.READ_ACCESS,
                        AclConstants.EXECUTE_ACCESS,
                        AclConstants.WRITE_ACCESS]:
                for ident in [self.userName,
                              AclConstants.OWNER,
                              repo.getUserName()]:
                    self.acl.append((key, ident, AclConstants.ALLOWED))
        return XmlDocument._checkAclChange(self, repo)

    def _checkOtherChange(self, repo):
        """
        See if the doc def has changed
        """
        XmlDocument._checkOtherChange(self, repo)
        if self.passwordHash != repo.fetchResource(self.path).getPassword():
            self.status |= StatusFlags.OTHER_CHANGE
        return

    def _setBase(self, basePath):
        """
        Adjust for a relative installation
        """
        XmlDocument._setBase(self, basePath)
        self.basePath = Uri.BaseJoin(basePath, self.basePath)
        return


class Group(XmlDocument):
    resourceType = ResourceType.GROUP

    def __init__(self, groupName, acl, owner, imt, docDef, basePath, members):
        XmlDocument.__init__(self, basePath + '/' + groupName, acl, owner,
                             imt, docDef)

        if not groupName:
            raise SyntaxError("Must specify a group name in a member definition")
        self.groupName = groupName
        self.members = members
        self.basePath = basePath

        self._membersToAdd = []
        self._membersToRemove = []
        return

    def checkStatus(self, repo, force=0, checkAcl=True):
        rt = XmlDocument.checkStatus(self, repo, force, checkAcl)

        ## Should this be 'self.status & StatusFlags.ACTION_REQUIRED'??
        if self.status & StatusFlags.NOT_PRESENT:
            self._membersToAdd = self.members
        return rt

    def createContent(self, repo, quiet):
        """
        Called when we need to add our content to the repo
        """
        self._message("Create Group: %s"%self.path, quiet)
        self._message("creating members: %s" % self.members, quiet)
        g = repo.fetchResource(self.basePath).createGroup(self.groupName)
        if self.imt == 'text/xml':
            self.status &= ~StatusFlags.IMT_CHANGE
        return

    def updateContent(self, repo, quiet):
        """
        Called when we need to update our content in the repo
        """
        raise RuntimeError("Unable to update content for %s" % self.path)

    def updateOther(self, repo, quiet):
        XmlDocument.updateOther(self, repo, quiet)

        g = repo.fetchUserOrGroupByName(self.groupName)
        for m in self._membersToAdd:
            self._message("Adding member %s to group %s" % (m, self.groupName), quiet)
            mem = repo.fetchUserOrGroupByName(m)
            g.addMember(mem)
        for m in self._membersToRemove:
            self._message("Removing member %s from group %s" % (m, self.groupName), quiet)
            mem = repo.fetchUserOrGroupByName(m)
            g.removeMember(mem)
        return

    def _checkAclChange(self, repo):

        #Groups get thier ACL set automatically by the system.  We need to account for that here.
        #So, we need to update the ACL list before we do the check, unless an ACL list was specified

        if not self.acl:
            for key in [AclConstants.DELETE_ACCESS,
                        AclConstants.CHANGE_PERMISSIONS_ACCESS,
                        AclConstants.WRITE_USER_MODEL_ACCESS,
                        AclConstants.CHANGE_OWNER_ACCESS,
                        AclConstants.READ_ACCESS,
                        AclConstants.EXECUTE_ACCESS,
                        AclConstants.WRITE_ACCESS]:
                for ident in [self.groupName,
                              AclConstants.OWNER,
                              repo.getUserName()]:
                    self.acl.append((key, ident, AclConstants.ALLOWED))
        return XmlDocument._checkAclChange(self, repo)

    def _checkOtherChange(self, repo):
        """
        See if the doc def has changed
        """
        XmlDocument._checkOtherChange(self, repo)

        curMembers = [r.getUsername() for r in
            repo.fetchUserOrGroupByName(self.groupName).getMembers()]
        for m in curMembers:
            if m not in self.members:
                self._membersToRemove.append(m)
        for m in self.members:
            if m not in curMembers:
                self._membersToAdd.append(m)
        if len(self._membersToRemove) or len(self._membersToAdd):
            self.status |= StatusFlags.OTHER_CHANGE
        return

    def _setBase(self, basePath):
        """
        Adjust for a relative installation
        """
        XmlDocument._setBase(self, basePath)
        self.basePath = Uri.BaseJoin(basePath, self.basePath)
        return

    def _gatherDependencies(self, basePath):
        XmlDocument._gatherDependencies(self, basePath)
        for m in self.members:
            if m not in g_safeAclIdents:
                self.dependencies.append(m)
        return


class Product:
    name = ""
    version = 0
    description = ""

    def __init__(self, resourceList, name='', version=0, description='',
                 useIndicator=None):
        self.resourceList = resourceList
        self.filteredList = None
        self.sortedList = None
        self.notDirectlyChangedList = None
        self.basePath = '/'
        self.name = name
        self.version = version
        self.description = description
        if useIndicator is None:
            useIndicator = sys.stdout.isatty()
        self.useIndicator = useIndicator
        return

    def setBase(self, basePath):
        """
        Processes self.resourceList, setting the destination
        container on each resource to be the given basePath.
        """
        # append leading and trailing slashes if absent
        if basePath[-1] != '/':
            basePath += '/'
        if basePath[0] != '/':
            basePath = '/' + basePath

        for r in self.resourceList:
            r._setBase(basePath)
        self.basePath = basePath
        return

    def setIndicator(self):
        """
        Processes self.resourceList, hooking each resource into the
        progress indicator.
        """
        for r in self.resourceList:
            r._setIndicator(self.progressIndicator)
        return

    def filter(self, repo, checkAcl=True):
        """
        Processes self.resourceList, creating (if not already created)
        3 more lists: self.sortedList, which is the resources that
        have content or docdef changes and thus need to have their
        dependencies expanded; self.filteredList, which is
        the resources that have other changes; and
        self.notDirectlyChangedList, which is resources with no changes.
        """
        if self.filteredList is not None:
            return

        # resources w/content or docdef changes
        self.sortedList = []
        # resources w/minor changes
        self.filteredList = []
        # resources with no direct changes
        self.notDirectlyChangedList = []

        for r in self.resourceList:
            status = r.checkStatus(repo, checkAcl=checkAcl)
            if status == StatusFlags.UNKNOWN:
                raise RuntimeError("Status of %s not determined" % r.path)
            if status & (StatusFlags.CONTENT_CHANGE | StatusFlags.NOT_PRESENT):
                self.sortedList.append(r)
            elif status == StatusFlags.NO_CHANGE:
                self.notDirectlyChangedList.append(r)
            else:
                self.filteredList.append(r)
        return

    def sort(self, repo, quiet=True):
        # Sort our resources by dependency

        # We assume that the resources have been filtered, so first gather
        # dependencies on the sortedList
        for r in self.sortedList:
            r._gatherDependencies(self.basePath)

        sorted = []
        provides = []  # list of resource paths
        unsorted = self.sortedList

        # Transfer from the unsorted list to the res list.  If we don't
        # transfer any, then blindly move one over and print an error
        satisfied = True
        while unsorted and satisfied:
            # Start off by assuming nothing will be satified this round
            satisfied = []

            # Find all resources whose dependencies are satisfied
            for resource in unsorted:
                for path in resource.dependencies:
                    if path not in provides:
                        if repo.hasResource(path):
                            provides.append(path)
                        else:
                            # Unknown
                            break
                else:
                    # All dependencies satisfied
                    satisfied.append(resource)
                    provides.append(resource.path)
                    if resource.resourceType == ResourceType.USER:
                        provides.append(resource.userName)
                    elif resource.resourceType == ResourceType.GROUP:
                        provides.append(resource.groupName)

            for resource in satisfied:
                sorted.append(resource)
                unsorted.remove(resource)

        # Gather the paths which have failed for the remaining resources
        if unsorted:
            failed = []
            for resource in unsorted:
                for path in resource.dependencies:
                    if path not in provides:
                        failed.append((path, resource.path))

            failed.sort()
            sys.stderr.write("Failed dependencies:\n")
            for item in failed:
                sys.stderr.write("\t%s needed by %s\n" % item)
                sys.stderr.flush()

        #Add to sorted list any whose deps have changed
        sorted_paths = dict([ (r.path, None) for r in sorted ])
        lookupPath = dict([ (r.path, r) for r in self.notDirectlyChangedList ])
        deps_cache = {}
        def needsUpdate(path):
            if unicode(path) in sorted_paths:
                return True
            resource = lookupPath.get(path)
            if not resource:
                return False
            deps = deps_cache.get(resource)
            if deps is None:
                resource._gatherDependencies(self.basePath)
                deps = resource.dependencies
                deps_cache[resource] = deps
                #print "*"*10, "Dependencies:", deps
            for dep in deps:
                if needsUpdate(dep):
                    sorted.append(resource)
                    sorted_paths[resource.path] = None
                    #print "*"*10, "Appending to sorted", path
                    return True
            return False

        #import pprint; pprint.pprint(lookupPath.keys())
        for resource in self.notDirectlyChangedList:
            #Mutates sorted list itself, so we don't care about return val
            #Blah blah ugly use of side effects blah blah.  :-)
            needsUpdate(resource.path)

        self.sortedList = sorted
        return not unsorted

    def serialize(self, stream=sys.stdout, refUri=''):
        import Serialize
        stream.write(Serialize.Serialize(self, refUri))
        return

    def install(self, repo, basePath, quiet, do_update=False, checkAcl=True):
        """
        Install the product into the repo
        """
        action = ('Installing/Updating', 'Updating')[do_update and 1 or 0]
        header = ""
        if self.version:
            header = "\n%s %s version %s" % (action, self.name, self.version)
        else:
            header = "\n%s %s" % (action, self.name)
        print header

        self.setBase(basePath)
        self.filter(repo, checkAcl=checkAcl)

        if not self.sort(repo):
            raise RuntimeError("Unresolved dependencies exist")


        if self.useIndicator:
            #Calculate the total size
            #We go over the resource list twice, filter, sort, header
            total = len(self.sortedList) + len(self.sortedList) + len(self.filteredList)
            if total:
                self.progressIndicator = ProgressIndicator.ProgressIndicator(self.name)
                self.progressIndicator.newIndicator(total)
                self.setIndicator()

        ctr = 0
        #First install the sorted list
        for r in self.sortedList:
            if r.status & StatusFlags.NOT_PRESENT:
                r.createContent(repo, quiet)
                #Might need to remove owner update if not specified
                if r.owner is None:
                    r.status &= ~StatusFlags.OWNER_CHANGE

            else:
                r.updateContent(repo, quiet)
            r.status = r.status & ~StatusFlags.CONTENT_CHANGE

            # save some memory, assuming we're done with cached docs
            if hasattr(r, 'cachedDoc'):
                r.cachedDoc = None
            if hasattr(r, 'stringContent'):
                r.stringContent = None

            if self.useIndicator:
                self.progressIndicator.updateProgress(ctr)
            ctr += 1

        #Then the filtered list
        for r in self.filteredList + self.sortedList:
            if r.status & StatusFlags.IMT_CHANGE:
                r.updateImt(repo, quiet)
                r.status &= ~StatusFlags.IMT_CHANGE
            if r.status & StatusFlags.ACL_CHANGE:
                r.updateAcl(repo, quiet)
                r.status &= ~StatusFlags.ACL_CHANGE
            if r.status & StatusFlags.OWNER_CHANGE:
                r.updateOwner(repo, quiet)
                r.status &= ~StatusFlags.OWNER_CHANGE
            if r.status & StatusFlags.OTHER_CHANGE:
                r.updateOther(repo, quiet)
                r.status &= ~StatusFlags.OTHER_CHANGE

            # save some memory, assuming we're done with cached docs
            if hasattr(r, 'cachedDoc'):
                r.cachedDoc = None
            if hasattr(r, 'stringContent'):
                r.stringContent = None

            if self.useIndicator:
                self.progressIndicator.updateProgress(ctr)
            ctr += 1

        # more potential memory savings
        self.sortedList = None
        self.filteredList = None

        return


def Deserialize(stream, refUri=''):
    import Serialize
    return Serialize.Deserialize(stream, refUri)

