Merge pull request #3 from bradbishop/enumerate

Mapper refactoring
diff --git a/OpenBMCMapper.py b/OpenBMCMapper.py
index 30a93fb..1233c70 100644
--- a/OpenBMCMapper.py
+++ b/OpenBMCMapper.py
@@ -22,6 +22,11 @@
 MAPPER_NAME = 'org.openbmc.objectmapper'
 MAPPER_IFACE = MAPPER_NAME + '.ObjectMapper'
 MAPPER_PATH = '/org/openbmc/objectmapper/objectmapper'
+ENUMERATE_IFACE = 'org.openbmc.Object.Enumerate'
+
+class MapperNotFoundException(Exception):
+	def __init__(self, msg):
+		super(MapperNotFoundException, self).__init__(msg)
 
 class Path:
 	def __init__(self, path):
@@ -45,10 +50,22 @@
 			last = self.depth()
 		return prefix + '/'.join(self.parts[first:last])
 
+def org_dot_openbmc_match(name):
+	return 'org.openbmc' in name
+
+class ListMatch(object):
+	def __init__(self, l):
+		self.l = l
+
+	def __call__(self, match):
+		return match in self.l
+
 class IntrospectionNodeParser:
-	def __init__(self, data):
+	def __init__(self, data, tag_match = bool, intf_match = bool):
 		self.data = data
 		self.cache = {}
+		self.tag_match = tag_match
+		self.intf_match = intf_match
 
 	def parse_args(self):
 		return [ x.attrib for x in self.data.findall('arg') ]
@@ -64,16 +81,18 @@
 		iface = {}
 		iface['method'] = {}
 		iface['signal'] = {}
-		name = self.data.attrib['name']
 
 		for node in self.data:
-			p = IntrospectionNodeParser(node)
 			if node.tag not in ['method', 'signal']:
 				continue
+			if not self.tag_match(node.tag):
+				continue
+			p = IntrospectionNodeParser(
+					node, self.tag_match, self.intf_match)
 			n, element = p.parse_method_or_signal()
 			iface[node.tag][n] = element
 
-		return name, iface
+		return iface
 
 	def parse_node(self):
 		if self.cache:
@@ -83,10 +102,13 @@
 		self.cache['children'] = []
 
 		for node in self.data:
-			p = IntrospectionNodeParser(node)
 			if node.tag == 'interface':
-				name, ifaces = p.parse_interface()
-				self.cache['interfaces'][name] = ifaces
+				p = IntrospectionNodeParser(
+						node, self.tag_match, self.intf_match)
+				name = p.data.attrib['name']
+				if not self.intf_match(name):
+					continue
+				self.cache['interfaces'][name] = p.parse_interface()
 			elif node.tag == 'node':
 				self.cache['children'] = self.parse_children()
 
@@ -102,19 +124,24 @@
 		return any('/' in s for s in self.get_children())
 
 class IntrospectionParser:
-	def __init__(self, name, bus):
+	def __init__(self, name, bus, tag_match = bool, intf_match = bool):
 		self.name = name
 		self.bus = bus
+		self.tag_match = tag_match
+		self.intf_match = intf_match
 
 	def _introspect(self, path):
 		try:
-			obj = self.bus.get_object(self.name, path)
-			iface = dbus.Interface(obj, dbus.BUS_DAEMON_IFACE + '.Introspectable')
+			obj = self.bus.get_object(self.name, path, introspect = False)
+			iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
 			data = iface.Introspect()
 		except dbus.DBusException:
 			return None
 
-		return IntrospectionNodeParser(ElementTree.fromstring(data))
+		return IntrospectionNodeParser(
+				ElementTree.fromstring(data),
+				self.tag_match,
+				self.intf_match)
 
 	def _discover_flat(self, path, parser):
 		items = {}
@@ -148,3 +175,184 @@
 			items.update(callback(path + k, parser))
 
 		return items
