From 4f69d7826cd4bfb3446f5a12b096075485f5fcd2 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 30 Dec 2014 01:52:10 +0100 Subject: [PATCH] Added FAST (bycode) annotation scanning --- .../util/annotation/AnnotationDefaults.java | 61 ++ .../util/annotation/AnnotationDetector.java | 917 ++++++++++++++++++ .../src/dorkbox/util/annotation/Builder.java | 119 +++ .../util/annotation/ClassFileBuffer.java | 240 +++++ .../util/annotation/ClassFileIterator.java | 135 +++ .../util/annotation/ClassIterator.java | 60 ++ .../src/dorkbox/util/annotation/Cursor.java | 84 ++ .../dorkbox/util/annotation/FileIterator.java | 144 +++ .../src/dorkbox/util/annotation/Reporter.java | 46 + .../util/annotation/ReporterFunction.java | 40 + .../util/annotation/ZipFileIterator.java | 105 ++ 11 files changed, 1951 insertions(+) create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDefaults.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDetector.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/Builder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/ClassFileBuffer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/ClassFileIterator.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/ClassIterator.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/Cursor.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/FileIterator.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/Reporter.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/ReporterFunction.java create mode 100644 Dorkbox-Util/src/dorkbox/util/annotation/ZipFileIterator.java diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDefaults.java b/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDefaults.java new file mode 100644 index 0000000..26b8a17 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDefaults.java @@ -0,0 +1,61 @@ +package dorkbox.util.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * for specifying the default report methods, without constantly creating new objects + */ +public class AnnotationDefaults { + public static final ReporterFunction getTypeName = new ReporterFunction() { + @Override + public String report(Cursor cursor) { + return cursor.getTypeName(); + } + }; + public static final ReporterFunction> getAnnotationType = new ReporterFunction>() { + @Override + public Class report(Cursor cursor) { + return cursor.getAnnotationType(); + } + }; + public static final ReporterFunction getElementType = new ReporterFunction() { + @Override + public ElementType report(Cursor cursor) { + return cursor.getElementType(); + } + }; + public static final ReporterFunction getMemberName = new ReporterFunction() { + @Override + public String report(Cursor cursor) { + return cursor.getMemberName(); + } + }; + public static final ReporterFunction> getType = new ReporterFunction>() { + @Override + public Class report(Cursor cursor) { + return cursor.getType(); + } + }; + public static final ReporterFunction> getConstructor = new ReporterFunction>() { + @Override + public Constructor report(Cursor cursor) { + return cursor.getConstructor(); + } + }; + public static final ReporterFunction getField = new ReporterFunction() { + @Override + public Field report(Cursor cursor) { + return cursor.getField(); + } + }; + public static final ReporterFunction getMethod = new ReporterFunction() { + @Override + public Method report(Cursor cursor) { + return cursor.getMethod(); + } + }; +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDetector.java b/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDetector.java new file mode 100644 index 0000000..e5a61d2 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/AnnotationDetector.java @@ -0,0 +1,917 @@ +/* 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) + * + * 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.util.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; + +import org.slf4j.LoggerFactory; + +/** + * {@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 + */ +public final class AnnotationDetector implements Builder, Cursor { + + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AnnotationDetector.class); + + // Constant Pool type tags + 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(final File[] filesOrDirectories, final String[] pkgNameFilter) { + this(Thread.currentThread().getContextClassLoader(), filesOrDirectories, null, pkgNameFilter); + } + + 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) { + LOG.warn("No files or directories to scan!"); + } else if (LOG.isTraceEnabled()) { + LOG.trace("Files and root directories scanned:\n{}", + Arrays.toString(filesOrDirectories).replace(", ", "\n")); + } + } else { + this.cfIterator = iterator; + + if (LOG.isTraceEnabled()) { + LOG.trace("Class Files from the custom classfileiterator scanned."); + } + } + + } + + /** + * 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 Set files = new HashSet(); + final String[] pkgNameFilter; + if (packageNames.length == 0) { + pkgNameFilter = null; + final String[] fileNames = System.getProperty("java.class.path").split(File.pathSeparator); + for (int i = 0; i < fileNames.length; ++i) { + files.add(new File(fileNames[i])); + } + } 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(filesOrDirectories, 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...) }. + */ + @SuppressWarnings("unchecked") + @Override + public Builder forAnnotations(final Class... annotations) { + this.annotations = new HashMap>(annotations.length); + // map "raw" type names to Class object + for (int i = 0; i < annotations.length; ++i) { + this.annotations.put("L" + annotations[i].getName().replace('.', '/') + ";", annotations[i]); + } + return this; + } + + /** + * See {@link Builder#on(java.lang.annotation.ElementType...) }. + */ + @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...) }. + */ + @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(dorkbox.util.annotation.AnnotationDetector.Reporter) }. + */ + @Override + public void report(final Reporter reporter) throws IOException { + this.reporter = reporter; + detect(this.cfIterator); + } + + /** + * See {@link Builder#collect(dorkbox.util.annotation.AnnotationDetector.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 (LOG.isTraceEnabled()) { + LOG.trace("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/ + final boolean isVfs = "vfs".equals(url.getProtocol()); + if ("file".equals(url.getProtocol()) || 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 (LOG.isTraceEnabled()) { + LOG.trace("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 (LOG.isTraceEnabled()) { + int minor = di.readUnsignedShort(); + int maj = di.readUnsignedShort(); + LOG.trace("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); + LOG.trace("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); + LOG.trace("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))) { + LOG.trace("Attribute: {}", name); + readAnnotations(di, reporterType); + } else { + LOG.trace("Attribute: {} (ignored)", name); + 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) { + LOG.trace("Annotation: {} (ignored)", rawTypeName); + continue; + } + LOG.trace("Annotation: ''{}'' on type ''{}'', member ''{}'' (reported)", + this.annotationType.getName(), getTypeName(), getMemberName()); + 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 (LOG.isTraceEnabled()) { + LOG.trace("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>(); + } + } + assert args != null; + 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)); + } + +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/Builder.java b/Dorkbox-Util/src/dorkbox/util/annotation/Builder.java new file mode 100644 index 0000000..32e4ef3 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/Builder.java @@ -0,0 +1,119 @@ +/* Builder.java + * + * Created: 2014-06-15 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2014 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.util.List; + +/** + * {@code Builder} offers a fluent API for using {@link AnnotationDetector}. + * Its only role is to offer a more clean API. + * + * @author Ronald K. Muller + * @since annotation-detector 3.1.0 + */ +public interface Builder { + + /** + * Specify the annotation types to report. + */ + @SuppressWarnings("unchecked") + Builder forAnnotations(final Class... annotations); + + /** + * Specify the annotation types to report. + */ + Builder forAnnotations(Class annotation); + + + /** + * Specify the Element Types to scan. If this method is not called, + * {@link ElementType#TYPE} is used as default. + *

+ * Valid types are: + *

    + *
  • {@link ElementType#TYPE} + *
  • {@link ElementType#METHOD} + *
  • {@link ElementType#FIELD} + *
+ * An {@code IllegalArgumentException} is thrown if another Element Type is specified or + * no types are specified. + */ + Builder on(final ElementType type); + + /** + * Specify the Element Types to scan. If this method is not called, + * {@link ElementType#TYPE} is used as default. + *

+ * Valid types are: + *

    + *
  • {@link ElementType#TYPE} + *
  • {@link ElementType#METHOD} + *
  • {@link ElementType#FIELD} + *
+ * An {@code IllegalArgumentException} is thrown if another Element Type is specified or + * no types are specified. + */ + Builder on(final ElementType... types); + + /** + * Filter the scanned Class Files based on its name and the directory or jar file it is + * stored. + *

+ * If the Class File is stored as a single file in the file system the {@code File} + * argument in {@link FilenameFilter#accept(java.io.File, java.lang.String) } is the + * absolute path to the root directory scanned. + *

+ * If the Class File is stored in a jar file the {@code File} argument in + * {@link FilenameFilter#accept(java.io.File, java.lang.String)} is the absolute path of + * the jar file. + *

+ * The {@code String} argument is the full name of the ClassFile in native format, + * including package name, like {@code eu/infomas/annotation/AnnotationDetector$1.class}. + *

+ * Note that all non-Class Files are already filtered and not seen by the filter. + * + * @param filter The filter, never {@code null} + */ + Builder filter(final FilenameFilter filter); + + /** + * Report the detected annotations to the specified {@code Reporter} instance. + * + * @see Reporter#report(dorkbox.util.annotations.Cursor) + * @see #collect(dorkbox.util.annotations.ReporterFunction) + */ + void report(final Reporter reporter) throws IOException; + + /** + * Report the detected annotations to the specified {@code ReporterFunction} instance and + * collect the returned values of + * {@link ReporterFunction#report(dorkbox.util.annotations.Cursor) }. + * The collected values are returned as a {@code List}. + * + * @see #report(dorkbox.util.annotations.Reporter) + */ + List collect(final ReporterFunction reporter) throws IOException; +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileBuffer.java b/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileBuffer.java new file mode 100644 index 0000000..a678a17 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileBuffer.java @@ -0,0 +1,240 @@ +/* ClassFileBuffer.java + * + * Created: 2011-10-10 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2011 - 2013 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * {@code ClassFileBuffer} is used by {@link AnnotationDetector} to efficiently read Java + * ClassFile files from an {@link InputStream} and parse the content via the {@link DataInput} + * interface. + *

+ * Note that Java ClassFile files can grow really big, + * {@code com.sun.corba.se.impl.logging.ORBUtilSystemException} is 128.2 kb! + * + * @author Ronald K. Muller + * @since annotation-detector 3.0.0 + */ +final class ClassFileBuffer implements DataInput { + + private byte[] buffer; + private int size; // the number of significant bytes read + private int pointer; // the "read pointer" + + /** + * Create a new, empty {@code ClassFileBuffer} with the default initial capacity (8 kb). + */ + ClassFileBuffer() { + this(8 * 1024); + } + + /** + * Create a new, empty {@code ClassFileBuffer} with the specified initial capacity. + * The initial capacity must be greater than zero. The internal buffer will grow + * automatically when a higher capacity is required. However, buffer resizing occurs + * extra overhead. So in good initial capacity is important in performance critical + * situations. + */ + ClassFileBuffer(final int initialCapacity) { + if (initialCapacity < 1) { + throw new IllegalArgumentException("initialCapacity < 1: " + initialCapacity); + } + this.buffer = new byte[initialCapacity]; + } + + /** + * Clear and fill the buffer of this {@code ClassFileBuffer} with the + * supplied byte stream. + * The read pointer is reset to the start of the byte array. + */ + public void readFrom(final InputStream in) throws IOException { + this.pointer = 0; + this.size = 0; + int n; + do { + n = in.read(this.buffer, this.size, this.buffer.length - this.size); + if (n > 0) { + this.size += n; + } + resizeIfNeeded(); + } while (n >= 0); + } + + /** + * Sets the file-pointer offset, measured from the beginning of this file, + * at which the next read or write occurs. + */ + public void seek(final int position) throws IOException { + if (position < 0) { + throw new IllegalArgumentException("position < 0: " + position); + } + if (position > this.size) { + throw new EOFException(); + } + this.pointer = position; + } + + /** + * Return the size (in bytes) of this Java ClassFile file. + */ + public int size() { + return this.size; + } + + // DataInput + + @Override + public void readFully(final byte[] bytes) throws IOException { + readFully(bytes, 0, bytes.length); + } + + @Override + public void readFully(final byte[] bytes, final int offset, final int length) + throws IOException { + + if (length < 0 || offset < 0 || offset + length > bytes.length) { + throw new IndexOutOfBoundsException(); + } + if (this.pointer + length > this.size) { + throw new EOFException(); + } + System.arraycopy(this.buffer, this.pointer, bytes, offset, length); + this.pointer += length; + } + + @Override + public int skipBytes(final int n) throws IOException { + seek(this.pointer + n); + return n; + } + + @Override + public byte readByte() throws IOException { + if (this.pointer >= this.size) { + throw new EOFException(); + } + return this.buffer[this.pointer++]; + } + + @Override + public boolean readBoolean() throws IOException { + return readByte() != 0; + } + + @Override + public int readUnsignedByte() throws IOException { + if (this.pointer >= this.size) { + throw new EOFException(); + } + return read(); + } + + @Override + public int readUnsignedShort() throws IOException { + if (this.pointer + 2 > this.size) { + throw new EOFException(); + } + return (read() << 8) + read(); + } + + @Override + public short readShort() throws IOException { + return (short)readUnsignedShort(); + } + + @Override + public char readChar() throws IOException { + return (char)readUnsignedShort(); + } + + @Override + public int readInt() throws IOException { + if (this.pointer + 4 > this.size) { + throw new EOFException(); + } + return (read() << 24) + + (read() << 16) + + (read() << 8) + + read(); + } + + @Override + public long readLong() throws IOException { + if (this.pointer + 8 > this.size) { + throw new EOFException(); + } + return ((long)read() << 56) + + ((long)read() << 48) + + ((long)read() << 40) + + ((long)read() << 32) + + (read() << 24) + + (read() << 16) + + (read() << 8) + + read(); + } + + @Override + public float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + @Override + public double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * This methods throws an {@link UnsupportedOperationException} because the method + * is deprecated and not used in the context of this implementation. + * + * @deprecated Does not support UTF-8, use readUTF() instead + */ + @Override + @Deprecated + public String readLine() throws IOException { + throw new UnsupportedOperationException("readLine() is deprecated and not supported"); + } + + @Override + public String readUTF() throws IOException { + return DataInputStream.readUTF(this); + } + + // private + + private int read() { + return this.buffer[this.pointer++] & 0xff; + } + + private void resizeIfNeeded() { + if (this.size >= this.buffer.length) { + final byte[] newBuffer = new byte[this.buffer.length * 2]; + System.arraycopy(this.buffer, 0, newBuffer, 0, this.buffer.length); + this.buffer = newBuffer; + } + } + +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileIterator.java b/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileIterator.java new file mode 100644 index 0000000..5f2641b --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/ClassFileIterator.java @@ -0,0 +1,135 @@ +/* ClassFileIterator.java + * + * Created: 2011-10-10 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2011 - 2013 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; + +/** + * {@code ClassFileIterator} is used to iterate over all Java ClassFile files available within + * a specific context. + *

+ * For every Java ClassFile ({@code .class}) an {@link InputStream} is returned. + * + * @author Ronald K. Muller + * @since annotation-detector 3.0.0 + */ +public class ClassFileIterator implements ClassIterator { + + private FileIterator fileIter; + protected final String[] pkgNameFilter; + + private ZipFileIterator zipIter; + private boolean isFile; + + /** + * Create a new {@code ClassFileIterator} returning all Java ClassFile files available + * from the specified files and/or directories, including sub directories. + *

+ * If the (optional) package filter is defined, only class files staring with one of the + * defined package names are returned. + * NOTE: package names must be defined in the native format (using '/' instead of '.'). + */ + protected ClassFileIterator(final String[] pkgNameFilter) { + this.pkgNameFilter = pkgNameFilter; + } + + /** + * Create a new {@code ClassFileIterator} returning all Java ClassFile files available + * from the specified files and/or directories, including sub directories. + *

+ * If the (optional) package filter is defined, only class files staring with one of the + * defined package names are returned. + * NOTE: package names must be defined in the native format (using '/' instead of '.'). + */ + protected ClassFileIterator(final File[] filesOrDirectories, final String[] pkgNameFilter) { + this.fileIter = new FileIterator(filesOrDirectories); + this.pkgNameFilter = pkgNameFilter; + } + + /** + * Return the name of the Java ClassFile returned from the last call to {@link #next()}. + * The name is either the path name of a file or the name of an ZIP/JAR file entry. + */ + @Override + public String getName() { + // Both getPath() and getName() are very light weight method calls + return this.zipIter == null ? + this.fileIter.getFile().getPath() : + this.zipIter.getEntry().getName(); + } + + /** + * Return {@code true} if the current {@link InputStream} is reading from a plain + * {@link File}. + * Return {@code false} if the current {@link InputStream} is reading from a + * ZIP File Entry. + */ + @Override + public boolean isFile() { + return this.isFile; + } + + /** + * Return the next Java ClassFile as an {@code InputStream}. + *

+ * NOTICE: Client code MUST close the returned {@code InputStream}! + */ + @Override + public InputStream next(final FilenameFilter filter) throws IOException { + while (true) { + if (this.zipIter == null) { + final File file = this.fileIter.next(); + if (file == null) { + return null; + } else { + final String path = file.getPath(); + if (path.endsWith(".class") && (filter == null || + filter.accept(this.fileIter.getRootFile(), this.fileIter.relativize(path)))) { + this.isFile = true; + return new FileInputStream(file); + } else if (this.fileIter.isRootFile() && endsWithIgnoreCase(path, ".jar")) { + this.zipIter = new ZipFileIterator(file, this.pkgNameFilter); + } // else just ignore + } + } else { + final InputStream is = this.zipIter.next(filter); + if (is == null) { + this.zipIter = null; + } else { + this.isFile = false; + return is; + } + } + } + } + + // private + + private static boolean endsWithIgnoreCase(final String value, final String suffix) { + final int n = suffix.length(); + return value.regionMatches(true, value.length() - n, suffix, 0, n); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/ClassIterator.java b/Dorkbox-Util/src/dorkbox/util/annotation/ClassIterator.java new file mode 100644 index 0000000..862072a --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/ClassIterator.java @@ -0,0 +1,60 @@ +/* ClassFileIterator.java + * + * Created: 2011-10-10 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2011 - 2013 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; + +/** + * {@code ClassFileIterator} is used to iterate over all Java ClassFile files available within + * a specific context. + *

+ * For every Java ClassFile ({@code .class}) an {@link InputStream} is returned. + * + * @author Ronald K. Muller + * @since annotation-detector 3.0.0 + */ +public interface ClassIterator { + + /** + * Return the name of the Java ClassFile returned from the last call to {@link #next()}. + * The name is either the path name of a file or the name of an ZIP/JAR file entry. + */ + String getName(); + + /** + * Return {@code true} if the current {@link InputStream} is reading from a plain + * {@link File}. + * Return {@code false} if the current {@link InputStream} is reading from a + * ZIP File Entry. + */ + boolean isFile(); + + /** + * Return the next Java ClassFile as an {@code InputStream}. + *

+ * NOTICE: Client code MUST close the returned {@code InputStream}! + */ + InputStream next(final FilenameFilter filter) throws IOException; +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/Cursor.java b/Dorkbox-Util/src/dorkbox/util/annotation/Cursor.java new file mode 100644 index 0000000..acaa969 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/Cursor.java @@ -0,0 +1,84 @@ +/* Cursor.java + * + * Created: 2014-06-15 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2014 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * {@code Cursor} offers a "cursor interface" for working with {@link AnnotationDetector}. + * + * @author Ronald K. Muller + * @since annotation-detector 3.1.0 + */ +public interface Cursor { + + /** + * Return the type name of the currently reported Java Class File. + */ + String getTypeName(); + + /** + * Return the Annotation Type currently reported. + */ + Class getAnnotationType(); + + /** + * Return the {@code ElementType} of the currently reported {@code Annotation}. + */ + ElementType getElementType(); + + /** + * Return the member name of the currently reported {@code Annotation}. + * In case of an annotation on type level, "<clinit>" is reported. + */ + String getMemberName(); + + /** + * Return the {@link Class type} of the currently reported Java Class File. + */ + Class getType(); + + /** + * Return the {@link Constructor} instance of the currently reported annotated Constructor. + */ + Constructor getConstructor(); + + /** + * Return the {@link Field} instance of the currently reported annotated Field. + */ + Field getField(); + + /** + * Return the {@link Method} instance of the currently reported annotated Method. + */ + Method getMethod(); + + /** + * Return the {@code Annotation} of the reported Annotated Element. + */ + T getAnnotation(Class annotationClass); + +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/FileIterator.java b/Dorkbox-Util/src/dorkbox/util/annotation/FileIterator.java new file mode 100644 index 0000000..5b5668b --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/FileIterator.java @@ -0,0 +1,144 @@ +/* FileIterator.java + * + * Created: 2011-10-10 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2011 - 2013 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.File; +import java.util.Deque; +import java.util.LinkedList; +import java.util.NoSuchElementException; + +/** + * {@code FileIterator} enables iteration over all files in a directory and all its sub + * directories. + *

+ * Usage: + *

+ * FileIterator iter = new FileIterator(new File("./src"));
+ * File f;
+ * while ((f = iter.next()) != null) {
+ *     // do something with f
+ *     assert f == iter.getCurrent();
+ * }
+ * 
+ * + * @author Ronald K. Muller + * @since annotation-detector 3.0.0 + */ +final class FileIterator { + + private final Deque stack = new LinkedList(); + private int rootCount; + private File currentRoot; + private File current; + + /** + * Create a new {@code FileIterator} using the specified 'filesOrDirectories' as root. + *

+ * If 'filesOrDirectories' contains a file, the iterator just returns that single file. + * If 'filesOrDirectories' contains a directory, all files in that directory + * and its sub directories are returned (depth first). + * + * @param filesOrDirectories Zero or more {@link File} objects, which are iterated + * in the specified order (depth first) + */ + FileIterator(final File... filesOrDirectories) { + addReverse(filesOrDirectories); + this.rootCount = this.stack.size(); + } + + /** + * Return the last returned file or {@code null} if no more files are available. + * + * @see #next() + */ + File getFile() { + return this.current; + } + + File getRootFile() { + return this.currentRoot; + } + + /** + * Relativize the absolute full (file) 'path' against the current root file. + *

+ * Example:
+ * Let current root be "/path/to/dir". + * Then {@code relativize("/path/to/dir/with/file.ext")} equals "with/file.ext" (without + * leading '/'). + *

+ * Note: the paths are not canonicalized! + */ + String relativize(final String path) { + assert path.startsWith(this.currentRoot.getPath()); + return path.substring(this.currentRoot.getPath().length() + 1); + } + + /** + * Return {@code true} if the current file is one of the files originally + * specified as one of the constructor file parameters, i.e. is a root file + * or directory. + */ + boolean isRootFile() { + if (this.current == null) { + throw new NoSuchElementException(); + } + return this.stack.size() < this.rootCount; + } + + /** + * Return the next {@link File} object or {@code null} if no more files are + * available. + * + * @see #getFile() + */ + File next() { + if (this.stack.isEmpty()) { + this.current = null; + return null; + } else { + this.current = this.stack.removeLast(); + if (this.current.isDirectory()) { + if (this.stack.size() < this.rootCount) { + this.rootCount = this.stack.size(); + this.currentRoot = this.current; + } + addReverse(this.current.listFiles()); + return next(); + } else { + return this.current; + } + } + } + + // private + + /** + * Add the specified files in reverse order. + */ + private void addReverse(final File[] files) { + for (int i = files.length - 1; i >= 0; --i) { + this.stack.add(files[i]); + } + } + +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/Reporter.java b/Dorkbox-Util/src/dorkbox/util/annotation/Reporter.java new file mode 100644 index 0000000..a737b74 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/Reporter.java @@ -0,0 +1,46 @@ +/* Reporter.java + * + * Created: 2014-06-15 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2014 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + + +/** + * {@code Reporter} is used to report the detected annotations. + *

+ * This interface is a so called "Single Abstract Method" (SAM) or "Functional Interface", so + * can be used as a Lambda in Java 8 (see examples). + * + * @see Builder#report(dorkbox.util.annotation.Reporter) + * + * @author Ronald K. Muller + * @since annotation-detector 3.1.0 + */ +public interface Reporter { + + /** + * This method is called when an {@code Annotation} is detected. Invoke methods on the + * provided {@code Cursor} reference to get more specific information about the + * {@code Annotation}. + * + */ + void report(Cursor cursor); + +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/ReporterFunction.java b/Dorkbox-Util/src/dorkbox/util/annotation/ReporterFunction.java new file mode 100644 index 0000000..5261fdc --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/ReporterFunction.java @@ -0,0 +1,40 @@ +/* ReporterFunction.java + * + * Created: 2014-06-15 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2014 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +/** + * {@code ReporterFunction} is used to report the detected annotations. + * + * @see Builder#collect(dorkbox.util.annotation.ReporterFunction) + * + * @author Ronald K. Muller + * @since annotation-detector 3.1.0 + */ +public interface ReporterFunction { + + /** + * This method is called when an {@code Annotation} is detected. + * Invoke methods on the {@code Cursor} to get more specific information about the + * {@code Annotation}. + */ + T report(Cursor cursor); +} diff --git a/Dorkbox-Util/src/dorkbox/util/annotation/ZipFileIterator.java b/Dorkbox-Util/src/dorkbox/util/annotation/ZipFileIterator.java new file mode 100644 index 0000000..98eccbc --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/annotation/ZipFileIterator.java @@ -0,0 +1,105 @@ +/* ZipFileIterator.java + * + * Created: 2011-10-10 (Year-Month-Day) + * Character encoding: UTF-8 + * + ****************************************** LICENSE ******************************************* + * + * Copyright (c) 2011 - 2013 XIAM Solutions B.V. (http://www.xiam.nl) + * + * 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.util.annotation; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * {@code ZipFileIterator} is used to iterate over all entries in a given {@code zip} or + * {@code jar} file and returning the {@link InputStream} of these entries. + *

+ * It is possible to specify an (optional) entry name filter. + *

+ * The most efficient way of iterating is used, see benchmark in test classes. + * + * @author Ronald K. Muller + * @since annotation-detector 3.0.0 + */ +final class ZipFileIterator { + + private final File file; + private final ZipFile zipFile; + private final String[] entryNameFilter; + private final Enumeration entries; + + private ZipEntry current; + + /** + * Create a new {@code ZipFileIterator} instance. + * + * @param zipFile The ZIP file used to iterate over all entries + * @param entryNameFilter (optional) file name filter. Only entry names starting with + * one of the specified names in the filter are returned + */ + ZipFileIterator(final File file, final String[] entryNameFilter) throws IOException { + this.file = file; + this.zipFile = new ZipFile(file); + this.entryNameFilter = entryNameFilter; + + this.entries = this.zipFile.entries(); + } + + public ZipEntry getEntry() { + return this.current; + } + + public InputStream next(final FilenameFilter filter) throws IOException { + while (this.entries.hasMoreElements()) { + this.current = this.entries.nextElement(); + if (filter == null || accept(this.current, filter)) { + return this.zipFile.getInputStream(this.current); + } + } + // no more entries in this ZipFile, so close ZipFile + try { + // zipFile is never null here + this.zipFile.close(); + } catch (IOException ex) { + // suppress IOException, otherwise close() is called twice + } + return null; + } + + private boolean accept(final ZipEntry entry, final FilenameFilter filter) { + if (entry.isDirectory()) { + return false; + } + final String name = entry.getName(); + if (name.endsWith(".class") && filter.accept(this.file, name)) { + if (this.entryNameFilter == null) { + return true; + } + for (final String entryName : this.entryNameFilter) { + if (name.startsWith(entryName)) { + return true; + } + } + } + return false; + } +}