/* ClassInfo Copyright (C) 1998-1999 Jochen Hoenicke. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id$ */ package net.sf.jode.bytecode; import net.sf.jode.GlobalOptions; import net.sf.jode.util.UnifyHash; import java.io.DataInputStream; import java.io.BufferedInputStream; import java.io.DataOutputStream; import java.io.OutputStream; import java.io.IOException; import java.io.FileNotFoundException; import java.util.Enumeration; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; ///#def COLLECTIONS java.util import java.util.Arrays; import java.util.Iterator; import java.util.ListIterator; import java.util.List; import java.util.ArrayList; ///#enddef ///#def COLLECTIONEXTRA java.lang import java.lang.Comparable; ///#enddef import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; /** * Represents a class or interface. It can't be used for primitive * or array types. Every class/interface is associated with a class * path, which is used to load the class and its dependent classes. * *
setSomething
* methods.
*
* setSomething
methods.
*
* getClassName()
also works for package scope classes,
* setClassName() must only be called on inner classes and will not
* change the bytecode name.java.util.Map$Entry
is
* Entry
. If you change its ClassName to
* Yrtne
and save it, it will still be in a file called
* Map$Entry.class
, but a debugger would call it
* java.util.Map.Yrtne
. Note that you should also save
* Map
, because it also has a reference to the
* ClassName.
* java.lang.Object
has a super
* class. The super class is created in the same classpath as the
* current class. Interfaces always have
* java.lang.Object
as their super class.
* java.util.Map$Entry
. There are four different
* types of classes:
* null
and
* {@link #isMethodScoped()} returns false
.
* null
.
*
* The bytecode name ({@link #getName()}) of an inner class is
* in normally of the form Package.Outer$Inner
. However,
* ClassInfo also supports differently named classes, as long as the
* InnerClass attribute is present. The method
* {@link #getClassName()} returns the name of the inner class
* (Inner
in the above example).
*
* You can get all inner classes of a class with the
* method {@link #getClasses}.
* true
and
* {@link #getClassName()} returns not null
. In
* that case {@link #getOuterClass()} returns null
,
* too.Package.Outer$Number$Inner
. However,
* ClassInfo also supports differently named classes, as long as the
* InnerClass attribute is present. true
and
* {@link #getClassName()} returns null
. In that
* case {@link #getOuterClass()} returns null
,
* too.Package.Outer$Number
.
* However, ClassInfo also supports differently named classes, as
* long as the InnerClass attribute is present. load
a class from its classpath instead. This may
* be useful for special kinds of input streams, that ClassPath
* doesn't handle.
*
* @param input The input stream, containing the class in standard
* bytecode format.
* @param howMuch The amount of information that should be read in, one
* of HIERARCHY, PUBLICDECLARATIONS, DECLARATIONS or ALL.
* @exception ClassFormatException if the file doesn't denote a valid
* class.
* @exception IOException if input throws an exception.
* @exception IllegalStateException if this ClassInfo was modified.
* @see #load
*/
public void read(DataInputStream input, int howMuch)
throws IOException
{
if (modified)
throw new IllegalStateException(name);
if (status >= howMuch)
return;
/* Since we have to read the whole class anyway, we load all
* info, that we may need later and that does not take much memory.
*/
if (howMuch <= DECLARATIONS)
howMuch = DECLARATIONS;
/* header */
if (input.readInt() != 0xcafebabe)
throw new ClassFormatException("Wrong magic");
int version = input.readUnsignedShort();
version |= input.readUnsignedShort() << 16;
if (version < (45 << 16 | 0))
throw new ClassFormatException("Wrong class version");
/* constant pool */
ConstantPool cpool = new ConstantPool();
cpool.read(input);
/* modifiers */
modifiers = input.readUnsignedShort();
/* name */
String className = cpool.getClassName(input.readUnsignedShort());
if (name == null)
name = className;
else if (!name.equals(className))
throw new ClassFormatException("wrong name " + className);
/* superclass */
int superID = input.readUnsignedShort();
superclass = superID == 0 ? null
: classpath.getClassInfo(cpool.getClassName(superID));
/* interfaces */
int count = input.readUnsignedShort();
interfaces = new ClassInfo[count];
for (int i = 0; i < count; i++) {
interfaces[i] = classpath.getClassInfo
(cpool.getClassName(input.readUnsignedShort()));
}
/* fields */
count = input.readUnsignedShort();
fields = new FieldInfo[count];
for (int i = 0; i < count; i++) {
fields[i] = new FieldInfo();
fields[i].read(cpool, input, howMuch);
}
/* methods */
count = input.readUnsignedShort();
methods = new MethodInfo[count];
for (int i = 0; i < count; i++) {
methods[i] = new MethodInfo();
methods[i].read(cpool, input, howMuch);
}
/* initialize inner classes to empty array, in case there
* is no InnerClasses attribute.
*/
innerClasses = EMPTY_INNER;
/* attributes */
readAttributes(cpool, input, howMuch);
/* All classes that are mentioned in the constant pool must
* have an empty outer class info. This is specified in the
* 2nd edition of the JVM specification.
*/
Iterator iter = cpool.iterateClassNames();
while (iter.hasNext()) {
ClassInfo ci = classpath.getClassInfo((String) iter.next());
if (ci.status < OUTERCLASS)
ci.mergeOuterInfo(null, null, -1, false);
}
/* Set status */
status = howMuch;
}
/****** WRITING CLASS FILES ***************************************/
/**
* Reserves constant pool entries for String, Integer and Float
* constants needed by the bytecode. These constants should have
* small constant pool indices so that a ldc instead of a ldc_w
* bytecode can be used.
*/
private void reserveSmallConstants(GrowableConstantPool gcp) {
for (int i = 0; i < fields.length; i++)
fields[i].reserveSmallConstants(gcp);
for (int i = 0; i < methods.length; i++)
methods[i].reserveSmallConstants(gcp);
}
/**
* Reserves all constant pool entries needed by this class. This
* is necessary, because the constant pool is the first thing
* written to the class file.
*/
private void prepareWriting(GrowableConstantPool gcp) {
gcp.putClassName(name);
gcp.putClassName(superclass.name);
for (int i = 0; i < interfaces.length; i++)
gcp.putClassName(interfaces[i].name);
for (int i = 0; i < fields.length; i++)
fields[i].prepareWriting(gcp);
for (int i = 0; i < methods.length; i++)
methods[i].prepareWriting(gcp);
for (int i = 0; i < innerClasses.length; i++)
gcp.putClassName(innerClasses[i].name);
if (sourceFile != null) {
gcp.putUTF8("SourceFile");
gcp.putUTF8(sourceFile);
}
/* All classes mentioned in the constant pool must have an
* outer class info. This is clearly specified in the 2nd
* edition of the JVM specification.
*/
hasInnerClassesAttr = false;
Iterator iter = gcp.iterateClassNames();
while (iter.hasNext()) {
ClassInfo ci = classpath.getClassInfo((String) iter.next());
if (ci.status < OUTERCLASS) {
GlobalOptions.err.println
("WARNING: " + ci.name + "'s outer class isn't known.");
} else {
if ((ci.outerClass != null || ci.methodScoped)
&& ! hasInnerClassesAttr) {
gcp.putUTF8("InnerClasses");
hasInnerClassesAttr = true;
}
if (ci.outerClass != null)
gcp.putClassName(ci.outerClass.name);
if (ci.className != null)
gcp.putUTF8(ci.className);
}
}
prepareAttributes(gcp);
}
/**
* Count the attributes needed by the class.
*/
protected int getAttributeCount() {
int count = super.getAttributeCount();
if (sourceFile != null)
count++;
if (hasInnerClassesAttr)
count++;
return count;
}
/**
* Write the attributes needed by the class, namely SourceFile
* and InnerClasses attributes.
*/
protected void writeAttributes(GrowableConstantPool gcp,
DataOutputStream output)
throws IOException {
super.writeAttributes(gcp, output);
if (sourceFile != null) {
output.writeShort(gcp.putUTF8("SourceFile"));
output.writeInt(2);
output.writeShort(gcp.putUTF8(sourceFile));
}
List outers = new ArrayList();
Iterator iter = gcp.iterateClassNames();
while (iter.hasNext()) {
ClassInfo ci = classpath.getClassInfo((String) iter.next());
while (ci != null
&& ci.status >= OUTERCLASS
&& (ci.outerClass != null || ci.methodScoped)) {
/* Order is important so remove ci if it
* already exists and add it to the end. This
* way the outermost classes go to the end.
*/
outers.remove(ci);
outers.add(ci);
ci = ci.outerClass;
}
}
if (hasInnerClassesAttr) {
int count = outers.size();
output.writeShort(gcp.putUTF8("InnerClasses"));
output.writeInt(2 + count * 8);
output.writeShort(count);
ListIterator listiter = outers.listIterator(count);
while (listiter.hasPrevious()) {
ClassInfo ci = (ClassInfo) listiter.previous();
output.writeShort(gcp.putClassName(ci.name));
output.writeShort(ci.outerClass == null ? 0 :
gcp.putClassName(ci.outerClass.name));
output.writeShort(ci.className == null ? 0 :
gcp.putUTF8(ci.className));
output.writeShort(ci.modifiers);
}
}
}
/**
* Writes a class to the given DataOutputStream. Of course this only
* works if ALL information for this class is loaded/set. If this
* class has an outer class, inner classes or extra classes, their
* status must contain at least the OUTERCLASS information.
* @param out the output stream.
* @exception IOException if out throws io exception.
* @exception IllegalStateException if not enough information is set.
*/
public void write(DataOutputStream out) throws IOException {
if (status < ALL)
throw new IllegalStateException("state is "+status);
GrowableConstantPool gcp = new GrowableConstantPool();
reserveSmallConstants(gcp);
prepareWriting(gcp);
out.writeInt(0xcafebabe);
out.writeShort(3);
out.writeShort(45);
gcp.write(out);
out.writeShort(javaModifiersToBytecode(modifiers));
out.writeShort(gcp.putClassName(name));
out.writeShort(gcp.putClassName(superclass.getName()));
out.writeShort(interfaces.length);
for (int i = 0; i < interfaces.length; i++)
out.writeShort(gcp.putClassName(interfaces[i].getName()));
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++)
fields[i].write(gcp, out);
out.writeShort(methods.length);
for (int i = 0; i < methods.length; i++)
methods[i].write(gcp, out);
writeAttributes(gcp, out);
}
/**
* Loads the contents of a class from its class path.
* @param howMuch The amount of information that should be loaded
* at least, one of {@link #OUTERCLASS}, {@link #HIERARCHY}, {@link
* #PUBLICDECLARATIONS}, {@link #DECLARATIONS}, {@link #NODEBUG},
* {@link #ALMOSTALL} or {@link #ALL}. Note that more information
* than requested can be loaded if this is convenient.
* @exception ClassFormatException if the file doesn't denote a
* valid class.
* @exception FileNotFoundException if class wasn't found in classpath.
* @exception IOException if an io exception occured while reading
* the class.
* @exception SecurityException if a security manager prohibits loading
* the class.
* @exception IllegalStateException if this ClassInfo was modified by
* calling one of the setSomething methods.
*/
public void load(int howMuch)
throws IOException
{
if (modified)
throw new IllegalStateException(name);
if (status >= howMuch)
return;
if (classpath.loadClass(this, howMuch)) {
if (status < howMuch)
throw new IllegalStateException("state = "+status);
return;
}
throw new FileNotFoundException(name);
}
/**
* Guess the contents of a class. This is a last resort if the
* file can't be read by the class path. It generates outer class
* information based on the class name, assumes that the class
* extends java.lang.Object, implements no interfaces and has no
* fields, methods or inner classes.
*
* @param howMuch The amount of information that should be read,
* e.g. {@link #HIERARCHY}.
* @see #OUTERCLASS
* @see #HIERARCHY
* @see #PUBLICDECLARATIONS
* @see #DECLARATIONS
* @see #ALMOSTALL
* @see #ALL
*/
public void guess(int howMuch)
{
if (howMuch <= status)
throw new IllegalStateException("status = "+status);
isGuessed = true;
if (howMuch >= OUTERCLASS) {
modifiers = Modifier.PUBLIC | 0x20;
int dollar = name.lastIndexOf('$');
if (dollar == -1) {
/* normal class */
} else if (Character.isDigit(name.charAt(dollar+1))) {
/* anonymous class */
methodScoped = true;
} else {
className = name.substring(dollar+1);
int prevDollar = name.lastIndexOf('$', dollar);
if (prevDollar >= 0
&& Character.isDigit(name.charAt(prevDollar))) {
/* probably method scoped class, (or inner class
* of anoymous class) */
methodScoped = true;
outerClass = classpath.getClassInfo
(name.substring(0, prevDollar));
} else {
/* inner class, we assume it is static, so we don't
* get an exception when we search for the this$0
* parameter in an constructor invocation.
*/
modifiers |= Modifier.STATIC;
outerClass = classpath.getClassInfo
(name.substring(0, dollar));
}
}
}
if (howMuch >= HIERARCHY) {
if (name.equals("java.lang.Object"))
superclass = null;
else
superclass = classpath.getClassInfo("java.lang.Object");
interfaces = new ClassInfo[0];
}
if (howMuch >= PUBLICDECLARATIONS) {
methods = new MethodInfo[0];
fields = new FieldInfo[0];
innerClasses = EMPTY_INNER;
}
status = howMuch;
}
/**
* This is the counter part to load and guess. It will drop all
* informations bigger than "keep" and clean up the memory. Note
* that drop should be used with care if more than one thread
* accesses this ClassInfo.
* @param keep tells how much info we should keep, can be
* {@link #NONE} or anything that load
accepts.
* @see #load
*/
public void drop(int keep) {
if (status <= keep)
return;
if (modified) {
System.err.println("Dropping info between " + keep + " and "
+ status + " in modified class " + this + ".");
Thread.dumpStack();
return;
}
if (keep < HIERARCHY) {
superclass = null;
interfaces = null;
}
if (keep < OUTERCLASS) {
methodScoped = false;
outerClass = null;
innerClasses = null;
}
if (keep < PUBLICDECLARATIONS) {
fields = null;
methods = null;
status = keep;
} else {
if (status >= DECLARATIONS)
/* We don't drop non-public declarations, since this
* is not worth it.
*/
keep = DECLARATIONS;
for (int i = 0; i < fields.length; i++)
fields[i].drop(keep);
for (int i = 0; i < methods.length; i++)
methods[i].drop(keep);
}
if (keep < ALMOSTALL)
sourceFile = null;
super.drop(keep);
status = keep;
}
/**
* Returns the full qualified name of this class.
* @return the full qualified name of this class, an interned string.
*/
public String getName() {
return name;
}
/**
* Tells whether the information in this class was guessed by a call
* to {@link #guess}.
* @return true if the information was guessed.
*/
public boolean isGuessed() {
return isGuessed;
}
/**
* Returns the java class name of a class, without package or
* outer classes. This is null for an anonymous class. For other
* classes it is the name that occured after the
* class
keyword (provided it was compiled from
* java).
* This need OUTERCLASS information loaded to work properly.
*
* @return the short name of this class. Returns null for
* anonymous classes.
*
* @exception IllegalStateException if OUTERCLASS information wasn't
* loaded yet. */
public String getClassName() {
if (status < OUTERCLASS)
throw new IllegalStateException("status is "+status);
if (className != null || isMethodScoped())
return className;
int dot = name.lastIndexOf('.');
return name.substring(dot+1);
}
/**
* Returns the ClassInfo object for the super class.
* @return the short name of this class.
* @exception IllegalStateException if HIERARCHY information wasn't
* loaded yet.
*/
public ClassInfo getSuperclass() {
if (status < HIERARCHY)
throw new IllegalStateException("status is "+status);
return superclass;
}
/**
* Returns the ClassInfo object for the super class.
* @return the short name of this class.
* @exception IllegalStateException if HIERARCHY information wasn't
* loaded yet.
*/
public ClassInfo[] getInterfaces() {
if (status < HIERARCHY)
throw new IllegalStateException("status is "+status);
return interfaces;
}
/**
* Gets the modifiers of this class, e.g. public or abstract. The
* information is only available if at least {@link #HIERARCHY} is
* loaded.
* @return a bitboard of the modifiers.
* @see Class#getModifiers
* @see Modifier
*/
public int getModifiers() {
if (modifiers == -1)
throw new IllegalStateException("status is "+status);
return modifiers;
}
/**
* Checks whether this class info represents an interface. The
* information is only available if at least {@link #HIERARCHY} is
* loaded.
* @return true if this class info represents an interface.
*/
public boolean isInterface() {
return Modifier.isInterface(getModifiers());
}
/**
* Searches for a field with given name and type signature.
* @param name the name of the field.
* @param typeSig the {@link TypeSignature type signature} of the
* field.
* @return the field info for the field.
*/
public FieldInfo findField(String name, String typeSig) {
if (status < PUBLICDECLARATIONS)
throw new IllegalStateException("status is "+status);
for (int i = 0; i < fields.length; i++)
if (fields[i].getName().equals(name)
&& fields[i].getType().equals(typeSig))
return fields[i];
return null;
}
/**
* Searches for a method with given name and type signature.
* @param name the name of the method.
* @param typeSig the {@link TypeSignature type signature} of the
* method.
* @return the method info for the method.
*/
public MethodInfo findMethod(String name, String typeSig) {
if (status < PUBLICDECLARATIONS)
throw new IllegalStateException("status is "+status);
for (int i = 0; i < methods.length; i++)
if (methods[i].getName().equals(name)
&& methods[i].getType().equals(typeSig))
return methods[i];
return null;
}
/**
* Gets the methods of this class.
*/
public MethodInfo[] getMethods() {
if (status < PUBLICDECLARATIONS)
throw new IllegalStateException("status is "+status);
return methods;
}
/**
* Gets the fields (class and member variables) of this class.
*/
public FieldInfo[] getFields() {
if (status < PUBLICDECLARATIONS)
throw new IllegalStateException("status is "+status);
return fields;
}
/**
* Returns the outer class of this class if it is an inner class.
* This needs the OUTERCLASS information loaded.
* @return The class that declared this class, null if the class
* isn't declared in a class scope
*
* @exception IllegalStateException if OUTERCLASS information
* wasn't loaded yet.
*/
public ClassInfo getOuterClass() {
if (status < OUTERCLASS)
throw new IllegalStateException("status is "+status);
return outerClass;
}
/**
* Tells whether the class was declared inside a method.
* This needs the OUTERCLASS information loaded.
* @return true if this is a method scoped or an anonymous class,
* false otherwise.
*
* @exception IllegalStateException if OUTERCLASS information
* wasn't loaded yet.
*/
public boolean isMethodScoped() {
if (status < OUTERCLASS)
throw new IllegalStateException("status is "+status);
return methodScoped;
}
/**
* Gets the inner classes declared in this class.
* This needs at least PUBLICDECLARATION information loaded.
* @return an array containing the inner classes, guaranteed != null.
* @exception IllegalStateException if PUBLICDECLARATIONS information
* wasn't loaded yet.
*/
public ClassInfo[] getClasses() {
if (status < PUBLICDECLARATIONS)
throw new IllegalStateException("status is "+status);
return innerClasses;
}
public String getSourceFile() {
return sourceFile;
}
/**
* Sets the name of this class info. Note that by changing the
* name you may overwrite an already loaded class. This can have
* ugly effects, as references to that overwritten class may still
* exist.
*/
public void setName(String newName) {
/* The class name is used as index in the hash table. We have
* to update the class path and tell it about the name change.
*/
classpath.renameClassInfo(this, newName);
name = newName.intern();
status = ALL;
modified = true;
}
public void setSuperclass(ClassInfo newSuper) {
superclass = newSuper;
status = ALL;
modified = true;
}
public void setInterfaces(ClassInfo[] newIfaces) {
interfaces = newIfaces;
status = ALL;
modified = true;
}
public void setModifiers(int newModifiers) {
modifiers = newModifiers;
status = ALL;
modified = true;
}
public void setMethods(MethodInfo[] mi) {
methods = mi;
status = ALL;
modified = true;
}
public void setFields(FieldInfo[] fi) {
fields = fi;
status = ALL;
modified = true;
}
public void setOuterClass(ClassInfo oc) {
outerClass = oc;
status = ALL;
modified = true;
}
public void setMethodScoped(boolean ms) {
methodScoped = ms;
status = ALL;
modified = true;
}
public void setClasses(ClassInfo[] ic) {
innerClasses = ic.length == 0 ? EMPTY_INNER : ic;
status = ALL;
modified = true;
}
public void setSourceFile(String newSource) {
sourceFile = newSource;
status = ALL;
modified = true;
}
/**
* Gets the serial version UID of this class. If a final static
* long serialVersionUID field is present, its constant value
* is returned. Otherwise the UID is calculated with the algorithm
* in the serial version spec.
* @return the serial version UID of this class.
* @exception IllegalStateException if DECLARATIONS aren't loaded.
* @exception NoSuchAlgorithmException if SHA-1 message digest is not
* supported (needed for calculation of UID.
*/
public long getSerialVersionUID() throws NoSuchAlgorithmException {
if (status < DECLARATIONS)
throw new IllegalStateException("status is "+status);
FieldInfo fi = findField("serialVersionUID", "J");
if (fi != null
&& ((fi.getModifiers() & (Modifier.STATIC | Modifier.FINAL))
== (Modifier.STATIC | Modifier.FINAL))
&& fi.getConstant() != null)
return ((Long) fi.getConstant()).longValue();
final MessageDigest md = MessageDigest.getInstance("SHA");
OutputStream digest = new OutputStream() {
public void write(int b) {
md.update((byte) b);
}
public void write(byte[] data, int offset, int length) {
md.update(data, offset, length);
}
};
DataOutputStream out = new DataOutputStream(digest);
try {
out.writeUTF(this.name);
// just look at interesting bits of modifiers
int modifs = javaModifiersToBytecode(this.modifiers)
& (Modifier.ABSTRACT | Modifier.FINAL
| Modifier.INTERFACE | Modifier.PUBLIC);
out.writeInt(modifs);
ClassInfo[] interfaces = (ClassInfo[]) this.interfaces.clone();
Arrays.sort(interfaces);
for (int i = 0; i < interfaces.length; i++)
out.writeUTF(interfaces[i].name);
FieldInfo[] fields = (FieldInfo[]) this.fields.clone();
Arrays.sort(fields);
for (int i = 0; i < fields.length; i++) {
modifs = fields[i].getModifiers();
if ((modifs & Modifier.PRIVATE) != 0
&& (modifs & (Modifier.STATIC
| Modifier.TRANSIENT)) != 0)
continue;
out.writeUTF(fields[i].getName());
out.writeInt(modifs);
out.writeUTF(fields[i].getType());
}
MethodInfo[] methods = (MethodInfo[]) this.methods.clone();
Arrays.sort(methods);
for (int i = 0; i < methods.length; i++) {
modifs = methods[i].getModifiers();
/* The modifiers of