+
+class PathTreeItemIterator(object):
+	def __init__(self, path_tree, subtree, depth):
+		self.path_tree = path_tree
+		self.path = []
+		self.itlist = []
+		self.subtree = ['/'] + filter(bool, subtree.split('/'))
+		self.depth = depth
+		d = path_tree.root
+		for k in self.subtree:
+			try:
+				d = d[k]['children']
+			except KeyError:
+				raise KeyError(subtree)
+		self.it = d.iteritems()
+
+	def __iter__(self):
+		return self
+
+	def __next__(self):
+		return super(PathTreeItemIterator, self).next()
+
+	def next(self):
+		key, value = self._next()
+		path = self.subtree[0] + '/'.join(self.subtree[1:] + self.path)
+		return path, value.get('data')
+
+	def _next(self):
+		try:
+			while True:
+				x = self.it.next()
+				depth_exceeded = len(self.path) +1 > self.depth
+				if self.depth and depth_exceeded:
+					continue
+				self.itlist.append(self.it)
+				self.path.append(x[0])
+				self.it = x[1]['children'].iteritems()
+				break;
+
+		except StopIteration:
+			if not self.itlist:
+				raise StopIteration
+
+			self.it = self.itlist.pop()
+			self.path.pop()
+			x = self._next()
+		
+		return x
+
+class PathTreeKeyIterator(PathTreeItemIterator):
+	def __init__(self, path_tree, subtree, depth):
+		super(PathTreeKeyIterator, self).__init__(path_tree, subtree, depth)
+
+	def next(self):
+		return super(PathTreeKeyIterator, self).next()[0]
+
+class PathTree:
+	def __init__(self):
+		self.root = {}
+
+	def _try_delete_parent(self, elements):
+		if len(elements) == 1:
+			return False
+
+		kids = 'children'
+		elements.pop()
+		d = self.root
+		for k in elements[:-1]:
+			d = d[k][kids]
+
+		if 'data' not in d[elements[-1]] and not d[elements[-1]][kids]:
+			del d[elements[-1]]
+			self._try_delete_parent(elements)
+
+	def _get_node(self, key):
+		kids = 'children'
+		elements = ['/'] + filter(bool, key.split('/'))
+		d = self.root
+		for k in elements[:-1]:
+			try:
+				d = d[k][kids]
+			except KeyError:
+				raise KeyError(key)
+		
+		return d[elements[-1]]
+
+	def __iter__(self):
+		return self
+
+	def __missing__(self, key):
+		for x in self.iterkeys():
+			if key == x:
+				return False
+		return True
+
+	def __delitem__(self, key):
+		kids = 'children'
+		elements = ['/'] + filter(bool, key.split('/'))
+		d = self.root
+		for k in elements[:-1]:
+			try:
+				d = d[k][kids]
+			except KeyError:
+				raise KeyError(key)
+
+		del d[elements[-1]]
+		self._try_delete_parent(elements)
+
+	def __setitem__(self, key, value):
+		kids = 'children'
+		elements = ['/'] + filter(bool, key.split('/'))
+		d = self.root
+		for k in elements[:-1]:
+			d = d.setdefault(k, {kids: {}})[kids]
+
+		children = d.setdefault(elements[-1], {kids: {}})[kids]
+		d[elements[-1]].update({kids: children, 'data': value})
+
+	def __getitem__(self, key):
+		return self._get_node(key).get('data')
+
+	def setdefault(self, key, default):
+		if not self.get(key):
+			self.__setitem__(key, default)
+
+		return self.__getitem__(key)
+
+	def get(self, key, default = None):
+		try:
+			x = self.__getitem__(key)
+		except KeyError:
+			x = default
+
+		return x
+
+	def get_children(self, key):
+		return [ x for x in self._get_node(key)['children'].iterkeys() ]
+
+	def demote(self, key):
+		n = self._get_node(key)
+		if 'data' in n:
+			del n['data']
+
+	def keys(self, subtree = '/', depth = None):
+		return [ x for x in self.iterkeys(subtree, depth) ]
+
+	def values(self, subtree = '/', depth = None):
+		return [ x[1] for x in self.iteritems(subtree, depth) ]
+
+	def items(self, subtree = '/', depth = None):
+		return [ x for x in self.iteritems(subtree, depth) ]
+
+	def dataitems(self, subtree = '/', depth = None):
+		return [ x for x in self.iteritems(subtree, depth) \
+				if x[1] is not None ]
+
+	def iterkeys(self, subtree = '/', depth = None):
+		if not self.root:
+			return {}.iterkeys()
+		return PathTreeKeyIterator(self, subtree, depth)
+
+	def iteritems(self, subtree = '/', depth = None):
+		if not self.root:
+			return {}.iteritems()
+		return PathTreeItemIterator(self, subtree, depth)
+
+class Mapper:
+	def __init__(self, bus):
+		self.bus = bus
+		obj = bus.get_object(MAPPER_NAME, MAPPER_PATH, introspect = False)
+		self.iface = dbus.Interface(
+				obj, dbus_interface = MAPPER_IFACE)
+
+	def get_object(self, path):
+		return self.iface.GetObject(path)
+
+	def get_subtree_paths(self, path = '/', depth = 0):
+		return self.iface.GetSubTreePaths(path, depth)
+
+	def get_subtree(self, path = '/', depth = 0):
+		return self.iface.GetSubTree(path, depth)
diff --git a/phosphor-mapper b/phosphor-mapper
index 313baa2..36bf09b 100644
--- a/phosphor-mapper
+++ b/phosphor-mapper
@@ -21,160 +21,20 @@
 import dbus.service
 import dbus.mainloop.glib
 import gobject
