/* AnnotationDetector.java * * Created: 2011-10-10 (Year-Month-Day) * Character encoding: UTF-8 * ****************************************** LICENSE ******************************************* * * Copyright (c) 2011 - 2014 XIAM Solutions B.V. (http://www.xiam.nl) * Copyright 2014 dorkbox, llc * * Licensed 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 * * 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 * limitations under the License. */ package dorkbox.annotation; import java.io.DataInput; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.JarURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; /** * {@code AnnotationDetector} reads Java Class Files ("*.class") and reports the * found annotations via a simple, developer friendly API. *

* A Java Class File consists of a stream of 8-bit bytes. All 16-bit, 32-bit, and 64-bit * quantities are constructed by reading in two, four, and eight consecutive 8-bit * bytes, respectively. Multi byte data items are always stored in big-endian order, * where the high bytes come first. In the Java platforms, this format is * supported by interfaces {@link java.io.DataInput} and {@link java.io.DataOutput}. *

* A class file consists of a single ClassFile structure: *

 * ClassFile {
 *   u4 magic;
 *   u2 minor_version;
 *   u2 major_version;
 *   u2 constant_pool_count;
 *   cp_info constant_pool[constant_pool_count-1];
 *   u2 access_flags;
 *   u2 this_class;
 *   u2 super_class;
 *   u2 interfaces_count;
 *   u2 interfaces[interfaces_count];
 *   u2 fields_count;
 *   field_info fields[fields_count];
 *   u2 methods_count;
 *   method_info methods[methods_count];
 *   u2 attributes_count;
 *   attribute_info attributes[attributes_count];
 * }
 *
 * Where:
 * u1 unsigned byte {@link java.io.DataInput#readUnsignedByte()}
 * u2 unsigned short {@link java.io.DataInput#readUnsignedShort()}
 * u4 unsigned int {@link java.io.DataInput#readInt()}
 *
 * Annotations are stored as Attributes, named "RuntimeVisibleAnnotations" for
 * {@link java.lang.annotation.RetentionPolicy#RUNTIME} and "RuntimeInvisibleAnnotations" for
 * {@link java.lang.annotation.RetentionPolicy#CLASS}.
 * 
* References: * *

* Similar projects / libraries: *

*

* All above mentioned projects make use of a byte code manipulation library (like BCEL, * ASM or Javassist). * * @author Ronald K. Muller * @since annotation-detector 3.0.0 * * @author dorkbox, llc */ @SuppressWarnings("unused") public final class AnnotationDetector implements Builder, Cursor { private static final boolean DEBUG = false; // Constant Pool type ta gs private static final int CP_UTF8 = 1; private static final int CP_INTEGER = 3; private static final int CP_FLOAT = 4; private static final int CP_LONG = 5; private static final int CP_DOUBLE = 6; private static final int CP_CLASS = 7; private static final int CP_STRING = 8; private static final int CP_REF_FIELD = 9; private static final int CP_REF_METHOD = 10; private static final int CP_REF_INTERFACE = 11; private static final int CP_NAME_AND_TYPE = 12; private static final int CP_METHOD_HANDLE = 15; // Java VM SE 7 private static final int CP_METHOD_TYPE = 16; // Java VM SE 7 private static final int CP_INVOKE_DYNAMIC = 18; // Java VM SE 7 // AnnotationElementValue / Java raw types private static final int BYTE = 'B'; private static final int CHAR = 'C'; private static final int DOUBLE = 'D'; private static final int FLOAT = 'F'; private static final int INT = 'I'; private static final int LONG = 'J'; private static final int SHORT = 'S'; private static final int BOOLEAN = 'Z'; private static final int ARRAY = '['; // used for AnnotationElement only private static final int STRING = 's'; private static final int ENUM = 'e'; private static final int CLASS = 'c'; private static final int ANNOTATION = '@'; private final ClassLoader loader; // The buffer is reused during the life cycle of this AnnotationDetector instance private final ClassFileBuffer cpBuffer = new ClassFileBuffer(); private final ClassIterator cfIterator; // The Element Types to detect private final Set elementTypes = EnumSet.of(ElementType.TYPE); // Reusing the constantPool is not needed for better performance private Object[] constantPool; // The cached annotation types to report, maps raw Annotation type name to Class object private Map> annotations; private FilenameFilter filter; private Reporter reporter; // The current annotation reported private Class annotationType; // The 'raw' name of the current interface or class being scanned and reported // (using '/' instead of '.' in package name) private String typeName; // The current method or field (if any) being scanned private String memberName; // The current Element Type beinig reported private ElementType elementType; // The method descriptor of the currently reported annotated method in "raw" // format (as it appears in the Java Class File). Example method descriptors: // "()V" no arguments, return type void // "(Ljava/lang/String;II)I" String, int, int as arguments, return type int private String methodDescriptor; private AnnotationDetector(ClassLoader loader, final File[] filesOrDirectories, ClassIterator iterator, final String[] pkgNameFilter) { this.loader = loader; if (iterator == null) { this.cfIterator = new ClassFileIterator(filesOrDirectories, pkgNameFilter); if (filesOrDirectories.length == 0) { if (DEBUG) { System.err.println("No files or directories to scan!"); } } else if (DEBUG) { System.err.println("Files and root directories scanned:"); System.err.println(Arrays.toString(filesOrDirectories).replace(", ", "\n")); } } else { this.cfIterator = iterator; if (DEBUG) { System.err.println("Class Files from the custom classfileiterator scanned."); } } } /** * Gets the version number. */ public static String getVersion() { return "2.14"; } /** * Factory method, starting point for the fluent interface. * Only scan Class Files in the specified packages. If nothing is specified, all classes * on the class path are scanned. */ public static Builder scanClassPath(final String... packageNames) throws IOException { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); return scanClassPath(loader, packageNames); } /** * Factory method, starting point for the fluent interface. * Only scan Class Files in the specified packages. If nothing is specified, all classes * on the class path are scanned. */ public static Builder scanClassPath(ClassLoader loader, final String... packageNames) throws IOException { final String[] pkgNameFilter; // DORKBOX added boolean isCustomLoader = "dorkbox.classloader.ClassLoader" == loader.getClass().getName(); if (isCustomLoader) { final List fileNames; // scanning the classpath if (packageNames.length == 0) { pkgNameFilter = null; List asList = Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator)); fileNames = new ArrayList(asList.size()); for (String s : asList) { File file = new File(s); fileNames.add(file.toURI().toURL()); } } // scanning specific packages else { pkgNameFilter = new String[packageNames.length]; for (int i = 0; i < pkgNameFilter.length; i++) { pkgNameFilter[i] = packageNames[i].replace('.', '/'); if (!pkgNameFilter[i].endsWith("/")) { pkgNameFilter[i] = pkgNameFilter[i].concat("/"); } } fileNames = new ArrayList(); for (final String packageName : pkgNameFilter) { final Enumeration resourceEnum = loader.getResources(packageName); while (resourceEnum.hasMoreElements()) { final URL url = resourceEnum.nextElement(); fileNames.add(url); } } } return new AnnotationDetector(loader, null, new CustomClassloaderIterator(fileNames, packageNames), pkgNameFilter); } else { final Set files = new HashSet(); if (packageNames.length == 0) { pkgNameFilter = null; final String[] fileNames = System.getProperty("java.class.path").split(File.pathSeparator); for (final String fileName : fileNames) { files.add(new File(fileName)); } } else { pkgNameFilter = new String[packageNames.length]; for (int i = 0; i < pkgNameFilter.length; i++) { pkgNameFilter[i] = packageNames[i].replace('.', '/'); if (!pkgNameFilter[i].endsWith("/")) { pkgNameFilter[i] = pkgNameFilter[i].concat("/"); } } for (final String packageName : pkgNameFilter) { addFiles(loader, packageName, files); } } return new AnnotationDetector(loader, files.toArray(new File[files.size()]), null, pkgNameFilter); } } /** * Factory method, starting point for the fluent interface. * Scan all files specified by the classFileIterator. */ public static Builder scan(ClassLoader loader, final ClassIterator iterator) { return new AnnotationDetector(loader, null, iterator, null); } /** * Factory method, starting point for the fluent interface. * Scan all files in the specified jar files and directories. */ public static Builder scanFiles(ClassLoader loader, final File... filesOrDirectories) { return new AnnotationDetector(loader, filesOrDirectories, null, null); } /** * Factory method, starting point for the fluent interface. * Scan all files in the specified jar files and directories. */ public static Builder scanFiles(final File... filesOrDirectories) { return new AnnotationDetector(Thread.currentThread().getContextClassLoader(), filesOrDirectories, null, null); } /** * See {@link Builder#forAnnotations(java.lang.Class...) }. */ @Override public Builder forAnnotations(final Class annotation) { this.annotations = new HashMap>(1); // map "raw" type names to Class object this.annotations.put("L" + annotation.getName().replace('.', '/') + ";", annotation); return this; } /** * See {@link Builder#forAnnotations(java.lang.Class...) }. */ @Override public Builder forAnnotations(final Class... annotations) { this.annotations = new HashMap>(annotations.length); // map "raw" type names to Class object for (final Class annotation : annotations) { this.annotations.put("L" + annotation.getName().replace('.', '/') + ";", annotation); } return this; } /** * See {@link Builder#on(java.lang.annotation.ElementType...) }. */ @SuppressWarnings("Duplicates") @Override public Builder on(final ElementType type) { if (type == null) { throw new IllegalArgumentException("At least one Element Type must be specified"); } this.elementTypes.clear(); switch (type) { case TYPE: case CONSTRUCTOR: case METHOD: case FIELD: this.elementTypes.add(type); break; default: throw new IllegalArgumentException("Unsupported: " + type); } return this; } /** * See {@link Builder#on(java.lang.annotation.ElementType...) }. */ @SuppressWarnings("Duplicates") @Override public Builder on(final ElementType... types) { if (types.length == 0) { throw new IllegalArgumentException("At least one Element Type must be specified"); } this.elementTypes.clear(); for (ElementType t : types) { switch (t) { case TYPE: case CONSTRUCTOR: case METHOD: case FIELD: this.elementTypes.add(t); break; default: throw new IllegalArgumentException("Unsupported: " + t); } } return this; } /** * See {@link Builder#filter(java.io.FilenameFilter) }. */ @Override public Builder filter(final FilenameFilter filter) { if (filter == null) { throw new NullPointerException("'filter' may not be null"); } this.filter = filter; return this; } /** * See {@link Builder#report(Reporter) }. */ @Override public void report(final Reporter reporter) throws IOException { this.reporter = reporter; detect(this.cfIterator); } /** * See {@link Builder#collect(ReporterFunction) }. */ @Override public List collect(final ReporterFunction reporter) throws IOException { final List list = new ArrayList(); this.reporter = new Reporter() { @Override public void report(Cursor cursor) { list.add(reporter.report(cursor)); } }; detect(this.cfIterator); return list; } /** * See {@link Cursor#getTypeName() }. */ @Override public String getTypeName() { return this.typeName.replace('/', '.'); } /** * See {@link Cursor#getAnnotationType() }. */ @Override public Class getAnnotationType() { return this.annotationType; } /** * See {@link Cursor#getElementType() }. */ @Override public ElementType getElementType() { return this.elementType; } /** * See {@link Cursor#getMemberName() }. */ @Override public String getMemberName() { return this.memberName; } /** * See {@link Cursor#getType() }. */ @Override public Class getType() { return loadClass(this.loader, getTypeName()); } /** * See {@link Cursor#getField() }. */ @Override public Field getField() { if (this.elementType != ElementType.FIELD) { throw new IllegalStateException("Illegal to call getField() when " + this.elementType + " is reported"); } try { return getType().getDeclaredField(this.memberName); } catch (NoSuchFieldException ex) { throw assertionError("Cannot find Field '%s' for type %s", this.memberName, getTypeName()); } } /** * See {@link Cursor#getConstructor() }. */ @Override public Constructor getConstructor() { if (this.elementType != ElementType.CONSTRUCTOR) { throw new IllegalStateException("Illegal to call getMethod() when " + this.elementType + " is reported"); } try { final Class[] parameterTypes = parseArguments(this.methodDescriptor); return getType().getConstructor(parameterTypes); } catch (NoSuchMethodException ex) { throw assertionError("Cannot find Contructor '%s(...)' for type %s", this.memberName, getTypeName()); } } /** * See {@link Cursor#getMethod() }. */ @Override public Method getMethod() { if (this.elementType != ElementType.METHOD) { throw new IllegalStateException("Illegal to call getMethod() when " + this.elementType + " is reported"); } try { final Class[] parameterTypes = parseArguments(this.methodDescriptor); return getType().getDeclaredMethod(this.memberName, parameterTypes); } catch (NoSuchMethodException ex) { throw assertionError("Cannot find Method '%s(...)' for type %s", this.memberName, getTypeName()); } } /** * See {@link Cursor#getAnnotation(java.lang.Class) }. */ @Override public T getAnnotation(final Class annotationClass) { if (!annotationClass.equals(this.annotationType)) { throw new IllegalStateException("Illegal to call getAnnotation() when " + this.annotationType.getName() + " is reported"); } final AnnotatedElement ae; switch (this.elementType) { case TYPE: ae = getType(); break; case FIELD: ae = getField(); break; case METHOD: ae = getMethod(); break; default: throw new AssertionError(this.elementType); } return ae.getAnnotation(annotationClass); } // private private static void addFiles(ClassLoader loader, String resourceName, Set files) throws IOException { final Enumeration resourceEnum = loader.getResources(resourceName); while (resourceEnum.hasMoreElements()) { final URL url = resourceEnum.nextElement(); if (DEBUG) { System.err.println("Resource URL: " + url); } // Handle JBoss VFS URL's which look like (example package 'nl.dvelop'): // vfs:/foo/bar/website.war/WEB-INF/classes/nl/dvelop/ // vfs:/foo/bar/website.war/WEB-INF/lib/dwebcore-0.0.1.jar/nl/dvelop/ String protocol = url.getProtocol(); final boolean isVfs = "vfs".equals(protocol); if ("file".equals(protocol) || isVfs) { final File dir = toFile(url); if (dir.isDirectory()) { files.add(dir); } else if (isVfs) { //Jar file via JBoss VFS protocol - strip package name String jarPath = dir.getPath(); final int idx = jarPath.indexOf(".jar"); if (idx > -1) { jarPath = jarPath.substring(0, idx + 4); final File jarFile = new File(jarPath); if (jarFile.isFile()) { files.add(jarFile); } } } else { throw assertionError("Not a recognized file URL: %s", url); } } else { // Resource in Jar File final File jarFile = toFile(((JarURLConnection) url.openConnection()).getJarFileURL()); if (jarFile.isFile()) { files.add(jarFile); } else { throw assertionError("Not a File: %s", jarFile); } } } } private static File toFile(final URL url) throws IOException { // only correct way to convert the URL to a File object, also see issue #16 // Do not use URLDecoder try { return new File(url.toURI()); } catch (URISyntaxException ex) { throw new IOException(ex.getMessage()); } } private void detect(final ClassIterator iterator) throws IOException { InputStream stream; boolean mustEndInClass = iterator instanceof ClassFileIterator; while ((stream = iterator.next(this.filter)) != null) { try { this.cpBuffer.readFrom(stream); String name = iterator.getName(); // SOME files can actually have CAFEBABE (binary files), but are NOT CLASSFILES! Explicitly define this! if (hasCafebabe(this.cpBuffer)) { if (mustEndInClass && !name.endsWith(".class")) { continue; } if (DEBUG) { System.err.println("Class File: " + name); } read(this.cpBuffer); } // else ignore } finally { // closing InputStream from ZIP Entry is handled by ZipFileIterator if (iterator.isFile()) { stream.close(); } } } } private boolean hasCafebabe(final ClassFileBuffer buffer) throws IOException { return buffer.size() > 4 && buffer.readInt() == 0xCAFEBABE; } /** * Inspect the given (Java) class file in streaming mode. */ private void read(final DataInput di) throws IOException { readVersion(di); readConstantPoolEntries(di); readAccessFlags(di); readThisClass(di); readSuperClass(di); readInterfaces(di); readFields(di); readMethods(di); readAttributes(di, ElementType.TYPE); } private void readVersion(final DataInput di) throws IOException { // sequence: minor version, major version (argument_index is 1-based) if (DEBUG) { int minor = di.readUnsignedShort(); int maj = di.readUnsignedShort(); System.err.println("Java Class version " + maj + "." + minor); } else { di.skipBytes(4); } } private void readConstantPoolEntries(final DataInput di) throws IOException { final int count = di.readUnsignedShort(); this.constantPool = new Object[count]; for (int i = 1; i < count; ++i) { if (readConstantPoolEntry(di, i)) { // double slot ++i; } } } /** * Return {@code true} if a double slot is read (in case of Double or Long constant). */ private boolean readConstantPoolEntry(final DataInput di, final int index) throws IOException { final int tag = di.readUnsignedByte(); switch (tag) { case CP_METHOD_TYPE: di.skipBytes(2); // readUnsignedShort() return false; case CP_METHOD_HANDLE: di.skipBytes(3); return false; case CP_INTEGER: case CP_FLOAT: case CP_REF_FIELD: case CP_REF_METHOD: case CP_REF_INTERFACE: case CP_NAME_AND_TYPE: case CP_INVOKE_DYNAMIC: di.skipBytes(4); // readInt() / readFloat() / readUnsignedShort() * 2 return false; case CP_LONG: case CP_DOUBLE: di.skipBytes(8); // readLong() / readDouble() return true; case CP_UTF8: this.constantPool[index] = di.readUTF(); return false; case CP_CLASS: case CP_STRING: // reference to CP_UTF8 entry. The referenced index can have a higher number! this.constantPool[index] = di.readUnsignedShort(); return false; default: throw new ClassFormatError("Unkown tag value for constant pool entry: " + tag); } } private void readAccessFlags(final DataInput di) throws IOException { di.skipBytes(2); // u2 } private void readThisClass(final DataInput di) throws IOException { this.typeName = resolveUtf8(di); } private void readSuperClass(final DataInput di) throws IOException { di.skipBytes(2); // u2 } private void readInterfaces(final DataInput di) throws IOException { final int count = di.readUnsignedShort(); di.skipBytes(count * 2); // count * u2 } private void readFields(final DataInput di) throws IOException { final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { readAccessFlags(di); this.memberName = resolveUtf8(di); // decriptor is Field type in raw format, we do not need it, so skip //final String descriptor = resolveUtf8(di); di.skipBytes(2); if (DEBUG) { System.err.println("Field: " + this.memberName); } readAttributes(di, ElementType.FIELD); } } private void readMethods(final DataInput di) throws IOException { final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { readAccessFlags(di); this.memberName = resolveUtf8(di); this.methodDescriptor = resolveUtf8(di); if (DEBUG) { System.err.println("Method: " + this.memberName); } readAttributes(di, "".equals(this.memberName) ? ElementType.CONSTRUCTOR : ElementType.METHOD); } } private void readAttributes(final DataInput di, final ElementType reporterType) throws IOException { final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { final String name = resolveUtf8(di); // in bytes, use this to skip the attribute info block final int length = di.readInt(); if (this.elementTypes.contains(reporterType) && ("RuntimeVisibleAnnotations".equals(name) || "RuntimeInvisibleAnnotations".equals(name))) { if (DEBUG) { System.err.println("Attribute: " + name); } readAnnotations(di, reporterType); } else { if (DEBUG) { System.err.println("Attribute: " + name + " (ignored)"); } di.skipBytes(length); } } } private void readAnnotations(final DataInput di, final ElementType elementType) throws IOException { // the number of Runtime(In)VisibleAnnotations final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { final String rawTypeName = readAnnotation(di); this.annotationType = this.annotations.get(rawTypeName); if (this.annotationType == null) { if (DEBUG) { System.err.println("Annotation: " + rawTypeName + " (ignored)"); } continue; } if (DEBUG) { System.err.println("Annotation: '" + this.annotationType.getName() + "' on type '" + getTypeName() + "', member '" + getMemberName() + "' (reported)"); } this.elementType = elementType; this.reporter.report(this); } } private String readAnnotation(final DataInput di) throws IOException { final String rawTypeName = resolveUtf8(di); // num_element_value_pairs final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { if (DEBUG) { System.err.println("Anntotation Element: " + resolveUtf8(di)); } else { di.skipBytes(2); } readAnnotationElementValue(di); } return rawTypeName; } private void readAnnotationElementValue(final DataInput di) throws IOException { final int tag = di.readUnsignedByte(); switch (tag) { case BYTE: case CHAR: case DOUBLE: case FLOAT: case INT: case LONG: case SHORT: case BOOLEAN: case STRING: di.skipBytes(2); break; case ENUM: di.skipBytes(4); // 2 * u2 break; case CLASS: di.skipBytes(2); break; case ANNOTATION: readAnnotation(di); break; case ARRAY: final int count = di.readUnsignedShort(); for (int i = 0; i < count; ++i) { readAnnotationElementValue(di); } break; default: throw new ClassFormatError("Not a valid annotation element type tag: 0x" + Integer.toHexString(tag)); } } /** * Look up the String value, identified by the u2 index value from constant pool * (direct or indirect). */ private String resolveUtf8(final DataInput di) throws IOException { final int index = di.readUnsignedShort(); final Object value = this.constantPool[index]; final String s; if (value instanceof Integer) { s = (String) this.constantPool[(Integer) value]; } else { s = (String) value; } return s; } /** * Return the method arguments of the currently reported annotated method as a * {@code Class} array. */ // incorrect detection of dereferencing possible null pointer // TODO: https://github.com/checkstyle/checkstyle/issues/14 fixed in 5.8? private Class[] parseArguments(final String descriptor) { final int n = descriptor.length(); // "minimal" descriptor: no arguments: "()V", first character is always '(' if (n < 3 || descriptor.charAt(0) != '(') { throw unparseable(descriptor, "Wrong format"); } List> args = null; for (int i = 1; i < n; ++i) { char c = descriptor.charAt(i); if (i == 1) { if (c == ')') { return new Class[0]; } else { args = new LinkedList>(); } } int j; switch (c) { case 'V': args.add(void.class); break; case 'Z': args.add(boolean.class); break; case 'C': args.add(char.class); break; case 'B': args.add(byte.class); break; case 'S': args.add(short.class); break; case 'I': args.add(int.class); break; case 'F': args.add(float.class); break; case 'J': args.add(long.class); break; case 'D': args.add(double.class); break; case '[': j = i; do { c = descriptor.charAt(++j); } while (c == '['); // multi dimensional array if (c == 'L') { j = descriptor.indexOf(';', i + 1); } args.add(loadClass(this.loader, descriptor.substring(i, j + 1))); i = j; break; case 'L': j = descriptor.indexOf(';', i + 1); args.add(loadClass(this.loader, descriptor.substring(i + 1, j))); i = j; break; case ')': // end of argument type list, stop parsing return args.toArray(new Class[args.size()]); default: throw unparseable(descriptor, "Not a recognoized type: " + c); } } throw unparseable(descriptor, "No closing parenthesis"); } /** * Load the class, but do not initialize it. */ private static Class loadClass(ClassLoader loader, final String rawClassName) { final String typeName = rawClassName.replace('/', '.'); try { return Class.forName(typeName, false, loader); } catch (ClassNotFoundException ex) { throw assertionError("Cannot load type '%s', scanned file not on class path? (%s)", typeName, ex); } } /** * The method descriptor must always be parseable, so if not an AssertionError is thrown. */ private static AssertionError unparseable(final String descriptor, final String cause) { return assertionError("Unparseble method descriptor: '%s' (cause: %s)", descriptor, cause); } private static AssertionError assertionError(String message, Object... args) { return new AssertionError(String.format(message, args)); } }