• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3#  Copyright (C) 2018 Google, Inc.
4#
5#  Licensed under the Apache License, Version 2.0 (the "License");
6#  you may not use this file except in compliance with the License.
7#  You may obtain a copy of the License at:
8#
9#  http://www.apache.org/licenses/LICENSE-2.0
10#
11#  Unless required by applicable law or agreed to in writing, software
12#  distributed under the License is distributed on an "AS IS" BASIS,
13#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#  See the License for the specific language governing permissions and
15#  limitations under the License.
16import os
17import re
18
19# Matches a JavaDoc comment followed by an @Rpc annotation.
20import subprocess
21
22"""A regex that captures the JavaDoc comment and function signature."""
23JAVADOC_RPC_REGEX = re.compile(
24    # Capture the entire comment string.
25    r'(?P<comment>/\*\*(?:(?!/\*\*).)*?\*/)(?:(?:(?!\*/).)*?)\s*'
26    # Find at least one @Rpc Annotation
27    r'(?:@\w+\s*)*?(?:@Rpc.*?\s*)(?:@\w+\s*)*?'
28    # Capture the function signature, ignoring the throws statement
29    # (the throws information will be pulled from the comment).
30    r'(?P<function_signature>.*?)(?:throws.*?)?{',
31    flags=re.MULTILINE | re.DOTALL)
32
33"""
34Captures javadoc "frills" like the ones found below:
35
36/**
37  *
38  */
39
40/** */
41
42"""
43CAPTURE_JAVADOC_FRILLS = re.compile(
44    r'(^\s*(/\*\*$|/\*\* |\*($| ))|\s*\*/\s*$)',
45    re.MULTILINE)
46
47"""A regex to capture the individual pieces of the function signature."""
48CAPTURE_FUNCTION_SIGNATURE = re.compile(
49    # Capture any non-static function
50    r'(public|private)'
51    # Allow synchronized and @Annotations()
52    r'[( synchronized)(@\w+\(.*?\)?)]*?'
53    # Return Type (Allow n-number of generics and arrays)
54    r'(?P<return_type>\w+(?:[\[\]<>\w ,]*?)?)\s+'
55    # Capture functionName
56    r'(?P<function_name>\w*)\s*'
57    # Capture anything enclosed in parens
58    r'\((?P<parameters>.*)\)',
59    re.MULTILINE | re.DOTALL)
60
61"""Matches a parameter and its RPC annotations."""
62CAPTURE_PARAMETER = re.compile(
63    r'(?:'
64    r'(?P<optional>@RpcOptional\s+)?'
65    r'(?P<rpc_param>@RpcParameter\(.*?\)\s*)?'
66    r'(?P<default>@RpcDefault\((?P<default_value>.*)\)\s*)?'
67    r')*'
68    r'(?P<param_type>\w+)\s+(?P<param_name>\w+)',
69    flags=re.MULTILINE | re.DOTALL)
70
71
72class Facade(object):
73    """A class representing a Facade.
74
75    Attributes:
76        path: the path the facade is located at.
77        directory: the
78    """
79
80    def __init__(self, path):
81        self.path = path
82        self.directory = os.path.dirname(self.path)
83        # -5 removes the '.java' file extension
84        self.name = path[path.rfind('/') + 1:-5]
85        self.rpcs = list()
86
87
88def main():
89    basepath = os.path.abspath(os.path.join(os.path.dirname(
90        os.path.realpath(__file__)), '..'))
91
92    facades = list()
93
94    for path, dirs, files in os.walk(basepath):
95        for file_name in files:
96            if file_name.endswith('Facade.java'):
97                facades.append(parse_facade_file(os.path.join(path, file_name)))
98
99    basepath = os.path.abspath(os.path.join(os.path.dirname(
100        os.path.realpath(__file__)), '..'))
101    write_output(facades, os.path.join(basepath, 'Docs/ApiReference.md'))
102
103
104def write_output(facades, output_path):
105    facades = sorted(facades, key=lambda x: x.directory)
106
107    git_rev = None
108    try:
109        git_rev = subprocess.check_output('git rev-parse HEAD',
110                                          shell=True).decode('utf-8').strip()
111    except subprocess.CalledProcessError as e:
112        # Getting the commit ID is optional; we continue if we cannot get it
113        pass
114
115    with open(output_path, 'w') as fd:
116        if git_rev:
117            fd.write('Generated at commit `%s`\n\n' % git_rev)
118        fd.write('# Facade Groups')
119        prev_directory = ''
120        for facade in facades:
121            if facade.directory != prev_directory:
122                fd.write('\n\n## %s\n\n' % facade.directory[
123                                           facade.directory.rfind('/') + 1:])
124                prev_directory = facade.directory
125            fd.write('  * [%s](#%s)\n' % (facade.name, facade.name.lower()))
126
127        fd.write('\n# Facades\n\n')
128        for facade in facades:
129            fd.write('\n## %s' % facade.name)
130            for rpc in facade.rpcs:
131                fd.write('\n\n### %s\n\n' % rpc.name)
132                fd.write('%s\n' % rpc)
133
134
135def parse_facade_file(file_path):
136    """Parses a .*Facade.java file and represents it as a Facade object"""
137    facade = Facade(file_path)
138    with open(file_path, 'r') as content_file:
139        content = content_file.read()
140        matches = re.findall(JAVADOC_RPC_REGEX, content)
141        for match in matches:
142            rpc_function = DocumentedFunction(
143                match[0].replace('\\n', '\n'),  # match[0]: JavaDoc comment
144                match[1].replace('\\n', '\n'))  # match[1]: function signature
145            facade.rpcs.append(rpc_function)
146    facade.rpcs.sort(key=lambda rpc: rpc.name)
147    return facade
148
149
150class DefaultValue(object):
151    """An object representation of a default value.
152
153    Functions as Optional in Java, or a pointer in C++.
154
155    Attributes:
156        value: the default value
157    """
158    def __init__(self, default_value=None):
159        self.value = default_value
160
161
162class DocumentedValue(object):
163    def __init__(self):
164        """Creates an empty DocumentedValue object."""
165        self.type = 'void'
166        self.name = None
167        self.description = None
168        self.default_value = None
169
170    def set_type(self, param_type):
171        self.type = param_type
172        return self
173
174    def set_name(self, name):
175        self.name = name
176        return self
177
178    def set_description(self, description):
179        self.description = description
180        return self
181
182    def set_default_value(self, default_value):
183        self.default_value = default_value
184        return self
185
186    def __str__(self):
187        if self.name is None:
188            return self.description
189        if self.default_value is None:
190            return '%s: %s' % (self.name, self.description)
191        else:
192            return '%s: %s (default: %s)' % (self.name, self.description,
193                                             self.default_value.value)
194
195
196class DocumentedFunction(object):
197    """A combination of all function documentation into a single object.
198
199    Attributes:
200        _description: A string that describes the function.
201        _parameters: A dictionary of {parameter name: DocumentedValue object}
202        _return: a DocumentedValue with information on the returned value.
203        _throws: A dictionary of {throw type (str): DocumentedValue object}
204
205    """
206    def __init__(self, comment, function_signature):
207        self._name = None
208        self._description = None
209        self._parameters = {}
210        self._return = DocumentedValue()
211        self._throws = {}
212
213        self._parse_comment(comment)
214        self._parse_function_signature(function_signature)
215
216    @property
217    def name(self):
218        return self._name
219
220    def _parse_comment(self, comment):
221        """Parses a JavaDoc comment into DocumentedFunction attributes."""
222        comment = str(re.sub(CAPTURE_JAVADOC_FRILLS, '', comment))
223        tag = 'description'
224        tag_data = ''
225        for line in comment.split('\n'):
226            line.strip()
227            if line.startswith('@'):
228                self._finalize_tag(tag, tag_data)
229                tag_end_index = line.find(' ')
230                tag = line[1:tag_end_index]
231                tag_data = line[tag_end_index + 1:]
232            else:
233                if not tag_data:
234                    whitespace_char = ''
235                elif (line.startswith(' ')
236                      or tag_data.endswith('\n')
237                      or line == ''):
238                    whitespace_char = '\n'
239                else:
240                    whitespace_char = ' '
241                tag_data = '%s%s%s' % (tag_data, whitespace_char, line)
242        self._finalize_tag(tag, tag_data.strip())
243
244    def __str__(self):
245        params_signature = ', '.join(['%s %s' % (param.type, param.name)
246                                      for param in self._parameters.values()])
247        params_description = '\n    '.join(['%s: %s' % (param.name,
248                                                        param.description)
249                                            for param in
250                                            self._parameters.values()])
251        if params_description:
252            params_description = ('\n**Parameters:**\n\n    %s\n' %
253                                  params_description)
254        return_description = '\n' if self._return else ''
255        if self._return:
256            return_description += ('**Returns:**\n\n    %s' %
257                                   self._return.description)
258        return (
259            # ReturnType functionName(Parameters)
260            '%s %s(%s)\n\n'
261            # Description
262            '%s\n'
263            # Params & Return
264            '%s%s' % (self._return.type, self._name,
265                      params_signature, self._description,
266                      params_description, return_description)).strip()
267
268    def _parse_function_signature(self, function_signature):
269        """Parses the function signature into DocumentedFunction attributes."""
270        header_match = re.search(CAPTURE_FUNCTION_SIGNATURE, function_signature)
271        self._name = header_match.group('function_name')
272        self._return.set_type(header_match.group('return_type'))
273
274        for match in re.finditer(CAPTURE_PARAMETER,
275                                 header_match.group('parameters')):
276            param_name = match.group('param_name')
277            param_type = match.group('param_type')
278            if match.group('default_value'):
279                default = DefaultValue(match.group('default_value'))
280            elif match.group('optional'):
281                default = DefaultValue(None)
282            else:
283                default = None
284
285            if param_name in self._parameters:
286                param = self._parameters[param_name]
287            else:
288                param = DocumentedValue()
289            param.set_type(param_type)
290            param.set_name(param_name)
291            param.set_default_value(default)
292
293    def _finalize_tag(self, tag, tag_data):
294        """Finalize the JavaDoc @tag by adding it to the correct field."""
295        name = tag_data[:tag_data.find(' ')]
296        description = tag_data[tag_data.find(' ') + 1:].strip()
297        if tag == 'description':
298            self._description = tag_data
299        elif tag == 'param':
300            if name in self._parameters:
301                param = self._parameters[name]
302            else:
303                param = DocumentedValue().set_name(name)
304                self._parameters[name] = param
305            param.set_description(description)
306        elif tag == 'return':
307            self._return.set_description(tag_data)
308        elif tag == 'throws':
309            new_throws = DocumentedValue().set_name(name)
310            new_throws.set_description(description)
311            self._throws[name] = new_throws
312
313
314if __name__ == '__main__':
315    main()
316