# ldapObject.py
#
# Copyright 2000,2001,2003 Wichert Akkerman <wichert@deephackmode.org>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# Calculate shared library dependencies

"""LDAP object helper

This python module implements the ldapObject class, which represents an entry
in a LDAP directory. You can use an ldapObject as a normal map, when you are
ready to commit your changes to the LDAP directory the object will perform the
necessary actions itself.

Creating a new entry in a directory
===================================
  Adding a new entry to a directory is simple: first you create a ldapObject
  with the right dn, then you fill it with the necessary data and finally you
  commit it to the directory using the modify() function.

  A simple example::

    import ldap, ldapObject
    
    # Open a connection to the LDAP directory
    db=ldap.open("ldap", 389)
    db.simple_bind("user", "password")
    
    # Create a new object
    obj=ldapObject.ldapObject("sn=John Doe,ou=People,dc=domain")
    obj["objectClass"].append("inetOrgPerson")
    obj["sn"]="John Doe"
    obj["givenName"]="John"
    
    # Commit changes to the directory
    try:
        obj.modify(db)
    except ldap.LDAPError, e:
        print "Error adding entry %s: %s" % (obj.dn, str(e))

Turning LDAP search results into ldapObjects
============================================

 Since ldapObject aims to be a minimal class it does not have specific
 functionality to search in a LDAP directory. However you can easily convert
 the results from ldap.search_s into a sequence of ldapObjects using map().

 An example::

    import ldap, ldapObject
    
    # Open a connection to the LDAP directory
    db=ldap.open("ldap", 389)
    db.simple_bind("user", "password")
    
    # Perform a search for all objects with a uid attribute
    results=db.search_s("ou=People,dc=domain", ldap.SCOPE_SUBTREE, "uid=*")
    results=map(ldapObject.ldapObject, results)

Deriving from ldapObject
========================

  You might want to dervice a new class from ldapObject if you want to add new
  functions or change the sorting order. 

  Simple derived class
  --------------------

    This example shows a new class that represents a companies office::

      class Office(ldapObject.ldapObject):
              "A simple class to represent an office"
      
              ObjectClasses   = [ "locality", "organizationalUnit" ]
      
              def __init__(self, init=""):
                      "Initialize the Office object"
      
                      ldapObject.ldapObject.__init__(self, init)
      
                      # Add the essential objectClasses
                      for oc in self.ObjectClases:
                              if not oc in self.attr["ObjectClass"]:
                                      self.attr["ObjectClass"].append(oc)
      
                      # Snapshot the current state of the object
                      self._snapshot()
      
              def name(self):
                      '''Return a string with a name for this office. The name is
                      assembled from the location, state and country.'''
      
                      name=[]
                      if self.attr.has_key("l"):
                              name.append(self.attr["l"][0])
                      if self.attr.has_key("st"):
                              name.append(self.attr["st"][0])
                      if self.attr.has_key("c"):
                              name.append(data.Countries[self.attr["c"][0]])
      
                      if len(name):
                              name=", ".join(name)
                      else:
                              name=self.attr["ou"]
      
                      return name

  Changing the sort order
  -----------------------

   The sorting order for classes can be changed by modifying the SortOrder
   variable. SortOder is a list for tuples containing the attribute to sort on
   and the direction (one of ldapObject.SORT_ASCENDING or
   ldapObject.SORT_DESCENDING).

   An example::

      class Office(ldapObject.ldapObject):
              "A simple class to represent an office"
      
              SortOrder	= [
                      ("c",   ldapObject.SORT_ASCENDING), # Sort by country first
                      ("st",  ldapObject.SORT_ASCENDING), # Then by state
      		("l",   ldapObject.SORT_ASCENDING), # And finally sort by city
              ]

@var SORT_ASCENDING: use ascending sort order, used for ldapObject.SortOrder.
@var SORT_DESCENDING: use ascending sort order, used for ldapObject.SortOrder.
"""

__docformat__	= "epytext en"

# Import system packages we need
import copy, types, sys
import ldap, ldap.modlist, ldif

# Sort orders
SORT_ASCENDING	= 0
SORT_DESCENDING	= 1

class ldapObjectException(Exception):
	"""ldapObject exception class

	@ivar reason: reason for raising this exception
	@type reason: string
	"""
	def __init__(self, reason):
		"""Constructor.

		@param reason: reason for raising this exception
		@type reason:  string
		"""
		self.reason=reason

	def __str__(self):
		return self.reason


