001: package csdl.jblanket.modifier;
002:
003: import csdl.jblanket.JBlanket;
004: import csdl.jblanket.JBlanketException;
005: import csdl.jblanket.methodset.MethodSet;
006: import csdl.jblanket.methodset.MethodSetManager;
007: import csdl.jblanket.util.JarFactory;
008: import csdl.jblanket.util.MethodCategories;
009:
010: import java.io.File;
011: import java.io.FileInputStream;
012: import java.io.FileNotFoundException;
013: import java.io.FileOutputStream;
014: import java.io.IOException;
015: import java.text.ParseException;
016: import java.util.ArrayList;
017: import java.util.Date;
018: import java.util.Iterator;
019: import java.util.List;
020: import java.util.StringTokenizer;
021:
022: import org.apache.tools.ant.DirectoryScanner;
023:
024: /**
025: * Provides the command line interface to modify byte code in a Java class or JAR files.
026: * This is the first step in using JBlanket.
027: * <p>
028: * The following tags are <b>required</b> command line arguments for the main methods:
029: * <ul>
030: * <p>
031: * 'classDir' - the directory containing all .class or .jar files to modify<br>
032: * <i>For example</i>: -classDir c:\cvs\jblanket\build\classes
033: * <p>
034: * 'include' - file(s) to be modified located in classDir<br>
035: * <i>For example</i>: -include csdl\jblanket\example\foo\Foo.class
036: * <p>
037: * 'exclude' - file(s) to remain untouched in classDir<br>
038: * <i>For example</i>: -exclude csdl\jblanket\example\foo\TestFoo2.class
039: * <p>
040: * 'testGrammar' - grammar describing the names of the Test classes<br>
041: * <i>For example</i>: -testGrammar Test*.class
042: * </ul>
043: * <p>
044: * The following tags are <b>optional</b> command line arguments:
045: * <ul>
046: * <p>
047: * 'verbose' - describes if instrumentation should execute in verbose mode<br>
048: * <i>For example</i>: -verbose true
049: * <p>
050: * 'excludeOneLineMethods' - describes if one-line methods should be excluded<br>
051: * <i>For example</i>: -excludeOneLineMethods false
052: * <p>
053: * 'excludeConstructors' - describes if constructors should be excluded<br>
054: * <i>For example</i>: -excludeConstructors false
055: * <p>
056: * 'totalFile' - name of the XML file for all methods included in the coverage
057: * measurement<br>
058: * <i>For example</i>: -totalFile totalMethods.xml
059: * <p>
060: * 'untestableFile' - name of the XML file for all abstract or native methods<br>
061: * <i>For example</i>: -untestableFile untestableMethods.xml
062: * <p>
063: * 'excludedFile' - name of the XML file for all methods specifically excluded
064: * from the coverage measurement<br>
065: * <i>For example</i>: -excludedFile excludedMethods.xml
066: * <p>
067: * 'oneLineFile' - name of the XML file for methods containing one line of source
068: * code<br>
069: * <i>For example</i>: -oneLineFile oneLineMethods.xml
070: * <p>
071: * 'constructorFile' - name of the XML file for constructors<br>
072: * <i>For example</i>: -constructorFile constructorMethods.xml
073: * <p>
074: * 'packagePrefix' - name of a package prefix to include in the coverage
075: * measurement<br>
076: * <i>For example</i>: -packagePrefix csdl.jblanket.
077: * </ul>
078: * <p>
079: * Classes are modified inside the 'classDir' directory. Specify multiple files with a
080: * ';'-delimited list. Similarly, exclude multiple files with a ';'-delimited list. 'Test*.class'
081: * and '*Test.class' are the only valid grammars to describe the names of JUnit tests.
082: * <p>
083: * Default values provided for 'totalFile', 'untestableFile', 'excludedFile', 'oneLineFile', and
084: * 'constructorFile' are shown in the examples. However, the 'excludeOneLineMethods' or
085: * 'excludeConstructors' tags must be included to exclude methods with one line of source code or
086: * constructors from the final coverage calculation. Without the tags, percent coverage is equal to
087: * <ul>
088: * <pre>
089: * tested methods / (total methods - untestable methods - excluded methods)
090: * </pre>
091: * </ul>
092: * <p>
093: * With the 'excludeOneLineMethods' tag, percent coverage is equal to
094: * <ul>
095: * <pre>
096: * tested methods / ((total methods - untestable methods - excluded methods) - one-line methods)
097: * </pre>
098: * </ul>
099: * <p>
100: * With the 'excludeConstructors' tag, percent coverage is equal to
101: * <ul>
102: * <pre>
103: * tested methods / ((total methods - untestable methods - excluded methods) - constructors)
104: * </pre>
105: * </ul>
106: * <p>
107: * With both the 'excludeOneLineMethod' and 'excludeConstructors' tags, percent coverage is equal to
108: * <ul>
109: * <pre>
110: * tested methods / (((total methods - untestable methods - excluded methods) - one-line methods)
111: * - constructors)
112: * </pre>
113: * </ul>
114: *
115: * @author Joy M. Agustin
116: * @version $Id: Modifier.java,v 1.2 2005/02/19 05:55:19 timshadel Exp $
117: */
118: public class Modifier extends JBlanket {
119:
120: /** Grammar for names of test classes */
121: private String testGrammar = "";
122:
123: /** Container for untestable methods */
124: private MethodSet untestableSet;
125: /** Container for methods in excluded classes */
126: private MethodSet excludedSet;
127:
128: /** List of package prefixes to include in the coverage measurement */
129: private List packagePrefixes;
130:
131: /** Counts the methods found in the system */
132: private MethodCounter counter;
133:
134: /** Current date so that all files will have the same date */
135: private Date date;
136:
137: /**
138: * Constructs a new Modifier object.
139: * @param verbose describes if JBlanket should execute in verbose mode.
140: * @param testGrammar the grammar describing names of JUnit test classes.
141: * @param excludeOneLineMethods describes if one-line methods should be excluded.
142: * @param excludeConstructors describes if constructors should be excluded.
143: * @param excludeIndividualMethods TODO
144: * @param packagePrefixes the package prefixes to be modified.
145: *
146: * @throws ParseException if <code>testGrammar</code> is invalid.
147: */
148: public Modifier(boolean verbose, String testGrammar,
149: boolean excludeOneLineMethods, boolean excludeConstructors,
150: boolean excludeIndividualMethods, List packagePrefixes)
151: throws ParseException {
152: super ();
153: this .verbose = verbose;
154: if (isValidTestGrammar(testGrammar)) {
155: this .testGrammar = testGrammar;
156: }
157:
158: // initialize MethodSets
159: MethodSetManager manager = MethodSetManager.getInstance();
160: super .totalSet = manager.getMethodSet(super .categories
161: .getFileName("totalFile"));
162:
163: this .excludedSet = manager.getMethodSet(super .categories
164: .getFileName("excludedFile"));
165:
166: this .untestableSet = manager.getMethodSet(super .categories
167: .getFileName("untestableFile"));
168:
169: // initialize one-line methods MethodSet if needed
170: super .excludeOneLineMethods = excludeOneLineMethods;
171: if (super .excludeOneLineMethods) {
172: super .oneLineSet = manager.getMethodSet(super .categories
173: .getFileName("oneLineFile"));
174: }
175:
176: // initialize constructors MethodSet if needed
177: super .excludeConstructors = excludeConstructors;
178: if (super .excludeConstructors) {
179: super .constructorSet = manager
180: .getMethodSet(super .categories
181: .getFileName("constructorFile"));
182: }
183:
184: super .excludeIndividualMethods = excludeIndividualMethods;
185:
186: // set all package prefixes
187: if (packagePrefixes.size() > 0) {
188: this .packagePrefixes = packagePrefixes;
189: } else {
190: this .packagePrefixes = null;
191: }
192:
193: this .counter = new MethodCounter();
194:
195: this .date = new Date();
196: }
197:
198: /**
199: * Verifies if <code>testGrammar</code> is valid. The only grammars acceptable are class names
200: * that either begins or ends with 'Test' and end with either '.java' or '.class' file types.
201: * <p>
202: * NOTE: method is package private for testing, else should be private.
203: *
204: * @param testGrammar the grammar defining test class names.
205: * @return true if a part of <code>testGrammar</code>, false otherwise.
206: * @exception ParseException if format of <code>testGrammar</code> is unacceptable.
207: */
208: boolean isValidTestGrammar(String testGrammar)
209: throws ParseException {
210:
211: // remove any '.class' or '.java' from testGrammar or last '.'
212: if (testGrammar.endsWith(".java")
213: || testGrammar.endsWith(".class")
214: || testGrammar.lastIndexOf('.') == testGrammar.length() - 1) {
215: testGrammar = testGrammar.substring(0, testGrammar
216: .lastIndexOf('.'));
217: }
218:
219: // check if testGrammer ends with any other prefix
220: if (testGrammar.indexOf('.') > -1
221: && testGrammar.lastIndexOf('.') < testGrammar.length() - 1) {
222: throw new ParseException("Ill-formed suffix of grammar <"
223: + testGrammar + ">", testGrammar.lastIndexOf('.'));
224: }
225:
226: // check grammar
227: if (testGrammar.endsWith("*") || testGrammar.startsWith("*")) {
228: return true;
229: } else {
230: throw new ParseException(
231: "Ill-formed grammar for test class names", 0);
232: }
233: }
234:
235: /**
236: * Processes all files to include in the coverage measurement.
237: * <p>
238: * This method performs the following tasks:
239: * <ul>
240: * <li> Records all methods to include in coverage.
241: * <li> Modifies all methods in the byte code of the .class files.
242: * <li> Records methods with only one line of source code when specified by the user.
243: * <li> Records constructors when specified by the user.
244: * </ul>
245: *
246: * @param toDir the output directory for modified classes.
247: * @param includes list of fully qualified .class files to include in coverage.
248: * @throws JBlanketException if cannot save a modified file.
249: */
250: private void processIncludeClasses(String toDir, List includes)
251: throws JBlanketException {
252: // process each file
253: for (Iterator i = includes.iterator(); i.hasNext();) {
254:
255: // check if file is a .class; if modifying files in .jar, could be .xml, etc.
256: String className = (String) i.next();
257: if (!((className).endsWith(".class"))) {
258: continue;
259: }
260:
261: // check if file is supposed to be modified
262: if (this .packagePrefixes != null) {
263:
264: boolean modify = false;
265:
266: String fullyQualifiedClassName = className.replace(
267: File.separatorChar, '.');
268:
269: for (Iterator p = this .packagePrefixes.iterator(); p
270: .hasNext();) {
271:
272: String packagePrefix = (String) p.next();
273: // if true, modify this class because it begins with a packageprefix
274: if (fullyQualifiedClassName
275: .startsWith(packagePrefix)) {
276:
277: modify = true;
278: break;
279: }
280: }
281:
282: if (!modify) {
283: // found a class that does not begin with packageprefix, so get next class
284: continue;
285: }
286: }
287:
288: // change to full path
289: File classFile = new File(toDir, className);
290: ClassModifier classModifier = new ClassModifier(
291: super .verbose, this .testGrammar,
292: super .excludeOneLineMethods,
293: super .excludeConstructors,
294: super .excludeIndividualMethods, this .counter,
295: classFile);
296: classModifier.modifyMethods();
297: }
298:
299: // store all methods found in 'clazz's
300: try {
301:
302: this .counter.storeAllMethods();
303:
304: // output set of one-line methods
305: if (this .excludeOneLineMethods) {
306: storeMethods(this .oneLineSet, new File(super .categories
307: .getFileName("oneLineFile")));
308: }
309:
310: // output set of constructors
311: if (this .excludeConstructors) {
312: storeMethods(this .constructorSet,
313: new File(super .categories
314: .getFileName("constructorFile")));
315: }
316: } catch (IOException e) {
317: throw new JBlanketException("Unable to store methods", e);
318: }
319: }
320:
321: /**
322: * Stores all of the methods in <code>methodSet</code> to <code>fileName</code>.
323: *
324: * @param methodSet the MethodSet with the methods to store in fileName.
325: * @param file the output file.
326: * @throws IOException if cannot load 'totalFile', store to <code>fileName</code>.
327: */
328: protected void storeMethods(MethodSet methodSet, File file)
329: throws IOException {
330:
331: // throws FileNotFoundException
332: FileInputStream fistream = new FileInputStream(categories
333: .getFileName("totalFile"));
334: try {
335: this .totalSet.load(fistream);
336: } catch (ParseException e) {
337: // do nothing. Error occurs when file's timestamp cannot be parsed.
338: // Doesn't matter because do not need the date.
339: }
340:
341: methodSet.intersection(this .totalSet);
342:
343: // throws FileNotFoundException
344: FileOutputStream fostream = new FileOutputStream(file);
345: methodSet.store(fostream, null, this .date);
346:
347: fistream.close();
348: fostream.close();
349: }
350:
351: /**
352: * Processes all methods to exclude from the coverage measurement. All method type signatures
353: * are stored in the set of excluded methods.
354: *
355: * @param toDir the output directory for unmodified classes.
356: * @param excludes list of package prefix .class files to exclude from coverage.
357: * @exception JBlanketException if cannot process <code>excludes</code> methods.
358: */
359: private void processExcludeClasses(String toDir, List excludes)
360: throws JBlanketException {
361:
362: // process each file
363: for (Iterator i = excludes.iterator(); i.hasNext();) {
364:
365: // change to full path
366: String className = (String) i.next();
367:
368: // check for valid file
369: if (!className.endsWith(".class")) {
370: continue;
371: }
372:
373: File classFile = new File(toDir, className);
374: ClassModifier modifier = new ClassModifier(this .verbose,
375: counter, classFile);
376: modifier.excludeMethods();
377: }
378: }
379:
380: /**
381: * Processes all JAR files.
382: *
383: * @param dir the directory containing jars to modify.
384: * @param jars list of jar files to modify.
385: * @throws JBlanketException if cannot process <code>jar</code> files.
386: */
387: private void processJarFiles(String dir, List jars)
388: throws JBlanketException {
389:
390: // separator between directories
391: final String slash = File.separator;
392:
393: // process each file
394: for (Iterator i = jars.iterator(); i.hasNext();) {
395:
396: // create temporary directory for classes from JAR file
397: String temp = super .jblanketDir + slash + "temp";
398: File tempDir = new File(temp);
399:
400: // first, delete directory if it exists
401: if (tempDir.exists()) {
402: deleteDirectory(tempDir);
403: }
404:
405: // create a new one
406: tempDir.mkdirs();
407:
408: // create new JarFactory to process JAR file
409: JarFactory factory = new JarFactory(temp);
410: File jarFile = new File(dir, (String) i.next());
411: try {
412: factory.unJar(jarFile);
413: } catch (Exception e) {
414: // catches IOException and JarException
415: throw new JBlanketException("Unable to unjar JAR file "
416: + jarFile.getAbsoluteFile(), e);
417: }
418:
419: // get all files
420: DirectoryScanner scanner = new DirectoryScanner();
421: scanner.setIncludes(new String[] { "**" });
422: scanner.setExcludes(new String[] { "**" + slash
423: + "META-INF" + slash + "**" });
424: scanner.setBasedir(tempDir);
425: scanner.scan();
426: String files[] = scanner.getIncludedFiles();
427:
428: // process included files
429: ArrayList includes = new ArrayList();
430: for (int j = 0; j < files.length; j++) {
431: includes.add(files[j]);
432: }
433: processIncludeClasses(temp, includes);
434:
435: // re-pack the JAR file
436: try {
437: factory.jar(jarFile);
438: } catch (Exception e) {
439: // catches IOException and JarException
440: throw new JBlanketException("Unable to jar JAR file "
441: + jarFile.getAbsoluteFile(), e);
442: }
443:
444: // delete all files
445: deleteDirectory(tempDir);
446: }
447: }
448:
449: /**
450: * Deletes <code>dir</code>ectory.
451: * <p>
452: * Method is package private for testing purposes only, else should be private.
453: *
454: * @param dir the directory to remove.
455: */
456: void deleteDirectory(File dir) {
457:
458: // find all files and subdirectories
459: DirectoryScanner scanner = new DirectoryScanner();
460: scanner.setIncludes(new String[] { "**/**" });
461: scanner.setBasedir(dir);
462: scanner.scan();
463:
464: // delete files in dir
465: String[] files = scanner.getIncludedFiles();
466: for (int j = 0; j < files.length; j++) {
467: (new File(dir.getAbsolutePath(), files[j])).delete();
468: }
469:
470: // delete the dir
471: String[] directories = scanner.getIncludedDirectories();
472: for (int j = directories.length - 1; j > -1; j--) {
473: (new File(dir.getAbsolutePath(), directories[j])).delete();
474: }
475: }
476:
477: /**
478: * Updates the manager with all types of methods found. The total methods container will be
479: * altered to exclude all untestable methods, i.e., abstract and native methods, and all excluded
480: * methods in user specified excluded classes.
481: *
482: * @throws FileNotFoundException if cannot find 'totalFile'.
483: * @throws IOException if cannot read 'totalFile'.
484: */
485: private void updateTotalMethods() throws FileNotFoundException,
486: IOException {
487:
488: // store this untestableSet
489: FileOutputStream fostream = new FileOutputStream(categories
490: .getFileName("untestableFile"));
491: this .untestableSet.store(fostream, null, this .date);
492:
493: // store this excludedSet
494: fostream = new FileOutputStream(categories
495: .getFileName("excludedFile"));
496: this .excludedSet.store(fostream, null, this .date);
497:
498: // remove other methods from total methods because they cannot be invoked,
499: // so should not be included in the coverage measurement.
500: this .totalSet.difference(this .untestableSet);
501: this .totalSet.difference(this .excludedSet);
502: fostream = new FileOutputStream(categories
503: .getFileName("totalFile"));
504: this .totalSet.store(fostream, null, this .date);
505: fostream.close();
506: }
507:
508: /**
509: * Transforms a ';' delimited string to an array of Strings.
510: * <p>
511: * Method is package private for testing purposes only, else should be private.
512: *
513: * @param string the String to transform.
514: * @return ArrayList containing the Strings delimited by ';'.
515: */
516: static List stringToArrayList(String string) {
517:
518: List list = new ArrayList();
519:
520: if (string != null) {
521:
522: StringTokenizer tokens = new StringTokenizer(string, ";");
523: while (tokens.hasMoreElements()) {
524: list.add(tokens.nextToken());
525: }
526: }
527:
528: return list;
529: }
530:
531: /**
532: * Modifies the files in <code>classDir</code> in the Lists <code>includes</code> and
533: * <code>jars</code> and records the files in <code>excludes</code>.
534: *
535: * @param classDir the directory containing all the subdirectories and files.
536: * @param includes a List of all the class files to modify.
537: * @param excludes a List of all the class files to not modify.
538: * @param jars a List of all the JAR files to modify.
539: * @throws JBlanketException if unable to modify any of the files.
540: */
541: public void modify(String classDir, List includes, List excludes,
542: List jars) throws JBlanketException {
543:
544: processIncludeClasses(classDir, includes);
545: processExcludeClasses(classDir, excludes);
546: processJarFiles(classDir, jars);
547:
548: try {
549: updateTotalMethods();
550: } catch (IOException e) {
551: throw new JBlanketException(
552: "Unable to update the total methods set", e);
553: }
554: }
555:
556: /**
557: * Provides the command line arguments as an Array.
558: * <p>
559: * See main method below.
560: *
561: * @param args the command line arguments.
562: * @exception IOException if there is error reading unmodified files
563: * @throws Throwable if cannot parse an <code>includes</code> files.
564: */
565: public static void main(String args[]) throws IOException,
566: Throwable {
567: main(java.util.Arrays.asList(args));
568: }
569:
570: /**
571: * Processes the command line arguments as a List.
572: * <p>
573: * The command line argument tags are as follows:
574: * <pre>
575: * '-classDir' - directory containing classes to modify
576: * '-include' - names of files found in classDir to modify
577: * '-exclude' - names of files found in classDir to not modify
578: * '-verbose' - describes if should execute in verbose mode
579: * '-testGrammar' - grammar describing the names of the test classes
580: * '-excludeOneLineMethods' - describes if should exclude one-line methods
581: * '-excludeConstructors' - describes if should exclude constructors
582: * '-totalFile' - name of the output XML file for total methods
583: * '-untestableFile' - name of the output XML file for untestable methods
584: * '-excludedFile' - name of the output XML file for excluded methods
585: * '-oneLineFile' - name of the output XML file for one-line methods
586: * '-constructorFile' - name of the output XML file for constructors
587: * '-packagePrefix' - package prefixes to modify if modifying JAR files
588: * </pre>
589: * <p>
590: * After this method executes, all 'include' class or JAR files in -classDir are modified.
591: *
592: * @param args the List of command line arguments.
593: * @throws IOException if there is error reading unmodified files.
594: * @throws JBlanketException if unable to process files.
595: * @throws ParseException if <code>testGrammar</code> is invalid.
596: */
597: public static void main(List args) throws IOException,
598: JBlanketException, ParseException {
599:
600: // Directory of where original class files exist
601: String classDir = null;
602:
603: // List of files to be modified.
604: String includes = null;
605: // List of JAR files to be modified.
606: String jars = null;
607: // List of files to not modify.
608: String excludes = null;
609:
610: // Names of package prefixes to include in the coverage measurement
611: String packagePrefixes = null;
612:
613: // Verbose mode
614: boolean verbose = false;
615: // Grammar for names of test classes
616: String testGrammar = "";
617:
618: // Exclude one-line methods
619: boolean excludeOneLineMethods = false;
620: // Exclude constructors
621: boolean excludeConstructors = false;
622: boolean excludeIndividualMethods = false;
623:
624: MethodCategories categories = MethodCategories.getInstance();
625:
626: // index of current command line arguments.
627: int i;
628: // Parses args into corresponsing variables.
629: for (i = 0; i < args.size(); ++i) {
630: String argument = (String) args.get(i);
631: if (argument.equals("-classDir")) {
632: classDir = (String) args.get(++i);
633: } else if (argument.equals("-include")) {
634: String includeFile = (String) args.get(++i);
635: if (includeFile.endsWith(".jar")) {
636: jars = includeFile;
637: } else {
638: includes = includeFile;
639: }
640: } else if (argument.equals("-exclude")) {
641: excludes = (String) args.get(++i);
642: } else if (argument.equals("-verbose")) {
643: verbose = ((Boolean) args.get(++i)).booleanValue();
644: } else if (argument.equals("-testGrammar")) {
645: testGrammar = (String) args.get(++i);
646: } else if (argument.equals("-excludeOneLineMethods")) {
647: excludeOneLineMethods = ((Boolean) args.get(++i))
648: .booleanValue();
649: } else if (argument.equals("-excludeConstructors")) {
650: excludeConstructors = ((Boolean) args.get(++i))
651: .booleanValue();
652: } else if (argument.equals("-excludeIndividualMethods")) {
653: excludeIndividualMethods = ((Boolean) args.get(++i))
654: .booleanValue();
655: } else if (argument.equals("-totalFile")) {
656: categories.addCategory("totalFile", (String) args
657: .get(++i));
658: } else if (argument.equals("-untestableFile")) {
659: categories.addCategory("untestableFile", (String) args
660: .get(++i));
661: } else if (argument.equals("-excludedFile")) {
662: categories.addCategory("excludedFile", (String) args
663: .get(++i));
664: } else if (argument.equals("-oneLineFile")) {
665: categories.addCategory("oneLineFile", (String) args
666: .get(++i));
667: } else if (argument.equals("-constructorFile")) {
668: categories.addCategory("constructorFile", (String) args
669: .get(++i));
670: } else if (argument.equals("-packagePrefix")) {
671: packagePrefixes = (String) args.get(++i);
672: } else {
673: System.out.println("Incorrect usage: " + argument);
674: System.exit(1);
675: }
676: }
677: // Modify all classes specified by include
678: Modifier modifier = new Modifier(verbose, testGrammar,
679: excludeOneLineMethods, excludeConstructors,
680: excludeIndividualMethods,
681: stringToArrayList(packagePrefixes));
682: modifier.modify(classDir, stringToArrayList(includes),
683: stringToArrayList(excludes), stringToArrayList(jars));
684: }
685: }
|