blob: bac9e4fb4e36ff4bea5786829a67889b6ff0cdd7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
<<<<<<< Updated upstream
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
=======
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
>>>>>>> Stashed changes
* limitations under the License.
*/
package org.apache.jdo.tck.util.signature;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Tests classes for correct signatures.
*
* @author Martin Zaun
*/
public class SignatureVerifier {
/** The new-line character on this system. */
protected static final String NL = System.getProperty("line.separator");
/**
* All modifiers defined in java.lang.reflect.Modifier. This field is used to filter out
* vm-specific modifiers such as found in Sun's vm to identify Enum and Annotation.
*/
protected static final int ALL_MODIFIERS =
Modifier.ABSTRACT
| Modifier.FINAL
| Modifier.INTERFACE
| Modifier.NATIVE
| Modifier.PRIVATE
| Modifier.PROTECTED
| Modifier.PUBLIC
| Modifier.STATIC
| Modifier.STRICT
| Modifier.SYNCHRONIZED
| Modifier.TRANSIENT
| Modifier.VOLATILE;
/** Pseudo modifier for annotation */
protected static final int ANNOTATION = 0x2000;
/** Pseudo modifier for enum */
protected static final int ENUM = 0x4000;
/** A writer for standard output. */
protected final PrintWriter log;
/** Flag to print error output only. */
private final boolean quiet;
/** Flag to also print verbose output. */
private final boolean verbose;
/** The parse to be used for parsing signature descriptor files. */
protected final Parser parser = new Parser();
/** The classloader to be used for loading types. */
protected final ClassLoader classLoader;
/** The currently tested Class. */
protected Class<?> cls;
/** All untested, declared members of the current class. */
protected final Set<Member> members = new HashSet<>();
/** Collects names of loadable classes. */
private final Set<String> loading = new TreeSet<>();
/** Collects names of unloadable classes. */
private final Set<String> notLoading = new TreeSet<>();
/** Counts tested features (class, constructor, fields, methods). */
private int tested;
/** Counts missing members (constructor, fields, methods). */
private int missing;
/** Counts non-matching features (class, constructor, fields, methods). */
private int mismatch;
/** Counts matching features (class, constructor, fields, methods). */
private int matching;
/** Counts public non-standard members (constructor, fields, methods). */
private int nonStandard;
/** Counts other reported problems (e.g., accessing field values). */
private int otherProblems;
/**
* Constructs a test instance.
*
* @param loader ClassLoader
* @param log writer for logging
* @param quiet quiet option
* @param verbose verbose option
*/
public SignatureVerifier(ClassLoader loader, PrintWriter log, boolean quiet, boolean verbose) {
classLoader = this.getClass().getClassLoader();
this.log = log;
this.quiet = quiet;
this.verbose = (!quiet && verbose);
}
/**
* Constructs a test instance.
*
* @param log writer for logging
* @param quiet quiet option
* @param verbose verbose option
*/
public SignatureVerifier(PrintWriter log, boolean quiet, boolean verbose) {
this(SignatureVerifier.class.getClassLoader(), log, quiet, verbose);
}
// ----------------------------------------------------------------------
// Local Logging Methods
// ----------------------------------------------------------------------
/**
* Prints an error message.
*
* @param msg error message
*/
protected void logError(String msg) {
log.println(msg);
}
/**
* Prints an info message.
*
* @param msg info message
*/
protected void logInfo(String msg) {
if (!quiet) {
log.println(msg);
}
}
/**
* Prints a verbose message.
*
* @param msg verbose message
*/
protected void logVerbose(String msg) {
if (verbose) {
log.println(msg);
}
}
// ----------------------------------------------------------------------
// Test Methods
// ----------------------------------------------------------------------
/**
* Tests the signature of classes (in the specified classloader) against a list of signature
* descriptor files; returns with a status code.
*
* @param descrFileNames list of signature descriptor file names
* @return zero if all tests have passed and no problems were detected
* @throws IOException error reading file
* @throws ParseException error during parsing
*/
public int test(List<String> descrFileNames) throws IOException, ParseException {
// check argument
if (descrFileNames == null || descrFileNames.isEmpty()) {
final String m = ("ERROR: No signature descriptor file to parse.");
logError(m);
return -1;
}
// clear statistics
loading.clear();
notLoading.clear();
tested = 0;
missing = 0;
mismatch = 0;
matching = 0;
nonStandard = 0;
otherProblems = 0;
// process descriptor files
parser.parse(descrFileNames);
report();
// return a positive value in case of any problems
return (notLoading.size() + missing + mismatch + nonStandard + otherProblems);
}
/** Reports the results of the last signature test run. */
public void report() {
logInfo("");
logInfo("Signature Test Results");
logInfo("======================");
logInfo("");
logInfo(" tested features: " + tested);
logInfo("");
logInfo("Successes:");
logInfo(" matching features: " + matching);
logInfo(" loadable classes: " + loading.size());
logInfo("");
logInfo("Failures:");
logInfo(" missing features: " + missing);
logInfo(" non-matching features: " + mismatch);
logInfo(" non-standard features: " + nonStandard);
logInfo(" unloadable classes: " + notLoading.size());
logInfo(" other problems: " + otherProblems);
logInfo("");
}
// ----------------------------------------------------------------------
// Test Logic
// ----------------------------------------------------------------------
/**
* Handles class loading problems.
*
* @param t Throwable
*/
protected void handleNotLoading(Throwable t) {
notLoading.add(t.getMessage().replace('/', '.'));
final String m = ("--- failed loading class;" + NL + " caught: " + t);
logError(m);
}
/**
* Handles missing members.
*
* @param msg message
* @param exp expected
*/
protected void handleMissing(String msg, String exp) {
missing++;
final String m =
("--- "
+ msg
+ NL
+ " expected: "
+ exp
+ NL
+ " class: "
+ Formatter.toString(cls));
logError(m);
}
/**
* Handles non-matching features.
*
* @param msg message
* @param exp expected
* @param fnd feature
*/
protected void handleMismatch(String msg, String exp, String fnd) {
mismatch++;
final String m =
("--- "
+ msg
+ NL
+ " expected: "
+ exp
+ NL
+ " found: "
+ fnd
+ NL
+ " class: "
+ Formatter.toString(cls));
logError(m);
}
/**
* Handles public non-standard features.
*
* @param msg message
* @param fnd feature
*/
protected void handleNonStandard(String msg, String fnd) {
nonStandard++;
final String m =
("--- "
+ msg
+ NL
+ " found: "
+ fnd
+ NL
+ " class: "
+ Formatter.toString(cls));
logError(m);
}
/**
* Handles other problems.
*
* @param msg message
* @param exp expected
*/
protected void handleProblem(String msg, String exp) {
otherProblems++;
final String m =
("--- "
+ msg
+ NL
+ " expected: "
+ exp
+ NL
+ " class: "
+ Formatter.toString(cls));
logError(m);
}
/**
* Handles a perfect feature match.
*
* @param msg message
* @param fnd feature
*/
protected void handleMatch(String msg, String fnd) {
matching++;
final String m = ("+++ " + msg + fnd);
logVerbose(m);
}
/**
* Returns the class objects for given (Java) user type names.
*
* @param userTypeName user type names
* @return class objects
*/
protected Class<?>[] getClasses(String[] userTypeName) {
@SuppressWarnings("rawtypes")
final Class<?>[] cls = new Class[userTypeName.length];
for (int i = userTypeName.length - 1; i >= 0; i--) {
cls[i] = getClass(userTypeName[i]);
}
return cls;
}
/**
* Returns the class object for a given (Java) user type name.
*
* @param userTypeName user type name
* @return class object
*/
protected Class<?> getClass(String userTypeName) {
// use helper for retrieving class objects for primitive types
Class<?> cls = TypeHelper.primitiveClass(userTypeName);
if (cls != null) {
return cls;
}
// load class
try {
final String r = TypeHelper.reflectionTypeName(userTypeName);
cls = Class.forName(r, false, classLoader);
loading.add(userTypeName);
} catch (LinkageError | ClassNotFoundException err) {
handleNotLoading(err);
}
return cls;
}
/**
* Check an expected value expression (the value of a static field or the default value of an
* annotation method), comparing it to the actual value from the Field or Method object. Only
* supports primitive, enum, empty array of enum, empty array of annotation, and String.
*
* @param value the String form of the value from the signature file
* @param type the type as declared in the class
* @param actual the actual value
* @return the description of the expected value, or null if ok
* @throws java.lang.NumberFormatException format exception
*/
protected String checkValue(String value, String type, Object actual)
throws NumberFormatException {
// note array type
boolean isArray = false;
if (type.endsWith("[]")) {
isArray = true;
type = type.substring(0, type.length() - 2);
// remove { from beginning and } from end
value = value.substring(1, value.length() - 1);
}
// first check primitive type
final Object exp;
Class<?> expClass;
final boolean ok;
switch (type) {
case "byte":
if (isArray) {
ok = actual.getClass().getComponentType().equals(byte.class);
} else {
ok = Byte.valueOf(value).equals(actual);
}
break;
case "boolean":
if (isArray) {
ok = actual.getClass().getComponentType().equals(boolean.class);
} else {
ok = Boolean.valueOf(value).equals(actual);
}
break;
case "short":
if (isArray) {
ok = actual.getClass().getComponentType().equals(short.class);
} else {
ok = Short.valueOf(value).equals(actual);
}
break;
case "int":
if (isArray) {
ok = actual.getClass().getComponentType().equals(int.class);
} else {
ok = Integer.valueOf(value).equals(actual);
}
break;
case "long":
if (isArray) {
ok = actual.getClass().getComponentType().equals(long.class);
} else {
ok = Long.valueOf(value).equals(actual);
}
break;
case "float":
if (isArray) {
ok = actual.getClass().getComponentType().equals(float.class);
} else {
ok = Float.valueOf(value).equals(actual);
}
break;
case "double":
if (isArray) {
ok = actual.getClass().getComponentType().equals(double.class);
} else {
ok = Double.valueOf(value).equals(actual);
}
break;
case "char":
if (isArray) {
ok = actual.getClass().getComponentType().equals(char.class);
} else {
ok = Character.valueOf(value.charAt(1)).equals(actual);
}
// next check Class
break;
case "java.lang.Class":
if (isArray) {
ok = actual.getClass().getComponentType().equals(Class.class);
} else {
// strip ".class" from type name
int offset = value.indexOf(".class");
value = value.substring(0, offset);
ok = getClass(value).equals(actual);
}
// next check String
break;
case "java.lang.String":
if (isArray) {
ok = actual.getClass().getComponentType().equals(String.class);
} else {
// cut off '\"' chars at begin and end
final String s;
if (value.length() > 1) {
s = value.substring(1, value.length() - 1);
} else {
s = "";
}
ok = s.equals(actual);
}
// now check non-java.lang annotations and enums
break;
default:
expClass = getClass(type);
if (isArray) {
// check if the actual component type is the right class
// don't check the actual values because only empty arrays
// are supported.
ok = actual.getClass().getComponentType().equals(expClass);
} else if (expClass == null) {
System.out.println(
"WARNING : checkValue value="
+ value
+ " type="
+ type
+ " comes up with null"
+ " class");
ok = false;
} else if (expClass.isAnnotation()) {
// check whether the type isAssignableFrom the class of the actual value,
// if type is an annotation. The actual value is a dynamic proxy,
// so an equals comparison does not work.
ok = expClass.isAssignableFrom(actual.getClass());
} else {
// get the actual value which must be a static class.field
Object expectedValue = null;
try {
// now get actual value
// separate value name into class and field name
int lastDot = value.lastIndexOf(".");
String expectedClassName = value.substring(0, lastDot);
String expectedFieldName = value.substring(lastDot + 1);
// get Class object from class name
Class<?> expectedClass = getClass(expectedClassName);
if (expectedClass == null) throw new ClassNotFoundException();
// get Field object from Class and field name
Field expectedField = expectedClass.getField(expectedFieldName);
expectedValue = expectedField.get(null);
} catch (NoSuchFieldException
| ClassNotFoundException
| IllegalAccessException
| IllegalArgumentException
| SecurityException ex) {
handleNotLoading(ex);
}
ok = expectedValue.equals(actual);
}
break;
}
// return message if not ok
if (ok) return null;
else return value;
}
/**
* Validates a field against a prescribed signature.
*
* @param mods modifier
* @param type type
* @param name name
* @param value value
*/
protected void checkField(int mods, String type, String name, String value) {
tested++;
type = TypeHelper.qualifiedUserTypeName(type);
// get field
final Field field;
try {
field = cls.getDeclaredField(name);
} catch (NoSuchFieldException ex) {
handleMissing("missing field: ", Formatter.toString(mods, type, name, value));
return;
} catch (LinkageError err) {
handleNotLoading(err);
return;
}
// check modifiers
if (cls.isInterface()) {
// fields interfaces are implicitly public, static, and final
mods |= Modifier.PUBLIC;
mods |= Modifier.STATIC;
mods |= Modifier.FINAL;
}
if (mods != convertModifiers(field)) {
handleMismatch(
"field declaration: non-matching modifiers;",
Formatter.toString(mods, type, name, null),
Formatter.toString(field, null));
}
// check type
if (!TypeHelper.isNameMatch(type, field.getType())) {
handleMismatch(
"field declaration: non-matching type;",
Formatter.toString(mods, type, name, null),
Formatter.toString(field, null));
}
// check field value if any
Object actualValue = null;
if (value != null) {
// only support for public, static, and final fields
final int m = (Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL);
if ((mods & m) == 0) {
handleProblem(
"field declaration: ignoring field value " + "definition in descriptor file;",
Formatter.toString(mods, type, name, value));
} else {
// compare field's expected with found value
try {
actualValue = field.get(null);
} catch (IllegalAccessException ex) {
handleProblem(
"field declaration: cannot access field " + "value, exception: " + ex + ";",
Formatter.toString(mods, type, name, value));
}
String error = checkValue(value, type, actualValue);
if (error != null) {
handleMismatch(
"field declaration: non-matching values;",
Formatter.toString(mods, type, name, error),
Formatter.toString(field, "\"" + actualValue + "\""));
}
}
}
// field OK
members.remove(field);
handleMatch("has field: ", Formatter.toString(field, actualValue));
}
/**
* Validates a constructor against a prescribed signature.
*
* @param mods modifier
* @param params parameters
* @param excepts excepts
*/
protected void checkConstructor(int mods, String[] params, String[] excepts) {
tested++;
params = TypeHelper.qualifiedUserTypeNames(params);
excepts = TypeHelper.qualifiedUserTypeNames(excepts);
// get parameter classes
final Class<?>[] prms = getClasses(params);
if (prms == null) {
return;
}
// get constructor
final Constructor<?> ctor;
try {
ctor = cls.getDeclaredConstructor(prms);
} catch (NoSuchMethodException ex) {
String name = cls.getName();
final int i = name.lastIndexOf('.');
name = (i < 0 ? name : name.substring(i));
handleMissing("missing constructor: ", Formatter.toString(mods, null, name, params, excepts));
return;
} catch (LinkageError err) {
handleNotLoading(err);
return;
}
// check modifiers
if (mods != ctor.getModifiers()) {
handleMismatch(
"constructor declaration: non-matching modifiers;",
Formatter.toString(mods, null, cls.getName(), params, excepts),
Formatter.toString(ctor));
}
// check exceptions
if (!TypeHelper.isNameMatch(excepts, ctor.getExceptionTypes())) {
handleMismatch(
"method declaration: non-matching exceptions;",
Formatter.toString(mods, null, cls.getName(), params, excepts),
Formatter.toString(ctor));
}
// constructor OK
members.remove(ctor);
handleMatch("has constructor: ", Formatter.toString(ctor));
}
/**
* Validates a method against a prescribed signature.
*
* @param mods modifier
* @param result result
* @param name name
* @param params parameters
* @param excepts excepts
* @param value value
*/
protected void checkMethod(
int mods, String result, String name, String[] params, String[] excepts, String value) {
tested++;
params = TypeHelper.qualifiedUserTypeNames(params);
excepts = TypeHelper.qualifiedUserTypeNames(excepts);
result = TypeHelper.qualifiedUserTypeName(result);
// get parameter classes
final Class<?>[] prms = getClasses(params);
if (prms == null) {
return;
}
// get method
final Method method;
try {
method = cls.getDeclaredMethod(name, prms);
} catch (NoSuchMethodException ex) {
handleMissing("missing method: ", Formatter.toString(mods, result, name, params, excepts));
return;
} catch (LinkageError err) {
handleNotLoading(err);
return;
}
// check modifiers
if (cls.isInterface()) {
// methods in interfaces are implicitly public
mods |= Modifier.PUBLIC;
}
Class<?> resultType = getClass(result);
if (resultType == null) {
System.out.println(
"WARNING : checkMethod "
+ name
+ " result="
+ result
+ " comes up with null resultType!");
} else if (resultType.isAnnotation()) {
// add ANNOTATION modifier if the result type is an annotation
mods |= ANNOTATION;
}
if (mods != convertModifiers(method)) {
handleMismatch(
"method declaration: non-matching modifiers;",
Formatter.toString(mods, result, name, params, excepts),
Formatter.toString(method));
}
// check return type
if (!TypeHelper.isNameMatch(result, method.getReturnType())) {
handleMismatch(
"method declaration: non-matching return type",
Formatter.toString(mods, result, name, params, excepts),
Formatter.toString(method));
}
// check exceptions
if (!TypeHelper.isNameMatch(excepts, method.getExceptionTypes())) {
handleMismatch(
"method declaration: non-matching exceptions;",
Formatter.toString(mods, result, name, params, excepts),
Formatter.toString(method));
}
// check default value of an annotation element (method)
if (value != null) {
Object actualValue = null;
try {
actualValue = method.getDefaultValue();
} catch (Exception ex) {
handleProblem(
"method declaration: cannot access default " + "value, exception: " + ex + ";",
Formatter.toString(mods, result, name, value));
}
// check value expected versus actual
String wrong = checkValue(value, result, actualValue);
if (wrong != null) {
handleMismatch(
"method declaration: non-matching default value;",
Formatter.toString(mods, result, name + "() default ", wrong),
Formatter.toString(method));
}
}
// method OK
members.remove(method);
handleMatch("has method: ", Formatter.toString(method));
}
/**
* Validates a class declaration against a prescribed signature.
*
* @param mods modifier
* @param name name
* @param ext ext
* @param impl implementation
*/
protected void checkClass(int mods, String name, String[] ext, String[] impl) {
logVerbose("");
logVerbose("testing " + Formatter.toString(mods, name, ext, impl));
tested++;
ext = TypeHelper.qualifiedUserTypeNames(ext);
impl = TypeHelper.qualifiedUserTypeNames(impl);
// get and assign currently processed class
cls = getClass(name);
if (cls == null) {
return; // can't load class
}
// collect all declared members of current class
members.clear();
try {
members.addAll(Arrays.asList(cls.getDeclaredFields()));
members.addAll(Arrays.asList(cls.getDeclaredConstructors()));
members.addAll(Arrays.asList(cls.getDeclaredMethods()));
} catch (LinkageError err) {
handleNotLoading(err);
}
// check modifiers
final boolean isInterface = ((mods & Modifier.INTERFACE) != 0);
if (isInterface) {
mods |= Modifier.ABSTRACT;
}
if (mods != convertModifiers(cls)) {
handleMismatch(
"class declaration: non-matching modifiers;",
Formatter.toString(mods, name, ext, impl),
Formatter.toString(cls));
}
// check superclass and extended/implemented interfaces
final Class<?> superclass = cls.getSuperclass();
final Class<?>[] interfaces = cls.getInterfaces();
if (isInterface) {
// assert (impl.length == 0);
if (!TypeHelper.isNameMatch(ext, interfaces)) {
handleMismatch(
"interface declaration: non-matching interfaces;",
Formatter.toString(mods, name, ext, impl),
Formatter.toString(cls));
}
} else {
// assert (ext.length <= 1);
final String s = (ext.length == 0 ? "java.lang.Object" : ext[0]);
if (!TypeHelper.isNameMatch(s, superclass)) {
handleMismatch(
"class declaration: non-matching superclass;",
Formatter.toString(mods, name, ext, impl),
Formatter.toString(cls));
}
if (!TypeHelper.isNameMatch(impl, interfaces)) {
handleMismatch(
"class declaration: non-matching interfaces;",
Formatter.toString(mods, name, ext, impl),
Formatter.toString(cls));
}
}
handleMatch("has class: ", Formatter.toString(cls));
}
/** Runs checks on a class after its members have been validated. */
protected void postCheckClass() {
if (cls == null) {
return; // nothing to do if class couldn't be loaded
}
// check for public non-standard members
for (final Member m : members) {
if ((m.getModifiers() & Modifier.PUBLIC) != 0) {
handleNonStandard("non-standard, public member;", Formatter.toString(m));
}
}
}
/**
* Return modifiers for the class, but handling enum and annotation as if they had a special
* modifier.
*
* @param cls the class
* @return modifiers of the class with extra flags for enum and annotation
*/
protected int convertModifiers(Class<?> cls) {
int result = cls.getModifiers();
// first remove extraneous stuff
result &= ALL_MODIFIERS;
// if enum, set pseudo enum flag
if (cls.isEnum()) result |= ENUM;
// if annotation, set pseudo annotation flag
if (cls.isAnnotation()) result |= ANNOTATION;
return result;
}
/**
* Return modifiers for the method, but handling enum and annotation as if they had a special
* modifier.
*
* @param method the method
* @return modifiers of the class with extra flags for enum and annotation
*/
protected int convertModifiers(Method method) {
int result = method.getModifiers();
// first remove extraneous stuff
result &= ALL_MODIFIERS;
// if enum return type, set pseudo enum flag
if (method.getReturnType().isEnum()) result |= ENUM;
// if annotation, set pseudo annotation flag
if (method.getReturnType().isAnnotation()) result |= ANNOTATION;
// if return type is an enum class, un-set FINAL modifier in all methods
// because in Java 5, methods are generated as final; in Java 6, not
if (method.getDeclaringClass().isEnum()) result &= ~Modifier.FINAL;
return result;
}
/**
* Return modifiers for the field, but handling enum as if it had a special modifier.
*
* @param field the field
* @return modifiers of the class with extra flag for enum
*/
protected int convertModifiers(Field field) {
int result = field.getModifiers();
// first remove extraneous stuff
result &= ALL_MODIFIERS;
// if enum, set pseudo enum flag
if (field.isEnumConstant()) result |= ENUM;
return result;
}
// ----------------------------------------------------------------------
// Parser for Signature Descriptor Files
// ----------------------------------------------------------------------
/** For parsing of signature descriptor files. */
protected class Parser {
/** The current descriptor file being parsed. */
private File descriptorFile;
/** The line number reader for the current descriptor file. */
private LineNumberReader ir;
/** A look-ahead token to be read next. */
private String nextToken;
/**
* Returns an error message reporting an unexpected end of file.
*
* @return message
*/
protected String msgUnexpectedEOF() {
return ("unexpected end of file at line: "
+ ir.getLineNumber()
+ ", file: "
+ descriptorFile.getPath());
}
/**
* Returns an error message reporting an unextected token.
*
* @param t token
* @return error message
*/
protected String msgUnexpectedToken(String t) {
return ("unexpected token: '"
+ t
+ "'"
+ " in line: "
+ ir.getLineNumber()
+ ", file: "
+ descriptorFile.getPath());
}
/**
* Retrieves the look-ahead token to be parsed next.
*
* @return look ahead
*/
protected String getLookAhead() {
final String t = nextToken;
nextToken = null;
return t;
}
/**
* Sets the look-ahead token to be parsed next.
*
* @param t token
*/
protected void setLookAhead(String t) {
// assert (nextToken == null);
nextToken = t;
}
/**
* Skips any "white space" and /* style comments and returns whether there are more characters
* to be parsed.
*
* @return true if there are more characters to be parsed
* @throws IOException IO error
*/
protected boolean skip() throws IOException {
Character c;
Character cnext;
// Permitted states are white, commentSlashStar, commentSlashSlash
String state = "white";
final Character SLASH = '/';
final Character STAR = '*';
while (true) {
c = peek();
if (c == null) return false;
if (state.equals("commentSlashStar") && STAR.equals(c)) {
cnext = peek();
if (cnext == null) return false;
if (SLASH.equals(cnext)) {
state = "white";
}
continue;
}
if (state.equals("white")) {
if (SLASH.equals(c)) {
cnext = peek();
if (cnext == null) return false;
if (STAR.equals(cnext)) {
state = "commentSlashStar";
continue;
}
if (SLASH.equals(cnext)) {
state = "commentSlashSlash";
continue;
}
}
if (!Character.isWhitespace(c)) {
break;
}
}
if (state.equals("commentSlashSlash")
&& (c == Character.LINE_SEPARATOR || c == '\n' || c == '\r')) {
state = "white";
continue;
}
}
ir.reset();
return true;
}
/**
* Returns next char, or null if there is none
*
* @return next char
* @throws IOException IO error
*/
protected Character peek() throws IOException {
ir.mark(1);
int i;
if ((i = ir.read()) < 0) {
return null;
}
return Character.valueOf((char) i);
}
/**
* Scans for an (unqualified) identifier.
*
* @return <code>null</code> if the next token is not an identifier
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanIdentifier() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (!Character.isJavaIdentifierStart(t.charAt(0))) {
setLookAhead(t); // not an identifier
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (!Character.isJavaIdentifierStart(c)) {
ir.reset(); // not start of an identifier
return null;
}
// parse remaining chars
final StringBuilder sb = new StringBuilder();
do {
sb.append(c);
int i;
ir.mark(1);
if ((i = ir.read()) < 0) break;
c = (char) i;
} while (Character.isJavaIdentifierPart(c));
ir.reset(); // not part of an identifier
return sb.toString();
}
/**
* Scans for a number literal.
*
* @return <code>null</code> if the next token is not a number
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanNumberLiteral() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (!(t.charAt(0) == '-' || Character.isDigit(t.charAt(0)))) {
setLookAhead(t); // not a number literal
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (Character.isDigit(c)) {
} else if (c == '-') {
skip();
} else {
ir.reset(); // not start of a number
return null;
}
// parse remaining chars
final StringBuilder sb = new StringBuilder();
do {
sb.append(c);
int i;
ir.mark(1);
if ((i = ir.read()) < 0) break;
c = (char) i;
} while (Character.isLetterOrDigit(c) || c == '-' || c == '.');
ir.reset(); // not part of a number
return sb.toString();
}
/**
* Scans for a character literal.
*
* @return <code>null</code> if the next token is not a character
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanCharacterLiteral() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (t.charAt(0) != '\'') {
setLookAhead(t); // not a char literal
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (c != '\'') {
ir.reset(); // not start of a char literal
return null;
}
// parse remaining two chars
final StringBuilder sb = new StringBuilder();
for (int j = 0; j < 2; j++) {
sb.append(c);
int i;
if ((i = ir.read()) < 0) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
c = (char) i;
}
if (c != '\'') {
throw new ParseException(msgUnexpectedToken(String.valueOf(c)), 0);
}
sb.append(c); // keep '\'' part of a char literal
return sb.toString();
}
/**
* Scans for a string literal.
*
* @return <code>null</code> if the next token is not a string
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanStringLiteral() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (t.charAt(0) != '\"') {
setLookAhead(t); // not a string literal
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (c != '\"') {
ir.reset(); // not start of a string literal
return null;
}
// parse remaining chars
final StringBuilder sb = new StringBuilder();
do {
sb.append(c);
int i;
if ((i = ir.read()) < 0) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
c = (char) i;
} while (c != '\"'); // not supported: nested '\"' char sequences
sb.append(c); // keep '\"' part of a string literal
return sb.toString();
}
/**
* Scans for an array literal. Limitation: only the empty array "{}" can be scanned.
*
* @return <code>null</code> if the next token is not an array
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanArrayLiteral() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (t.charAt(0) != '{' || t.charAt(1) != '}') {
setLookAhead(t); // not an array literal
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (c != '{') {
ir.reset(); // not start of an array literal
return null;
}
c = (char) ir.read();
if (c != '}') {
ir.reset(); // not end of an array literal
return null;
}
return "{}";
}
/**
* Scans for an at-sign
*
* @return <code>null</code> if the next token is not an at-sign
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String scanAtSign() throws IOException, ParseException {
// parse stored token if any
String t;
if ((t = getLookAhead()) != null) {
if (t.charAt(0) != '@') {
setLookAhead(t); // not an at-sign
return null;
}
return t;
}
// parse first char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
ir.mark(1);
char c = (char) ir.read();
if (c != '@') {
ir.reset(); // not an at-sign
return null;
}
return "@";
}
/**
* Returns the next token to be parsed.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseToken() throws IOException, ParseException {
String t;
if ((t = getLookAhead()) != null) {
} else if ((t = scanIdentifier()) != null) {
} else if ((t = scanNumberLiteral()) != null) {
} else if ((t = scanStringLiteral()) != null) {
} else if ((t = scanCharacterLiteral()) != null) {
} else if ((t = scanAtSign()) != null) {
} else {
setLookAhead(t); // not an identifier, number, or string
// next non-white char
if (!skip()) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
t = String.valueOf((char) ir.read());
}
// log.println("parseToken() : '" + t + "'");
return t;
}
/**
* Parses the next token and validates it against an expected one.
*
* @param token the token
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandToken(String token) throws IOException, ParseException {
final String t = parseToken();
if (!t.equals(token)) {
throw new ParseException(msgUnexpectedToken(t), 0);
}
return t;
}
/**
* Parses a literal.
*
* @return <code>null</code> if the next token is not a literal
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseLiteral() throws IOException, ParseException {
String t;
if ((t = scanNumberLiteral()) != null) {
} else if ((t = scanStringLiteral()) != null) {
} else if ((t = scanCharacterLiteral()) != null) {
} else if ((t = scanArrayLiteral()) != null) {
}
// log.println("parseLiteral() : '" + t + "'");
return t;
}
/**
* Parses the next token and validates that it is a literal.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandLiteral() throws IOException, ParseException {
final String l = parseLiteral();
if (l == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return l;
}
/**
* Parses the next token and validates that it is a constant, which is either a literal or a
* static member.
*
* @return the description of the field
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandConstant() throws IOException, ParseException {
final String literal = parseLiteral();
if (literal != null) {
return literal;
}
final String field = demandIdentifier();
if (field == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return field;
}
/**
* Parses an annotation which is an at-sign followed by a fully qualified interface name.
*
* @return <code>null</code> if the next token is not an annotation, otherwise return the
* annotation type name without the at-sign.
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseAnnotation() throws IOException, ParseException {
String t;
// parse stored token if any
if ((t = getLookAhead()) != null) {
if (t.charAt(0) != '@') {
setLookAhead(t); // not a annotation
return null;
}
} else if ((t = scanAtSign()) != null) {
} else {
return null;
}
final String identifier = demandIdentifier();
if (identifier == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return identifier;
}
/**
* Parses the default value specification of an annotation element and validates that it is an
* element value, which is either an annotation or a constant.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandElementValue() throws IOException, ParseException {
final String annotation = parseAnnotation();
if (annotation != null) {
return annotation;
}
final String constant = demandConstant();
if (constant == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return constant;
}
/**
* Parses any available Java modifiers.
*
* @return an int value with the parsed modifiers' bit set
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected int parseModifiers() throws IOException, ParseException {
int m = 0;
label:
while (true) {
// parse known modifiers
final String t = parseToken();
switch (t) {
case "abstract":
m |= Modifier.ABSTRACT;
break;
case "annotation":
m |= (ANNOTATION + Modifier.ABSTRACT + Modifier.INTERFACE);
break;
case "enum":
m |= ENUM;
break;
case "final":
m |= Modifier.FINAL;
break;
case "interface":
m |= Modifier.INTERFACE;
break;
case "native":
m |= Modifier.NATIVE;
break;
case "private":
m |= Modifier.PRIVATE;
break;
case "protected":
m |= Modifier.PROTECTED;
break;
case "public":
m |= Modifier.PUBLIC;
break;
case "static":
m |= Modifier.STATIC;
break;
case "strictfp":
m |= Modifier.STRICT;
break;
case "synchronized":
m |= Modifier.SYNCHRONIZED;
break;
case "transient":
m |= Modifier.TRANSIENT;
break;
case "varargs":
m |= Modifier.TRANSIENT;
break;
case "volatile":
m |= Modifier.VOLATILE;
break;
default:
setLookAhead(t); // not a modifier
break label;
}
}
// log.println("parseModifiers() : '" + Modifier.toString(m) + "'");
return m;
}
/**
* Parses a (qualified) identifier.
*
* @return <code>null</code> if the next token is not an identifier
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseIdentifier() throws IOException, ParseException {
String t = scanIdentifier();
if (t != null) {
// parse dot-connected identifiers
final StringBuilder id = new StringBuilder(t);
String tt = parseToken();
while (tt.equals(".")) {
id.append(".");
tt = parseIdentifier();
if (tt == null) {
throw new ParseException(msgUnexpectedToken(tt), 0);
}
id.append(tt);
tt = parseToken();
}
setLookAhead(tt); // not a dot token
t = id.toString();
}
// log.println("parseIdentifier() : '" + t + "'");
return t;
}
/**
* Parses the next token(s) and validates that it is an identifier.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandIdentifier() throws IOException, ParseException {
final String id = parseIdentifier();
if (id == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return id;
}
/**
* Parses a comma-separated list of identifiers.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String[] demandIdentifierList() throws IOException, ParseException {
final List<String> ids = new ArrayList<>();
ids.add(demandIdentifier());
String t;
while ((t = parseToken()).equals(",")) {
ids.add(demandIdentifier());
}
setLookAhead(t); // not an identifier
return ids.toArray(new String[ids.size()]);
}
/**
* Parses a type expression.
*
* @return <code>null</code> if the next token is not a type
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseType() throws IOException, ParseException {
String t = parseIdentifier();
if (t != null) {
// parse array dimensions
final StringBuilder type = new StringBuilder(t);
while ((t = parseToken()).equals("[")) {
demandToken("]");
type.append("[]");
}
setLookAhead(t); // not an open bracket token
t = type.toString();
}
// log.println("parseType() : '" + t + "'");
return t;
}
/**
* Parses the next token and validates that it is a type expression.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String demandType() throws IOException, ParseException {
final String id = parseType();
if (id == null) {
throw new ParseException(msgUnexpectedToken(parseToken()), 0);
}
return id;
}
/**
* Parses a comma-separated parameter list.
*
* @return never <code>null</code>
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String[] parseParameterList() throws IOException, ParseException {
final List<String> types = new ArrayList<>();
String t = parseType();
if (t != null) {
types.add(t);
parseIdentifier(); // optional parameter name
while ((t = parseToken()).equals(",")) {
types.add(demandType());
parseIdentifier(); // optional parameter name
}
setLookAhead(t); // not a comma token
}
return types.toArray(new String[types.size()]);
}
/**
* Parses a class member declaration and provides the information to a field, constructor, or
* method handler.
*
* @return <code>null</code> if there's no member declaration
* @throws IOException IO error
* @throws ParseException parsing error
*/
protected String parseMember() throws IOException, ParseException {
// parse optional modifiers, type, and member name
final int mods = parseModifiers();
final String typeOrName = parseType();
if (typeOrName == null) {
if (mods != 0) {
throw new ParseException(msgUnexpectedEOF(), 0);
}
return null; // no member to parse
}
final String memberName = parseIdentifier(); // null if constructor
// parse optional field value or parameter+exception list
final String value;
final String[] params;
final String[] excepts;
{
final String tvp = parseToken();
switch (tvp) {
case ";":
value = null;
params = null;
excepts = null;
break;
case "=":
// parse field value
value = demandLiteral();
demandToken(";");
params = null;
excepts = null;
break;
case "(":
// parse optional parameter and exception list
params = parseParameterList();
demandToken(")");
final String tt = parseToken();
switch (tt) {
case "throws":
excepts = demandIdentifierList();
demandToken(";");
value = null;
break;
case ";":
excepts = new String[] {};
value = null;
break;
case "default":
value = demandElementValue();
demandToken(";");
excepts = new String[] {};
break;
default:
throw new ParseException(msgUnexpectedToken(tt), 0);
}
break;
default:
throw new ParseException(msgUnexpectedToken(tvp), 0);
}
}
// verify field, constructor, or method
String name = memberName;
if (params == null) {
checkField(mods, typeOrName, memberName, value);
} else {
if (memberName == null) {
name = typeOrName;
checkConstructor(mods, params, excepts);
} else {
checkMethod(mods, typeOrName, memberName, params, excepts, value);
}
}
// log.println("parseMember() : " + name);
return name;
}
/**
* Parses a class definition and provides the information to a handler.
*
* @return <code>null</code> if there's no class definition
* @throws IOException IO error
* @throws ParseException parsing error
*/
@SuppressWarnings("empty-statement")
protected String parseClass() throws IOException, ParseException {
// parse optional modifiers, class token, and class name
if (!skip()) {
return null; // eof, no class to parse
}
final int mods = parseModifiers();
final String tc = parseToken();
if (!tc.equals("class")) { // token 'interface' parsed as modifier
setLookAhead(tc);
}
final String name = demandIdentifier();
// parse optional extends and implements clauses
final String[] ext;
final String[] impl;
{
String tei = parseToken();
if (tei.equals("extends")) {
ext = demandIdentifierList();
tei = parseToken();
} else {
ext = new String[] {};
}
if (((mods & Modifier.INTERFACE) == 0) && tei.equals("implements")) {
impl = demandIdentifierList();
tei = parseToken();
} else {
impl = new String[] {};
}
if (!tei.equals("{")) {
throw new ParseException(msgUnexpectedToken(tei), 0);
}
}
// verify class header
checkClass(mods, name, ext, impl);
// process members; empty-statement intentional
while (parseMember() != null)
;
demandToken("}");
// verify class
postCheckClass();
// log.println("parseClass() : " + name);
return name;
}
/**
* Parses a list of signature descriptor files and processes the class definitions.
*
* @param descrFileNames list of signature descriptor file names
* @throws IOException IO error
* @throws ParseException parsing error
*/
@SuppressWarnings("empty-statement")
public void parse(List<String> descrFileNames) throws IOException, ParseException {
for (final String fileName : descrFileNames) {
logInfo("");
logInfo("parsing descriptor file: " + fileName);
try {
descriptorFile = new File(fileName);
ir = new LineNumberReader(new FileReader(descriptorFile));
ir.setLineNumber(1);
setLookAhead(null);
// empty-statement intentional
while (parseClass() != null)
;
} finally {
descriptorFile = null;
if (ir != null) {
ir.close();
}
setLookAhead(null);
}
}
}
}
// ----------------------------------------------------------------------
// Stand-Alone Command-Line Interface
// ----------------------------------------------------------------------
/** A writer for standard output. */
protected static final PrintWriter out = new PrintWriter(System.out, true);
/** Command line arguments */
public static final List<String> descrFileNames = new ArrayList<>();
/** Command line option 'quiet'. */
private static boolean optionQuiet;
/** Command line option 'verbose'. */
private static boolean optionVerbose;
/** Prints the CLI usage. */
public static void printUsage() {
out.println();
out.println("usage: SignatureVerifier [options] arguments");
out.println("options:");
out.println(" [-h|--help print usage and exit]");
out.println(" [-q|--quiet only print error messages]");
out.println(" [-v|--verbose print verbose messages]");
out.println("arguments:");
out.println(" <signature descriptor file>...");
out.println();
}
/**
* Parses command line arguments.
*
* @param args arguments
* @return status code
*/
public static int parseArgs(String[] args) {
out.println("parse main() arguments");
// parse this class' options and arguments
for (String arg : args) {
if (arg == null) {
continue;
}
if (arg.equalsIgnoreCase("-h") || arg.equalsIgnoreCase("--help")) {
return -1;
}
if (arg.equalsIgnoreCase("-v") || arg.equalsIgnoreCase("--verbose")) {
optionVerbose = true;
continue;
}
if (arg.equalsIgnoreCase("-q") || arg.equalsIgnoreCase("--quiet")) {
optionQuiet = true;
continue;
}
if (arg.startsWith("-")) {
out.println("Usage Error: unknown option " + arg);
return -1;
}
// collect argument
descrFileNames.add(arg);
}
// print args
if (false) {
out.println("descrFileNames = {");
for (String descrFileName : descrFileNames) {
out.println(" " + descrFileName);
}
out.println(" }");
out.println("optionQuiet = " + optionQuiet);
out.println("optionVerbose = " + optionVerbose);
}
// check args
if (descrFileNames.isEmpty()) {
out.println("Usage Error: Missing argument " + "<signature descriptor file>...");
return -1;
}
return 0;
}
/**
* Runs the signature test and exits with a status code.
*
* @param args command line arguments
*/
public static void main(String[] args) {
out.println("run SignatureVerifier ...");
if (parseArgs(args) != 0) {
printUsage();
out.println("abort.");
System.exit(-1);
}
int status = 0;
try {
final SignatureVerifier s = new SignatureVerifier(out, optionQuiet, optionVerbose);
status = s.test(descrFileNames);
} catch (IOException ex) {
out.println("ERROR: exception caught: " + ex);
// ex.printStackTrace();
out.println("abort.");
System.exit(-2);
} catch (ParseException ex) {
out.println("ERROR: exception caught: " + ex);
// ex.printStackTrace();
out.println("abort.");
System.exit(-3);
}
out.println();
out.println("done.");
System.exit(status);
}
}