-from OpenBMCMapper import Path, IntrospectionParser
+from OpenBMCMapper import Path, IntrospectionParser, PathTree
 import OpenBMCMapper
 
-class DictionaryCache:
-	def __init__(self):
-		self.items = {'/': {} }
-
-	def _merge_item(self, item):
-		path, bus, iface = item
-		if bus in self.items[path]:
-			self.items[path][bus].append(iface)
-		else:
-			self.items[path][bus] = [ iface ]
-
-	def add_item(self, item):
-		path, bus, iface = item
-		if path != '/':
-			parent = Path(path).fq(last = -1)
-			if parent not in self.items:
-				self.add_item((parent, None, None))
-
-		items = self.items.get(path, None)
-		if bus and items:
-			self._merge_item(item)
-		elif items:
-			pass
-		elif bus:
-			self.items[path] = { bus: [ iface ] }
-		else:
-			self.items[path] = {}
-
-	def add_items(self, items):
-		for i in items:
-			self.add_item(i)
-
-	def _has_children(self, path):
-		return len(self.get_subtree_paths(path, 1, 'fuzzy')) != 1
-
-	def _try_remove_path(self, path):
-		if path == '/':
-			return None
-
-		if not self._has_children(path):
-			del self.items[path]
-
-			# try and remove the parent
-			parent = Path(path).fq(last = -1)
-			if not self.items[parent]:
-				self._try_remove_path(parent)
-
-	def _remove_bus(self, path, bus):
-		del self.items[path][bus]
-		if not self.items[path]:
-			self._try_remove_path(path)
-
-	def _remove_iface(self, path, bus, iface):
-		self.items[path][bus].remove(iface)
-		if not self.items[path][bus]:
-			self._remove_bus(path, bus)
-
-	def remove_item(self, item):
-		self._remove_iface(*item)
-
-	def remove_items(self, items):
-		for x in items:
-			self.remove_item(x)
-
-	def remove_bus(self, name):
-		for path,x in self.items.items():
-			for bus,ifaces in x.items():
-				if name != bus:
-					continue
-				self.remove_items([ (path, bus, iface) \
-						for iface in ifaces ])
-
-	def remove_busses(self, names):
-		for x in names:
-			self.remove_bus(name)
-
-	def _get_busses(self):
-		busses = [ y.iterkeys() for y in [ x for x in self.items.itervalues() ] ]
-		return set( x for y in busses for x in y ) # flatten nested lists
-
-	def has_bus(self, bus):
-		return any(bus in b for b in self._get_busses())
-
-	def get_subtree_paths(self, root, depth, match_type):
-		return filter(PathMatch(root, depth, match_type),
-				self.items.iterkeys())
-
-	def get_subtree(self, root, depth, match_type):
-		return dict(filter(ObjectMatch(root, depth, match_type),
-				self.items.iteritems()))
-
-	@staticmethod
-	def _iface__str__(ifaces):
-		return '\n'.join(["    %s" %(x) for x in ifaces ])
-
-	@staticmethod
-	def _bus__str__(busses):
-		return '\n'.join([ "%s\n%s" %(x, DictionaryCache._iface__str__(y)) \
-				for x,y in busses.iteritems() ])
-
-	def __str__(self):
-		return '\n'.join([ "%s\n%s" %(x, DictionaryCache._bus__str__(y)) \
-				for x,y in self.items.iteritems() ])
-
-class PathMatch(object):
-	def __init__(self, path, depth, match_type):
-		p = Path(path)
-		self.path = p.fq()
-		self.depth = p.depth()
-		self.match_type = match_type
-		self.match_depth = None
-		if depth != -1:
-			self.match_depth = p.depth() + depth
-
-	def __call__(self, path):
-		p = Path(path)
-		depth = p.depth()
-
-		# 'is not None' is really needed because
-		# the match depth can be zero
-		if self.match_depth is not None and depth > self.match_depth:
-			return False
-
-		fuzzy_match = p.fq(last = self.depth) == self.path or \
-				not self.depth
-		depth_match = self.match_depth == depth
-
-		if self.match_type == 'fuzzy':
-			return fuzzy_match
-
-		return fuzzy_match and depth_match
-
-class ObjectMatch(PathMatch):
-	def __init__(self, path, depth, match_type):
-		super(ObjectMatch, self).__init__(path, depth, match_type)
-
-	def __call__(self, tup):
-		if not super(ObjectMatch, self).__call__(tup[0]):
-			return False
-		return tup[1]
-
 class ObjectMapper(dbus.service.Object):
 	def __init__(self, bus, path,
-			name_match = 'org.openbmc',
-			intf_match = 'org.openbmc'):
+			name_match = OpenBMCMapper.org_dot_openbmc_match,
+			intf_match = OpenBMCMapper.org_dot_openbmc_match):
 		super(ObjectMapper, self).__init__(bus.dbus, path)
