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