Added support for sha256new algorithm
[0publish.git] / merge.py
blob273c22d1159b41a5b56ce1f14c4c39c1eeabd4c0
1 from xml.dom import minidom, XMLNS_NAMESPACE, Node
2 from zeroinstall.injector.namespaces import XMLNS_IFACE
3 import xmltools
5 def childNodes(parent, namespaceURI = None, localName = None):
6 for x in parent.childNodes:
7 if x.nodeType != Node.ELEMENT_NODE: continue
8 if namespaceURI is not None and x.namespaceURI != namespaceURI: continue
10 if localName is None or x.localName == localName:
11 yield x
13 class Context:
14 def __init__(self, impl):
15 doc = impl.ownerDocument
16 self.attribs = {}
17 self.requires = []
18 self.commands = {}
20 node = impl
21 while True:
22 for name, value in node.attributes.itemsNS():
23 if name[0] == XMLNS_NAMESPACE:
24 xmltools.register_namespace(value, name[1])
25 elif name not in self.attribs:
26 self.attribs[name] = value
27 if node.nodeName == 'group':
28 # We don't care about <requires> or <command> inside <implementation>;
29 # they'll get copied over anyway
30 for x in childNodes(node, XMLNS_IFACE, 'requires'):
31 self.requires.append(x)
32 for x in childNodes(node, XMLNS_IFACE, 'command'):
33 command_name = x.getAttribute('name')
34 if command_name not in self.commands:
35 self.commands[command_name] = x
36 # (else the existing definition on the child should be used)
37 node = node.parentNode
38 if node.nodeName != 'group':
39 break
41 def find_impls(parent):
42 """Return all <implementation> children, including those inside groups."""
43 for x in childNodes(parent, XMLNS_IFACE):
44 if x.localName == 'implementation':
45 yield x
46 elif x.localName == 'group':
47 for y in find_impls(x):
48 yield y
50 def find_groups(parent):
51 """Return all <group> children, including those inside other groups."""
52 for x in childNodes(parent, XMLNS_IFACE, 'group'):
53 yield x
54 for y in find_groups(x):
55 yield y
57 def nodesEqual(a, b):
58 assert a.nodeType == Node.ELEMENT_NODE
59 assert b.nodeType == Node.ELEMENT_NODE
61 if a.namespaceURI != b.namespaceURI:
62 return False
64 if a.nodeName != b.nodeName:
65 return False
67 a_attrs = set(["%s %s" % (name, value) for name, value in a.attributes.itemsNS()])
68 b_attrs = set(["%s %s" % (name, value) for name, value in b.attributes.itemsNS()])
70 if a_attrs != b_attrs:
71 #print "%s != %s" % (a_attrs, b_attrs)
72 return False
74 a_children = list(childNodes(a))
75 b_children = list(childNodes(b))
77 if len(a_children) != len(b_children):
78 return False
80 for a_child, b_child in zip(a_children, b_children):
81 if not nodesEqual(a_child, b_child):
82 return False
84 return True
86 def score_subset(group, impl):
87 """Returns (is_subset, goodness)"""
88 for key in group.attribs:
89 if key not in impl.attribs.keys():
90 #print "BAD", key
91 return (0,) # Group sets an attribute the impl doesn't want
92 matching_commands = 0
93 for name, g_command in group.commands.iteritems():
94 if name not in impl.commands:
95 return (0,) # Group sets a command the impl doesn't want
96 if nodesEqual(g_command, impl.commands[name]):
97 # Prefer matching commands to overriding them
98 matching_commands += 1
99 for g_req in group.requires:
100 for i_req in impl.requires:
101 if nodesEqual(g_req, i_req): break
102 else:
103 return (0,) # Group adds a requires that the impl doesn't want
104 # Score result so we get groups that have all the same requires/commands first, then ones with all the same attribs
105 return (1, len(group.requires) + len(group.commands), len(group.attribs) + matching_commands)
107 # Note: the namespace stuff isn't quite right yet.
108 # Might get conflicts if both documents use the same prefix for different things.
109 def merge(data, local):
110 local_doc = minidom.parse(local)
111 master_doc = minidom.parseString(data)
113 # Merge each implementation in the local feed in turn (normally there will only be one)
114 for impl in find_impls(local_doc.documentElement):
115 # 1. Get the context of the implementation to add. This is:
116 # - The set of its requirements
117 # - The set of its commands
118 # - Its attributes
119 new_impl_context = Context(impl)
121 # 2. For each <group> in the master feed, see if it provides a compatible context:
122 # - A subset of the new implementation's requirements
123 # - A subset of the new implementation's command names
124 # - A subset of the new implementation's attributes (names, not values)
125 # Choose the most compatible <group> (the root counts as a minimally compatible group)
127 best_group = ((1, 0, 0), master_doc.documentElement) # (score, element)
129 for group in find_groups(master_doc.documentElement):
130 group_context = Context(group)
131 score = score_subset(group_context, new_impl_context)
132 if score > best_group[0]:
133 best_group = (score, group)
135 group = best_group[1]
136 group_context = Context(group)
138 new_commands = []
139 for name, new_command in new_impl_context.commands.iteritems():
140 old_command = group_context.commands.get(name, None)
141 if not (old_command and nodesEqual(old_command, new_command)):
142 new_commands.append(master_doc.importNode(new_command, True))
144 # If we have additional requirements, we'll need to create a subgroup and add them
145 if len(new_impl_context.requires) > len(group_context.requires) or new_commands:
146 subgroup = xmltools.create_element(group, 'group')
147 group = subgroup
148 #group_context = Context(group)
149 for x in new_impl_context.requires:
150 for y in group_context.requires:
151 if nodesEqual(x, y): break
152 else:
153 req = master_doc.importNode(x, True)
154 #print "Add", req
155 xmltools.insert_element(req, group)
156 for c in new_commands:
157 xmltools.insert_element(c, group)
159 new_impl = master_doc.importNode(impl, True)
161 # Attributes might have been set on a parent group; move to the impl
162 for name in new_impl_context.attribs:
163 #print "Set", name, value
164 xmltools.add_attribute_ns(new_impl, name[0], name[1], new_impl_context.attribs[name])
166 for name, value in new_impl.attributes.itemsNS():
167 if name[0] == XMLNS_NAMESPACE or \
168 (name in group_context.attribs and group_context.attribs[name] == value):
169 #print "Deleting duplicate attribute", name, value
170 new_impl.removeAttributeNS(name[0], name[1])
172 xmltools.insert_element(new_impl, group)
174 return master_doc.toxml('utf-8')