From fd4a5ceb5e3bc4dfad4222b790b3912f206860bd Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 16 Jul 2013 15:29:37 +0200 Subject: [PATCH] JANDEX-21 Detect no-args constructor --- src/main/java/org/jboss/jandex/ClassInfo.java | 60 +++++++++++++++++- .../java/org/jboss/jandex/IndexReader.java | 29 +++++++-- .../java/org/jboss/jandex/IndexWriter.java | 32 ++++++++-- src/main/java/org/jboss/jandex/Indexer.java | 62 +++++++++++++++++- .../org/jboss/jandex/test/BasicTestCase.java | 63 +++++++++++++++++++ .../jboss/jandex/test/CompositeTestCase.java | 5 +- .../org/jboss/jandex/test/DummyTopLevel.java | 25 ++++++++ ...DummyTopLevelWithoutNoArgsConstructor.java | 25 ++++++++ 8 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 src/test/java/org/jboss/jandex/test/DummyTopLevel.java create mode 100644 src/test/java/org/jboss/jandex/test/DummyTopLevelWithoutNoArgsConstructor.java diff --git a/src/main/java/org/jboss/jandex/ClassInfo.java b/src/main/java/org/jboss/jandex/ClassInfo.java index d7e6ec65..ea11da83 100644 --- a/src/main/java/org/jboss/jandex/ClassInfo.java +++ b/src/main/java/org/jboss/jandex/ClassInfo.java @@ -43,18 +43,21 @@ * */ public final class ClassInfo implements AnnotationTarget { + private final DotName name; private short flags; private final DotName superName; private final DotName[] interfaces; private final Map> annotations; + private final ValueHolder hasNoArgsConstructor; - ClassInfo(DotName name, DotName superName, short flags, DotName[] interfaces, Map> annotations) { + ClassInfo(DotName name, DotName superName, short flags, DotName[] interfaces, Map> annotations, ValueHolder hasNoArgsConstructor) { this.name = name; this.superName = superName; this.flags = flags; this.interfaces = interfaces; this.annotations = Collections.unmodifiableMap(annotations); + this.hasNoArgsConstructor = hasNoArgsConstructor; } /** @@ -68,8 +71,8 @@ public final class ClassInfo implements AnnotationTarget { * @param annotations the annotations on this class * @return a new mock class representation */ - public static final ClassInfo create(DotName name, DotName superName, short flags, DotName[] interfaces, Map> annotations) { - return new ClassInfo(name, superName, flags, interfaces, annotations); + public static final ClassInfo create(DotName name, DotName superName, short flags, DotName[] interfaces, Map> annotations, ValueHolder isTopLevelWithNoArgsConstructor) { + return new ClassInfo(name, superName, flags, interfaces, annotations, isTopLevelWithNoArgsConstructor); } public String toString() { @@ -95,4 +98,55 @@ public final DotName[] interfaces() { public final Map> annotations() { return annotations; } + + /** + * @return {@link Boolean#TRUE} in case of the Java class is top-level or + * static nested with no-args constructor, {@link Boolean#FALSE} if + * it is not and null if info not available + */ + public final Boolean hasNoArgsConstructor() { + return hasNoArgsConstructor.get(); + } + + public static class ValueHolder { + + T value; + + public ValueHolder(T initialValue) { + this.value = initialValue; + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + } + + } + + public static class ImmutableValueHolder extends ValueHolder { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + static final ImmutableValueHolder EMPTY_HOLDER = new ImmutableValueHolder(null); + static final ImmutableValueHolder TRUE_HOLDER = new ImmutableValueHolder(true); + static final ImmutableValueHolder FALSE_HOLDER = new ImmutableValueHolder(false); + + @SuppressWarnings("unchecked") + static ImmutableValueHolder emptyHolder() { + return (ImmutableValueHolder) EMPTY_HOLDER; + } + + public ImmutableValueHolder(T initialValue) { + super(initialValue); + } + + @Override + public void set(Object value) { + throw new UnsupportedOperationException(); + } + + } + } diff --git a/src/main/java/org/jboss/jandex/IndexReader.java b/src/main/java/org/jboss/jandex/IndexReader.java index 55cac3b6..4021e351 100644 --- a/src/main/java/org/jboss/jandex/IndexReader.java +++ b/src/main/java/org/jboss/jandex/IndexReader.java @@ -26,6 +26,9 @@ import java.util.List; import java.util.Map; +import org.jboss.jandex.ClassInfo.ImmutableValueHolder; +import org.jboss.jandex.ClassInfo.ValueHolder; + /** * Reads a Jandex index file and returns the saved index. See {@link Indexer} * for a thorough description of how the Index data is produced. @@ -92,18 +95,23 @@ public IndexReader(InputStream input) { */ public Index read() throws IOException { PackedDataInputStream stream = new PackedDataInputStream(new BufferedInputStream(input)); - if (stream.readInt() != MAGIC) + if (stream.readInt() != MAGIC) { + stream.close(); throw new IllegalArgumentException("Not a jandex index"); + } byte version = stream.readByte(); - if (version != VERSION) + // All previous versions are supported + if (version < 1 || version > VERSION) { + stream.close(); throw new UnsupportedVersion("Version: " + version); + } try { masterAnnotations = new HashMap>(); readClassTable(stream); readStringTable(stream); - return readClasses(stream); + return readClasses(stream, version); } finally { classTable = null; stringTable = null; @@ -112,7 +120,7 @@ public Index read() throws IOException { } - private Index readClasses(PackedDataInputStream stream) throws IOException { + private Index readClasses(PackedDataInputStream stream, byte version) throws IOException { int entries = stream.readPackedU32(); HashMap> subclasses = new HashMap>(); HashMap> implementors = new HashMap>(); @@ -123,6 +131,17 @@ private Index readClasses(PackedDataInputStream stream) throws IOException { DotName name = classTable[stream.readPackedU32()]; DotName superName = classTable[stream.readPackedU32()]; short flags = stream.readShort(); + + // Immutable value holders used here to save some resources + ValueHolder hasNoArgsConstructor; + if (version < 2) { + // hasNoArgsConstructor supported since version 2 + hasNoArgsConstructor = ImmutableValueHolder.emptyHolder(); + } else { + hasNoArgsConstructor = stream.readBoolean() ? ImmutableValueHolder.TRUE_HOLDER + : ImmutableValueHolder.FALSE_HOLDER; + } + int numIntfs = stream.readPackedU32(); DotName[] interfaces = new DotName[numIntfs]; for (int j = 0; j < numIntfs; j++) { @@ -130,7 +149,7 @@ private Index readClasses(PackedDataInputStream stream) throws IOException { } Map> annotations = new HashMap>(); - ClassInfo clazz = new ClassInfo(name, superName, flags, interfaces, annotations); + ClassInfo clazz = new ClassInfo(name, superName, flags, interfaces, annotations, hasNoArgsConstructor); classes.put(name, clazz); addClassToMap(subclasses, superName, clazz); for (DotName interfaceName : interfaces) { diff --git a/src/main/java/org/jboss/jandex/IndexWriter.java b/src/main/java/org/jboss/jandex/IndexWriter.java index 0b929790..7d7ef01f 100644 --- a/src/main/java/org/jboss/jandex/IndexWriter.java +++ b/src/main/java/org/jboss/jandex/IndexWriter.java @@ -86,23 +86,41 @@ public IndexWriter(OutputStream out) { this.out = out; } + /** + * Writes the specified index to the associated output stream. This may be called multiple times in order + * to write multiple indexes. The default version of index file is used. + * + * @param index + * @return the number of bytes written to the stream + * @throws IOException + */ + public int write(Index index) throws IOException { + return write(index, VERSION); + } + /** * Writes the specified index to the associated output stream. This may be called multiple times in order * to write multiple indexes. * * @param index the index to write to the stream + * @param version the index file version * @return the number of bytes written to the stream * @throws IOException if any i/o error occurs */ - public int write(Index index) throws IOException { + public int write(Index index, byte version) throws IOException { + + if (version < 1 || version > VERSION) { + throw new UnsupportedVersion("Version: " + version); + } + PackedDataOutputStream stream = new PackedDataOutputStream(new BufferedOutputStream(out)); stream.writeInt(MAGIC); - stream.writeByte(VERSION); + stream.writeByte(version); buildTables(index); writeClassTable(stream); writeStringTable(stream); - writeClasses(stream, index); + writeClasses(stream, index, version); stream.flush(); return stream.size(); } @@ -152,13 +170,19 @@ private int positionOf(DotName className) { } - private void writeClasses(PackedDataOutputStream stream, Index index) throws IOException { + private void writeClasses(PackedDataOutputStream stream, Index index, byte version) throws IOException { Collection classes = index.getKnownClasses(); stream.writePackedU32(classes.size()); for (ClassInfo clazz: classes) { stream.writePackedU32(positionOf(clazz.name())); stream.writePackedU32(clazz.superName() == null ? 0 : positionOf(clazz.superName())); stream.writeShort(clazz.flags()); + + // hasNoArgsConstructor supported since version 2 + if (version >= 2) { + stream.writeBoolean(clazz.hasNoArgsConstructor()); + } + DotName[] interfaces = clazz.interfaces(); stream.writePackedU32(interfaces.length); for (DotName intf: interfaces) diff --git a/src/main/java/org/jboss/jandex/Indexer.java b/src/main/java/org/jboss/jandex/Indexer.java index cad21272..5a046a54 100644 --- a/src/main/java/org/jboss/jandex/Indexer.java +++ b/src/main/java/org/jboss/jandex/Indexer.java @@ -31,6 +31,8 @@ import java.util.List; import java.util.Map; +import org.jboss.jandex.ClassInfo.ValueHolder; + /** * Analyzes and indexes the annotation and key structural information of a set * of classes. The indexer will purposefully skip any class that is not Java 5 @@ -76,7 +78,7 @@ public final class Indexer { private final static int CONSTANT_INVOKEDYNAMIC = 18; private final static int CONSTANT_METHODHANDLE = 15; private final static int CONSTANT_METHODTYPE = 16; - + // "RuntimeVisibleAnnotations" private final static byte[] RUNTIME_ANNOTATIONS = new byte[] { 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, @@ -97,6 +99,8 @@ public final class Indexer { private final static int HAS_RUNTIME_ANNOTATION = 1; private final static int HAS_RUNTIME_PARAM_ANNOTATION = 2; + private final static String INIT_METHOD_NAME = ""; + private static boolean match(byte[] target, int offset, byte[] expected) { if (target.length - offset < expected.length) return false; @@ -136,6 +140,8 @@ private static void skipFully(InputStream s, long n) throws IOException { private HashMap> classAnnotations; private StrongInternPool internPool; + private ValueHolder hasNoArgsConstructor; + // Index lifespan fields private Map> masterAnnotations; private Map> subclasses; @@ -195,10 +201,45 @@ private void processMethodInfo(DataInputStream data) throws IOException { MethodInfo method = new MethodInfo(currentClass, name, args, returnType, flags); + if(INIT_METHOD_NAME.equals(name) && args.length == 0) { + hasNoArgsConstructor.set(true); + } processAttributes(data, method); } } + private void detectNoArgsConstructor(DataInputStream data) throws IOException { + + int numFields = data.readUnsignedShort(); + + for (int i = 0; i < numFields; i++) { + // Flags, name, type + skipFully(data, 6); + skipAttributes(data); + } + + int numMethods = data.readUnsignedShort(); + + for (int i = 0; i < numMethods; i++) { + // Flags not needed + skipFully(data, 2); + String name = intern(decodeUtf8Entry(data.readUnsignedShort())); + String descriptor = decodeUtf8Entry(data.readUnsignedShort()); + + if(INIT_METHOD_NAME.equals(name)) { + + IntegerHolder pos = new IntegerHolder(); + Type[] args = parseMethodArgs(descriptor, pos); + + if(args.length == 0) { + hasNoArgsConstructor.set(true); + return; + } + } + skipAttributes(data); + } + } + private void processFieldInfo(DataInputStream data) throws IOException { int numFields = data.readUnsignedShort(); @@ -212,6 +253,16 @@ private void processFieldInfo(DataInputStream data) throws IOException { } } + private void skipAttributes(DataInputStream data) throws IOException { + int numAttrs = data.readUnsignedShort(); + for (int a = 0; a < numAttrs; a++) { + // Constant pool index + skipFully(data, 2); + long attributeLen = data.readInt() & 0xFFFFFFFFL; + skipFully(data, attributeLen); + } + } + private void processAttributes(DataInputStream data, AnnotationTarget target) throws IOException { int numAttrs = data.readUnsignedShort(); for (int a = 0; a < numAttrs; a++) { @@ -343,7 +394,8 @@ private void processClassInfo(DataInputStream data) throws IOException { } this.classAnnotations = new HashMap>(); - this.currentClass = new ClassInfo(thisName, superName, flags, interfaces, classAnnotations); + this.hasNoArgsConstructor = new ValueHolder(false); + this.currentClass = new ClassInfo(thisName, superName, flags, interfaces, classAnnotations, hasNoArgsConstructor); if (superName != null) addSubclass(superName, currentClass); @@ -653,8 +705,10 @@ public ClassInfo index(InputStream stream) throws IOException { boolean hasAnnotations = processConstantPool(data); processClassInfo(data); - if (!hasAnnotations) + if (!hasAnnotations) { + detectNoArgsConstructor(data); return currentClass; + } processFieldInfo(data); processMethodInfo(data); @@ -672,6 +726,8 @@ public ClassInfo index(InputStream stream) throws IOException { currentClass = null; classAnnotations = null; internPool = null; + + hasNoArgsConstructor = null; } } diff --git a/src/test/java/org/jboss/jandex/test/BasicTestCase.java b/src/test/java/org/jboss/jandex/test/BasicTestCase.java index 2e57141a..5bb0b118 100644 --- a/src/test/java/org/jboss/jandex/test/BasicTestCase.java +++ b/src/test/java/org/jboss/jandex/test/BasicTestCase.java @@ -19,6 +19,9 @@ package org.jboss.jandex.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; @@ -63,6 +66,22 @@ public class BasicTestCase { public class DummyClass implements Serializable { } + @TestAnnotation(name = "Test", ints = { 1, 2, 3, 4, 5 }, klass = Void.class, nested = @NestedAnnotation(1.34f), nestedArray = { + @NestedAnnotation(3.14f), @NestedAnnotation(2.27f) }, enums = { ElementType.TYPE, ElementType.PACKAGE }, longValue = 10) + public static class NestedA implements Serializable { + } + + @TestAnnotation(name = "Test", ints = { 1, 2, 3, 4, 5 }, klass = Void.class, nested = @NestedAnnotation(1.34f), nestedArray = { + @NestedAnnotation(3.14f), @NestedAnnotation(2.27f) }, enums = { ElementType.TYPE, ElementType.PACKAGE }, longValue = 10) + public static class NestedB implements Serializable { + + NestedB(Integer foo) { + } + } + + public static class NestedC implements Serializable { + } + @Test public void testIndexer() throws IOException { Indexer indexer = new Indexer(); @@ -89,6 +108,30 @@ public void testWriteRead() throws IOException { verifyDummy(index); } + @Test + public void testWriteReadPreviousVersion() throws IOException { + Indexer indexer = new Indexer(); + InputStream stream = getClass().getClassLoader().getResourceAsStream(DummyClass.class.getName().replace('.', '/') + ".class"); + indexer.index(stream); + Index index = indexer.complete(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new IndexWriter(baos).write(index, (byte)1); + + index = new IndexReader(new ByteArrayInputStream(baos.toByteArray())).read(); + assertNull(index.getClassByName(DotName.createSimple(DummyClass.class.getName())).hasNoArgsConstructor()); + } + + @Test + public void testHasNoArgsConstructor() throws IOException { + assertHasNoArgsConstructor(DummyClass.class, false); + assertHasNoArgsConstructor(NestedA.class, true); + assertHasNoArgsConstructor(NestedB.class, false); + assertHasNoArgsConstructor(NestedC.class, true); + assertHasNoArgsConstructor(DummyTopLevel.class, true); + assertHasNoArgsConstructor(DummyTopLevelWithoutNoArgsConstructor.class, false); + } + private void verifyDummy(Index index) { AnnotationInstance instance = index.getAnnotations(DotName.createSimple(TestAnnotation.class.getName())).get(0); @@ -111,6 +154,26 @@ private void verifyDummy(Index index) { implementors = index.getKnownDirectImplementors(DotName.createSimple(InputStream.class.getName())); assertEquals(0, implementors.size()); + + // Verify hasNoArgsConstructor + assertFalse(index.getClassByName(DotName.createSimple(DummyClass.class.getName())).hasNoArgsConstructor()); + } + + private void assertHasNoArgsConstructor(Class clazz, boolean result) throws IOException { + ClassInfo classInfo = getIndexForClass(clazz).getClassByName(DotName.createSimple(clazz.getName())); + assertNotNull(classInfo); + if(result) { + assertTrue(classInfo.hasNoArgsConstructor()); + } else { + assertFalse(classInfo.hasNoArgsConstructor()); + } + } + + private Index getIndexForClass(Class clazz) throws IOException { + Indexer indexer = new Indexer(); + InputStream stream = getClass().getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class"); + indexer.index(stream); + return indexer.complete(); } } diff --git a/src/test/java/org/jboss/jandex/test/CompositeTestCase.java b/src/test/java/org/jboss/jandex/test/CompositeTestCase.java index 339ff5e1..d49c5154 100644 --- a/src/test/java/org/jboss/jandex/test/CompositeTestCase.java +++ b/src/test/java/org/jboss/jandex/test/CompositeTestCase.java @@ -29,6 +29,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassInfo.ValueHolder; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; @@ -38,7 +39,7 @@ public class CompositeTestCase { private static final DotName BASE_NAME = DotName.createSimple("foo.Base"); private static final DotName OBJECT_NAME = DotName.createSimple("java.lang.Object"); - private static ClassInfo BASE_INFO = ClassInfo.create(BASE_NAME, OBJECT_NAME, (short) 0, new DotName[0], Collections.>emptyMap()); + private static ClassInfo BASE_INFO = ClassInfo.create(BASE_NAME, OBJECT_NAME, (short) 0, new DotName[0], Collections.>emptyMap(), new ValueHolder(false)); private static final DotName BAR_NAME = DotName.createSimple("foo.Bar"); private static final DotName FOO_NAME = DotName.createSimple("foo.Foo"); @@ -83,7 +84,7 @@ private int verifyClasses(Collection allKnownSubclasses) { private Index createIndex(DotName name) { Map> annotations = new HashMap>(); DotName baseName = BASE_NAME; - ClassInfo classInfo = ClassInfo.create(name, baseName, (short) 0, new DotName[0], annotations); + ClassInfo classInfo = ClassInfo.create(name, baseName, (short) 0, new DotName[0], annotations, new ValueHolder(false)); ClassInfo baseInfo = BASE_INFO; AnnotationValue[] values = new AnnotationValue[] {AnnotationValue.createStringValue("blah", "blah")}; diff --git a/src/test/java/org/jboss/jandex/test/DummyTopLevel.java b/src/test/java/org/jboss/jandex/test/DummyTopLevel.java new file mode 100644 index 00000000..1cfa650f --- /dev/null +++ b/src/test/java/org/jboss/jandex/test/DummyTopLevel.java @@ -0,0 +1,25 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2013 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 org.jboss.jandex.test; + +public final class DummyTopLevel { + + private DummyTopLevel() { + } + +} diff --git a/src/test/java/org/jboss/jandex/test/DummyTopLevelWithoutNoArgsConstructor.java b/src/test/java/org/jboss/jandex/test/DummyTopLevelWithoutNoArgsConstructor.java new file mode 100644 index 00000000..1ef49baa --- /dev/null +++ b/src/test/java/org/jboss/jandex/test/DummyTopLevelWithoutNoArgsConstructor.java @@ -0,0 +1,25 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2013 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 org.jboss.jandex.test; + +public final class DummyTopLevelWithoutNoArgsConstructor { + + public DummyTopLevelWithoutNoArgsConstructor(int age) { + } + +}