001/**
002 * www.jcoverage.com
003 * Copyright (C)2003 jcoverage ltd.
004 *
005 * This file is part of jcoverage.
006 *
007 * jcoverage is free software; you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published
009 * by the Free Software Foundation; either version 2 of the License,
010 * or (at your option) any later version.
011 *
012 * jcoverage is distributed in the hope that it will be useful, but
013 * WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with jcoverage; if not, write to the Free Software
019 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
020 * USA
021 *
022 */
023package com.jcoverage.coverage;
024
025import com.jcoverage.util.JavaClassHelper;
026import com.jcoverage.util.ClassHelper;
027
028import java.io.File;
029import java.io.FileInputStream;
030import java.io.InputStream;
031import java.io.IOException;
032
033import java.util.ResourceBundle;
034
035import org.apache.log4j.Logger;
036
037import org.apache.bcel.classfile.JavaClass;
038
039/**
040 * Add coverage instrumentation to existing classes.
041 */
042public class Instrument {
043  static final Logger logger=Logger.getLogger(Instrument.class);
044
045  File destinationDirectory=null;
046  String ignoreRegex=null;
047  File baseDir=null;
048
049  /**
050   * @param fi a file
051   * @return true if the specified file has "class" as its extension,
052   * false otherwise.
053   */
054  boolean isClass(File fi) {
055    if(logger.isDebugEnabled()) {
056      logger.debug("fi: "+fi.getName());
057    }
058
059    return fi.getName().endsWith(".class");
060  }
061
062  /**
063   * @param jc a compiled Java class
064   * @return true if the specified class implements the interface
065   * @link HasBeenInstrumented, otherwise false.
066   */
067  boolean isAlreadyInstrumented(JavaClass jc) {
068    if(logger.isDebugEnabled()) {
069      logger.debug("jc: "+jc.getClassName());
070    }
071
072    String[] interfaceNames=jc.getInterfaceNames();
073    for(int i=0;i<interfaceNames.length;i++) {
074      if(logger.isDebugEnabled()) {
075        logger.debug(jc.getClassName()+" implements "+interfaceNames[i]);
076      }
077
078      if(interfaceNames[i].equals(HasBeenInstrumented.class.getName())) {
079        if(logger.isInfoEnabled()) {
080          logger.info(jc.getClassName()+" has already been instrumented");
081        }
082        return true;
083      }
084    }
085    return false;
086  }
087
088  /**
089   * @param jc a compiled Java class
090   * @return true if the class represented by <code>jc</code> is an
091   * interface.
092   */
093  boolean isInterface(JavaClass jc) {
094    return !jc.isClass();
095  }
096
097  /**
098   * Add coverage instrumentation to the specified Java class.
099   * @param clazz a Java class file.
100   */
101  void instrument(File clazz) {
102    if(logger.isDebugEnabled()) {
103      logger.debug("name: "+clazz.getName());
104    }
105
106    InputStream is=null;
107
108    try {
109      is=new FileInputStream(clazz);
110      JavaClass jc=JavaClassHelper.newJavaClass(is,clazz.getName());
111      is.close();
112      is=null;
113
114      if(isInterface(jc)||isAlreadyInstrumented(jc)) {
115        if(destinationDirectory!=null) {
116          /**
117           * It is not normally necessary to do anything with an
118           * interface or class that has already been
119           * instrumented. However, if a destination directory has
120           * been specified we copy it to the destination directory,
121           * so that on subsequent invocations of "ant" the files will
122           * be seen as being upto date, and will not require
123           * instrumentation.
124           */
125          File outputDirectory=new File(destinationDirectory,ClassHelper.getPackageName(jc.getClassName()).replace('.','/'));
126          outputDirectory.mkdirs();
127          jc.dump(new File(outputDirectory,ClassHelper.getBaseName(jc.getClassName())+".class"));
128        }
129        return;
130      }
131
132      if(logger.isInfoEnabled()) {
133        logger.info("instrumenting "+jc.getClassName());
134      }
135
136      InstrumentClassGen instrument=new InstrumentClassGen(jc,ignoreRegex);
137      instrument.addInstrumentation();
138
139      if(logger.isDebugEnabled()) {
140        JavaClassHelper.dump(instrument.getClassGen().getJavaClass());
141      }
142
143      if(destinationDirectory==null) {
144        instrument.getClassGen().getJavaClass().dump(clazz);
145      } else {
146        File outputDirectory=new File(destinationDirectory,ClassHelper.getPackageName(jc.getClassName()).replace('.','/'));
147        outputDirectory.mkdirs();
148        instrument.getClassGen().getJavaClass().dump(new File(outputDirectory,ClassHelper.getBaseName(jc.getClassName())+".class"));
149      }
150
151      InstrumentationInternal i=(InstrumentationInternal)InstrumentationFactory.getInstance().newInstrumentation(jc.getClassName());
152
153      if(instrument.getSourceLineNumbers().isEmpty()) {
154        logger.warn("no source line numbers found for: "+jc.getClassName()+", compile with debug=\"yes\".");
155      }
156
157      i.setSourceLineNumbers(instrument.getSourceLineNumbers());
158      i.setSourceFileName(jc.getSourceFileName());
159      i.setSourceLineNumbersByMethod(instrument.getMethodLineNumbers());
160      i.setConditionalsByMethod(instrument.getMethodConditionals());
161      i.setMethodNamesAndSignatures(instrument.getMethodNamesAndSignatures());
162    } catch(IOException ex) {
163      if(logger.isDebugEnabled()) {
164        logger.debug(ex);
165      }
166      
167      if(is!=null) {
168        try {
169          is.close();
170        } catch(IOException whileClosing) {
171          if(logger.isDebugEnabled()) {
172            logger.debug(whileClosing);
173          }
174        }
175      }
176
177      throw new CoverageRuntimeException(ex);
178    }
179  }
180
181  void addInstrumentation(File fi) {
182    if(logger.isDebugEnabled()) {
183      logger.debug("fi: "+fi.getName());
184    }
185
186    if(fi.isDirectory()) {
187      File[] contents=fi.listFiles();
188      for(int i=0;i<contents.length;i++) {
189        addInstrumentation(contents[i]);
190      }
191    }
192
193    if(isClass(fi)) {
194      instrument(fi);
195    }
196  }
197
198  void addInstrumentation(String arg) {
199    if(logger.isDebugEnabled()) {
200      logger.debug("arg: "+arg);
201    }
202
203    if(baseDir==null) {
204      addInstrumentation(new File(arg));
205    } else {
206      addInstrumentation(new File(baseDir,arg));
207    }
208  }
209
210  void addInstrumentation(String[] args) {
211    for(int i=0;i<args.length;i++) {
212      if(args[i].equals("-d")) {
213        destinationDirectory=new File(args[++i]);
214        continue;
215      } else if(args[i].equals("-basedir")) {
216        baseDir=new File(args[++i]);
217        continue;
218      } else if(args[i].equals("-ignore")) {
219        ignoreRegex=args[++i];
220        continue;
221      }
222
223      addInstrumentation(args[i]);
224    }
225  }
226
227  public static void main(String[] args) {
228    Instrument instrument=new Instrument();
229    instrument.addInstrumentation(args);
230  }
231}