2 from xml
.dom
import minidom
, XMLNS_NAMESPACE
, Node
3 from zeroinstall
.injector
.namespaces
import XMLNS_IFACE
4 from zeroinstall
.injector
import model
, reader
5 from logging
import info
8 def childNodes(parent
, namespaceURI
= None, localName
= None):
9 for x
in parent
.childNodes
:
10 if x
.nodeType
!= Node
.ELEMENT_NODE
: continue
11 if namespaceURI
is not None and x
.namespaceURI
!= namespaceURI
: continue
13 if localName
is None or x
.localName
== localName
:
17 def __init__(self
, impl
):
18 doc
= impl
.ownerDocument
24 for name
, value
in node
.attributes
.itemsNS():
25 if name
not in self
.attribs
:
26 self
.attribs
[name
] = value
27 if node
.nodeName
== 'group':
28 # We don't care about <requires> inside <implementation>; they'll get copied over anyway
29 for x
in childNodes(node
, XMLNS_IFACE
, 'requires'):
30 self
.requires
.append(x
)
31 node
= node
.parentNode
32 if node
.nodeName
!= 'group':
35 def find_impls(parent
):
36 """Return all <implementation> children, including those inside groups."""
37 for x
in childNodes(parent
, XMLNS_IFACE
):
38 if x
.localName
== 'implementation':
40 elif x
.localName
== 'group':
41 for y
in find_impls(x
):
44 def find_groups(parent
):
45 """Return all <group> children, including those inside other groups."""
46 for x
in childNodes(parent
, XMLNS_IFACE
, 'group'):
48 for y
in find_groups(x
):
52 assert a
.nodeType
== Node
.ELEMENT_NODE
53 assert b
.nodeType
== Node
.ELEMENT_NODE
55 if a
.namespaceURI
!= b
.namespaceURI
:
58 if a
.nodeName
!= b
.nodeName
:
61 a_attrs
= set(["%s %s" % (name
, value
) for name
, value
in a
.attributes
.itemsNS()])
62 b_attrs
= set(["%s %s" % (name
, value
) for name
, value
in b
.attributes
.itemsNS()])
64 if a_attrs
!= b_attrs
:
65 #print "%s != %s" % (a_attrs, b_attrs)
68 a_children
= list(childNodes(a
))
69 b_children
= list(childNodes(b
))
71 if len(a_children
) != len(b_children
):
74 for a_child
, b_child
in zip(a_children
, b_children
):
75 if not nodesEqual(a_child
, b_child
):
80 def score_subset(group
, impl
):
81 """Returns (is_subset, goodness)"""
82 for key
in group
.attribs
:
83 if key
not in impl
.attribs
.keys():
85 return (0,) # Group sets an attribute the impl doesn't want
86 for g_req
in group
.requires
:
87 for i_req
in impl
.requires
:
88 if nodesEqual(g_req
, i_req
): break
90 return (0,) # Group adds a requires that the impl doesn't want
91 # Score result so we get groups that have all the same requires first, then ones with all the same attribs
92 return (1, len(group
.requires
), len(group
.attribs
))
94 def merge(data
, local
):
95 local_doc
= minidom
.parse(local
)
96 master_doc
= minidom
.parseString(data
)
98 # Merge each implementation in the local feed in turn (normally there will only be one)
99 for impl
in find_impls(local_doc
.documentElement
):
100 # 1. Get the context of the implementation to add. This is:
101 # - The set of its requirements
103 new_impl_context
= Context(impl
)
105 # 2. For each <group> in the master feed, see if it provides a compatible context:
106 # - A subset of the new implementation's requirements
107 # - A subset of the new implementation's attributes (names, not values)
108 # Choose the most compatible <group> (the root counts as a minimally compatible group)
110 best_group
= ((1, 0, 0), master_doc
.documentElement
) # (score, element)
112 for group
in find_groups(master_doc
.documentElement
):
113 group_context
= Context(group
)
114 score
= score_subset(group_context
, new_impl_context
)
115 if score
> best_group
[0]:
116 best_group
= (score
, group
)
118 group
= best_group
[1]
119 group_context
= Context(group
)
121 # If we have additional requirements, we'll need to create a subgroup and add them
122 if len(new_impl_context
.requires
) > len(group_context
.requires
):
123 subgroup
= xmltools
.create_element(group
, 'group')
125 group_context
= Context(group
)
126 for x
in new_impl_context
.requires
:
127 for y
in group_context
.requires
:
128 if nodesEqual(x
, y
): break
130 req
= master_doc
.importNode(x
, True)
132 xmltools
.insert_element(req
, group
)
134 new_impl
= master_doc
.importNode(impl
, True)
136 # Attributes might have been set on a parent group; move to the impl
137 for name
in new_impl_context
.attribs
:
138 #print "Set", name, value
139 new_impl
.setAttributeNS(name
[0], name
[1], new_impl_context
.attribs
[name
])
141 for name
, value
in new_impl
.attributes
.itemsNS():
142 if name
in group_context
.attribs
and group_context
.attribs
[name
] == value
:
143 #print "Deleting duplicate attribute", name, value
144 new_impl
.removeAttributeNS(name
[0], name
[1])
146 xmltools
.insert_element(new_impl
, group
)
148 return master_doc
.toxml()