# vim: set fileencoding=utf-8 : # # (C) 2013 Guido Günther # 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 3 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Some Pom parsing handling import tempfile import os from lxml import etree from . version import Version class Pom(object): """ Represents a maven POM for documentation check I{test_pom.py} """ ns = "http://maven.apache.org/POM/4.0.0" root = "/{%s}project" % ns groupid_node = "/{%s}groupId" % ns artifactid_node = "/{%s}artifactId" % ns version_node = "/{%s}version" % ns triple_nodes = (groupid_node, artifactid_node, version_node) parent_node = "/{%s}parent" % ns deps_node = "/{%s}dependencies" % ns dep_node = "/{%s}dependency" % ns version_path = root + version_node artifactid_path = root + artifactid_node groupid_path = root + groupid_node parent_path = root + parent_node deps_path = root + deps_node dep_path = root + deps_node + dep_node def __init__(self, tree, path=None): self._path = path self._tree = tree @property def path(self): """The path in the filesystem""" return self._path def get_version(self): searcher = etree.ETXPath(self.version_path) try: return Version(searcher(self._tree)[0].text) except IndexError: return None def set_version(self, version): searcher = etree.ETXPath(self.version_path) searcher(self._tree)[0].text = version version = property(get_version, set_version) @property def artifactid(self): """ The artifactid of this pom """ searcher = etree.ETXPath(self.artifactid_path) return searcher(self._tree)[0].text.strip() @property def groupid(self): """ The groupid of this pom. Might be empty. """ searcher = etree.ETXPath(self.groupid_path) try: return searcher(self._tree)[0].text.strip() except IndexError: return None def _parent_path(self, node): return self.root + self.parent_node + node def get_parent(self): """ Return the POMs parent returns: groupid, artifactid, version """ p = [] try: for node in self.triple_nodes: searcher = etree.ETXPath(self._parent_path(node)) p.append(searcher(self._tree)[0].text.strip()) except IndexError: return None return tuple(p) def set_parent(self, parent): """ Return the POMs parent @param parent: groupid, artifactid, version @type parent: C{tuple} """ old_parent = self.parent if not old_parent: # no parent pom so far if None in parent: raise ValueError("Need full parent triple") searcher = etree.ETXPath(self.root) root = searcher(self._tree)[0] # add an empty parent element parent_elem = etree.SubElement(root, self.parent_node[1:]) parent_elem.text='\n' for node, val in zip(self.triple_nodes, parent): if not val: continue searcher = etree.ETXPath(self._parent_path(node)) n = searcher(self._tree) if n: n[0].text = val else: # if the element is missing, add it n = etree.SubElement(parent_elem, node[1:]) n.text = val parent = property(get_parent, set_parent, doc="The POMs parent as triplet") @property def triplet(self): """ groupid, artifactid and version triplet The version and groupid can be C{None} which means "use the parents value" """ return (self.groupid, self.artifactid, self.version) @property def full_triplet(self): """ groupid, artifactid and version triplet All values are guaranteed to be different from C{None}. since missing values are fetched from the parent """ return ((self.groupid or self.parent[0], self.artifactid or self.parent[1], self.version or self.parent[2])) def is_parent_of(self, child): """ Check wheter this is the parent of pom I{child} @param child: the pom to check @type child: L{Pom} @return: C{True} if L{self} is the parent of L{child}, C{False} otherwise @rtype: C{bool} """ c = child.parent if not c: return False t = self.full_triplet return (c[0] == t[0] and c[1] == t[1] and c[2] == t[2]) def is_child_of(self, parent): """ Check wheter this is the child of pom I{parent} @param parent: the pom to check @type parent: L{Pom} @return: C{True} if L{self} is the child of L{parent}, C{False} otherwise @rtype: C{bool} """ return parent.is_parent_of(self) def find_parent(self, poms): """ Given a list of poms find our parent in it. @param poms: list of poms @type poms: C{list} of L{Pom}s @return: the parent pom @rtype: L{Pom} or None """ for pom in poms: if self.is_child_of(pom): return pom return None def _strip_ns(self, tag): """ Stip the namespace from an XML tag """ return tag[len(self.ns)+2:] def _parse_dep(self, depelem): """ Parse the XML of a POM dependency @param depelem: a dependency element @type depelem: lxml.etree._Element """ dep = {} for child in depelem: # FIXME: this strips comments if isinstance(child.tag, (str, unicode)): tag = self._strip_ns(child.tag) dep[tag] = child.text return dep def get_dependencies(self): """ Get the dependencies as a list """ return [ self._parse_dep(e) for e in self._get_nodes(self.dep_path) if e is not None ] def _get_nodes(self, path): """Return all nodes at xpath I{path} as an Iterator""" searcher = etree.ETXPath(path) return searcher(self._tree) def _add_dependency(self, dep): searcher = etree.ETXPath(self.deps_path) deps = searcher(self._tree)[0] dep_elem = etree.SubElement(deps, self.dep_node[1:]) dep_elem.text='\n' for node, val in dep.items(): if not val: continue n = etree.SubElement(dep_elem, '{%s}%s' % (self.ns, node)) n.text = val n.tail = '\n' def _remove_dependencies(self): searcher = etree.ETXPath(self.deps_path) deps = searcher(self._tree)[0] if deps is not None: deps.clear() def set_dependencies(self, deps): """ Set the dependencies """ if not self.dependencies: searcher = etree.ETXPath(self.root) root = searcher(self._tree)[0] # add an empty parent element parent_elem = etree.SubElement(root, self.deps_node[1:]) parent_elem.text='\n' else: self._remove_dependencies() for dep in deps: self._add_dependency(dep) dependencies = property(get_dependencies, set_dependencies) def write(self, path=None): if path: self._path = path if not self.path: raise ValueError("Illegal path name") tmp, name = tempfile.mkstemp(dir=os.path.dirname(self.path)) self._tree.write(name) os.chmod(name, 0o644) os.rename(name, self.path) @classmethod def read(klass, path, sloppy=False): parser = etree.XMLParser(recover=True) if sloppy else None return klass(etree.parse(path, parser=parser), path) @classmethod def parse(klass, xml, sloppy=False): parser = etree.XMLParser(recover=True) if sloppy else None return klass(etree.fromstring(xml, parser=parser))