org.weakref.jmx.internal.paranamer.JavadocParanamer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jmxutils Show documentation
Show all versions of jmxutils Show documentation
Exporting JMX mbeans made easy
/*
* Copyright 2007 Paul Hammant
* Copyright 2007 ThinkTank Maths Limited
*
* ThinkTank Maths Limited grants a non-revocable, perpetual licence
* to Paul Hammant for unlimited use, relicensing and redistribution. No
* explicit permission is required from ThinkTank Maths Limited for
* any future decisions made with regard to this file.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holders nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.weakref.jmx.internal.paranamer;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Implementation of {@link Paranamer} which can access Javadocs at runtime to extract
* parameter names of methods. Works with:-
*
* - Javadoc in zip file
* - Javadoc in directory
* - Javadoc at remote URL
*
* Future implementations may be able to take multiple sources, but this version must be
* instantiated with the correct location of the Javadocs for the package you wish to
* extract the parameter names. Note that if a zip archive contains multiple
* "package-list" files, the first one will be used to index the packages which may be
* queried.
*
* Note that this does not perform any caching of entries (except what it finds in the
* package-list file, which is very lightweight)... every lookup will involve a disc hit.
* If you want to speed up performance, use a {@link CachingParanamer}.
*
* Implementation note: the constructors of this implementation let the client know if I/O
* problems will stop the recovery of parameter names. It might be preferable to suppress
* exceptions and simply return NO_PARAMETER_NAMES_LIST.
*
* TODO: example use code
*
* Known issues:-
*
* - Only tested with Javadoc 1.3 - 1.6
* - Doesn't handle methods that declare the generic type as a parameter (rare use case)
* - Some "erased" generic methods fail, e.g. File.compareTo(File), which is erased to
* File.compareTo(Object).
* - URL implementation is really slow
* - Doesn't support nested classes (due to limitations in the Java 1.4 reflection API)
*
*
* @author Samuel Halliday, ThinkTank Maths Limited
*/
public class JavadocParanamer implements Paranamer {
private static final String IE =
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)";
private static final ParameterNamesNotFoundException CLASS_NOT_SUPPORTED =
new ParameterNamesNotFoundException("class not supported");
/** In the case of an archive, this stores the path up to the base of the Javadocs */
private String base = null;
private final boolean isArchive;
private final boolean isDirectory;
private final boolean isURI;
/**
* Regardless of the implementation, this stores the base location of the remote or
* local file or directory.
*/
private final URI location;
/** The packages which are supported by this instance. Contains Strings */
private final Set packages = new HashSet();
/**
* Construct a Javadoc reading implementation of {@link Paranamer} using a local
* directory or zip archive as a source.
*
* @param archiveOrDirectory
* either a zip archive of Javadocs or the base directory of Javadocs.
* @throws IOException
* if there was an error when reading from either the archive or the
* package-list file.
* @throws FileNotFoundException
* if the archive, directory or package-list
file does not
* exist.
* @throws NullPointerException
* if any parameter is null
* @throws IllegalArgumentException
* If the given parameter is not a file or directory or if it is a file
* but not a javadoc zip archive.
*/
public JavadocParanamer(File archiveOrDirectory) throws IOException {
if (archiveOrDirectory == null)
throw new NullPointerException();
if (!archiveOrDirectory.exists())
throw new FileNotFoundException(
archiveOrDirectory.getAbsolutePath());
isURI = false;
location = archiveOrDirectory.toURI();
if (archiveOrDirectory.isDirectory()) {
// is a directory
isArchive = false;
isDirectory = true;
// check that "package-list" exists
File dir = archiveOrDirectory;
File packageList =
new File(dir.getAbsolutePath() + "/package-list");
if (!packageList.isFile())
throw new FileNotFoundException("No package-list found at "
+ dir.getAbsolutePath()
+ ". Not a valid Javadoc directory.");
// it appear to be a valid Javadoc directory
FileInputStream input = new FileInputStream(packageList);
try {
String packageListString = streamToString(input);
parsePackageList(packageListString);
} finally {
input.close();
}
} else if (archiveOrDirectory.isFile()) {
// is a file
isArchive = true;
isDirectory = false;
File archive = archiveOrDirectory;
if (!archive.getAbsolutePath().toLowerCase().endsWith(".zip"))
throw new IllegalArgumentException(archive.getAbsolutePath()
+ " is not a zip file.");
// check that a "package-list" exists somewhere in the archive
ZipFile zip = new ZipFile(archive);
try {
// we need to check for a file named "package-list".
// There may be multiple files in the archive
// but we cannot use ZipFile.getEntry for suffix names
// so we have to look through all the entries.
// We then pick the largest file.
Enumeration entries = zip.entries();
// grr... http://javablog.co.uk/2007/11/25/enumeration-and-iterable
// Set
SortedMap packageLists = new TreeMap();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
String name = entry.getName();
if (name.endsWith("package-list")) {
Long size = new Long(entry.getSize());
packageLists.put(size, entry);
}
}
if (packageLists.size() == 0)
throw new FileNotFoundException(
"no package-list found in archive");
// pick the largest package-list file, it's most likely the one we want
ZipEntry entry =
(ZipEntry) packageLists.get(packageLists.lastKey());
String name = entry.getName();
base =
name.substring(0, name.length()
- "package-list".length());
InputStream input = zip.getInputStream(entry);
try {
String packageListString = streamToString(input);
parsePackageList(packageListString);
} finally {
input.close();
}
} finally {
zip.close();
}
} else
throw new IllegalArgumentException(
archiveOrDirectory.getAbsolutePath()
+ " is neither a directory nor a file.");
}
/**
* @param url
* @throws IOException
* if there was a problem connecting to the remote Javadocs
* @throws FileNotFoundException
* if the url does not have a /package-list
* @throws NullPointerException
* if any parameter is null
*/
public JavadocParanamer(URL url) throws IOException {
if (url == null)
throw new NullPointerException();
isArchive = false;
isDirectory = false;
isURI = true;
try {
location = new URI(url.toString());
} catch (URISyntaxException e) {
throw new IOException(e.getMessage());
}
// check the package-list
URL packageListURL = new URL(url.toString() + "/package-list");
InputStream input = urlToInputStream(packageListURL);
try {
String packageList = streamToString(input);
parsePackageList(packageList);
} finally {
input.close();
}
}
public String[] lookupParameterNames(AccessibleObject methodOrConstructor) {
return lookupParameterNames(methodOrConstructor, true);
}
public String[] lookupParameterNames(AccessibleObject methodOrConstructor, boolean throwExceptionIfMissing) {
if (methodOrConstructor == null)
throw new NullPointerException();
Class klass;
String name;
Class[] types;
if (methodOrConstructor instanceof Constructor) {
Constructor constructor = (Constructor) methodOrConstructor;
klass = constructor.getDeclaringClass();
name = constructor.getName();
types = constructor.getParameterTypes();
} else if (methodOrConstructor instanceof Method) {
Method method = (Method) methodOrConstructor;
klass = method.getDeclaringClass();
name = method.getName();
types = method.getParameterTypes();
} else
throw new IllegalArgumentException();
// quick check to see if we support the package
if (!packages.contains(klass.getPackage().getName()))
throw CLASS_NOT_SUPPORTED;
try {
String[] names = getParameterNames(klass, name, types);
if (names == null) {
if (throwExceptionIfMissing) {
throw new ParameterNamesNotFoundException(
methodOrConstructor.toString());
} else {
return Paranamer.EMPTY_NAMES;
}
}
return names;
} catch (IOException e) {
if (throwExceptionIfMissing) {
throw new ParameterNamesNotFoundException(
methodOrConstructor.toString() + " due to an I/O error: "
+ e.getMessage());
} else {
return Paranamer.EMPTY_NAMES;
}
}
}
// throws CLASS_NOT_SUPPORTED if the class file is not found in the javadocs
// return null if the parameter names were not found
private String[] getParameterNames(Class klass,
String constructorOrMethodName, Class[] types) throws IOException {
// silly request for names of a parameterless method/constructor!
if ((types != null) && (types.length == 0))
return new String[0];
String path = getCanonicalName(klass).replace('.', '/');
if (isArchive) {
ZipFile archive = new ZipFile(new File(location));
ZipEntry entry = archive.getEntry(base + path + ".html");
if (entry == null)
throw CLASS_NOT_SUPPORTED;
InputStream input = archive.getInputStream(entry);
return getParameterNames2(input, constructorOrMethodName, types);
} else if (isDirectory) {
File file = new File(location.getPath() + "/" + path + ".html");
if (!file.isFile())
throw CLASS_NOT_SUPPORTED;
FileInputStream input = new FileInputStream(file);
return getParameterNames2(input, constructorOrMethodName, types);
} else if (isURI) {
try {
URL url = new URL(location.toString() + "/" + path + ".html");
InputStream input = urlToInputStream(url);
return getParameterNames2(input, constructorOrMethodName, types);
} catch (FileNotFoundException e) {
throw CLASS_NOT_SUPPORTED;
}
}
throw new RuntimeException(
"bug in JavadocParanamer. Should not reach here.");
}
/*
* Parse the Javadoc String and return the parameter names for the given constructor
* or method. Return null if no method/constructor is found. Note that types will
* never have length zero... we already deal with that situation higher up in the
* chain. Don't forget to close the input!
*/
private String[] getParameterNames2(InputStream input,
String constructorOrMethodName, Class[] types) throws IOException {
String javadoc = streamToString(input);
input.close();
// String we're looking for is like
//
// NAME="constructorOrMethodName(obj.ClassName, ...)"...noise...
// Parameters: parameter_name_1
...noise...
// parameter_name_2
...noise...
// ...
// parameter_name_N
...noise...
//
// We cannot rely on the Parameters line existing as it depends on the author
// having correctly marked-up their code. The NAME element is auto-generated
// and should be checked for aggressively.
//
// Also note that Javadoc parameter names may differ from the names in the source.
// we don't have Pattern/Matcher :-(
StringBuffer regex = new StringBuffer();
regex.append("NAME=\"");
regex.append(constructorOrMethodName);
// quotes needed to escape array brackets
regex.append("\\(\\Q");
for (int i = 0; i < types.length; i++) {
if (i != 0)
regex.append(", ");
// canonical name deals with arrays
regex.append(getCanonicalName(types[i]));
}
regex.append("\\E\\)\"");
// FIXME: handle Javadoc 1.3, 1.4 and 1.5 as well (this is 1.6)
Pattern pattern = Pattern.compile(regex.toString());
Matcher matcher = pattern.matcher(javadoc);
if (!matcher.find())
// not found
return Paranamer.EMPTY_NAMES;
// found it. Lookup the parameter names.
String[] names = new String[types.length];
// now we're sure we have the right method, find the parameter names!
String regexParams = "([^<]*)
";
Pattern patternParams = Pattern.compile(regexParams);
int start = matcher.end();
Matcher matcherParams = patternParams.matcher(javadoc);
for (int i = 0; i < types.length; i++) {
boolean find = matcherParams.find(start);
if (!find)
return Paranamer.EMPTY_NAMES;
start = matcherParams.end();
names[i] = matcherParams.group(1);
}
return names;
}
// doesn't support names of nested classes
private String getCanonicalName(Class klass) {
if (klass.isArray())
return getCanonicalName(klass.getComponentType()) + "[]";
return klass.getName();
}
// storing the list of packages that we support is very lightweight
private void parsePackageList(String packageList) throws IOException {
StringReader reader = new StringReader(packageList);
BufferedReader breader = new BufferedReader(reader);
String line;
while ((line = breader.readLine()) != null) {
packages.add(line);
}
}
// read an InputStream into a UTF-8 String
private String streamToString(InputStream input) throws IOException {
InputStreamReader reader;
try {
reader = new InputStreamReader(input, "UTF-8");
} catch (UnsupportedEncodingException e) {
// this should never happen
reader = new InputStreamReader(input);
}
BufferedReader breader = new BufferedReader(reader);
String line;
StringBuffer builder = new StringBuffer();
while ((line = breader.readLine()) != null) {
builder.append(line);
builder.append("\n");
}
return builder.toString();
}
private InputStream urlToInputStream(URL url) throws IOException {
URLConnection conn = url.openConnection();
// pretend to be IE6
conn.setRequestProperty("User-Agent", IE);
// allow both GZip and Deflate (ZLib) encodings
conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
conn.connect();
String encoding = conn.getContentEncoding();
if ((encoding != null) && encoding.equalsIgnoreCase("gzip"))
return new GZIPInputStream(conn.getInputStream());
else if ((encoding != null) && encoding.equalsIgnoreCase("deflate"))
return new InflaterInputStream(conn.getInputStream(), new Inflater(
true));
else
return conn.getInputStream();
}
}