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