Prepare Open vSwitch 1.1.2 release.
[sliver-openvswitch.git] / ovsdb / ovsdb-doc.in
1 #! @PYTHON@
2
3 from datetime import date
4 import getopt
5 import os
6 import re
7 import sys
8 import xml.dom.minidom
9
10 import ovs.json
11 from ovs.db import error
12 import ovs.db.schema
13
14 argv0 = sys.argv[0]
15
16 def textToNroff(s, font=r'\fR'):
17     def escape(match):
18         c = match.group(0)
19         if c == '-':
20             if font == r'\fB':
21                 return r'\-'
22             else:
23                 return '-'
24         if c == '\\':
25             return r'\e'
26         elif c == '"':
27             return r'\(dq'
28         elif c == "'":
29             return r'\(cq'
30         else:
31             raise error.Error("bad escape")
32
33     # Escape - \ " ' as needed by nroff.
34     s = re.sub('([-"\'\\\\])', escape, s)
35     if s.startswith('.'):
36         s = '\\' + s
37     return s
38
39 def escapeNroffLiteral(s):
40     return r'\fB%s\fR' % textToNroff(s, r'\fB')
41
42 def inlineXmlToNroff(node, font):
43     if node.nodeType == node.TEXT_NODE:
44         return textToNroff(node.data, font)
45     elif node.nodeType == node.ELEMENT_NODE:
46         if node.tagName in ['code', 'em', 'option']:
47             s = r'\fB'
48             for child in node.childNodes:
49                 s += inlineXmlToNroff(child, r'\fB')
50             return s + font
51         elif node.tagName == 'ref':
52             s = r'\fB'
53             if node.hasAttribute('column'):
54                 s += node.attributes['column'].nodeValue
55             elif node.hasAttribute('table'):
56                 s += node.attributes['table'].nodeValue
57             elif node.hasAttribute('group'):
58                 s += node.attributes['group'].nodeValue
59             else:
60                 raise error.Error("'ref' lacks column and table attributes")
61             return s + font
62         elif node.tagName == 'var':
63             s = r'\fI'
64             for child in node.childNodes:
65                 s += inlineXmlToNroff(child, r'\fI')
66             return s + font
67         else:
68             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
69     else:
70         raise error.Error("unknown node %s in inline xml" % node)
71
72 def blockXmlToNroff(nodes, para='.PP'):
73     s = ''
74     for node in nodes:
75         if node.nodeType == node.TEXT_NODE:
76             s += textToNroff(node.data)
77             s = s.lstrip()
78         elif node.nodeType == node.ELEMENT_NODE:
79             if node.tagName in ['ul', 'ol']:
80                 if s != "":
81                     s += "\n"
82                 s += ".RS\n"
83                 i = 0
84                 for liNode in node.childNodes:
85                     if (liNode.nodeType == node.ELEMENT_NODE
86                         and liNode.tagName == 'li'):
87                         i += 1
88                         if node.tagName == 'ul':
89                             s += ".IP \\bu\n"
90                         else:
91                             s += ".IP %d. .25in\n" % i
92                         s += blockXmlToNroff(liNode.childNodes, ".IP")
93                     elif (liNode.nodeType != node.TEXT_NODE
94                           or not liNode.data.isspace()):
95                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
96                 s += ".RE\n"
97             elif node.tagName == 'dl':
98                 if s != "":
99                     s += "\n"
100                 s += ".RS\n"
101                 prev = "dd"
102                 for liNode in node.childNodes:
103                     if (liNode.nodeType == node.ELEMENT_NODE
104                         and liNode.tagName == 'dt'):
105                         if prev == 'dd':
106                             s += '.TP\n'
107                         else:
108                             s += '.TQ\n'
109                         prev = 'dt'
110                     elif (liNode.nodeType == node.ELEMENT_NODE
111                           and liNode.tagName == 'dd'):
112                         if prev == 'dd':
113                             s += '.IP\n'
114                         prev = 'dd'
115                     elif (liNode.nodeType != node.TEXT_NODE
116                           or not liNode.data.isspace()):
117                         raise error.Error("<dl> element may only have <dt> and <dd> children")
118                     s += blockXmlToNroff(liNode.childNodes, ".IP")
119                 s += ".RE\n"
120             elif node.tagName == 'p':
121                 if s != "":
122                     if not s.endswith("\n"):
123                         s += "\n"
124                     s += para + "\n"
125                 s += blockXmlToNroff(node.childNodes, para)
126             else:
127                 s += inlineXmlToNroff(node, r'\fR')
128         else:
129             raise error.Error("unknown node %s in block xml" % node)
130     if s != "" and not s.endswith('\n'):
131         s += '\n'
132     return s
133
134 def typeAndConstraintsToNroff(column):
135     type = column.type.toEnglish(escapeNroffLiteral)
136     constraints = column.type.constraintsToEnglish(escapeNroffLiteral)
137     if constraints:
138         type += ", " + constraints
139     return type
140
141 def columnToNroff(columnName, column, node):
142     type = typeAndConstraintsToNroff(column)
143     s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type)
144     s += blockXmlToNroff(node.childNodes, '.IP') + "\n"
145     return s
146
147 def columnGroupToNroff(table, groupXml):
148     introNodes = []
149     columnNodes = []
150     for node in groupXml.childNodes:
151         if (node.nodeType == node.ELEMENT_NODE
152             and node.tagName in ('column', 'group')):
153             columnNodes += [node]
154         else:
155             introNodes += [node]
156
157     summary = []
158     intro = blockXmlToNroff(introNodes)
159     body = ''
160     for node in columnNodes:
161         if node.tagName == 'column':
162             columnName = node.attributes['name'].nodeValue
163             column = table.columns[columnName]
164             body += columnToNroff(columnName, column, node)
165             summary += [('column', columnName, column)]
166         elif node.tagName == 'group':
167             title = node.attributes["title"].nodeValue
168             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
169             summary += [('group', title, subSummary)]
170             body += '.ST "%s:"\n' % textToNroff(title)
171             body += subIntro + subBody
172         else:
173             raise error.Error("unknown element %s in <table>" % node.tagName)
174     return summary, intro, body
175
176 def tableSummaryToNroff(summary, level=0):
177     s = ""
178     for type, name, arg in summary:
179         if type == 'column':
180
181             s += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % (
182                 r'\ \ ' * level, name, typeAndConstraintsToNroff(arg))
183         else:
184             if s != "":
185                 s += "_\n"
186             s += """.T&
187 li | s
188 l | l.
189 %s%s
190 _
191 """ % (r'\ \ ' * level, name)
192             s += tableSummaryToNroff(arg, level + 1)
193     return s
194
195 def tableToNroff(schema, tableXml):
196     tableName = tableXml.attributes['name'].nodeValue
197     table = schema.tables[tableName]
198
199     s = """.bp
200 .SS "%s Table"
201 """ % tableName
202     summary, intro, body = columnGroupToNroff(table, tableXml)
203     s += intro
204
205     s += r"""
206 .sp
207 .ce 1
208 \fB%s\fR Table Columns:
209 .TS
210 center box;
211 l | l.
212 Column  Type
213 =
214 """ % tableName
215     s += tableSummaryToNroff(summary)
216     s += ".TE\n"
217
218     s += body
219     return s
220
221 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
222     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
223     doc = xml.dom.minidom.parse(xmlFile).documentElement
224
225     schemaDate = os.stat(schemaFile).st_mtime
226     xmlDate = os.stat(xmlFile).st_mtime
227     d = date.fromtimestamp(max(schemaDate, xmlDate))
228
229     if title == None:
230         title = schema.name
231
232     # Putting '\" pt as the first line tells "man" that the manpage
233     # needs to be preprocessed by "pic" and "tbl".
234     s = r''''\" pt
235 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
236 .\" -*- nroff -*-
237 .de TQ
238 .  br
239 .  ns
240 .  TP "\\$1"
241 ..
242 .de ST
243 .  PP
244 .  RS -0.15in
245 .  I "\\$1"
246 .  RE
247 ..
248 ''' % (title, d.strftime("%B %Y"))
249
250     s += '.SH "%s DATABASE"\n' % schema.name
251
252     tables = ""
253     introNodes = []
254     tableNodes = []
255     summary = []
256     for dbNode in doc.childNodes:
257         if (dbNode.nodeType == dbNode.ELEMENT_NODE
258             and dbNode.tagName == "table"):
259             tableNodes += [dbNode]
260
261             name = dbNode.attributes['name'].nodeValue
262             if dbNode.hasAttribute("title"):
263                 title = dbNode.attributes['title'].nodeValue
264             else:
265                 title = name + " configuration."
266             summary += [(name, title)]
267         else:
268             introNodes += [dbNode]
269
270     s += blockXmlToNroff(introNodes) + "\n"
271     tableSummary = r"""
272 .sp
273 .ce 1
274 \fB%s\fR Database Tables:
275 .TS
276 center box;
277 l | l
278 lb | l.
279 Table   Purpose
280 =
281 """ % schema.name
282     for name, title in summary:
283         tableSummary += "%s\t%s\n" % (name, textToNroff(title))
284     tableSummary += '.TE\n'
285     s += tableSummary
286
287     if erFile:
288         s += """
289 .sp 1
290 .SH "TABLE RELATIONSHIPS"
291 .PP
292 The following diagram shows the relationship among tables in the
293 database.  Each node represents a table.  Tables that are part of the
294 ``root set'' are shown with double borders.  Each edge leads from the
295 table that contains it and points to the table that its value
296 represents.  Edges are labeled with their column names.  Thick lines
297 represent strong references; thin lines represent weak references.
298 .RS -1in
299 """
300         erStream = open(erFile, "r")
301         for line in erStream:
302             s += line + '\n'
303         erStream.close()
304         s += ".RE\n"
305
306     for node in tableNodes:
307         s += tableToNroff(schema, node) + "\n"
308     return s
309
310 def usage():
311     print """\
312 %(argv0)s: ovsdb schema documentation generator
313 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
314 usage: %(argv0)s [OPTIONS] SCHEMA XML
315 where SCHEMA is an OVSDB schema in JSON format
316   and XML is OVSDB documentation in XML format.
317
318 The following options are also available:
319   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
320   --title=TITLE               use TITLE as title instead of schema name
321   -h, --help                  display this help message
322   -V, --version               display version information\
323 """ % {'argv0': argv0}
324     sys.exit(0)
325
326 if __name__ == "__main__":
327     try:
328         try:
329             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
330                                               ['er-diagram=', 'title=',
331                                                'help', 'version'])
332         except getopt.GetoptError, geo:
333             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
334             sys.exit(1)
335
336         er_diagram = None
337         title = None
338         for key, value in options:
339             if key == '--er-diagram':
340                 er_diagram = value
341             elif key == '--title':
342                 title = value
343             elif key in ['-h', '--help']:
344                 usage()
345             elif key in ['-V', '--version']:
346                 print "ovsdb-doc (Open vSwitch) @VERSION@"
347             else:
348                 sys.exit(0)
349
350         if len(args) != 2:
351             sys.stderr.write("%s: exactly 2 non-option arguments required "
352                              "(use --help for help)\n" % argv0)
353             sys.exit(1)
354
355         # XXX we should warn about undocumented tables or columns
356         s = docsToNroff(args[0], args[1], er_diagram)
357         for line in s.split("\n"):
358             line = line.strip()
359             if len(line):
360                 print line
361
362     except error.Error, e:
363         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
364         sys.exit(1)
365
366 # Local variables:
367 # mode: python
368 # End: