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