001 /* 002 * Copyright 2007 Paul Hammant 003 * Copyright 2007 ThinkTank Maths Limited 004 * 005 * ThinkTank Maths Limited grants a non-revocable, perpetual licence 006 * to Paul Hammant for unlimited use, relicensing and redistribution. No 007 * explicit permission is required from ThinkTank Maths Limited for 008 * any future decisions made with regard to this file. 009 * 010 * Redistribution and use in source and binary forms, with or without 011 * modification, are permitted provided that the following conditions 012 * are met: 013 * 014 * 1. Redistributions of source code must retain the above copyright 015 * notice, this list of conditions and the following disclaimer. 016 * 2. Redistributions in binary form must reproduce the above copyright 017 * notice, this list of conditions and the following disclaimer in the 018 * documentation and/or other materials provided with the distribution. 019 * 3. Neither the name of the copyright holders nor the names of its 020 * contributors may be used to endorse or promote products derived from 021 * this software without specific prior written permission. 022 * 023 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 024 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 025 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 026 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 027 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 028 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 029 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 030 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 031 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 032 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 033 * THE POSSIBILITY OF SUCH DAMAGE. 034 */ 035 package com.thoughtworks.paranamer; 036 037 import java.io.BufferedReader; 038 import java.io.File; 039 import java.io.FileInputStream; 040 import java.io.FileNotFoundException; 041 import java.io.IOException; 042 import java.io.InputStream; 043 import java.io.InputStreamReader; 044 import java.io.StringReader; 045 import java.io.UnsupportedEncodingException; 046 import java.lang.reflect.AccessibleObject; 047 import java.lang.reflect.Constructor; 048 import java.lang.reflect.Method; 049 import java.net.URI; 050 import java.net.URISyntaxException; 051 import java.net.URL; 052 import java.net.URLConnection; 053 import java.util.Enumeration; 054 import java.util.HashSet; 055 import java.util.Set; 056 import java.util.SortedMap; 057 import java.util.TreeMap; 058 import java.util.regex.Matcher; 059 import java.util.regex.Pattern; 060 import java.util.zip.GZIPInputStream; 061 import java.util.zip.Inflater; 062 import java.util.zip.InflaterInputStream; 063 import java.util.zip.ZipEntry; 064 import java.util.zip.ZipException; 065 import java.util.zip.ZipFile; 066 067 /** 068 * Implementation of {@link Paranamer} which can access Javadocs at runtime to extract 069 * parameter names of methods. Works with:- 070 * <ul> 071 * <li>Javadoc in zip file</li> 072 * <li>Javadoc in directory</li> 073 * <li>Javadoc at remote URL</li> 074 * </ul> 075 * Future implementations may be able to take multiple sources, but this version must be 076 * instantiated with the correct location of the Javadocs for the package you wish to 077 * extract the parameter names. Note that if a zip archive contains multiple 078 * "package-list" files, the first one will be used to index the packages which may be 079 * queried. 080 * <p> 081 * Note that this does not perform any caching of entries (except what it finds in the 082 * package-list file, which is very lightweight)... every lookup will involve a disc hit. 083 * If you want to speed up performance, use a {@link CachingParanamer}. 084 * <p> 085 * Implementation note: the constructors of this implementation let the client know if I/O 086 * problems will stop the recovery of parameter names. It might be preferable to suppress 087 * exceptions and simply return NO_PARAMETER_NAMES_LIST. 088 * <p> 089 * TODO: example use code 090 * <p> 091 * Known issues:- 092 * <ul> 093 * <li>Only tested with Javadoc 1.3 - 1.6</li> 094 * <li>Doesn't handle methods that declare the generic type as a parameter (rare use case)</li> 095 * <li>Some "erased" generic methods fail, e.g. File.compareTo(File), which is erased to 096 * File.compareTo(Object).</li> 097 * <li>URL implementation is really slow</li> 098 * <li>Doesn't support nested classes (due to limitations in the Java 1.4 reflection API)</li> 099 * </ul> 100 * 101 * @author Samuel Halliday, ThinkTank Maths Limited 102 */ 103 public class JavadocParanamer implements Paranamer { 104 105 private static final String IE = 106 "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"; 107 108 private static final ParameterNamesNotFoundException CLASS_NOT_SUPPORTED = 109 new ParameterNamesNotFoundException("class not supported"); 110 111 /** In the case of an archive, this stores the path up to the base of the Javadocs */ 112 private String base = null; 113 private final boolean isArchive; 114 private final boolean isDirectory; 115 116 private final boolean isURI; 117 118 /** 119 * Regardless of the implementation, this stores the base location of the remote or 120 * local file or directory. 121 */ 122 private final URI location; 123 124 /** The packages which are supported by this instance. Contains Strings */ 125 private final Set packages = new HashSet(); 126 127 /** 128 * Construct a Javadoc reading implementation of {@link Paranamer} using a local 129 * directory or zip archive as a source. 130 * 131 * @param archiveOrDirectory 132 * either a zip archive of Javadocs or the base directory of Javadocs. 133 * @throws IOException 134 * if there was an error when reading from either the archive or the 135 * package-list file. 136 * @throws FileNotFoundException 137 * if the archive, directory or <code>package-list</code> file does not 138 * exist. 139 * @throws NullPointerException 140 * if any parameter is null 141 * @throws IllegalArgumentException 142 * If the given parameter is not a file or directory or if it is a file 143 * but not a javadoc zip archive. 144 */ 145 public JavadocParanamer(File archiveOrDirectory) throws IOException { 146 if (archiveOrDirectory == null) 147 throw new NullPointerException(); 148 149 if (!archiveOrDirectory.exists()) 150 throw new FileNotFoundException( 151 archiveOrDirectory.getAbsolutePath()); 152 153 isURI = false; 154 location = archiveOrDirectory.toURI(); 155 156 if (archiveOrDirectory.isDirectory()) { 157 // is a directory 158 isArchive = false; 159 isDirectory = true; 160 // check that "package-list" exists 161 File dir = archiveOrDirectory; 162 File packageList = 163 new File(dir.getAbsolutePath() + "/package-list"); 164 if (!packageList.isFile()) 165 throw new FileNotFoundException("No package-list found at " 166 + dir.getAbsolutePath() 167 + ". Not a valid Javadoc directory."); 168 // it appear to be a valid Javadoc directory 169 FileInputStream input = new FileInputStream(packageList); 170 try { 171 String packageListString = streamToString(input); 172 parsePackageList(packageListString); 173 } finally { 174 input.close(); 175 } 176 } else if (archiveOrDirectory.isFile()) { 177 // is a file 178 isArchive = true; 179 isDirectory = false; 180 File archive = archiveOrDirectory; 181 if (!archive.getAbsolutePath().toLowerCase().endsWith(".zip")) 182 throw new IllegalArgumentException(archive.getAbsolutePath() 183 + " is not a zip file."); 184 // check that a "package-list" exists somewhere in the archive 185 ZipFile zip = new ZipFile(archive); 186 try { 187 // we need to check for a file named "package-list". 188 // There may be multiple files in the archive 189 // but we cannot use ZipFile.getEntry for suffix names 190 // so we have to look through all the entries. 191 // We then pick the largest file. 192 Enumeration entries = zip.entries(); 193 // grr... http://javablog.co.uk/2007/11/25/enumeration-and-iterable 194 // Set<ZipEntry> 195 SortedMap packageLists = new TreeMap(); 196 while (entries.hasMoreElements()) { 197 ZipEntry entry = (ZipEntry) entries.nextElement(); 198 String name = entry.getName(); 199 if (name.endsWith("package-list")) { 200 Long size = new Long(entry.getSize()); 201 packageLists.put(size, entry); 202 } 203 } 204 if (packageLists.size() == 0) 205 throw new FileNotFoundException( 206 "no package-list found in archive"); 207 208 // pick the largest package-list file, it's most likely the one we want 209 ZipEntry entry = 210 (ZipEntry) packageLists.get(packageLists.lastKey()); 211 String name = entry.getName(); 212 base = 213 name.substring(0, name.length() 214 - "package-list".length()); 215 InputStream input = zip.getInputStream(entry); 216 try { 217 String packageListString = streamToString(input); 218 parsePackageList(packageListString); 219 } finally { 220 input.close(); 221 } 222 } finally { 223 zip.close(); 224 } 225 } else 226 throw new IllegalArgumentException( 227 archiveOrDirectory.getAbsolutePath() 228 + " is neither a directory nor a file."); 229 } 230 231 /** 232 * @param url 233 * @throws IOException 234 * if there was a problem connecting to the remote Javadocs 235 * @throws FileNotFoundException 236 * if the url does not have a <code>/package-list</code> 237 * @throws NullPointerException 238 * if any parameter is null 239 */ 240 public JavadocParanamer(URL url) throws IOException { 241 if (url == null) 242 throw new NullPointerException(); 243 244 isArchive = false; 245 isDirectory = false; 246 isURI = true; 247 try { 248 location = new URI(url.toString()); 249 } catch (URISyntaxException e) { 250 throw new IOException(e.getMessage()); 251 } 252 253 // check the package-list 254 URL packageListURL = new URL(url.toString() + "/package-list"); 255 InputStream input = urlToInputStream(packageListURL); 256 try { 257 String packageList = streamToString(input); 258 parsePackageList(packageList); 259 } finally { 260 input.close(); 261 } 262 } 263 264 public int areParameterNamesAvailable(Class clazz, 265 String constructorOrMethodName) { 266 if ((clazz == null) || (constructorOrMethodName == null)) 267 throw new NullPointerException(); 268 269 // due to general problems with this method, we just delegate 270 // the first match we find to lookupParameterNames 271 AccessibleObject accessible = null; 272 if (constructorOrMethodName.equals("<init>")) 273 accessible = clazz.getDeclaredConstructors()[0]; 274 else { 275 Method[] methods = clazz.getMethods(); 276 if (methods == null) 277 // this method doesn't exist 278 return NO_PARAMETER_NAMES_FOR_CLASS_AND_MEMBER; 279 for (int i = 0; i < methods.length; i++) { 280 if (methods[i].getName().equals(constructorOrMethodName)) { 281 accessible = methods[i]; 282 break; 283 } 284 } 285 } 286 if (accessible == null) 287 // this method doesn't exist 288 return NO_PARAMETER_NAMES_FOR_CLASS_AND_MEMBER; 289 290 try { 291 lookupParameterNames(accessible); 292 return PARAMETER_NAMES_FOUND; 293 } catch (ParameterNamesNotFoundException e) { 294 if (e == CLASS_NOT_SUPPORTED) 295 return NO_PARAMETER_NAMES_FOR_CLASS; 296 return NO_PARAMETER_NAMES_FOR_CLASS_AND_MEMBER; 297 } 298 } 299 300 public String[] lookupParameterNames(AccessibleObject methodOrConstructor) { 301 return lookupParameterNames(methodOrConstructor, true); 302 } 303 304 public String[] lookupParameterNames(AccessibleObject methodOrConstructor, boolean throwExceptionIfMissing) { 305 if (methodOrConstructor == null) 306 throw new NullPointerException(); 307 308 Class klass; 309 String name; 310 Class[] types; 311 312 if (methodOrConstructor instanceof Constructor) { 313 Constructor constructor = (Constructor) methodOrConstructor; 314 klass = constructor.getDeclaringClass(); 315 name = constructor.getName(); 316 types = constructor.getParameterTypes(); 317 } else if (methodOrConstructor instanceof Method) { 318 Method method = (Method) methodOrConstructor; 319 klass = method.getDeclaringClass(); 320 name = method.getName(); 321 types = method.getParameterTypes(); 322 } else 323 throw new IllegalArgumentException(); 324 325 // quick check to see if we support the package 326 if (!packages.contains(klass.getPackage().getName())) 327 throw CLASS_NOT_SUPPORTED; 328 329 try { 330 String[] names = getParameterNames(klass, name, types); 331 if (names == null) { 332 if (throwExceptionIfMissing) { 333 throw new ParameterNamesNotFoundException( 334 methodOrConstructor.toString()); 335 } else { 336 return Paranamer.EMPTY_NAMES; 337 } 338 } 339 return names; 340 } catch (IOException e) { 341 if (throwExceptionIfMissing) { 342 throw new ParameterNamesNotFoundException( 343 methodOrConstructor.toString() + " due to an I/O error: " 344 + e.getMessage()); 345 } else { 346 return Paranamer.EMPTY_NAMES; 347 } 348 } 349 } 350 351 // throws CLASS_NOT_SUPPORTED if the class file is not found in the javadocs 352 // return null if the parameter names were not found 353 private String[] getParameterNames(Class klass, 354 String constructorOrMethodName, Class[] types) throws IOException { 355 // silly request for names of a parameterless method/constructor! 356 if ((types != null) && (types.length == 0)) 357 return new String[0]; 358 359 String path = getCanonicalName(klass).replace('.', '/'); 360 if (isArchive) { 361 ZipFile archive = new ZipFile(new File(location)); 362 ZipEntry entry = archive.getEntry(base + path + ".html"); 363 if (entry == null) 364 throw CLASS_NOT_SUPPORTED; 365 InputStream input = archive.getInputStream(entry); 366 return getParameterNames2(input, constructorOrMethodName, types); 367 } else if (isDirectory) { 368 File file = new File(location.getPath() + "/" + path + ".html"); 369 if (!file.isFile()) 370 throw CLASS_NOT_SUPPORTED; 371 FileInputStream input = new FileInputStream(file); 372 return getParameterNames2(input, constructorOrMethodName, types); 373 } else if (isURI) { 374 try { 375 URL url = new URL(location.toString() + "/" + path + ".html"); 376 InputStream input = urlToInputStream(url); 377 return getParameterNames2(input, constructorOrMethodName, types); 378 } catch (FileNotFoundException e) { 379 throw CLASS_NOT_SUPPORTED; 380 } 381 } 382 throw new RuntimeException( 383 "bug in JavadocParanamer. Should not reach here."); 384 } 385 386 /* 387 * Parse the Javadoc String and return the parameter names for the given constructor 388 * or method. Return null if no method/constructor is found. Note that types will 389 * never have length zero... we already deal with that situation higher up in the 390 * chain. Don't forget to close the input! 391 */ 392 private String[] getParameterNames2(InputStream input, 393 String constructorOrMethodName, Class[] types) throws IOException { 394 String javadoc = streamToString(input); 395 input.close(); 396 397 // String we're looking for is like 398 // 399 // NAME="constructorOrMethodName(obj.ClassName, ...)"...noise... 400 // <DT><B>Parameters:</B><DD><CODE>parameter_name_1</CODE>...noise... 401 // <DD><CODE>parameter_name_2</CODE>...noise... 402 // ... 403 // <DD><CODE>parameter_name_N</CODE>...noise... 404 // 405 // We cannot rely on the Parameters line existing as it depends on the author 406 // having correctly marked-up their code. The NAME element is auto-generated 407 // and should be checked for aggressively. 408 // 409 // Also note that Javadoc parameter names may differ from the names in the source. 410 411 // we don't have Pattern/Matcher :-( 412 StringBuffer regex = new StringBuffer(); 413 regex.append("NAME=\""); 414 regex.append(constructorOrMethodName); 415 // quotes needed to escape array brackets 416 regex.append("\\(\\Q"); 417 for (int i = 0; i < types.length; i++) { 418 if (i != 0) 419 regex.append(", "); 420 // canonical name deals with arrays 421 regex.append(getCanonicalName(types[i])); 422 } 423 regex.append("\\E\\)\""); 424 425 // FIXME: handle Javadoc 1.3, 1.4 and 1.5 as well (this is 1.6) 426 427 Pattern pattern = Pattern.compile(regex.toString()); 428 Matcher matcher = pattern.matcher(javadoc); 429 if (!matcher.find()) 430 // not found 431 return Paranamer.EMPTY_NAMES; 432 433 // found it. Lookup the parameter names. 434 String[] names = new String[types.length]; 435 // now we're sure we have the right method, find the parameter names! 436 String regexParams = "<DD><CODE>([^<]*)</CODE>"; 437 Pattern patternParams = Pattern.compile(regexParams); 438 int start = matcher.end(); 439 Matcher matcherParams = patternParams.matcher(javadoc); 440 for (int i = 0; i < types.length; i++) { 441 boolean find = matcherParams.find(start); 442 if (!find) 443 return Paranamer.EMPTY_NAMES; 444 start = matcherParams.end(); 445 names[i] = matcherParams.group(1); 446 } 447 return names; 448 } 449 450 // doesn't support names of nested classes 451 private String getCanonicalName(Class klass) { 452 if (klass.isArray()) 453 return getCanonicalName(klass.getComponentType()) + "[]"; 454 455 return klass.getName(); 456 } 457 458 // storing the list of packages that we support is very lightweight 459 private void parsePackageList(String packageList) throws IOException { 460 StringReader reader = new StringReader(packageList); 461 BufferedReader breader = new BufferedReader(reader); 462 String line; 463 while ((line = breader.readLine()) != null) { 464 packages.add(line); 465 } 466 } 467 468 // read an InputStream into a UTF-8 String 469 private String streamToString(InputStream input) throws IOException { 470 InputStreamReader reader; 471 try { 472 reader = new InputStreamReader(input, "UTF-8"); 473 } catch (UnsupportedEncodingException e) { 474 // this should never happen 475 reader = new InputStreamReader(input); 476 } 477 BufferedReader breader = new BufferedReader(reader); 478 String line; 479 StringBuffer builder = new StringBuffer(); 480 while ((line = breader.readLine()) != null) { 481 builder.append(line); 482 builder.append("\n"); 483 } 484 return builder.toString(); 485 } 486 487 private InputStream urlToInputStream(URL url) throws IOException { 488 URLConnection conn = url.openConnection(); 489 // pretend to be IE6 490 conn.setRequestProperty("User-Agent", IE); 491 // allow both GZip and Deflate (ZLib) encodings 492 conn.setRequestProperty("Accept-Encoding", "gzip, deflate"); 493 conn.connect(); 494 String encoding = conn.getContentEncoding(); 495 if ((encoding != null) && encoding.equalsIgnoreCase("gzip")) 496 return new GZIPInputStream(conn.getInputStream()); 497 else if ((encoding != null) && encoding.equalsIgnoreCase("deflate")) 498 return new InflaterInputStream(conn.getInputStream(), new Inflater( 499 true)); 500 else 501 return conn.getInputStream(); 502 } 503 504 }