001: package csdl.jblanket.modifier;
002:
003: import java.io.File;
004: import java.io.FileInputStream;
005: import java.util.ArrayList;
006: import java.util.Date;
007:
008: import org.apache.bcel.Constants;
009: import org.apache.bcel.classfile.Method;
010: import org.apache.bcel.generic.ALOAD;
011: import org.apache.bcel.generic.ASTORE;
012: import org.apache.bcel.generic.ConstantPoolGen;
013: import org.apache.bcel.generic.InstructionConstants;
014: import org.apache.bcel.generic.InstructionFactory;
015: import org.apache.bcel.generic.InstructionList;
016: import org.apache.bcel.generic.LineNumberGen;
017: import org.apache.bcel.generic.LocalVariableGen;
018: import org.apache.bcel.generic.MethodGen;
019: import org.apache.bcel.generic.ObjectType;
020: import org.apache.bcel.generic.POP;
021: import org.apache.bcel.generic.PUSH;
022: import org.apache.bcel.generic.Type;
023:
024: import csdl.jblanket.methodset.MethodInfo;
025: import csdl.jblanket.methodset.MethodSet;
026: import csdl.jblanket.methodset.MethodSetManager;
027: import csdl.jblanket.util.MethodCategories;
028:
029: /**
030: * Provides a Modifier for methods in .class files. Methods are either instrumented or recorded
031: * for exclusion from coverage. Modifying a method here is performed unconditionally. Therefore,
032: * without first checking at the ClassModifier level to see if a class as a whole has been
033: * previously modified can result in numerous modifications to a method.
034: * <p>
035: * Recording a method for exclusion from coverage leaves its byte code unchanged.
036: *
037: * @author Joy M. Agustin
038: * @version $Id: MethodModifier.java,v 1.5 2005/03/08 08:02:01 timshadel Exp $
039: */
040: public class MethodModifier {
041:
042: /** Method to modify */
043: private MethodGen method;
044:
045: /** Grammar for names of test classes */
046: private String testGrammar;
047:
048: /** Describes if one-line methods should be excluded from the coverage measurement */
049: private boolean excludeOneLineMethods;
050: /** Describes if constructors should be excluded from the coverage measurement */
051: private boolean excludeConstructors;
052:
053: /** Container for all MethodSets */
054: private MethodSetManager manager;
055: /** Container for all method categories */
056: private MethodCategories categories;
057:
058: private MethodSet excludedIndividualSet;
059:
060: private boolean excludeIndividualMethods;
061:
062: /**
063: * Constructs a new MethodModifier object.
064: * @param verbose describes if JBlanket should execute in verbose mode.
065: * @param testGrammar the grammar describing test class names.
066: * @param excludeOneLineMethods describes exclusion of one-line methods.
067: * @param excludeIndividualMethods describes exclusion of individual methods.
068: * @param excludeConstructors describes exclusion of constructors.
069: * @param method the method to modify.
070: */
071: public MethodModifier(boolean verbose, String testGrammar,
072: boolean excludeOneLineMethods,
073: boolean excludeIndividualMethods,
074: boolean excludeConstructors, MethodGen method) {
075:
076: this .method = method;
077:
078: this .testGrammar = testGrammar;
079:
080: this .excludeOneLineMethods = excludeOneLineMethods;
081: this .excludeConstructors = excludeConstructors;
082:
083: this .manager = MethodSetManager.getInstance();
084: this .categories = MethodCategories.getInstance();
085:
086: this .excludeIndividualMethods = excludeIndividualMethods;
087: this .excludedIndividualSet = new MethodSet();
088: if (this .excludeIndividualMethods) {
089: File excludeIndividualFile = new File(this .categories
090: .getFileName("excludedIndividualFile"));
091: // file will not exist if this is the first time excluding individual files
092: if (excludeIndividualFile.exists()) {
093: loadMethods(this .excludedIndividualSet,
094: excludeIndividualFile);
095: } else {
096: this .excludedIndividualSet = new MethodSet();
097: }
098: }
099: }
100:
101: /**
102: * Processes a method. The method represented by this MethodModifier is not modified if it is
103: * abstract or native. Furthermore, it is not modified if <code>excludeConstructors</code> or
104: * <code>excludeOneLineMethods</code> is set. Otherwise, the this method is modified.
105: *
106: * @param pool the ConstantPoolGen for this method.
107: * @param isModified describes if this method was previously modified.
108: * @return modified <code>Method</code> version of this MethodModified object.
109: */
110: public Method processMethod(ConstantPoolGen pool, boolean isModified) {
111:
112: if (isExcluded()) {
113: return this .method.getMethod();
114: }
115:
116: if (isConstructor() || !isMethodUntestable()) {
117:
118: if (!hasLineNumbers()) {
119: String methodName = this .method.getClassName() + "."
120: + this .method.getName();
121: String message = "No line numbers detected in "
122: + methodName
123: + ". "
124: + "Either remove the 'oneLineFile' tag or turn debug on when compiling.";
125: throw new UnsupportedOperationException(message);
126: }
127:
128: // exclude constructors
129: if (this .excludeConstructors && isConstructor()) {
130: excludeMethod(manager.getMethodSet(categories
131: .getFileName("constructorFile")));
132: }
133: // exclude one-line methods, include all constructors since not considered as methods
134: else if (this .excludeOneLineMethods && isOneLine()
135: && !isConstructor()) {
136: excludeMethod(manager.getMethodSet(categories
137: .getFileName("oneLineFile")));
138: }
139: // modify all other methods if they were not previously modified
140: else if (!isModified) {
141: return modifyMethod(pool);
142: }
143: }
144: // exclude untestable methods
145: else {
146: excludeMethod(manager.getMethodSet(categories
147: .getFileName("untestableFile")));
148: }
149:
150: return this .method.getMethod();
151: }
152:
153: /**
154: * @return true if the current method is excluded.
155: */
156: private boolean isExcluded() {
157: return (excludeIndividualMethods && excludedIndividualSet
158: .contains(getMethodInfo()));
159: }
160:
161: /**
162: * @return true if method is abstract, native, or an empty void method.
163: */
164: private boolean isMethodUntestable() {
165: if (this .method.isAbstract()) {
166: return true;
167: }
168: if (this .method.isNative()) {
169: return true;
170: }
171: // get line numbers to check size of method
172: LineNumberGen[] lineNumbers = this .method.getLineNumbers();
173:
174: // method body - blank
175: // one line - compiler inserted return statement (?)
176: if (this .method.getReturnType() == Type.VOID
177: && lineNumbers.length == 1) {
178: return true;
179: }
180:
181: return false;
182: }
183:
184: /**
185: * Checks if a method contains line numbers. If so, then it is valid. If not, then the method
186: * is invalid. Line numbers are added by enabling debugging when compiling.
187: *
188: * @return true if <code>method</code> contains line numbers, false otherwise.
189: */
190: public boolean hasLineNumbers() {
191:
192: LineNumberGen[] lineNumbers = this .method.getLineNumbers();
193: if (lineNumbers.length == 0) {
194: return false;
195: }
196:
197: return true;
198: }
199:
200: /**
201: * Checks if <code>method</code> is a constructor. It does not matter if it's implemented by
202: * the programmer or automatically generated by the compiler. If it is a constructor,
203: * <code>method</code> is stored. Valid constructor method name is <init>.
204: *
205: * @return true if <code>method</code> is a constructor, false otherwise.
206: */
207: public boolean isConstructor() {
208:
209: if ("<init>".equals(this .method.getName())) {
210: return true;
211: }
212:
213: return false;
214: }
215:
216: /**
217: * Checks if <code>method</code> contains one line of source code. If so, <code>method</code>
218: * is stored.
219: *
220: * @return true if <code>method</code> contains one line, false otherwise.
221: */
222: public boolean isOneLine() {
223:
224: // get line numbers to check size of method
225: LineNumberGen[] lineNumbers = this .method.getLineNumbers();
226:
227: // one line - method body
228: // one line - compiler inserted return statement (?)
229: if (this .method.getReturnType() == Type.VOID
230: && lineNumbers.length == 2) {
231: return true;
232: }
233: // one line - return statement
234: else if (this .method.getReturnType() != Type.VOID
235: && lineNumbers.length == 1) {
236: return true;
237: }
238:
239: return false;
240: }
241:
242: /**
243: * Modifies <code>method</code> to store its type signature when invoked during testing.
244: * The modification inserts a call to <code>MethodCollector.storeMethodTypeSignature</code>
245: * at the beginning of <code>method</code>.
246: *
247: * @param pool the ConstantPool generator of class containing <code>method</code>.
248: * @return the modified method.
249: */
250: private Method modifyMethod(ConstantPoolGen pool) {
251:
252: InstructionList oldList = method.getInstructionList();
253:
254: // create newList to add to oldList
255: InstructionList newList = null;
256: newList = addStoreMethodTypeSignature(pool, method);
257:
258: // insert new list before old list
259: oldList.append(oldList.getStart(), newList);
260: this .method.setInstructionList(oldList);
261: this .method.setMaxStack();
262: Method m = this .method.getMethod();
263:
264: // REMINDER: must do method.getMethod() before can free memory
265: oldList.dispose();
266: newList.dispose();
267:
268: return m;
269: }
270:
271: /**
272: * Creates an InstructionList for inserting the static method call to
273: * <code>MethodCollector.storeMethodTypeSignature</code>. From the byte code, this additional
274: * method call records the name of the class <code>method</code> belongs to, the name of
275: * <code>method</code>, and the list of <code>method</code>'s parameter types.
276: *
277: * @param pool the ContantPool generator of the class containing <code>method</code>.
278: * @param method the method to alter.
279: * @return list of instructions for modification.
280: */
281: private InstructionList addStoreMethodTypeSignature(
282: ConstantPoolGen pool, MethodGen method) {
283:
284: // get elements of method's type signature
285: String methodName = method.getName();
286: String className = method.getClassName();
287: Type[] types = method.getArgumentTypes();
288:
289: // common objects used below
290: String arrayList = "java.util.ArrayList";
291: String reportClass = "csdl.jblanket.modifier.MethodCollector";
292: ObjectType arrayObject = new ObjectType(arrayList);
293:
294: // objects used for adding new instructions to byte code
295: InstructionFactory factory = new InstructionFactory(pool);
296: InstructionList newList = new InstructionList();
297: LocalVariableGen variable;
298:
299: // create new "java.util.ArrayList" instance "params" in method to store method type signatures
300: variable = method.addLocalVariable("params", new ObjectType(
301: arrayList), null, null);
302: int params = variable.getIndex();
303: newList.append(factory.createNew(arrayList));
304: newList.append(InstructionConstants.DUP);
305: newList.append(factory.createInvoke(arrayList, "<init>",
306: Type.VOID, Type.NO_ARGS, Constants.INVOKESPECIAL));
307: variable.setStart(newList.append(new ASTORE(params)));
308:
309: // add instructions to add array elements when byte code is executed
310: for (int i = 0; i < types.length; i++) {
311: newList.append(new ALOAD(params));
312: newList.append(new PUSH(pool, types[i].getSignature()));
313: newList.append(factory.createInvoke(arrayList, "add",
314: Type.BOOLEAN, new Type[] { Type.OBJECT },
315: Constants.INVOKEVIRTUAL));
316: // pop off the boolean return type from "add"
317: newList.append(new POP());
318: }
319:
320: // using MethodCollector, call static method that stores the method type signature
321: Type[] invokeMethodParams = new Type[] { Type.STRING,
322: Type.STRING, arrayObject, Type.STRING };
323: newList.append(new PUSH(pool, className));
324: newList.append(new PUSH(pool, methodName));
325: newList.append(new ALOAD(params));
326: newList.append(new PUSH(pool, this .testGrammar));
327: newList.append(factory.createInvoke(reportClass,
328: "storeMethodTypeSignature", Type.VOID,
329: invokeMethodParams, Constants.INVOKESTATIC));
330:
331: return newList;
332: }
333:
334: /**
335: * Records this method to exclude from coverage and stores it in <code>excludeSet</code>.
336: *
337: * @param excludeSet the container storing methods to exclude from coverage.
338: */
339: public void excludeMethod(MethodSet excludeSet) {
340: MethodInfo methodInfo = getMethodInfo();
341: excludeSet.add(methodInfo);
342: }
343:
344: /**
345: * @return Construct a MethodInfo object for the current method.
346: */
347: private MethodInfo getMethodInfo() {
348: String className = this .method.getClassName();
349: String methodName = this .method.getName();
350: Type[] paramTypes = this .method.getArgumentTypes();
351: ArrayList paramList = new ArrayList();
352: for (int i = 0; i < paramTypes.length; i++) {
353: paramList.add(MethodCollector.reconstructType(paramTypes[i]
354: .getSignature()));
355: }
356: // reconstruct names of constructors from default names
357: if ("<init>".equals(methodName)) {
358: methodName = MethodCollector.removePackagePrefix(className);
359: }
360: if ("<clinit>".equals(methodName)) {
361: methodName = MethodCollector.removePackagePrefix(className)
362: + "[static initializer]";
363: }
364: methodName = methodName.replaceAll("<", "_").replaceAll(">",
365: "_");
366: MethodInfo methodInfo = new MethodInfo(className, methodName,
367: paramList);
368: return methodInfo;
369: }
370:
371: /**
372: * Loads all the method information into <code>jblanketSet</code> from <code>file</code>
373: * in the jblanket.dir system property.
374: *
375: * @param methodSet the collection of method information to load.
376: * @param file the input file.
377: * @return the Date found in <code>file</code>.
378: */
379: private Date loadMethods(MethodSet methodSet, File file) {
380:
381: Date timeStamp = null;
382: try {
383: // throws FileNotFoundException
384: FileInputStream in = new FileInputStream(file);
385: // throws PareseException
386: timeStamp = methodSet.load(in);
387: // throws IOException
388: in.close();
389: } catch (Exception e) {
390: // TODO: Figure out how to log this
391: //System.out.println("Unable to read file " + file.getAbsolutePath() + ". Exception: " + e);
392: e.printStackTrace();
393: }
394:
395: return timeStamp;
396: }
397: }
|