#! /usr/bin/python ## Alchemist - The core component of the system config tools package ## Copyright (C) 2000 Red Hat, Inc. ## Copyright (C) 2000-2009 Philipp Knirsch ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU 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., 675 Mass Ave, Cambridge, MA 02139, USA. import sys import string import gzip from types import * import xml.sax.handler class Entity: """Base Entity Class for all our classes Our base Entity class from which all other objects are derived. All entities have 3 attributes. Derived classes may define other additional attributes and/or operations. C specific API calls are not implemented. Attributes: - @b name - Semantic name of the entity. (E.g. "apache" or "vh"). - @b type - Entity type which specifies if it is e.g. a list or a scalar. - @b source - Place from where the entity came from (the origin context) """ def __init__(self, n=''): """ Public constructor Initializes the base entity. Inits the name, type and source which are common attributes to all entities. @param self Instance of class object @param n Optional name of entity. Default is the empty string """ self.name = n """Name of this Entity. Should be related to the purpose of the entity resp. it's use. """ self.type = None """Type of this Entity. One of 'string' , 'base64' , 'int' , 'float' , 'bool' , 'copy' or 'list' """ self.source = None """Source of this Entity. Usually something like "RHN" or "Local" """ def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. @param self Instance of class object @return New instance with copy of given object """ n = Entity() n.copy(self) return n def copy(self, other=None): """ Copy operator The copy() method initializes the instance from another instance. Similart to the copy operator in C++. @param self Instance of class object @param other Instance of class object from which self is being initialized """ if other == None: return self.__copy__() self.name = other.name self.type = other.type self.source = other.source def setName(self, n): """ Name attribute mutator method Mutator method for setting the name attribute of this entity. @param self Instance of class object @param n New name of this entity @see getName() """ self.name = n def getName(self): """ Name attribute accessor method Accessor method for getting the name of this entity. @param self Instance of class object @return @c string Name of this Entity @see setName() """ return self.name def setType(self, t): """ Type attribute mutator method Mutator method for setting the type attribute of this entity. Valid types are the following: - string - base64 - int - float - bool - copy - list @param self Instance of class object @param t New type of this entity @see getType() """ self.type = t def getType(self): """ Type attribute accessor method Accessor method for getting the type of this entity. @param self Instance of class object @return @c string Type of this Entity @see setType() """ return self.type def setSource(self, s): """ Source attribute mutator method Mutator method for setting the source attribute of this entity. A source is meant to be a unique identifier which specifies from where the information originated. Usually something like 'RHN' or 'local' or 'proc' . @param self Instance of class object @param s New source of this entity @see setSource() """ self.source = s def getSource(self): """ Source attribute accessor method Accessor method for getting the source of this entity. @param self Instance of class object @return @c string Source of this Entity @see getSource() """ return self.source def isIdentical(self, other): """ Identical comperator method Comperator method for evaluating if self is identical to other @param self Instance of class object @param other Instance of class object to compare with @return @b bool Truth value if self is identical to other """ return self == other def isAlive(self): """ Alive check Accessor method to verify if entity is alive. @param self Instance of class object @return @b bool Truth value if self is alive """ return True class ContextHandler(xml.sax.handler.ContentHandler): """ XML SAX2 content handler for Contexts """ def __init__(self, ctx, data, delete, id): self.ctx = ctx self.data = data self.delete = delete self.id = id self.currentity = None """XML parser entity storage Reference to current entity during XML parsing. """ self.currtype = None """XML parser type storage Type of current entity during XML parsing. """ self.currstate = '' self.currlevel = 0 def startElement(self, name, attrs): """ Private XML decoding SAX parser start element callback This is the wrapper callback method for our SAX parser for start elements. As we have to deal with 3 different types of information contained in the root element adm_context we use a form of state machine in order to know where we are and in turn what real start element method we have to call. The three different types of information contained in the root element of our XML encoded Context are these: 'delete' -- A single delete set entry. Simple element which contains a path to be deleted. Multiple root entries may exits. 'id' -- Identity tree element which describes the merge history of the Context. Only 1 root id element may exist. 'datatree' -- The "real" content of the Context. Here all the configuration information is stored. Only 1 root datatree may exist. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of opening element 'attrs' -- Python dictionary of attributes of this element **SEE:** '' -- "Context.fromXML()":#Context.fromXML '' -- "Context.__deleteStartElement()":#Context.__deleteStartElement '' -- "Context.__idStartElement()":#Context.__idStartElement '' -- "Context.__dataStartElement()":#Context.__dataStartElement """ if self.currstate == '': if self.currlevel > 1: raise TypeError, 'Wrong level in root element parsing: '+str(self.currlevel) if name == 'delete': self.currstate = 'delete' self.__deleteStartElement(name, attrs) elif name == 'id': self.__idStartElement(name, attrs) self.currstate = 'id' self.currlevel = self.currlevel + 1 elif name == 'datatree': self.__dataStartElement(name, attrs) self.currstate = 'data' self.currlevel = self.currlevel + 1 elif name == 'adm_context': self.currlevel = 1 else: raise NameError, 'Root start element not one of delete, id, data or adm_context: '+name elif self.currstate == 'id': self.__idStartElement(name, attrs) self.currlevel = self.currlevel + 1 elif self.currstate == 'data': self.__dataStartElement(name, attrs) self.currlevel = self.currlevel + 1 else: raise TypeError, 'Unkown state of XML parser: '+self.currstate def endElement(self, name): """ Private XML decoding SAX parser end element callback Same as for the start element SAX parser callback method, it's again just a wrapper for our state machine. It's a lot easier as we only have to deal with closing elements and therefore no newly created entities or anything else complex. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of closing element (unused) **SEE:** '' -- "Context.fromXML()":#Context.fromXML """ if self.currstate == '': if name == 'adm_context': self.currlevel = self.currlevel - 1 else: raise NameError, 'Root end element not one of delete or adm_context: '+name elif self.currstate == 'delete': self.__deleteEndElement(name) self.currstate = '' elif self.currstate == 'id': self.__idEndElement(name) self.currlevel = self.currlevel - 1 if self.currlevel == 1: self.currentity = None self.currtype = None self.currstate = '' elif self.currstate == 'data': self.__dataEndElement(name) self.currlevel = self.currlevel - 1 if self.currlevel == 1: self.currentity = None self.currtype = None self.currstate = '' else: raise TypeError, 'Unkown state of XML parser: '+self.currstate def characters(self, data): """ Private XML decoding SAX parser chardata callback The last of the three needed SAX parser callback methods this one handles the character data of an element. And again just like for the start element and end element callbacks this one is only a wrapper for the state machine. Currently char data is only used by the scalar data types, but we do in order to keep it correctly we do it in a well defined manner here. **PARAMS:** 'self' -- Instance of class object 'data' -- Character data of the current element **SEE:** '' -- "Context.fromXML()":#Context.fromXML """ if self.currstate == 'delete': self.__deleteCharData(data) elif self.currstate == 'id': self.__idCharData(data) elif self.currstate == 'data': self.__dataCharData(data) def __deleteStartElement(self, name, attrs): """ Private XML decoding SAX parser delete set start element callback Nothing has to be done here. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of opening element 'attrs' -- Python dictionary of attributes of this element **SEE:** '' -- "Context.__StartElement()":#Context.__StartElement """ pass def __deleteEndElement(self, name): """ Private XML decoding SAX parser delete set end element callback Nothing has to be done here. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of closing element **SEE:** '' -- "Context.__EndElement()":#Context.__EndElement """ pass def __deleteCharData(self, data): """ Private XML decoding SAX parser delete set chardata callback As the whole delete set handling is very simple, so are the state machine callback methods for it. This is actually the only method for the delete set doing anything. It just sets the delete set path value for the given chardata. **PARAMS:** 'self' -- Instance of class object 'data' -- Character data of the current element **SEE:** '' -- "Context.__CharData()":#Context.__CharData """ self.setDelete(data) def __idStartElement(self, name, attrs): """ Private XML decoding SAX parser identity tree start element callback This SAX parser start element callback method is similar to the one for the data tree, only that it is less complex. The first identity element we find will be the identity of this Context. All other identity elements should appear in the tree under this first identity element and will be treated as the parents of this Context. All parent identites are handled the same way, so we can always use the same logic for the real parsing of the elements. The two differences between the data tree parser and this one are that the identity tree never contains character data and that we don't have the backwards reference as with our data tree. The first thing doesn't concern us, it just simplyfies the chardata callback method for the identity tree (basically it's empty). The second one though is a smaller problem as we need to climb up the tree again when an identity element has been processed. We do this by using the self.currentity variable as a dictionary of parents. That way it always contains all the parents of the current tree and we can easily trace back during sequential parsing. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of opening element 'attrs' -- Python dictionary of attributes of this element **SEE:** '' -- "Context.__StartElement()":#Context.__StartElement """ if self.currentity == None: self.id.setName(attrs['NAME']) self.id.setSerial(attrs['SERIAL']) self.currentity = {} self.currentity[self.currlevel] = self.id return if name == 'null': return nid = Identity(attrs['NAME'], attrs['SERIAL']) if self.currentity[self.currlevel-1].getParentA() == None: self.currentity[self.currlevel-1].setParentA(nid) else: self.currentity[self.currlevel-1].setParentB(nid) self.currentity[self.currlevel] = nid def __idEndElement(self, name): """ Private XML decoding SAX parser identity tree end element callback Again a very simple method, as the end element callback hardly ever does anything really complicated. We just make sure here that the entity for the current level is reset so that if we ever have a damaged tree we will get an exception. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of closing element **SEE:** '' -- "Context.__EndElement()":#Context.__EndElement """ if name == 'null': return self.currentity[self.currlevel] = None def __idCharData(self, data): """ Private XML decoding SAX parser identity tree chardata callback The SAX parser chardata callback for the identity tree doesn't do anything as we never store any information on the chardata for the identity tree but keep it all in attributes. **PARAMS:** 'self' -- Instance of class object 'data' -- Character data of the current element **SEE:** '' -- "Context.__CharData()":#Context.__CharData """ pass def __dataStartElement(self, name, attrs): """ Private XML decoding SAX parser data start element callback SAX XML parser handler for start tags. Will be called for every start of an element in our XML stream. In order to keep symmetry with encoding and decoding we do a small trick here: The name of the Context is being encoded as the root element of our XML stream in toXML(). So the first element we encounter will be our Context name. Every other element contained inside this one will be part of the data List and parsed into that. Other than that we simply add a new child to the current data entity, set all attributes (depending on the type) and switch the current type and data entity to the newly created. Sanity checks should be but in to make sure that the XML stream meets the following conditions: - Only data enities of type 'LIST' may contain other entities - Only our supported data types should be allowed - Only our supported attributes for the specific types should be allowd This would basically be the sanity and verification step where we can verify that the data structure is well formed. This is different from the possible XML based DTD check which is application specific and not part of the Alchemist (actually currently not being done either). **PARAMS:** 'self' -- Instance of class object 'name' -- Name of opening element 'attrs' -- Python dictionary of attributes of this element **SEE:** '' -- "Context.fromXML()":#Context.fromXML """ # # Check if we just started processing. In that case our current # entity will still be none and we do the following things: # - Set the name of this Context to the name of the element as # this is has been encoded that way. # - Set the current entity to the root List to reflect that we # are now actually beginning to parse the content. # - Set the current type to 'LIST', which is what our root # List should be ;) if self.currentity == None: self.ctx.setName(name) self.currentity = self.data self.currtype = 'LIST' return type = attrs['TYPE'] if attrs.has_key('VALUE'): value = attrs['VALUE'] else: value = None child = self.currentity.addChild(type, name) if attrs.has_key('SOURCE'): child.setSource(attrs['SOURCE']) if attrs.has_key('PROTECTED'): child.setProtected(attrs['PROTECTED']) if type == 'LIST': if attrs.has_key('ANONYMOUS'): child.setAnonymous(attrs['ANONYMOUS']) if attrs.has_key('ATOMIC'): child.setAtomic(attrs['ATOMIC']) if attrs.has_key('PREPEND'): child.setPrepend(attrs['PREPEND']) else: child.setValue(value) self.currentity = child self.currtype = type def __dataEndElement(self, name): """ Private XML decoding SAX parser data end element callback When we reach the end of an element we don't have to do an awfull lot anymore. The attributes have already been processed during the start of the element, the value of the entity has been processed in the __dataCharData() method, so the only thing we have to make sure here is to "go up" one level again. **PARAMS:** 'self' -- Instance of class object 'name' -- Name of closing element (unused) **SEE:** '' -- "Context.fromXML()":#Context.fromXML """ self.currentity = self.currentity.getContainer() self.currtype = self.currentity.getType() def __dataCharData(self, data): """ Private XML decoding SAX parser data chardata callback Here we actually set the value of our current data entity. This only happens for scalar data entities though, so if we should come in here for a list or even during the startup (which could happen) we simply ignore that data as Lists cannot contain anything else than other data entites and all character data therefor is simply filler stuff. **PARAMS:** 'self' -- Instance of class object 'data' -- Character data of the current element **SEE:** '' -- "Context.fromXML()":#Context.fromXML """ if self.currentity == None: return if self.currtype == 'LIST': return self.currentity.setValue(data) class Context(Entity): """ Root structure for our information storage A Context is our information "root" structure. Each merge will be done on a Context, which in turn will then merge the data trees of the two Contexts and update the merge history in the Identity tree. """ def __init__ (self, name='', serial=0, ctx=None, xml=None): """ Public constructor Initializes all of our Context specific things. A Context contains a few more things than a simple entity: 'data' -- A List of the contents of this context 'delete' -- A python list of strings of references to be deleted 'id' -- The Identity tree of this context (containing it's own Identity as root node) It also supports a copy constructore equivalent from C++ where you can specify another Context from which this one gets initialized. Otherwise an empty Context will be created with no name, a empty Identity and empty data and delete lists. **PARAMS:** 'self' -- Instance of class object 'vals' -- Dictionary of variable args. Parsed in order to decided what kind of constructor to use """ Entity.__init__(self) self.setType('context') self.use_xmllib = 0 """XML LIB Sax parser usage flag Flag to indicate wether we want to use the slow but rocksolid xmllib XML parser or the PyExpat one. """ self.data = None """List data container List container for complete data content of this Context. """ self.delete = None """Python list of deletes Python list of strings of references to be deleted. """ self.id = None """Identity tree of this Context Contains the complete merge history and the Identity of this Context. """ if ctx != None: self.copy(ctx) return if xml != None: self.__init__(name='', serial=0) self.fromXML(xml) return if name != None and serial != None: self.setName(name) self.id = Identity(self.getName(), serial) self.delete = [] self.data = ListData() self.data.setContainer(self.data) self.data.setSource(self.getName()) self.data.setContext(self) return raise ContextError, "Illegal constructor parameters" def __getitem__(self, key): """ Implementation of Python sequence index accessor Override of __getitem__() operator for Python sequences. Basically the sequence accessor method for Python sequences. That way a Context can be handled similarily like a sequence. This allows us to write a little nicer looking code when accessing the data of a Context. **PARAMS:** 'self' -- Instance of class object 'key' -- Index to be accessed. One of 'data' , 'delete' or 'id' **RETURNS:** *ref* -- Reference to the requested data **EXCEPTIONS:** *KeyError* -- Raised if none of the valid keys was tried to accessed **SEE:** '' -- "Context.__setitem__()":#Context.__setitem__ """ if key == 'data': return self.data elif key == 'delete': return self.delete elif key == 'id': return self.id else: raise KeyError def __setitem__(self, key, value): """ Implementation of Python sequence index mutator Override of __setitem__() operator for Python sequences. This is the mutator method for Python sequences. It shoudldn't be used too often as the behaviour hasn't been defined, but for symmetry reasons we provide it here. **PARAMS:** 'self' -- Instance of class object 'key' -- Index to be accessed. One of 'data' , 'delete' or 'id' 'value' -- Value to which the element at key should be set. **EXCEPTIONS:** *KeyError* -- Raised if none of the valid keys was tried to accessed **SEE:** '' -- "Context.__getitem__()":#Context.__getitem__ """ if key == 'data': self.data = value elif key == 'delete': self.delete = value elif key == 'id': self.id = value else: raise KeyError def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Context* -- New instance with copy of given object """ n = Context({'name':self.id.getName(), 'serial':self.id.getSerial()}) n.copy(self) return n def copy(self, other=None): """ Copy operator Copy operator, like in the Entity class. First calls it's parent copy operator and then copies the rest of the data over to itself. **PARAMS:** 'self' -- Instance of class object 'other' -- Instance of Context to copy from """ if other == None: return self.__copy__() Entity.copy(self, other) self.data = other.data.__copy__() self.delete = other.delete[:] self.id = other.id.__copy__() def create(self, n='', ser=0): """ Context creation method For completeness of the API we provide another way of creating a Context in the procedural way (not by using a constructor). **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Context* -- Newly created empty Context instance **SEE:** '' -- "Context.__init__()":#Context.__init__ """ return Context(n, ser) def merge(self, name, serial, other): """ Merthod to merge two contexts This method is the starting point for all merges. A merge should always be invoked by an application using the Context merge call, never on Lists alone. **PARAMS:** 'self' -- Instance of first Context to merge 'serial -- Serial number of the context 'other' -- Instance of second Context to merge **RETURNS:** *Context* -- Newly created Context containing merge result **SEE:** '' -- "List.merge()":#List.merge """ nc = self.__copy__() nc.setName(name) id = nc.getIdentityRoot() id.setSerial(serial) id.setName(name) id.setParentA(self.getIdentityRoot()) id.setParentB(other.getIdentityRoot()) for i in xrange(other.getNumDeletes()): try: c = nc.getDataByPath(other.getDelete(i)) if c.isProtected() == 0: p = c.getContainer() p.delChild(c.getPos()) except: nc.setDelete(other.getDelete(i)) nc.getDataRoot().setSource(nc.getName()) nc.data = nc.getDataRoot().merge(other.getDataRoot()) return nc def flatten(self): """ Flatten resp. process a context Flattens or rather processes the Context and resolves all references and deletes. Usually called before merging two Contexts on both Contexts. **PARAMS:** 'self' -- Instance of Context to be flattened/processed """ self.delete = [] self.__flattenCopies(self.getDataRoot()) def __flattenCopies(self, data): if data.getType() == Data.ADM_TYPE_LIST: i = 0 while i < data.getNumChildren(): child = data.getChildByIndex(i) if child.getType() == Data.ADM_TYPE_LIST: self.__flattenCopies(data.getChildByIndex(i)) if child.getType() == Data.ADM_TYPE_COPY: data.delChild(child) i = i + 1 def toXML(self): """ Public XML encoding method Our public interface for encoding the the complete Context to XML. Calls the internal private methods __idToXML(), __deleteToXML() and __dataToXML() which actually perform the real work for the specific parts. This method only outputs the XML header as that is pretty much content independant. :) **PARAMS:** 'self' -- Instance of class object **RETURNS:** *string* -- XML encoded string of this Context **SEE:** '' -- "Context.__idToXML()":#Context.__idToXML '' -- "Context.__deleteToXML()":#Context.__deleteToXML '' -- "Context.__dataToXML()":#Context.__dataToXML """ ret = '\n' ret = ret + '\n' ret = ret + self.__idToXML(self.getIdentityRoot()) ret = ret + self.__deleteToXML() ret = ret + self.__dataToXML(self.getDataRoot()) ret = ret + '\n' return ret def __idToXML(self, data, depth=1): """ Private XML identity tree encoding method This method encods our identity tree the same way the C Alchemist does. Similar to the __dataToXML() method only a little simpler. Just as there subtrees are encoded exactly the same as our main tree. This symmetry makes creation really easy. **PARAMS:** 'self' -- Instance of class object 'data' -- Identity tree to be encoded 'depth' -- Current list depth **RETURNS:** *string* -- XML encoded string of this Identity tree **SEE:** '' -- "Context.toXML()":#Context.toXML """ if data == None: return ' '*depth + '\n' ret = ' '*depth + '\n' ret = ret + self.__idToXML(data.getParentA(), depth+1) ret = ret + self.__idToXML(data.getParentB(), depth+1) ret = ret + ' '*depth + '\n' return ret def __deleteToXML(self): """ Private XML delete set encoding method XML encodes all delete sets of this Context. Extremely simple, just creates a list of elements containing the references. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *string* -- XML encoded string of this delete set **SEE:** '' -- "Context.toXML()":#Context.toXML """ ret = '' for i in xrange(self.getNumDeletes()): ret = ret + ' \n' return ret def __dataToXML(self, data, depth=1): """ Private XML data encoding method Private method which recursively creates our XML encoded output of our internal data structure. Doesn't use any of the xmllib or modules but does it on the fly on it's own. **PARAMS:** 'self' -- Instance of class object 'data' -- Data object to be encoded 'depth' -- Current list depth **RETURNS:** *string* -- XML encoded string of this Data object **SEE:** '' -- "Context.toXML()":#Context.toXML """ ret = ' '*depth + '<' if depth == 1: ret = ret + 'datatree' else: ret = ret + data.getName() ret = ret + ' TYPE="' + Data.ADM_XML_TYPES[data.getType()] + '"' if depth > 1 and data.getSource() != None and data.getSource() != self.getName(): ret = ret + ' SOURCE="' + data.getSource() + '"' if data.isProtected(): ret = ret + ' PROTECTED="TRUE"' if data.getType() == Data.ADM_TYPE_LIST: if data.isAnonymous(): ret = ret + ' ANONYMOUS="TRUE"' if data.isAtomic(): ret = ret + ' ATOMIC="TRUE"' if data.isPrepend(): ret = ret + ' PREPEND="TRUE"' ret = ret + '>\n' for index in range(data.getNumChildren()): ret = ret + self.__dataToXML( \ data.getChildByIndex(index), depth + 1) ret = ret + ' '*depth + '\n' else: ret = ret + ' VALUE="' + str(data.getValue()) + '"/>\n' return ret def fromXML(self, xmlstr): """ Public XML decoding method The symmetrical call for XML decoding is fromXML(). That one actually uses a SAX parser to do the work. The magic lies in the handle methods we set for the parsing. It's fairly efficient and quick by still keeping it pretty simple. **PARAMS:** 'self' -- Instance of class object 'xml' -- XML string of data to be decoded into this Context **SEE:** '' -- "Context.__StartElement()":#Context.__StartElement '' -- "Context.__EndElement()":#Context.__EndElement '' -- "Context.__CharData()":#Context.__CharData """ p = xml.sax.make_parser() handler = ContextHandler(self, self.data, self.delete, self.id) p.setContentHandler(handler) p.feed(xmlstr) def setDelete(self, ref): """ Delete set mutator method Mutator method for delete set of this Context. As due to efficiency we don't want to keep the delete references inside of our data tree we store them inside a simple Python list. A delete reference can only appear once per Context, therefore we only add it once to the delete list. **PARAMS:** 'self' -- Instance of class object 'ref' -- Reference to data entity to be deleted **SEE:** '' -- "Context.getDelete()":#Context.getDelete '' -- "Context.clearDeleteByIndex()":#Context.clearDeleteByIndex '' -- "Context.clearDeleteByString()":#Context.clearDeleteByString '' -- "Context.getNumDeletes()":#Context.getNumDeletes """ if self.delete.count(ref) > 0: return self.delete.append(ref) def getDelete(self, pos): """ Delete set accessor method Accessor method by index for delete se of this Context. Simply returns the reference at the given position. Might raise a KeyError exception in case we try to access a non existing delete reference. **PARAMS:** 'self' -- Instance of class object 'pos' -- Index of delete reference to be returned **RETURNS:** *string* -- String of reference of delete set at given position **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds **SEE:** '' -- "Context.setDelete()":#Context.setDelete '' -- "Context.clearDeleteByIndex()":#Context.clearDeleteByIndex '' -- "Context.clearDeleteByString()":#Context.clearDeleteByString '' -- "Context.getNumDeletes()":#Context.getNumDeletes """ return self.delete[pos] def clearDeleteByIndex(self, idx): """ Delete set removal by index method Removes a delete reference at a certain position from the delete set. **PARAMS:** 'self' -- Instance of class object 'idx' -- Index of the delete reference to be removed **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds **SEE:** '' -- "Context.setDelete()":#Context.setDelete '' -- "Context.getDelete()":#Context.getDelete '' -- "Context.clearDeleteByString()":#Context.clearDeleteByString '' -- "Context.getNumDeletes()":#Context.getNumDeletes """ self.delete.pop(idx) def clearDeleteByString(self, ref): """ Delete set removal by name method Removes a delete reference with a certain name from the delete set. **PARAMS:** 'self' -- Instance of class object 'ref' -- Reference name to be deleted from the delete set **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds **SEE:** '' -- "Context.setDelete()":#Context.setDelete '' -- "Context.getDelete()":#Context.getDelete '' -- "Context.clearDeleteByIndex()":#Context.clearDeleteByIndex '' -- "Context.getNumDeletes()":#Context.getNumDeletes """ self.delete.remove(ref) def getNumDeletes(self): """ Number of delete set entries aggregate method Aggregation method to return the number of elements in our delete set. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *int* -- Number of elements in our delete set **SEE:** '' -- "Context.setDelete()":#Context.setDelete '' -- "Context.getDelete()":#Context.getDelete '' -- "Context.clearDeleteByIndex()":#Context.clearDeleteByIndex '' -- "Context.clearDeleteByString()":#Context.clearDeleteByString """ return len(self.delete) def getIdentityRoot(self): """ Identity root accessor method Accessor method for the Identity tree of this Context. The Identity tree, or short id, of a Context is simply speaking it's merge history. If a Context is created its Identity tree will contain only single Identity with no parents. If this Context is being involved in some mergeing later one it's Identity will be either updated or (if it is a parent of a merge) will be part of the parents of a merged Context. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Identity* -- Root node of Identity tree of this Context **SEE:** '' -- "Identity":#Identity """ return self.id def getDataRoot(self): """ Data root accessor method Accessor method for the root List of this Context. The root List contains all data stored inside a context and can never be deleted. The root List has a couple of properties that make it a little unique (although it is still a normal List): - The parent of the root List is the root List itself. - The name of the root List is "". This is a reserved name for the root List. - The root List is created during creation of the associated Context and is therefore always connected with that Context. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *List* -- Root node of data List of this Context **SEE:** '' -- "List":#List """ return self.data def getDataByPath(self, path): """ Data entity accessor by path In order to have an easy interface to access data entities of our data set in the tree we provide an additional access method where we can specify the complete path into our tree at which we want to access an element. **PARAMS:** 'self' -- Instance of class object 'path' -- Path to data entity we want to access **RETURNS:** *Data* -- Reference to Data entity pointed to by path **EXCEPTIONS:** *KeyError* -- Raised if no entity with that path exists **SEE:** '' -- "List.__getitem__()":#List.__getitem__ """ if path[0] != '/': raise KeyError keys = string.split(path[1:], '/') data = self.getDataRoot() for i in keys: data = data[i] return data class Identity(Entity): """ Identity information of a Context The Identity class is used for merge contexts and merge contex identification. Each Context has one unique Identity and (if it is the product of a merge) has the corresponding Identities as parents (A and B). That way we sort of build the merge history tree from ground up and can later on see what was merged and when. Example: ID1 ID2 \ / \ / ID4 ID3 \ / \ / ID5 """ def __init__(self, n='', serial=0): """ Public constructor Initializes the base entity. Inits the name, type and source which are common attributes to all entities. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional name of Identity. Default is the empty string """ Entity.__init__(self, n) self.setType('id') self.parentA = None """First parent of this Identity Each Identity has up to two parents. This attribute contains the first parent of the Identity. """ self.parentB = None """Second parent of this Identity The other possible parent of the Identity. Using both parents we have sorft of a reversed merge tree (upside down). """ self.serial = serial """Serial number of the Identity Contains the serial number of an Identity. Should be unique but it's up to the Identity and the application to set and use it, it's currently not used internally by the algorithms. """ def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Identity* -- New instance with copy of given object """ n = Identity() n.copy(self) return n def copy(self, other=None): """ Copy operator Data entity copy operator. Similar to the copy operator of C++, this one takes 2 arguments and copies the other into this (self). **PARAMS:** 'self' -- Instance of class object 'other' -- Instance of Identity to copy from """ if other == None: return self.__copy__() Entity.copy(self, other) self.parentA = other.parentA self.parentB = other.parentB self.serial = other.serial def setParentA(self, p): """ parentA attribute mutator method Mutator method to set the first parent of this Identity. **PARAMS:** 'self' -- Instance of class object 'p' -- Reference to Identity object of first parent **SEE:** '' -- "Identity.getParentA()":#Identity.getParentA """ self.parentA = p def getParentA(self): """ parentA attribute accessor method Accessor method for first parent of this Identity. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *ref* -- Reference to Identity object of first parent **SEE:** '' -- "Identity.setParentA()":#Identity.setParentA """ return self.parentA def setParentB(self, p): """ parentB attribute mutator method Mutator method to set the second parent of this Identity. **PARAMS:** 'self' -- Instance of class object 'p' -- Reference to Identity object of second parent **SEE:** '' -- "Identity.getParentB()":#Identity.getParentB """ self.parentB = p def getParentB(self): """ parentB attribute accessor method Accessor method for second parent of this Identity. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *ref* -- Reference to Identity object of second parent **SEE:** '' -- "Identity.setParentB()":#Identity.setParentB """ return self.parentB def setSerial(self, s): """ serial number attribute mutator mthod Mutator method to set the serial number of this Identity. **PARAMS:** 'self' -- Instance of class object 's' -- New serial number of this Identity **SEE:** '' -- "Identity.getSerial()":#Identity.getSerial """ self.serial = s def getSerial(self): """ serial number attribute accessor mthod Accessor method for serial number of this Identity. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *int* -- Serial number of this Identity **SEE:** '' -- "Identity.setSerial()":#Identity.setSerial """ return self.serial class Data(Entity): """ Base Data Entity class The Data class represents an entity which will actually contain data and appear only in the data tree of a Context. These will be scalar types and lists. Each Data entity will have several common attributes and the accompanying mutator/accessor methods. The following common attributes for Data entities exist in addition to the Entity attributes: 'parent' -- Parent of this Data entity 'context' -- Context of this Data entity 'protected' -- Flag wether this Data entity is protected or not 'value' -- Value of this Data entity """ ## From alchemist.h: ADM_TYPE_UNKNOWN = 0 """Alchemist Data Model unknown type constant """ ADM_TYPE_LIST = 1 """Alchemist Data Model list type constant """ ADM_TYPE_COPY = 2 """Alchemist Data Model copy type constant """ ADM_TYPE_INT = 3 """Alchemist Data Model int type constant """ ADM_TYPE_FLOAT = 4 """Alchemist Data Model float type constant """ ADM_TYPE_BOOL = 5 """Alchemist Data Model bool type constant """ ADM_TYPE_STRING = 6 """Alchemist Data Model string type constant """ ADM_TYPE_BASE64 = 7 """Alchemist Data Model base64 type constant """ ADM_XML_TYPES = ('UNKNOWN', 'LIST', 'COPY', 'INT', 'FLOAT', 'BOOL', 'STRING', 'BASE64') """Alchemist Data Model XML encoded data type list """ def __init__(self, n=''): """ Public constructor Initializes data entity specific attributes. Additional to the Entity attributes these are the new ones: 'parent' -- Parent of this Data entity 'context' -- Context of this Data entity 'protected' -- Flag wether this Data entity is protected or not 'value' -- Value of this Data entity **PARAMS:** 'self' -- Instance of class object 'n' -- Optional name of Identity. Default is the empty string """ Entity.__init__(self, n) self.parent = None """Parent of this Data entity The parent attribute contains a reference to a Data entity which contains this object. That way a Data entity can easily walk the complete data tree up and downwards. Every Data entity has a parent which has to be a List. The only slightly special case is the root List of a Context. That List has itself as a parent. That way the rule holds for every Data entity and we can easily check wether we are at the root List or not (by comparing the parent with the current object or by checking wether the name is "" as that is the reserved name of the root List entity). """ self.context = None """Context of this Data entity Each Data entity has to be linked/contained to/in a Context. As children usually get added to a Context's content via the addChild() method of the List data type it is automatically linked there. No Data entity should exists in "empty" space, meaning having no parent or not linked to any context. """ self.protected = 0 """Flag wether this Data entity is protected or not The protected attribute is the first Data entity and merge algorithm specific attribute. It is used to specify wether a Data entity may be overwritten or not. If it is set then the Data entity will not be modified during merging. For List entities this means that if the List is protected no changes whatsoever will be done to it during a merge which effectively then disables merges with this List. For scalar/atmoic values this has the logical behaviour of a protected value not being overwritable and therefore winning during a merge. """ self.value = None """Value of this Data entity The value attribute is also common to all Data entities, although for some entities this doesn't have a real meaning, e.g. for Lists. Usually though it will contain the value to which the Data entity has been set. Each derived class may do verifications during the setValue() method in order to allow only legal values. This is not done for the general implementation however as there are no boundaries for the general case. """ def __copy__(self): n = Data() n.copy(self) return n def copy(self, other=None): """ Copy operator Data entity copy operator. Similar to the copy operator of C++, this one takes 2 arguments and copies the other into this (self). **PARAMS:** 'self' -- Instance of class object 'other' -- Instance of Context to copy from """ if other == None: return self.__copy__() Entity.copy(self, other) self.parent = other.parent self.context = other.context self.protected = other.protected self.value = other.value def merge(self, other): """ Data entity merge method In order to have a unified way of merging without having to worry which data type we are trying to merge we use the simple OO technique of basic class implementations and overrides in derived classes for special data types (like Lists). The basic implementation returns a copy of either self or other, depending on the protected status of self. **PARAMS:** 'self' -- Instance of first data entity to merge 'other' -- Instance of second data entity to merge with **RETURNS:** *Data* -- A reference to a copy of either self (if protected) or other """ if self.isProtected(): return self.__copy__() else: return other.__copy__() def setContainer(self, p): """ Container attribute mutator method Mutator method to set the container of this Data entity. This is valid for all Data enties as they need to have a parent. The only special case is the root Data entity of a Context. The parent of that Data entity is itself. **PARAMS:** 'self' -- Instance of class object 'p' -- Reference to Data object of container **SEE:** '' -- "Data.getContainer()":#Data.getContainer """ self.parent = p def getContainer(self): """ Container attribute accessor method Accessor method to get the reference to the Data object of the container of the this object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Data* -- A reference to the container Data entity of this object **SEE:** '' -- "Data.setContainer()":#Data.setContainer """ return self.parent def setContext(self, c): """ Context attribute mutator method Mutator method to set the Context in which this Data entity resides. Each Data entity has to exist in a Context (linked with it). If a Data entity is created it's always done with the addChild() method of our List class which will automatically link it to the Context to which the List belongs to which we add this new Data entity. **PARAMS:** 'self' -- Instance of class object 'c' -- Reference to Context object to which we link this object **SEE:** '' -- "Data.getContext()":#Data.getContext """ self.context = c def getContext(self): """ Context attribute accessor method Accessor method to get the reference to the Context object to which this object is linked. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Context* -- A reference to the Context object to which this object is linked **SEE:** '' -- "Data.setContext()":#Data.setContext """ return self.context def setProtected(self, f): """ Protected attribute mutator method Mutator method to set the protected flag for this Data entity. **PARAMS:** 'self' -- Instance of class object 'f' -- Boolean wether the Data object should be protected or not **SEE:** '' -- "Data.getProtected()":#Data.getProtected """ self.protected = f def isProtected(self): """ Protected attribute validation method Validation method to check wether this Data entity is protected or not. A protected Data entity may not be overwritten and a merge with it therefor will return a copy of iself only. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Bool* -- Boolean indication wether this Data object is protected or not **SEE:** '' -- "Data.setProtected()":#Data.setProtected """ return self.protected def setValue(self, v): """ Value attribute mutator method Mutator method to set the value of this Data entity. It's basically up to the application to decide what to put in here, but a derived Data entity might implement a check here to verify the input and reject it if it is not valid. **PARAMS:** 'self' -- Instance of class object 'v' -- Value which should be set for this Data object **SEE:** '' -- "Data.getValue()":#Data.getValue """ self.value = v def getValue(self): """ Value attribute accessor method Accessor method to get the value for this Data object. Some Data entites don't actually have values, like the List entity, so a value might have not actual meaning for some Data entities. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *ref* -- A reference to the value for this Data object **SEE:** '' -- "Data.setValue()":#Data.setValue """ return self.value def getPos(self): """ Position index in container As each Data entity is contained in a container and we often have to access entities by their position this is another convenience method which returns the position of a Data entity inside it's parent container. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *int* -- Position of this object in it's parent. **EXCEPTIONS:** *NameError* -- Raised if entity has no parent *IndexError* -- Raised if entity is not a child of its parent **SEE:** '' -- "List.getChildByIndex()":#List.getChildByIndex """ parent = self.getContainer() if parent == None: raise NameError, 'Data entity has no parent.' for i in xrange(parent.getNumChildren()): if parent.getChildByIndex(i) is self: return i raise IndexError def unlink(self): """ Data removal method Removes this data element from it's parent """ parent = self.getContainer() if parent == None: raise NameError, 'Data entity has no parent.' parent.delChild(self) class ListData(Data): """ List Data Entity class The List entity is probably our single most important entity and data type so we have to take care to do a pretty good (and fast) job with it. Lists will be used as our general container type for all data we operate on. It is basically a ordered set of entities with some special attributes: 'anoymous' -- An anonymous List is a List which may contain children with equal names. Children in an anonymous List cannot be referenced (and therefore not deleted either). During a merge equally named children will not be overwritten but appended. Handled by the List itself. 'atomic' -- Specifies that during a merge this list will either be replaced completely or not be changed (sort of a scalar-ization of a list). Only used by merge algorithm, not by the List itself. 'prepend' -- Defines wether during a merge the children of this List should appear at the front or at the back of the merged context. Only used by merge algorithm, not by the List itself. Internally we use some very fancy data structures in order to achive the speed we need and to offer constant insert, find and delete operation times ( O(1) is guaranteed with this implementation for most of our API methods (With the exception of delChild(), but this shouldn't be called that often anyway)). """ def __init__(self, n=''): """ Public constructor Initilaizes the various List related attributes. Additional to the Entity and Data class attributes are the following attributes: Anonymous -- Specifies wether this List is an anonymous List or not. Atomic -- Specifies wether this List is an atmoic List or not. Prepend -- Specifies wether this List should be prepended or not. """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_LIST) self.anonymous = 0 """Anonymous List flag Indicates wether this List is anonymous. Anonymous Lists may contain multiple children with the same name. There can be no reference to a child inside an anonymous List. """ self.atomic = 0 """Atomic List flag Indicates wether this List is atomic. Atomic List do not merge their contents but rather are either replaced completely or are not changed at all. Or to put it differently, they merge like atomic scalars, either one or the other wins. """ self.prepend = 0 """Prepend List flag Indicates wether a List is set to prepend. A List that is set to prepend will appear at the front of the merge List during a merge operation. """ self.pvalue = [] """Python list of references to children by position (index) This python list contains references to all children. Due to the nature of lists in Python they are accessible only by index resp. position. This one is needed for indexed access to children. """ self.nvalue = {} """Python dictionary with names containing lists of references to children. A little more complicated structure to offer fast access to the children by name and optionally the n-th child with the same name. This enables to to work efficiently based on names even with anonymous Lists. """ def __len__(self): """ Overloaded Python sequence length operator Returns the number of children of this list. Overloads the Python operator for that so that we can use our List class just like a Python sequence. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *int* -- Number of children of this List """ return self.getNumChildren() def __getitem__(self, key): """ Python sequence accessor operator overload In order to have a nice interface to our Lists we not only provide the standard defined API but also the Python specific seqeuence index accessor operator with which we can access the children of a list in the following manner: *integer* -- Access the n-th element of the List *string* -- Access the first child with given name *string/integer* -- Access the n-th child with given name Especially the last one is quite nice as it allows us to easily walk anonymous Lists (to be explained later). **PARAMS:** 'self' -- Instance of class object 'key' -- Child index we want to access **RETURNS:** *Data* -- Reference to the child specified by the given key **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds or invalid """ if type(key) == StringType: return self.getChildByName(key) else: return self.getChildByIndex(key) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *List* -- New instance with copy of given object """ n = ListData() n.copy(self) return n def copy(self, other=None): """ Copy operator List entity copy operator. Similar to the copy operator of C++, this one takes 2 arguments and copies the other into this (self). Special case for List type as we need to copy all the children as well. **PARAMS:** 'self' -- Instance of class object 'other' -- Instance of Context to copy from """ if other == None: return self.__copy__() Data.copy(self, other) self.parent = other.parent self.context = other.context self.protected = other.protected self.anonymous = other.anonymous self.atomic = other.atomic self.prepend = other.prepend for i in range(other.getNumChildren()): self.addChild(other.getChildByIndex(i).__copy__()) # # This is the engine core. The basic List merging is being done in # this method, based on the various attributes and logic operations # defined upon them. # # @params self List instance of first List to be merged # @params other List instance of second List to be merged # @returns Merged List with self having priority over other # def merge(self, other): """ List merge method This is probably the single most important method of the whole Alchemist. It merges two given List objects and returns the merged result. The logic is pretty straight forward and defined by the setting of the various attributes of the given Lists: - If self is protected return a copy of self. - If self is atomic but not protected return a copy of other. - Set the insertion point according to the prepend attribute of other. - If self is anonymous simply insert all children of other at the insertion point and return the new List. - If self is not anonymous copy self to a new List and for each element in other check wether it exists in self or not and merge the two and put the result in place. All remaining elements of other are simply inserted at the insertion point. The whole thing sounds more complex than it is, we just have to be very carefull to do the right thing for the various combinations of attributes. **PARAMS:** 'self' -- Instance of List object to be merged 'other' -- Instance of List object to merge with **RETURNS:** *List* -- New List with merge result of the two Lists """ if self.getType() != other.getType(): raise TypeError, "Data types of entites don't match: "+self.getType()+" != "+other.getType() if self.isProtected(): return self.__copy__() if self.isAtomic(): return other.__copy__() if other.isPrepend(): p = 0 else: p = self.getNumChildren() nl = self if self.isAnonymous(): for i in range(other.getNumChildren()): oi = other.getChildByIndex(i) if other.isPrepend(): nl.addChild(oi.__copy__(), pos=p) else: nl.addChild(oi.__copy__()) p = p + 1 else: for i in range(other.getNumChildren()): oi = other.getChildByIndex(i) try: c = nl.getChildByName(oi.getName()) c = c.merge(oi) nl.addChild(c, pos=p) p = p + 1 except KeyError: nl.addChild(oi, pos=p) p = p + 1 return nl def setAnonymous(self, f): """ Anonymous attribute mutator method Mutator method for the anonymous flag of this List. An anonymous List is a List which can contain multiple children with the same name. There are a couple of restrictions on anonymous Lists: - No references can be made to children inside an anonymous List. - As a result no delete can be done on a single child inside an anonymous List. - Merges between anonymous Lists will always result in simply pre- or appending the second List to the first List. - Anonymous Lists can logically not be reverted to non anonymous Lists. **PARAMS:** 'self' -- Instance of class object 'f' -- Flag wether the List should be anonymous or not **SEE:** '' -- "List.isAnonymous()":#List.isAnonymous """ self.anonymous = f def isAnonymous(self): """ Anonymous attribute validation method Validation method to check wether this List entity is anonymous or not. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Bool* -- Boolean indication wether this List object is anonymous or not **SEE:** '' -- "List.setAnonymous()":#List.setAnonymous """ return self.anonymous def setAtomic(self, f): """ Atomic attribute mutator method Mutator method for the atomic flag of this List. An atmoic List is bascially a List that behaves like an atomic scalar data type. Therefore during merges it is either completely replaced or mot changed at all. **PARAMS:** 'self' -- Instance of class object 'f' -- Flag wether the List should be atomic or not **SEE:** '' -- "List.isAtomic()":#List.isAtomic """ self.atomic = f def isAtomic(self): """ Atomic attribute validation method Validation method to check wether this List entity is atomic or not. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Bool* -- Boolean indication wether this List object is atomic or not **SEE:** '' -- "List.setAtomic()":#List.setAtomic """ return self.atomic def setPrepend(self, f): """ Prepend attribute mutator method Mutator method for the prepend flag of this List. This attribute again affects the merge beahviour of the List. During a merge if that List is set to prepend all elements will be put in front instead of being appended. Usually the second List of a merge decides wether it will be prepended or not. Also this logically will only have an effect if the first List is neither protected (in which case the second List will be ignored completely) nor atomic (which would produce a copy of either the first or the second List). **PARAMS:** 'self' -- Instance of class object 'f' -- Flag wether the List should be set to prepend or not **SEE:** '' -- "List.isPrepend()":#List.isPrepend """ self.prepend = f def isPrepend(self): """ Prepend attribute validation method Validation method to check wether this List entity is set to prepend or not. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Bool* -- Boolean indication wether this List object is set to prepend or not **SEE:** '' -- "List.setPrepend()":#List.setPrepend """ return self.prepend def addChild(self, t, n='', v='', pos=-1): """ Add a child to the List object This method is our main interface to add new children to a List. By API definition it appends a new child with the given type, optional name and optional value at the end of the List. We have exteneded the Python interface with 2 additional features which are NOT standard API calls but Python specific: If the first parameter (or rather the type) is not a String it is assumed to be a valid Data entity and will be added instead of creating a new one. This allows us to write stuff like 'child = list.addChild(StringData('foo', 'bar')) which is more OO like (as we are using classes here anyway). The second addition is the optional position where we want the child to be added. By default it is appended but a position can be used to insert it at any place. This feature is used for moving children around in a List and should not be relied upon on in other languages to exist. **PARAMS:** 'self' -- Instance of class object 't' -- Type of the new child. Optional Data entity object 'n' -- optional name of the new child 'v' -- Optional value of the new child 'pos' -- Index where child should be added into List **RETURNS:** *Data* -- Reference to newly added child entity """ if type(t) == StringType or type(t) == UnicodeType: tstr = t.upper() if tstr == 'STRING': t = StringData(n, v) elif tstr == 'INT': t = IntData(n, v) elif tstr == 'BASE64': t = Base64Data(n, v) elif tstr == 'FLOAT': t = FloatData(n, v) elif tstr == 'BOOL': t = BoolData(n, v) elif tstr == 'COPY': t = CopyData(n, v) elif tstr == 'LIST': t = ListData(n) else: raise TypeError elif type(t) == IntType: if t == Data.ADM_TYPE_STRING: t = StringData(n, v) elif t == Data.ADM_TYPE_INT: t = IntData(n, v) elif t == Data.ADM_TYPE_BASE64: t = Base64Data(n, v) elif t == Data.ADM_TYPE_FLOAT: t = FloatData(n, v) elif t == Data.ADM_TYPE_BOOL: t = BoolData(n, v) elif t == Data.ADM_TYPE_COPY: t = CopyData(n, v) elif t == Data.ADM_TYPE_LIST: t = ListData(n) else: raise TypeError n = t.getName() nv = self.nvalue # # Anonymous is basically the only attribute of the List that # it takes care of by itself. When a List is anonymous it can # have multiple childs with the same name. # We use a neat trick to do this: If the item is new both # are treated the same. # In case we have an entry already though in the case of the # non anonymous list we first remove that entry from our List. # The effect is that in non anonymous Lists we will never have # more than 1 entry per nv[name] list, which is exactly what # we want. # if not nv.has_key(n): nv[n] = [] elif not self.isAnonymous() and len(nv[n]) > 0: self.delChild(nv[n][0]) nv[n].append(t) # # 11/17/00 PK: the addChild() function should not use the # prepend attribute to decide where to put the new child but # rather leave it up to the merge algorithm to move things # around (it's up to the 'external' objects to use the # attribute, but it's not the List's responsibility to use it # internally, just like with most other attributes) # if pos == -1: self.pvalue.append(t) else: self.pvalue.insert(pos, t) t.setContainer(self) if t.getSource() == '': t.setSource(self.getSource()) t.setContext(self.getContext()) return t def getChildByName(self, key): """ Child accessor method by name Accessor method that returns a child by a given name. For an anonymous List this method returns the first child found that matches the given name. **PARAMS:** 'self' -- Instance of class object 'n' -- Name of the child to be accessed **RETURNS:** *Data* -- Reference to child entity for given name **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds or invalid **SEE:** '' -- "List.__getitem__()":#List.__getitem__ """ if '/' in key: pos = string.index(key, '/') name = key[:pos] index = int(key[pos+1:]) else: name = key index = 0 return self.nvalue[name][index] def getChildByIndex(self, key): """ Child accessor method by position Accessor method that returns a child by a given index. **PARAMS:** 'self' -- Instance of class object 'idx' -- Index of the child to be accessed **RETURNS:** *Data* -- Reference to child entity for given name **EXCEPTIONS:** *KeyError* -- Raised if index is out of bounds **SEE:** '' -- "List.__getitem__()":#List.__getitem__ """ return self.pvalue[key] def getNumChildren(self): """ Number of children aggregate method Aggregation method to return the number of children in our List object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *int* -- Number of children in our List object """ return len(self.pvalue) def moveChild(self, e, p): """ Child movement method Moves the given child (which has to be an element of the List) to the given position. **PARAMS:** 'self' -- Instance of class object 'e' -- Reference to child to be moved 'p' -- Position where to move the child **RETURNS:** *Data* -- Reference to the moved child (should be same as 'e' """ self.delChild(e) self.addChild(e, pos=p) def copyData(self, e): """ Child copy and append method Creates a copy of the given Data entity and appends it to the List. **PARAMS:** 'self' -- Instance of class object 'e' -- Reference to child to be copied and appended **RETURNS:** *Data* -- Reference to the newly appended child """ self.addChild(e.__copy__()) def delChild(self, e): """ Private child removal method Removes the given child from the List. Can be done either by position or by Data entity reference. **PARAMS:** 'self' -- Instance of class object 'e' -- Position or reference to child to be deleted """ if type(e) == IntType: etmp = self.pvalue[e] else: etmp = e self.nvalue[etmp.getName()].remove(etmp) self.pvalue.remove(etmp) class StringData(Data): """ String Data Entity class Our String scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=''): """ Public constructor Initializes the String entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_STRING) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *String* -- New instance with copy of given object """ n = StringData() n.copy(self) return n class Base64Data(Data): """ Base64 Data Entity class Our Base64 scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=''): """ Public constructor Initializes the Base64 entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_BASE64) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Base64* -- New instance with copy of given object """ n = Base64Data() n.copy(self) return n class IntData(Data): """ Int Data Entity class Our Int scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=0): """ Public constructor Initializes the Int entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_INT) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Int* -- New instance with copy of given object """ n = IntData() n.copy(self) return n def setValue(self, v): """ Value attribute mutator method Mutator method to set the value of this Data entity. It's basically up to the application to decide what to put in here, but a derived Data entity might implement a check here to verify the input and reject it if it is not valid. **PARAMS:** 'self' -- Instance of class object 'v' -- Value which should be set for this Data object **SEE:** '' -- "Data.getValue()":#Data.getValue """ if type(v) == IntType: self.value = v elif (type(v) == StringType or type(v) == UnicodeType) and v != "": self.value = int(v) else: self.value = 0 class FloatData(Data): """ Float Data Entity class Our Float scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=0.0): """ Public constructor Initializes the Float entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_FLOAT) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Float* -- New instance with copy of given object """ n = FloatData() n.copy(self) return n def setValue(self, v): """ Value attribute mutator method Mutator method to set the value of this Data entity. It's basically up to the application to decide what to put in here, but a derived Data entity might implement a check here to verify the input and reject it if it is not valid. **PARAMS:** 'self' -- Instance of class object 'v' -- Value which should be set for this Data object **SEE:** '' -- "Data.getValue()":#Data.getValue """ try: self.value = float(v) except: self.value = 0.0 class BoolData(Data): """ Bool Data Entity class Our Bool scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=0): """ Public constructor Initializes the Bool entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_BOOL) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Bool* -- New instance with copy of given object """ n = BoolData() n.copy(self) return n def setValue(self, v): """ Value attribute mutator method Mutator method to set the value of this Data entity. It's basically up to the application to decide what to put in here, but a derived Data entity might implement a check here to verify the input and reject it if it is not valid. **PARAMS:** 'self' -- Instance of class object 'v' -- Value which should be set for this Data object **SEE:** '' -- "Data.getValue()":#Data.getValue """ if type(v) == StringType or type(v) == UnicodeType: if v.lower() == "true" or v.lower() == "on" or v != "0": self.value = True else: self.value = False else: try: self.value = bool(v) except: self.value = False class CopyData(Data): """ Copy Data Entity class Our Copy scalar basic type. Nothing fancy here. """ def __init__(self, n='', v=''): """ Public constructor Initializes the Copy entity. Sets the type, optional name and optional value accordingly. **PARAMS:** 'self' -- Instance of class object 'n' -- Optional initial name of the Data entity 'v' -- Optional initial value of the Data entity """ Data.__init__(self, n) self.setType(Data.ADM_TYPE_COPY) self.setValue(v) def __copy__(self): """ Private copy operator This method creates a copy of this object and returns the new object. **PARAMS:** 'self' -- Instance of class object **RETURNS:** *Copy* -- New instance with copy of given object """ n = CopyData() n.copy(self) return n # # Global module procedures # def merge(name, ser, ctx1, ctx2): """ Merge two contexts This global procedure merges the two given Contexts and sets the name and serial number of the newly created returned Context correspondigly. **PARAMS:** 'name' -- Name of the new Context 'ser' -- Serial number of the new Context 'ctx1' -- First Context to be merged 'ctx2' -- Second Context to be merged with the first **RETURNS:** *Context* -- New instance with a new Context with the given name and serial number containing the merge of the two Contexts """ cret = ctx1.merge(name, ser, ctx2) return cret # # Check if we let the Alchemist run in 'standalone' mode and not load it as # a module. In that case run some tests to see what we are doing. # if __name__ == '__main__': # # Pretty neat little test to actually build up a Context that looks # similar to what we will be using for bootstrapping BlackBoxes. # Contains most of the data types and combinations (except the # Protected attribute which really only makes sense for merging). # Also uses the toXML() and fromXML() calls to feedback verify that # the encoding functions do the right thing. # Merging is tested as well by merging the same Context with itself, # so reference problems should show up fairly quickly. # def test(): ctx = Context(name='local', serial=1) bs = ctx.data bs.setSource('Local') boxes = bs.addChild ('list', 'boxes') input1 = boxes.addChild ('list', 'input1') input1.addChild ('string', 'location', 'input1.py') config = input1.addChild ('list', 'config') input2 = boxes.addChild ('list', 'input2') input2.addChild ('string', 'location', 'input2.py') config = input2.addChild ('list', 'config') apps = bs.addChild ('list', 'apps') test = apps.addChild ('list', 'test') inputs = test.addChild ('list', 'inputs') inputs.setAnonymous(1) inputs.addChild ('string', 'boxname', 'input1') inputs.addChild ('string', 'boxname', 'input2') # print bs['apps']['test']['inputs'] # print inputs # print inputs[0] # print inputs['boxname/0'] # print inputs[1] # print inputs['boxname/1'] test.addChild ('string', 'output', 'forge') testrev = apps.addChild ('list', 'testrev') inputs = testrev.addChild ('list', 'inputs') inputs.addChild ('string', 'boxname', 'input2') inputs.addChild ('string', 'boxname', 'input1') testrev.addChild ('string', 'output', 'forge') tst = ctx.__copy__() # print tst.toXML() xml = ctx.toXML() #print xml ctx2 = Context(name='local', serial=1) ctx2.fromXML(xml) print ctx2.toXML() ctx3 = ctx.merge(name='local', serial=1, other=ctx2) #print ctx3.toXML() return ctx # # First test of co def test2(): c2 = Context(name='Bar', serial=1) c = Context(ctx=c2) c2.setName('Foo') c.setDelete('/apache/vh/1') print 'Context reference: ' + str(c) print 'Context getDelete(0): ' + c.getDelete(0) print 'Context c["delete"][0]: ' + c['delete'][0] print 'Context Name: ' + c.getName() print 'Context2 Name: ' + c2.getName() d = c['data'] print d c = Context(name='MyBootStrap', serial=2) d = c['data'] fbb = d.addChild(ListData('FileBlackBox')) fbb.addChild(StringData('dirname')).setValue('Log') fbb.addChild(StringData('user')).setValue('harald') fbb.addChild(IntData('port')).setValue(10) fbb.addChild(StringData('user')).setValue('florian') max = fbb.getNumChildren() for i in range(max): print fbb[i].getName() + ': ' + `fbb[i].getValue()` # # Tiny test to verify that merging is actually doing the right thing. # Not a very deep and thorough test, mind you, but it's a start for # 'usual case' behaviour test. # The Test.py will contain a lot more corner cases and boundary checks # for that which we can verify. This one here only give a quick'n'dirty # way to see if anything is working at all quickly. # def test3(): c1 = Context(name='Foo', serial=1) c2 = Context(name='Bar', serial=2) d1 = c1.getDataRoot() d2 = c2.getDataRoot() l1 = d1.addChild(Data.ADM_TYPE_LIST, 'buz') l1.setProtected(1) l1.setAnonymous(0) l1.setAtomic(0) l1.setPrepend(0) x1 = l1.addChild(Data.ADM_TYPE_STRING, 'abra', 'l1') x1.setProtected(0) x2 = l1.addChild(Data.ADM_TYPE_STRING, 'cada', 'l2') l2 = d2.addChild(Data.ADM_TYPE_LIST, 'buz') l2.setProtected(0) l2.setAnonymous(0) l2.setAtomic(0) l2.setPrepend(0) x2 = l2.addChild(Data.ADM_TYPE_STRING, 'abra', 'l2') x2.setProtected(0) c3 = merge('Joe', 3, c1, c2) c3.setDelete('abra') print c3.toXML() c4 = Context(name='Jack', serial=4) c4.fromXML(c3.toXML()) print c4.toXML() def test4(): try: ctx = Context(name = " asdsa", serial = 1) except ContextError, e: pass ctx = Context (name = 'name', serial = 1) ctxc = ctx.copy () ctx = Context (name = 'name', serial = 1) ctxc = ctx.copy () ctxcc = ctxc.copy () ctxcc.flatten () xml = ctxcc.toXML () ctxml = Context (xml = xml) ctxcc.setDelete ('/blah') delete = ctxcc.getDelete (0) ctxcc.clearDeleteByIndex (0) ctxcc.setDelete ('/blah') n = ctxcc.getNumDeletes () ctxcc.clearDeleteByString ('/blah') id = ctxcc.getIdentityRoot () ida = ctxcc.getIdentityRoot () if id.isIdentical(ida): print "success!" a = id.getParentA () b = id.getParentB () id.setName ('myname') n = id.getName () id.setSerial (1) s = id.getSerial () id.isAlive () l = ctxcc.getDataRoot () lb = ctxcc.getDataRoot () if l.isIdentical(lb): print "yay!" ctxr = l.getContext() if ctxr.isIdentical(ctxcc): print "woopie" t = l.getType () n = l.getNumChildren () c = l.getContext () l.isAlive () c = l.addChild (Data.ADM_TYPE_INT, 'int') c = l.getChildByIndex (0) c = l.getChildByName ('int') print c.getPos () print c.setValue (1) print c.getValue () d = l.addChild (Data.ADM_TYPE_BOOL, 'bool') print d.setValue (0) print d.getValue () print l[0], l[1] print l.moveChild (c, 1) print l[0], l[1] ll = l.addChild (Data.ADM_TYPE_LIST, 'list') ll.setProtected (0) ll.isProtected () ll.setAnonymous (1) ll.isAnonymous () ll.setAtomic (0) ll.isAtomic () ll.setPrepend (0) ll.isPrepend () ll.setSource ('src') ll.getSource () ll.setName ('newname') ll.getName () c = ll.addChild (Data.ADM_TYPE_INT, 'int') ll.copyData (c) ctr = ll.getContainer () #ll.unlink () c = l.addChild (Data.ADM_TYPE_FLOAT, 'float') c.setValue (0.0) c.getValue () c = l.addChild (Data.ADM_TYPE_STRING, 'string') c.setValue ('string') c.getValue () c = l.addChild (Data.ADM_TYPE_BASE64, 'base64') c.setValue ('YmFzZTY0') c.getValue () c = l.addChild (Data.ADM_TYPE_COPY, 'copy') c.setValue ('/blah') c.getValue () x = ctxcc.getDataByPath ('/int') m = merge ('merged', 1, ctxc, ctxcc) print m.toXML() print ctxcc.toXML() print ctxc.toXML() print ctx.toXML() # # Small profiling test. We might want to do this sometime later when # we really know that the system is doing the right thing(tm) but need # a little more performance in order to squeeze a little more out of # our algorithms and data structures. # def profile(): import profile import pstats profile.run('test()', 'Alchemist.py.prof') p = pstats.Stats('Alchemist.py.prof') p.sort_stats('time').print_stats() # # Call our main test function # c = Context(name='Foo', serial=1) test4()