""" This module holds various versions of the persist store that work with XML files instead of a database. They are useful for creating human readable files mostly. Use with care as only the bare essentials have been tested. Created on Dec 13, 2011 @author: mike """ import os from lxml.etree import parse, Element, SubElement, tostring, fromstring from zope.interface import implements from mv3d.server.persist import IPersistStore class XMLFileStore(object): """ A store that uses a single XML file to hold all the data. """ implements(IPersistStore) filename = None document = None storeVersion = 1 isOpen = True def open(self, filename=None, data=None): #@ReservedAssignment """ Open the store. This will optionally create it if it doesn't exist. If the fileName parameters is not specified, then the store will be a memory only store. """ self.filename = filename if filename is not None and os.path.exists(filename): self.document = parse(filename).getroot() elif data is not None: self.document = fromstring(data) else: self.document = Element("XMLStore", storeVersion=str( self.storeVersion)) def close(self): """ Close the store and write the file. """ self.save() self.filename = None self.document = None self.isOpen = False def save(self): """ Save the data to disk """ data = tostring(self.document, pretty_print=True) if self.filename is not None: fil = open(self.filename, "w") fil.write(data) fil.close() return data def getAsString(self): """ Return the current state as a string. """ return tostring(self.document, pretty_print=True) def configure(self, cfg, section): """ Configure and open the store based on the config and section within that config given. """ def registerSchema(self, typeName, version, attributeList, force=False, transactionID=None): """ Install new schema into the store. This must be run once on the store for each type you expect to store there. typeName must be unique. Version is an integer specifying the version number of the type. attributeList includes all the schema data to register, and force will force registration of that schema. """ # self.document.attrib["xmlns:%s" % typeName] = ( # "http://www.mv3d.com/xmlstore/%s" % typeName) schemas = self.document.find("_Schemas") if schemas is None: schemas = SubElement(self.document, "_Schemas") isInstalled = len(schemas.xpath("%s[@version=%d]" % (typeName, version))) if isInstalled: return schema = SubElement(schemas, typeName, version=str(version)) for attr in attributeList: SubElement(schema, attr["name"], type=attr["type"]) def _getLatestSchema(self, typeName): """ Get the latest version of a schema that we know about. """ schemas = self.document.find("_Schemas") if schemas is None: return 0 highestVersion = 0 for element in schemas.findall(typeName): ver = int(element.attrib["version"]) if ver > highestVersion: highestVersion = ver return highestVersion def startTransaction(self): """ Begin a transaction on the store. This will return a transaction ID which can be passed to other methods in order to execute operations within the same transaction. Unfortunately, SQLite doesn't support multiple transactions """ def commitTransaction(self, transactionID): """ Successfully complete a transaction. Call with the transaction ID that was given out in startTransaction. """ def rollbackTransaction(self, transactionID): """ Revert any changes made during the specified transaction. The transaction is closed and the ID is no longer valid after this point. """ def new(self, typeName, data, transactionID=None): """ Store a new item. typeName must match a type which has had its schema registered with this store. """ element = SubElement(self.document, typeName) for key, value in data.iteritems(): SubElement(element, str(key)).text = str(value) if value is None: subElement = element.find(str(key)) subElement.text = "" SubElement(subElement, "None") SubElement(element, "_schemaVersion").text = str(self._getLatestSchema( typeName)) def _getMatchingElements(self, typeName, parameters): """ Returns an xpath query string """ def makeString(left, comparison, right): if isinstance(left, (tuple, list)): left = makeString(left[0], left[1], left[2]) if isinstance(right, (tuple, list)): right = makeString(right[0], right[1], right[2]) else: right = "'%s'" % right return "%s %s %s" % (left, comparison, right) if parameters is not None: left, comparison, right = parameters query = "%s[%s]" % (typeName, makeString(left, comparison, right)) else: query = "%s" % typeName return self.document.xpath(query) def update(self, typeName, parameters, data, transactionID=None): """ Update an existing item in the store. Parameters specifies a query that will match one or more records to be updated. data includes what fields to update and does not necessarily have to include all fields in the item. See query for more information on parameters. """ data = dict(data) data["_schemaVersion"] = str(self._getLatestSchema(typeName)) for element in self._getMatchingElements(typeName, parameters): for key, value in data.iteritems(): if value is None: subElement = element.find(str(key)) subElement.text = "" SubElement(subElement, "None") else: element.find(str(key)).text = str(value) def delete(self, typeName, parameters, specificVersion=None, transactionID=None): """ Remove an item or items from the store. As with update, parameters specifies a query that will match one or more records which will be deleted. If this is not done in a transaction, it can not be undone. See query for more information on parameters. """ for element in self._getMatchingElements(typeName, parameters): parent = element.getparent() parent.remove(element) def query(self, typeName, parameters, fields=None, transactionID=None, offset=0, limit= -1, order=None, count=False): """ Executes a query to return 0 or more results. If fields is not specified, all fields are returned including a version number for the schema from which the result came from. When fields is specified, results are only returned from the most recent schema version. This is because fields in previous schema versions may not be the same. parameters is a possibly nested set of 3 length tuples. Each one contains (left, operator, right). For boolean operators like and/or, left and right should be another 3 length tuple. Example: (("name", "=", "mike"), " and ", ("age", ">", "20")) All items are expected to be in store format already. """ elements = self._getMatchingElements(typeName, parameters) if count: return [{"count(*)":len(elements)}] def getElementKey(element): return element.find(order[0]).text if order is not None: elements.sort(key=getElementKey, reverse=order[1] == "desc") if limit != -1: limit = offset + limit else: limit = len(elements) results = [] for element in elements[offset:limit]: result = {} if fields is not None: subElements = element.xpath( " | ".join(fields) + " | _schemaVersion") else: subElements = list(element) for subElement in subElements: if subElement.text: result[subElement.tag] = subElement.text elif subElement.find("None") is not None: result[subElement.tag] = None else: result[subElement.tag] = subElement.text result["_schemaVersion"] = int(result["_schemaVersion"]) results.append(result) return results def analyze(self, classes=None, fix=False): """ Check for errors and optionally fix them. ** WARNING ** Setting fix to True may cause data loss in the case of errors. Use with care! """ return []