001: /*
002: * @(#)ModifiedClass.java
003: *
004: * Copyright (C) 2002-2004 Matt Albrecht
005: * groboclown@users.sourceforge.net
006: * http://groboutils.sourceforge.net
007: *
008: * Permission is hereby granted, free of charge, to any person obtaining a
009: * copy of this software and associated documentation files (the "Software"),
010: * to deal in the Software without restriction, including without limitation
011: * the rights to use, copy, modify, merge, publish, distribute, sublicense,
012: * and/or sell copies of the Software, and to permit persons to whom the
013: * Software is furnished to do so, subject to the following conditions:
014: *
015: * The above copyright notice and this permission notice shall be included in
016: * all copies or substantial portions of the Software.
017: *
018: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
019: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
020: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
021: * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
022: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
023: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
024: * DEALINGS IN THE SOFTWARE.
025: */
026:
027: package net.sourceforge.groboutils.codecoverage.v2.compiler;
028:
029: import java.io.ByteArrayInputStream;
030: import java.io.ByteArrayOutputStream;
031: import java.io.IOException;
032: import java.util.ArrayList;
033: import java.util.HashMap;
034: import java.util.List;
035: import java.util.Map;
036:
037: import net.sourceforge.groboutils.codecoverage.v2.datastore.AnalysisModuleSet;
038: import net.sourceforge.groboutils.codecoverage.v2.datastore.ClassRecord;
039: import net.sourceforge.groboutils.codecoverage.v2.util.ChecksumUtil;
040: import net.sourceforge.groboutils.codecoverage.v2.util.ClassSignatureUtil;
041:
042: import org.apache.bcel.classfile.Attribute;
043: import org.apache.bcel.classfile.ClassParser;
044: import org.apache.bcel.classfile.JavaClass;
045: import org.apache.bcel.classfile.Method;
046: import org.apache.bcel.classfile.SourceFile;
047: import org.apache.bcel.generic.ConstantPoolGen;
048: import org.apache.bcel.generic.MethodGen;
049:
050: /**
051: * Refers to a class file that has been modified with additional logging
052: * statements.
053: *
054: * @author Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
055: * @version $Date: 2004/04/15 05:48:25 $
056: * @since December 17, 2002
057: */
058: public class ModifiedClass {
059: private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
060: .getLogger(ModifiedClass.class);
061:
062: // split these two strings apart so that this class itself can
063: // be post-compiled.
064: private static final String ALREADY_POST_COMPILED_FLAG_1 = "--..AlreadyPostCompiled";
065: private static final String ALREADY_POST_COMPILED_FLAG_2 =
066: // had to switch this from a char 0 due to parsing errors in
067: // java - it doesn't like finding a '0' in a class file string.
068: "..--" + (char) 1;
069: private static final String ALREADY_POST_COMPILED_FLAG;
070: static {
071: // don't let a compiler optimization create this string in this
072: // class, so that this class can be instrumented.
073: StringBuffer sb = new StringBuffer(ALREADY_POST_COMPILED_FLAG_1);
074: sb.append(ALREADY_POST_COMPILED_FLAG_2);
075: ALREADY_POST_COMPILED_FLAG = new String(sb);
076: }
077:
078: private String className;
079: private JavaClass origClass;
080: //private ClassGen modClass;
081: private ConstantPoolGen constantPool;
082: private ModifiedMethod[] modMethods;
083: private byte[] finalModifiedClass;
084:
085: /**
086: * constant pool index for the name of the class's signature.
087: */
088: private int classSigPoolIndex;
089:
090: /**
091: * constant pool index for the method-ref for invoking the logger.
092: */
093: private int staticMethodPoolIndex;
094:
095: /**
096: * the original class file's checksum.
097: */
098: private long checksum;
099:
100: /**
101: * We can't allow coverage of every method: some shouldn't be allowed,
102: * such as javac-generated methods like "class$".
103: */
104: private static final String[] IGNORE_METHODS = { "class$(Ljava/lang/String;)Ljava/lang/Class;", };
105:
106: /**
107: * Uses the default settings for the <tt>ParseCoverageLogger</tt>
108: * during the creation of the modified class.
109: *
110: * @param filename the name of the file the class file was pulled from.
111: * @throws IllegalStateException if the class file has already been
112: * modified (identified by a class name field).
113: */
114: public ModifiedClass(String filename, byte[] originalClassFile)
115: throws IOException {
116: this (new ParseCoverageLogger(), filename, originalClassFile);
117: }
118:
119: /**
120: * @param pcl the definition for the logger to create method references
121: * for. This allows for easier testing and customizations of the
122: * logger to compile for.
123: * @param filename the name of the file the class file was pulled from.
124: * @throws IllegalStateException if the class file has already been
125: * modified (identified by a class name field).
126: */
127: public ModifiedClass(ParseCoverageLogger pcl, String filename,
128: byte[] originalClassFile) throws IOException {
129: if (originalClassFile == null || filename == null
130: || pcl == null) {
131: throw new IllegalArgumentException("No null args.");
132: }
133:
134: updateChecksum(originalClassFile);
135: updateClassGen(pcl, originalClassFile, filename);
136: }
137:
138: /**
139: *
140: */
141: public String getClassName() {
142: return this .className;
143: }
144:
145: /**
146: *
147: */
148: public long getClassCRC() {
149: return this .checksum;
150: }
151:
152: /**
153: *
154: */
155: public String getClassSignature() {
156: return ClassSignatureUtil.getInstance().createClassSignature(
157: getClassName(), getClassCRC());
158: }
159:
160: /**
161: *
162: */
163: public String getSourceFileName() {
164: String ret = "";
165: Attribute attr[] = this .origClass.getAttributes();
166: for (int i = 0; i < attr.length; ++i) {
167: if (attr[i] instanceof SourceFile) {
168: ret = ((SourceFile) attr[i]).getSourceFileName();
169: break;
170: }
171: }
172: return ret;
173: }
174:
175: /**
176: *
177: */
178: public ClassRecord createClassRecord(AnalysisModuleSet ams) {
179: ModifiedMethod mmL[] = getMethods();
180: String methodSigs[] = new String[mmL.length];
181: for (int i = 0; i < mmL.length; ++i) {
182: methodSigs[mmL[i].getMethodIndex()] = mmL[i]
183: .getMethodName();
184: }
185: return new ClassRecord(getClassName(), getClassCRC(),
186: getSourceFileName(), methodSigs, ams);
187: }
188:
189: /**
190: * Return all modifiable methods owned by the class. This will not return
191: * methods deemed non-modifiable, such as abstract or native methods, or
192: * javac-generated methods.
193: */
194: public ModifiedMethod[] getMethods() {
195: if (this .modMethods == null) {
196: checkClose();
197:
198: Method mL[] = this .origClass.getMethods();
199: List methods = new ArrayList();
200: short methodIndex = 0;
201: for (int i = 0; i < mL.length; ++i) {
202: if (allowModification(mL[i])) {
203: ModifiedMethod mm = new ModifiedMethod(methodIndex,
204: this .classSigPoolIndex,
205: this .staticMethodPoolIndex, this .origClass,
206: mL[i], new MethodGen(mL[i], this .className,
207: this .constantPool));
208: methods.add(mm);
209: ++methodIndex;
210: }
211: }
212: this .modMethods = (ModifiedMethod[]) methods
213: .toArray(new ModifiedMethod[methods.size()]);
214: }
215: return this .modMethods;
216: }
217:
218: /**
219: * Returns the class in its current state, and closes off the modified
220: * class from further modifications.
221: */
222: public byte[] getModifiedClass() {
223: if (this .finalModifiedClass == null) {
224: checkClose();
225:
226: // commit the current changes to the generated class
227: updateClass();
228:
229: ByteArrayOutputStream baos = new ByteArrayOutputStream();
230: try {
231: this .origClass.dump(baos);
232: } catch (IOException e) {
233: throw new IllegalStateException(
234: "This should never be reached.");
235: }
236:
237: // close off the modified class from further modification
238: this .origClass = null;
239:
240: this .finalModifiedClass = baos.toByteArray();
241: }
242: return this .finalModifiedClass;
243: }
244:
245: /**
246: * String that gets placed within class files to flag that they've
247: * been post-compiled.
248: */
249: public static final String getPostCompiledSignature() {
250: return ALREADY_POST_COMPILED_FLAG;
251: }
252:
253: //-------------------------------------------------------------------------
254: // private members
255:
256: /**
257: * This method commits any changes to the class file, and closes the
258: * class off to any more changes. It can be called multiple times
259: * without error.
260: */
261: private void updateClass() {
262: // update the methods
263: ModifiedMethod mL[] = this .modMethods;
264: if (mL == null) {
265: // we've already committed the methods, or no modifications
266: // were done to the methods.
267: return;
268: }
269:
270: // we're committing the methods, so close off our list
271: this .modMethods = null;
272:
273: // create a map of all our modified methods.
274: Map mLmap = new HashMap();
275: for (int i = 0; i < mL.length; ++i) {
276: // commit the methods and remove them from our list.
277: ModifiedMethod m = mL[i];
278: mL[i] = null;
279: m.close();
280:
281: mLmap.put(m.getMethodName(), m.getNewMethod());
282: }
283:
284: // Replace the original methods with the modified methods.
285: Method origMethods[] = this .origClass.getMethods();
286: for (int i = 0; i < origMethods.length; ++i) {
287: Method m = (Method) mLmap
288: .get(createSignature(origMethods[i]));
289: if (m != null) {
290: LOG.debug("replacing method [" + origMethods[i] + "].");
291: origMethods[i] = m;
292: }
293: }
294: this .origClass.setMethods(origMethods);
295:
296: this .origClass.setConstantPool(this .constantPool
297: .getFinalConstantPool());
298: }
299:
300: /**
301: *
302: */
303: private void updateClassGen(ParseCoverageLogger pcl, byte[] code,
304: String filename) throws IOException {
305: ByteArrayInputStream bais = new ByteArrayInputStream(code);
306: ClassParser cp = new ClassParser(bais, filename);
307: this .origClass = cp.parse();
308: this .className = this .origClass.getClassName();
309: //this.modClass = new ClassGen( this.origClass );
310: //this.modClass.setMajor( this.origClass.getMajor() );
311: //this.modClass.setMinor( this.origClass.getMinor() );
312: this .constantPool = new ConstantPoolGen(this .origClass
313: .getConstantPool());
314:
315: addClassSignature(this .constantPool);
316: addMethodRef(pcl, this .constantPool);
317: }
318:
319: /**
320: * Adds the class signature to the constant pool of the class.
321: *
322: * @throws IllegalStateException if the class file has already been
323: * modified (identified by a class name field).
324: */
325: private void addClassSignature(ConstantPoolGen pool) {
326: if (pool == null) {
327: throw new IllegalArgumentException("No null args.");
328: }
329:
330: // check if the class signature has already been set
331: // see bug 903837
332: if (pool.lookupUtf8(getPostCompiledSignature()) != -1) {
333: throw new AlreadyPostCompiledException("Class '"
334: + this .className + "' has already been modified.");
335: }
336:
337: // add constant pool name reference
338: this .classSigPoolIndex = pool.addString(getClassSignature());
339: LOG.debug("Added pool index " + this .classSigPoolIndex
340: + " pointing to '" + getClassSignature());
341:
342: // and add the signature that the class has been post-compiled
343: pool.addString(getPostCompiledSignature());
344: }
345:
346: /**
347: *
348: */
349: private void addMethodRef(ParseCoverageLogger pcl,
350: ConstantPoolGen pool) {
351: if (pool == null || pcl == null) {
352: throw new IllegalArgumentException("No null args.");
353: }
354:
355: LOG.debug("Adding methodref to pool for method: "
356: + pcl.getClassName() + " # " + pcl.getMethodName()
357: + ": " + pcl.getMethodSignature());
358: this .staticMethodPoolIndex = pool.addMethodref(pcl
359: .getClassName(), pcl.getMethodName(), pcl
360: .getMethodSignature());
361: LOG.debug("Methodref pool index = "
362: + this .staticMethodPoolIndex);
363: }
364:
365: /**
366: *
367: */
368: private void updateChecksum(byte[] code) {
369: if (code == null) {
370: throw new IllegalArgumentException("no null args");
371: }
372: this .checksum = ChecksumUtil.getInstance().checksum(code);
373: }
374:
375: /**
376: *
377: */
378: private void checkClose() {
379: if (this .origClass == null) {
380: throw new IllegalStateException(
381: "Class has been closed from further modifications.");
382: }
383: }
384:
385: /**
386: * Returns <tt>true</tt> if the method can be modified, otherwise returns
387: * <tt>false</tt>. A method can be modified only if it is not native,
388: * not abstract, it has a code attribute, and is not one of the "ignored
389: * methods" defined at the top of this class.
390: */
391: private boolean allowModification(Method m) {
392: if (!isMarkable(m)) {
393: return false;
394: }
395:
396: String sig = createSignature(m);
397: for (int i = 0; i < IGNORE_METHODS.length; ++i) {
398: if (IGNORE_METHODS[i].equals(sig)) {
399: return false;
400: }
401: }
402: return true;
403: }
404:
405: /**
406: * Checks if the given method is markable only from the context of the
407: * method contents, not by looking at the signature.
408: */
409: static boolean isMarkable(Method m) {
410: return (!m.isNative() && !m.isAbstract() && m.getCode() != null);
411: }
412:
413: /**
414: *
415: */
416: private String createSignature(Method m) {
417: String sig = m.getName() + m.getSignature();
418: return sig;
419: }
420: }
|