-		self.cache = DictionaryCache()
+		self.cache = PathTree()
 		self.bus = bus
 		self.name_match = name_match
 		self.intf_match = intf_match
-		self.discovery_done = False
+		self.tag_match = OpenBMCMapper.ListMatch(['children', 'interface'])
+		self.service = None
 
 		gobject.idle_add(self.discover)
 		self.bus.dbus.add_signal_receiver(self.bus_handler,
@@ -189,26 +49,66 @@
 			signal_name = 'InterfacesRemoved',
 			sender_keyword = 'sender')
 
+	def bus_match(self, name):
+		if name == OpenBMCMapper.MAPPER_NAME:
+			return False
+		return self.name_match(name)
+
+	def discovery_pending(self):
+		return not bool(self.service)
+
 	def interfaces_added_handler(self, path, iprops, **kw):
-		for x in iprops.iterkeys():
-			if self.intf_match in x:
-				self.cache.add_item((path, kw['sender'], x))
+		if self.discovery_pending() or \
+				not self.bus_match(kw['sender']):
+			return
+		matches = [ x for x in iprops.iterkeys() if self.intf_match(x) ]
+		d = self.cache.setdefault(path, {})
+		d[path].setdefault(kw['sender'], []).extend(matches)
+		self.cache[path] = d
 
 	def interfaces_removed_handler(self, path, interfaces, **kw):
+		if self.discovery_pending() or \
+				not self.bus_match(kw['sender']):
+			return
+		item = self.cache[path]
+		name = kw['sender']
 		for x in interfaces:
-			self.cache.remove_item((path, kw['sender'], x))
+			item[name].remove(x)
+
+		# remove the owner if there aren't any interfaces left
+		if not item[name]:
+			del item[name]
+
+		# update if interfaces remain
+		if item:
+			self.cache[path] = item
+		# mark for removal if no interfaces remain
+		elif self.cache.get_children(item):
+			self.cache.demote(item)
+		# delete the entire path if everything is gone
+		else:
+			del self.cache[path]
 
 	def process_new_owner(self, name):
 		# unique name
-		return self.discover([ IntrospectionParser(name, self.bus.dbus) ])
+		return self.discover([ IntrospectionParser(name,
+			self.bus.dbus, self.tag_match, self.intf_match) ])
 
 	def process_old_owner(self, name):
