xref: /trunk/test/odf-validate/cov_schema.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
1*0e16a31dSJim Jagielski#**************************************************************
2*0e16a31dSJim Jagielski#
3*0e16a31dSJim Jagielski#  Licensed to the Apache Software Foundation (ASF) under one
4*0e16a31dSJim Jagielski#  or more contributor license agreements.  See the NOTICE file
5*0e16a31dSJim Jagielski#  distributed with this work for additional information
6*0e16a31dSJim Jagielski#  regarding copyright ownership.  The ASF licenses this file
7*0e16a31dSJim Jagielski#  to you under the Apache License, Version 2.0 (the
8*0e16a31dSJim Jagielski#  "License"); you may not use this file except in compliance
9*0e16a31dSJim Jagielski#  with the License.  You may obtain a copy of the License at
10*0e16a31dSJim Jagielski#
11*0e16a31dSJim Jagielski#    http://www.apache.org/licenses/LICENSE-2.0
12*0e16a31dSJim Jagielski#
13*0e16a31dSJim Jagielski#  Unless required by applicable law or agreed to in writing,
14*0e16a31dSJim Jagielski#  software distributed under the License is distributed on an
15*0e16a31dSJim Jagielski#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16*0e16a31dSJim Jagielski#  KIND, either express or implied.  See the License for the
17*0e16a31dSJim Jagielski#  specific language governing permissions and limitations
18*0e16a31dSJim Jagielski#  under the License.
19*0e16a31dSJim Jagielski#
20*0e16a31dSJim Jagielski#**************************************************************
21*0e16a31dSJim Jagielski
22*0e16a31dSJim Jagielskiimport os, sys
23*0e16a31dSJim Jagielskiimport xml.etree.ElementTree as ET
24*0e16a31dSJim Jagielski
25*0e16a31dSJim JagielskiHERE = os.path.dirname(os.path.abspath(__file__))
26*0e16a31dSJim JagielskiSCHEMAS = [("", "OpenDocument-v1.2-os-schema.rng"),
27*0e16a31dSJim Jagielski           ("manifest", "OpenDocument-v1.2-os-manifest-schema.rng")]
28*0e16a31dSJim JagielskiRNG = "{http://relaxng.org/ns/structure/1.0}"
29*0e16a31dSJim JagielskiXML_NS = "http://www.w3.org/XML/1998/namespace"
30*0e16a31dSJim JagielskiWILD = ("*", "*")
31*0e16a31dSJim Jagielski
32*0e16a31dSJim Jagielski
33*0e16a31dSJim Jagielskidef tag(e):
34*0e16a31dSJim Jagielski    return e.tag[len(RNG):]
35*0e16a31dSJim Jagielski
36*0e16a31dSJim Jagielski
37*0e16a31dSJim Jagielskiclass Schema:
38*0e16a31dSJim Jagielski    def __init__(self, path):
39*0e16a31dSJim Jagielski        self.prefixes = {"xml": XML_NS}
40*0e16a31dSJim Jagielski        for ev, v in ET.iterparse(path, events=("start-ns", "end")):
41*0e16a31dSJim Jagielski            if ev == "start-ns":
42*0e16a31dSJim Jagielski                p, uri = v
43*0e16a31dSJim Jagielski                if self.prefixes.setdefault(p, uri) != uri:
44*0e16a31dSJim Jagielski                    sys.exit("prefix %r bound twice in %s" % (p, path))
45*0e16a31dSJim Jagielski        self.root = ET.parse(path).getroot()
46*0e16a31dSJim Jagielski        self.defs = {}
47*0e16a31dSJim Jagielski        for d in self.root.iter(RNG + "define"):
48*0e16a31dSJim Jagielski            if d.get("name") in self.defs:
49*0e16a31dSJim Jagielski                sys.exit("duplicate define %s" % d.get("name"))
50*0e16a31dSJim Jagielski            self.defs[d.get("name")] = d
51*0e16a31dSJim Jagielski        self.cache = {}
52*0e16a31dSJim Jagielski        self.stack = set()
53*0e16a31dSJim Jagielski
54*0e16a31dSJim Jagielski    def qname(self, text, is_attr):
55*0e16a31dSJim Jagielski        text = text.strip()
56*0e16a31dSJim Jagielski        if ":" in text:
57*0e16a31dSJim Jagielski            p, local = text.split(":", 1)
58*0e16a31dSJim Jagielski            return self.prefixes[p], local
59*0e16a31dSJim Jagielski        if not is_attr:
60*0e16a31dSJim Jagielski            sys.exit("unprefixed element name %r" % text)
61*0e16a31dSJim Jagielski        return "", text
62*0e16a31dSJim Jagielski
63*0e16a31dSJim Jagielski    def names(self, nc, is_attr):
64*0e16a31dSJim Jagielski        t = tag(nc)
65*0e16a31dSJim Jagielski        if t == "name":
66*0e16a31dSJim Jagielski            if nc.get("ns") is not None:
67*0e16a31dSJim Jagielski                sys.exit("name with ns attribute")
68*0e16a31dSJim Jagielski            return [self.qname(nc.text, is_attr)]
69*0e16a31dSJim Jagielski        if t == "choice":
70*0e16a31dSJim Jagielski            parts = [self.names(c, is_attr) for c in nc]
71*0e16a31dSJim Jagielski            return None if any(p is None for p in parts) else [n for p in parts for n in p]
72*0e16a31dSJim Jagielski        return None
73*0e16a31dSJim Jagielski
74*0e16a31dSJim Jagielski    def merge_group(self, parts):
75*0e16a31dSJim Jagielski        out = {}
76*0e16a31dSJim Jagielski        for part in parts:
77*0e16a31dSJim Jagielski            for k, (req, vals) in part.items():
78*0e16a31dSJim Jagielski                if k in out:
79*0e16a31dSJim Jagielski                    r0, v0 = out[k]
80*0e16a31dSJim Jagielski                    vals = None if v0 is None or vals is None else v0 | vals
81*0e16a31dSJim Jagielski                    req = r0 or req
82*0e16a31dSJim Jagielski                out[k] = (req, vals)
83*0e16a31dSJim Jagielski        return out
84*0e16a31dSJim Jagielski
85*0e16a31dSJim Jagielski    def merge_choice(self, parts):
86*0e16a31dSJim Jagielski        out = {}
87*0e16a31dSJim Jagielski        for part in parts:
88*0e16a31dSJim Jagielski            for k, (req, vals) in part.items():
89*0e16a31dSJim Jagielski                if k in out:
90*0e16a31dSJim Jagielski                    r0, v0 = out[k]
91*0e16a31dSJim Jagielski                    vals = None if v0 is None or vals is None else v0 | vals
92*0e16a31dSJim Jagielski                    req = r0 and req
93*0e16a31dSJim Jagielski                else:
94*0e16a31dSJim Jagielski                    req = req and all(k in p for p in parts)
95*0e16a31dSJim Jagielski                out[k] = (req, vals)
96*0e16a31dSJim Jagielski        return out
97*0e16a31dSJim Jagielski
98*0e16a31dSJim Jagielski    def values(self, children):
99*0e16a31dSJim Jagielski        parts = [self.value_of(c) for c in children]
100*0e16a31dSJim Jagielski        return None if len(parts) != 1 else parts[0]
101*0e16a31dSJim Jagielski
102*0e16a31dSJim Jagielski    def value_of(self, n):
103*0e16a31dSJim Jagielski        t = tag(n)
104*0e16a31dSJim Jagielski        if t == "value":
105*0e16a31dSJim Jagielski            if n.get("type", "token") != "token" or n.get("datatypeLibrary"):
106*0e16a31dSJim Jagielski                return None
107*0e16a31dSJim Jagielski            return frozenset([n.text or ""])
108*0e16a31dSJim Jagielski        if t == "ref":
109*0e16a31dSJim Jagielski            return self.values(list(self.defs[n.get("name")]))
110*0e16a31dSJim Jagielski        if t == "choice":
111*0e16a31dSJim Jagielski            parts = [self.value_of(c) for c in n]
112*0e16a31dSJim Jagielski            return None if any(p is None for p in parts) else frozenset().union(*parts)
113*0e16a31dSJim Jagielski        return None
114*0e16a31dSJim Jagielski
115*0e16a31dSJim Jagielski    def attrs(self, n):
116*0e16a31dSJim Jagielski        t = tag(n)
117*0e16a31dSJim Jagielski        if t == "attribute":
118*0e16a31dSJim Jagielski            kids = list(n)
119*0e16a31dSJim Jagielski            if "name" in n.attrib:
120*0e16a31dSJim Jagielski                names, body = [self.qname(n.get("name"), True)], kids
121*0e16a31dSJim Jagielski            else:
122*0e16a31dSJim Jagielski                names, body = self.names(kids[0], True), kids[1:]
123*0e16a31dSJim Jagielski            if names is None:
124*0e16a31dSJim Jagielski                return {WILD: (False, None)}
125*0e16a31dSJim Jagielski            vals = self.values(body) if body else None
126*0e16a31dSJim Jagielski            req = len(names) == 1
127*0e16a31dSJim Jagielski            return {k: (req, vals) for k in names}
128*0e16a31dSJim Jagielski        if t == "ref":
129*0e16a31dSJim Jagielski            return self.attrs_define(n.get("name"))
130*0e16a31dSJim Jagielski        if t in ("element", "value", "data", "text", "empty", "list", "notAllowed"):
131*0e16a31dSJim Jagielski            return {}
132*0e16a31dSJim Jagielski        parts = [self.attrs(c) for c in n]
133*0e16a31dSJim Jagielski        if t == "choice":
134*0e16a31dSJim Jagielski            return self.merge_choice(parts)
135*0e16a31dSJim Jagielski        out = self.merge_group(parts)
136*0e16a31dSJim Jagielski        if t in ("optional", "zeroOrMore"):
137*0e16a31dSJim Jagielski            out = {k: (False, v) for k, (r, v) in out.items()}
138*0e16a31dSJim Jagielski        return out
139*0e16a31dSJim Jagielski
140*0e16a31dSJim Jagielski    def attrs_define(self, name):
141*0e16a31dSJim Jagielski        if name in self.cache:
142*0e16a31dSJim Jagielski            return self.cache[name]
143*0e16a31dSJim Jagielski        if name in self.stack:
144*0e16a31dSJim Jagielski            sys.exit("pattern recursion without element: %s" % name)
145*0e16a31dSJim Jagielski        self.stack.add(name)
146*0e16a31dSJim Jagielski        res = self.merge_group([self.attrs(c) for c in self.defs[name]])
147*0e16a31dSJim Jagielski        self.stack.discard(name)
148*0e16a31dSJim Jagielski        self.cache[name] = res
149*0e16a31dSJim Jagielski        return res
150*0e16a31dSJim Jagielski
151*0e16a31dSJim Jagielski    def element_defs(self):
152*0e16a31dSJim Jagielski        for e in self.root.iter(RNG + "element"):
153*0e16a31dSJim Jagielski            kids = list(e)
154*0e16a31dSJim Jagielski            if "name" in e.attrib:
155*0e16a31dSJim Jagielski                names, body = [self.qname(e.get("name"), False)], kids
156*0e16a31dSJim Jagielski            else:
157*0e16a31dSJim Jagielski                names, body = self.names(kids[0], False), kids[1:]
158*0e16a31dSJim Jagielski            yield names, self.merge_group([self.attrs(c) for c in body])
159*0e16a31dSJim Jagielski
160*0e16a31dSJim Jagielski
161*0e16a31dSJim Jagielskidef extract(path):
162*0e16a31dSJim Jagielski    s = Schema(path)
163*0e16a31dSJim Jagielski    elements, per_elem = set(), {}
164*0e16a31dSJim Jagielski    stats = {"element_nodes": 0, "wild_elements": 0, "wild_attr_elements": 0}
165*0e16a31dSJim Jagielski    for names, attrs in s.element_defs():
166*0e16a31dSJim Jagielski        stats["element_nodes"] += 1
167*0e16a31dSJim Jagielski        if names is None:
168*0e16a31dSJim Jagielski            stats["wild_elements"] += 1
169*0e16a31dSJim Jagielski            continue
170*0e16a31dSJim Jagielski        if WILD in attrs:
171*0e16a31dSJim Jagielski            stats["wild_attr_elements"] += len(names)
172*0e16a31dSJim Jagielski            del attrs[WILD]
173*0e16a31dSJim Jagielski        for nm in names:
174*0e16a31dSJim Jagielski            elements.add(nm)
175*0e16a31dSJim Jagielski            per_elem.setdefault(nm, []).append(attrs)
176*0e16a31dSJim Jagielski    pairs = {}
177*0e16a31dSJim Jagielski    for nm, defs in per_elem.items():
178*0e16a31dSJim Jagielski        keys = set().union(*defs)
179*0e16a31dSJim Jagielski        for k in keys:
180*0e16a31dSJim Jagielski            reqs = [d[k][0] if k in d else False for d in defs]
181*0e16a31dSJim Jagielski            vals = [d[k][1] if k in d else frozenset() for d in defs]
182*0e16a31dSJim Jagielski            vals = None if any(v is None for v in vals) else frozenset().union(*vals)
183*0e16a31dSJim Jagielski            pairs[(nm, k)] = (all(reqs), vals)
184*0e16a31dSJim Jagielski    return elements, pairs, stats
185*0e16a31dSJim Jagielski
186*0e16a31dSJim Jagielski
187*0e16a31dSJim Jagielskidef write(outdir, elements, pairs):
188*0e16a31dSJim Jagielski    os.makedirs(outdir, exist_ok=True)
189*0e16a31dSJim Jagielski    with open(os.path.join(outdir, "elements.tsv"), "w") as f:
190*0e16a31dSJim Jagielski        for ns, local in sorted(elements):
191*0e16a31dSJim Jagielski            f.write("%s\t%s\n" % (ns, local))
192*0e16a31dSJim Jagielski    with open(os.path.join(outdir, "attributes.tsv"), "w") as f, \
193*0e16a31dSJim Jagielski         open(os.path.join(outdir, "values.tsv"), "w") as g:
194*0e16a31dSJim Jagielski        for (en, an), (req, vals) in sorted(pairs.items()):
195*0e16a31dSJim Jagielski            f.write("%s\t%s\t%s\t%s\t%s\n" % (en + an + ("required" if req else "optional",)))
196*0e16a31dSJim Jagielski            for v in sorted(vals or ()):
197*0e16a31dSJim Jagielski                g.write("%s\t%s\t%s\t%s\t%s\n" % (en + an + (v,)))
198*0e16a31dSJim Jagielski
199*0e16a31dSJim Jagielski
200*0e16a31dSJim Jagielskidef main():
201*0e16a31dSJim Jagielski    if len(sys.argv) != 2:
202*0e16a31dSJim Jagielski        sys.exit("usage: python3 -I cov_schema.py <out-dir>")
203*0e16a31dSJim Jagielski    out = sys.argv[1]
204*0e16a31dSJim Jagielski    for sub, name in SCHEMAS:
205*0e16a31dSJim Jagielski        elements, pairs, stats = extract(os.path.join(HERE, "schema", name))
206*0e16a31dSJim Jagielski        write(os.path.join(out, sub), elements, pairs)
207*0e16a31dSJim Jagielski        enum = [p for p, (r, v) in pairs.items() if v]
208*0e16a31dSJim Jagielski        sys.stderr.write(
209*0e16a31dSJim Jagielski            "%s: elements=%d (element nodes=%d) attributes=%d pairs=%d required=%d "
210*0e16a31dSJim Jagielski            "enumerated pairs=%d values=%d\n"
211*0e16a31dSJim Jagielski            "  skipped wildcards: %d anyName/nsName element nodes, "
212*0e16a31dSJim Jagielski            "%d elements with a wildcard attribute\n"
213*0e16a31dSJim Jagielski            % (name, len(elements), stats["element_nodes"],
214*0e16a31dSJim Jagielski               len({k for (_, k) in pairs}), len(pairs),
215*0e16a31dSJim Jagielski               sum(1 for r, v in pairs.values() if r), len(enum),
216*0e16a31dSJim Jagielski               sum(len(pairs[p][1]) for p in enum),
217*0e16a31dSJim Jagielski               stats["wild_elements"], stats["wild_attr_elements"]))
218*0e16a31dSJim Jagielski
219*0e16a31dSJim Jagielski
220*0e16a31dSJim Jagielskimain()
221