class ldapObject:
	"""
	@cvar ObjectClasses: list of object classes this class is a member of
	@type ObjectClasses: sequence of strings
	@cvar SortOrder: Sorting order used when sorting ldapObjects.
	@type SortOrder: sequence of
	(attribute, SORT_ASCENDING|SORT_DESCENDING) pairs
	"""
	SortOrder	= [ ]
	ObjectClasses	= [ "top" ]

	def __init__(self,init=""):
		"""ldapObject constructor.

		There are three methods to initialize a ldapObject:

		 1. Using a DN
		 2. Using a tuple as returned by a LDAP search
		 3. Using another ldapObject
		"""

		if type(init)==types.StringType:
			# String initializer, assume it is a DN
			self.dn=init
			self.attr={ }
		elif type(init)==types.TupleType and \
			((type(init[0])==types.StringType) and (type(init[1])==types.DictionaryType)):
			# This looks like the result of a LDAP search
			self.dn=init[0]
			self.attr=init[1]
		elif type(init)==types.InstanceType and \
			(init.__dict__.has_key("orgAttr") and init.__dict__.has_key("attr") and init.__dict__.has_key("dn")):
			# This looks more like another ldapObject
			self.dn=other.dn
			self.attr=copy.deepcopy(other.attr)
		else:
			# Can't determine what type init is.. lets abort
			raise ldapObjectException, "Trying to initialize object with unknown initializer"

		# Having a objectClass is mandatory
		if not self.attr.has_key("objectClass"):
			self.attr["objectClass"]=["top"]

		for oc in self.ObjectClasses:
			if oc not in self.attr["objectClass"]:
				self.attr["objectClass"].append(oc)

		self._snapshot()


	def delete(self,db):
		"""Delete this object from the database.

		@param db: database to store data in
		@type db:  LDAP connection
		"""
		db.delete_s(self.dn)


	def load(self,db):
		"""Load this object from the database.

		@param db: database to store data in
		@type db:  LDAP connection
		"""

		(self.dn,self.attr)=db.search_s(self.dn, ldap.SCOPE_BASE, self._classfilter())[0]
		self._snapshot()


	def _snapshot(self):
		"""Snapshot current state.
		
		Assume we have an untouched object: initialize orgAttr and
		reset changes. For internal use only!"""
		self.orgAttr=copy.deepcopy(self.attr)
		self.changes=[]


	def update(self,db):
		"""Commit changes to directory.
		
		Build a list of changes made to our attributes and try to
		commit them to the database.
		
		@param db: database to store data in
		@type  db: LDAP connection
		"""

		try:
			todo=ldap.modlist.modifyModlist(self.orgAttr, self.attr)
			db.modify_s(self.dn, todo)
		except ldap.NO_SUCH_OBJECT:
			todo=ldap.modlist.addModlist(self.attr)
			db.add_s(self.dn, todo)
		# We have to reload to make sure self.attr and self.orgAttr
		# are up to date.
		self.load(db)


	def ldif(self, fd=sys.stdout):
		"""Output the object in LDIF format.

		@param fd: File to output LDIF data to
		@type  fd: file object
		@return: LDIF data
		@rtype:  string
		"""

		writer=ldif.LDIFWriter(fd)
		writer.unparse(self.dn, self.attr)


	def _classfilter(self):
		"""Return a LDAP search filter for our ObjectClasses"""

		return "".join(map(lambda x: "(objectClass=%s)" % x,
			self.ObjectClasses))


	def __str__(self):
		return self.ldif()


	def _getkey(self,key):
		"""Return the sorting-key used for sorting. Used by __cmp__
		to simplify our soring code. If an attribute is not found
		an empty list is returned instead"""

		if self.attr.has_key(key):
			return self.attr[key]
		else:
			return []


	def __cmp__(self, other):
		"""Compare ourself to another ldapObject class using the
		sorting options defined in SortOrder. Take into account
		that we are actually comparing lists of items."""

		for key in self.SortOrder:
			(a,b)=(self._getkey(key[0]), other._getkey(key[0]))

			if key[1]==SORT_ASCENDING:
				aWins=1
			else:
				aWins=-1

			for i in range(min(len(a), len(b))):
				(x,y)=(a[i].lower(),b[i].lower())

				if (x>y):
					return aWins
				elif (x<y):
					return -aWins

			if len(a)<len(b):
				return aWins
			elif len(a)>len(b):
				return -aWins

		return 0


# Set of members to emulate a mapping
	def clear(self):
		self.attr.clear()
		self.orgAttr.clear()

	def has_key(self,key):
		return self.attr.has_key(key)

	def items(self):
		return self.attr.items()

	def keys(self):
		return self.attr.keys()

	def __len__(self):
		return len(self.attr)

	def __delitem__(self, key):
		del self.attr[key]

	def __getitem__(self, key):
		return self.attr[key]

	def __setitem__(self, key, value):
		self.attr[key]=value