-		# unique name
-		self.cache.remove_bus(name)
+		for x,y in self.cache.dataitems():
+			if name not in y:
+				continue
+			del y[name]
+			if y:
+				self.cache[x] = y
+			elif self.cache.get_children(x):
+				self.cache.demote(x)
+			else:
+				del self.cache[x]
 
 	def bus_handler(self, service, old, new):
-		if not self.discovery_done or \
-				self.name_match not in service:
+		if self.discovery_pending() or \
+				not self.bus_match(service):
 			return
 
 		if new:
@@ -216,48 +116,48 @@
 		if old:
 			self.process_old_owner(old)
 
-	def add_match_interfaces(self, owner, path, interfaces):
-		for x in interfaces:
-			if self.intf_match not in x:
-				continue
+	def add_interfaces(self, path, owner, interfaces):
+		d = self.cache.setdefault(path, { })
+		d.setdefault(owner, []).extend(interfaces)
+		self.cache[path] = d
 
-			self.cache.add_item((path, owner, x))
-
-	def add_match_items(self, owner, bus_items):
+	def add_items(self, owner, bus_items):
 		for x,y in bus_items.iteritems():
-			self.add_match_interfaces(owner, x, y['interfaces'])
+			self.add_interfaces(x, owner, y['interfaces'])
 
 	def discover(self, owners = None):
-		discovery = not self.discovery_done
 		if not owners:
-			owners = self.bus.get_owners(self.name_match)
-			self.discovery_done = True
+			owners = [ IntrospectionParser(x, self.bus.dbus,
+				self.tag_match, self.intf_match) \
+						for x in self.bus.get_owner_names(self.bus_match) ]
 		for o in owners:
+			self.add_items(o.name, o.introspect())
 
-			# this only happens when an app
-			# grabs more than one well known name
-			if self.cache.has_bus(o.name):
-				continue
-
-			self.add_match_items(o.name, o.introspect())
-
-		if discovery:
+		if self.discovery_pending():
 			print "ObjectMapper discovery complete..."
+			self.service = dbus.service.BusName(
+					OpenBMCMapper.MAPPER_NAME, self.bus.dbus)
 
-	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 'sis', 'as')
-	def GetTreePaths(self, path, depth, match_type):
-		return self.cache.get_subtree_paths(path, depth, match_type)
+	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 's', 'a{sas}')
+	def GetObject(self, path):
+		o = self.cache.get(path)
+		if not o:
+			raise MapperNotFoundException(path)
+		return o
 
-	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 'sis', 'a{sa{sas}}')
-	def GetTree(self, path, depth, match_type):
-		return self.cache.get_subtree(path, depth, match_type)
+	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 'si', 'as')
+	def GetSubTreePaths(self, path, depth):
+		try:
+			return self.cache.iterkeys(path, depth)
+		except KeyError:
+			raise MapperNotFoundException(path)
 
-	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 'asis', 'a{sa{sas}}')
-	def GetTrees(self, paths, depth, match_type):
-		values = {}
-		for x in paths:
-			values.update(self.GetTree(x, depth, match_type))
-		return values
+	@dbus.service.method(OpenBMCMapper.MAPPER_IFACE, 'si', 'a{sa{sas}}')
+	def GetSubTree(self, path, depth):
+		try:
+			return { x:y for x, y in self.cache.dataitems(path, depth) }
+		except KeyError:
+			raise MapperNotFoundException(path)
 
 class BusWrapper:
 	def __init__(self, bus):
@@ -266,21 +166,16 @@
 	def get_service_names(self, match):
 		# these are well known names
 		return [ x for x in self.dbus.list_names() \
-				if match in x and x != OpenBMCMapper.MAPPER_NAME ]
+				if match(x) ]
 
 	def get_owner_names(self, match):
 		# these are unique connection names
 		return list( set( [ self.dbus.get_name_owner(x) \
 				for x in self.get_service_names(match) ] ) )
 
-	def get_owners(self, match):
-		return [ IntrospectionParser(x, self.dbus) \
-				for x in self.get_owner_names(match) ]
-
 if __name__ == '__main__':
 	dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 	bus = dbus.SystemBus()
-	s = dbus.service.BusName(OpenBMCMapper.MAPPER_NAME, bus)
 	o = ObjectMapper(BusWrapper(bus), OpenBMCMapper.MAPPER_PATH)
 	loop = gobject.MainLoop()