001/*
002 * Cobertura - http://cobertura.sourceforge.net/
003 *
004 * Copyright (C) 2003 jcoverage ltd.
005 * Copyright (C) 2005 Mark Doliner
006 * Copyright (C) 2005 Joakim Erdfelt
007 * Copyright (C) 2005 Grzegorz Lukasik
008 * Copyright (C) 2006 John Lewis
009 * Copyright (C) 2006 Jiri Mares
010 * Copyright (C) 2008 Scott Frederick
011 * Copyright (C) 2010 Tad Smith 
012 * Copyright (C) 2010 Piotr Tabor  
013 * Contact information for the above is given in the COPYRIGHT file.
014 *
015 * Cobertura is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU General Public License as published
017 * by the Free Software Foundation; either version 2 of the License,
018 * or (at your option) any later version.
019 *
020 * Cobertura is distributed in the hope that it will be useful, but
021 * WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
023 * General Public License for more details.
024 *
025 * You should have received a copy of the GNU General Public License
026 * along with Cobertura; if not, write to the Free Software
027 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
028 * USA
029 */
030
031package net.sourceforge.cobertura.instrument;
032
033import java.io.ByteArrayInputStream;
034import java.io.ByteArrayOutputStream;
035import java.io.File;
036import java.io.FileInputStream;
037import java.io.FileNotFoundException;
038import java.io.FileOutputStream;
039import java.io.IOException;
040import java.io.InputStream;
041import java.io.OutputStream;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.HashSet;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Set;
048import java.util.Vector;
049import java.util.regex.Pattern;
050import java.util.zip.ZipEntry;
051import java.util.zip.ZipInputStream;
052import java.util.zip.ZipOutputStream;
053
054import net.sourceforge.cobertura.coveragedata.CoverageDataFileHandler;
055import net.sourceforge.cobertura.coveragedata.ProjectData;
056import net.sourceforge.cobertura.instrument.CoberturaInstrumenter.InstrumentationResult;
057import net.sourceforge.cobertura.util.ArchiveUtil;
058import net.sourceforge.cobertura.util.CommandLineBuilder;
059import net.sourceforge.cobertura.util.Header;
060import net.sourceforge.cobertura.util.IOUtil;
061import net.sourceforge.cobertura.util.RegexUtil;
062
063import org.apache.log4j.Logger;
064
065/**
066 * <p>
067 * Add coverage instrumentation to existing classes.
068 * </p>
069 *
070 * <h3>What does that mean, exactly?</h3>
071 * <p>
072 * It means Cobertura will look at each class you give it.  It
073 * loads the bytecode into memory.  For each line of source,
074 * Cobertura adds a few extra instructions.  These instructions 
075 * do the following:
076 * </p>
077 * 
078 * <ol>
079 * <li>Get an instance of the ProjectData class.</li>
080 * <li>Call a method in this ProjectData class that increments
081 * a counter for this line of code.
082 * </ol>
083 */ 
084public class Main {
085        private static final LoggerWrapper logger = new LoggerWrapper();
086
087        private File destinationDirectory = null;
088
089        private final ClassPattern classPattern = new ClassPattern();
090        
091        private final CoberturaInstrumenter coberturaInstrumenter = new CoberturaInstrumenter();
092
093        /**
094         * @param entry A zip entry.
095         * @return True if the specified entry has "class" as its extension,
096         * false otherwise.
097         */
098        private static boolean isClass(ZipEntry entry)
099        {
100                return entry.getName().endsWith(".class");
101        }
102
103        private boolean addInstrumentationToArchive(CoberturaFile file, InputStream archive,
104                        OutputStream output) throws Exception
105        {
106                ZipInputStream zis = null;
107                ZipOutputStream zos = null;
108
109                try
110                {
111                        zis = new ZipInputStream(archive);
112                        zos = new ZipOutputStream(output);
113                        return addInstrumentationToArchive(file, zis, zos);
114                }
115                finally
116                {
117                        zis = (ZipInputStream)IOUtil.closeInputStream(zis);
118                        zos = (ZipOutputStream)IOUtil.closeOutputStream(zos);
119                }
120        }
121
122        private boolean addInstrumentationToArchive(CoberturaFile file, ZipInputStream archive,
123                        ZipOutputStream output) throws Exception
124        {
125                /*
126                 * "modified" is returned and indicates that something was instrumented.
127                 * If nothing is instrumented, the original entry will be used by the
128                 * caller of this method.
129                 */
130                boolean modified = false;
131                ZipEntry entry;
132                while ((entry = archive.getNextEntry()) != null)
133                {
134                        try
135                        {
136                                String entryName = entry.getName();
137
138                                /*
139                                 * If this is a signature file then don't copy it,
140                                 * but don't set modified to true.  If the only
141                                 * thing we do is strip the signature, just use
142                                 * the original entry.
143                                 */
144                                if (ArchiveUtil.isSignatureFile(entry.getName()))
145                                {
146                                        continue;
147                                }
148                                ZipEntry outputEntry = new ZipEntry(entry.getName());
149                                outputEntry.setComment(entry.getComment());
150                                outputEntry.setExtra(entry.getExtra());
151                                outputEntry.setTime(entry.getTime());
152                                output.putNextEntry(outputEntry);
153
154                                // Read current entry
155                                byte[] entryBytes = IOUtil
156                                                .createByteArrayFromInputStream(archive);
157
158                                // Instrument embedded archives if a classPattern has been specified
159                                if ((classPattern.isSpecified()) && ArchiveUtil.isArchive(entryName))
160                                {
161                                        Archive archiveObj = new Archive(file, entryBytes);
162                                        addInstrumentationToArchive(archiveObj);
163                                        if (archiveObj.isModified())
164                                        {
165                                                modified = true;
166                                                entryBytes = archiveObj.getBytes();
167                                                outputEntry.setTime(System.currentTimeMillis());
168                                        }
169                                }
170                                else if (isClass(entry) && classPattern.matches(entryName))
171                                {
172                                        try     {
173                                                InstrumentationResult res=coberturaInstrumenter.instrumentClass(new ByteArrayInputStream(entryBytes));
174                                                if(res!=null){
175                                                        logger.debug("Putting instrumented entry: "     + entry.getName());
176                                                        entryBytes=res.getContent();
177                                                        modified=true;
178                                                        outputEntry.setTime(System.currentTimeMillis());
179                                                }
180                                        }
181                                        catch (Throwable t)
182                                        {
183                                                if (entry.getName().endsWith("_Stub.class"))
184                                                {
185                                                        //no big deal - it is probably an RMI stub, and they don't need to be instrumented
186                                                        logger.debug("Problems instrumenting archive entry: " + entry.getName(), t);
187                                                }
188                                                else
189                                                {
190                                                        logger.warn("Problems instrumenting archive entry: " + entry.getName(), t);
191                                                }
192                                        }
193                                }
194
195                                // Add entry to the output
196                                output.write(entryBytes);
197                                output.closeEntry();
198                                archive.closeEntry();
199                        }
200                        catch (Exception e)
201                        {
202                                logger.warn("Problems with archive entry: " + entry.getName(), e);
203                        }
204                        catch (Throwable t)
205                        {
206                                logger.warn("Problems with archive entry: " + entry.getName(), t);
207                        }
208                        output.flush();
209                }
210                return modified;
211        }
212
213        private void addInstrumentationToArchive(Archive archive) throws Exception
214        {
215                InputStream in = null;
216                ByteArrayOutputStream out = null;
217                try
218                {
219                        in = archive.getInputStream();
220                        out = new ByteArrayOutputStream();
221                        boolean modified = addInstrumentationToArchive(archive.getCoberturaFile(), in, out);
222
223                        if (modified)
224                        {
225                                out.flush();
226                                byte[] bytes = out.toByteArray();
227                                archive.setModifiedBytes(bytes);
228                        }
229                }
230                finally
231                {
232                        in = IOUtil.closeInputStream(in);
233                        out = (ByteArrayOutputStream)IOUtil.closeOutputStream(out);
234                }
235        }
236
237        private void addInstrumentationToArchive(CoberturaFile archive)
238        {
239                logger.debug("Instrumenting archive " + archive.getAbsolutePath());
240
241                File outputFile = null;
242                ZipInputStream input = null;
243                ZipOutputStream output = null;
244                boolean modified = false;
245                try
246                {
247                        // Open archive
248                        try
249                        {
250                                input = new ZipInputStream(new FileInputStream(archive));
251                        }
252                        catch (FileNotFoundException e)
253                        {
254                                logger.warn("Cannot open archive file: "
255                                                + archive.getAbsolutePath(), e);
256                                return;
257                        }
258
259                        // Open output archive
260                        try
261                        {
262                                // check if destination folder is set
263                                if (destinationDirectory != null)
264                                {
265                                        // if so, create output file in it
266                                        outputFile = new File(destinationDirectory, archive.getPathname());
267                                }
268                                else
269                                {
270                                        // otherwise create output file in temporary location
271                                        outputFile = File.createTempFile(
272                                                        "CoberturaInstrumentedArchive", "jar");
273                                        outputFile.deleteOnExit();
274                                }
275                                output = new ZipOutputStream(new FileOutputStream(outputFile));
276                        }
277                        catch (IOException e)
278                        {
279                                logger.warn("Cannot open file for instrumented archive: "
280                                                + archive.getAbsolutePath(), e);
281                                return;
282                        }
283
284                        // Instrument classes in archive
285                        try
286                        {
287                                modified = addInstrumentationToArchive(archive, input, output);
288                        }
289                        catch (Throwable e)
290                        {
291                                logger.warn("Cannot instrument archive: "
292                                                + archive.getAbsolutePath(), e);
293                                return;
294                        }
295                }
296                finally
297                {
298                        input = (ZipInputStream)IOUtil.closeInputStream(input);
299                        output = (ZipOutputStream)IOUtil.closeOutputStream(output);
300                }
301
302                // If destination folder was not set, overwrite orginal archive with
303                // instrumented one
304                if (modified && (destinationDirectory == null))
305                {
306                        try
307                        {
308                                logger.debug("Moving " + outputFile.getAbsolutePath() + " to "
309                                                + archive.getAbsolutePath());
310                                IOUtil.moveFile(outputFile, archive);
311                        }
312                        catch (IOException e)
313                        {
314                                logger.warn("Cannot instrument archive: "
315                                                + archive.getAbsolutePath(), e);
316                                return;
317                        }
318                }
319                if ((destinationDirectory != null) && (!modified))
320                {
321                        outputFile.delete();
322                }
323        }
324        
325        
326        
327        private void addInstrumentationToSingleClass(File file){
328                logger.info("Instrumenting: "+file.getAbsolutePath()+" to "+destinationDirectory);
329                coberturaInstrumenter.addInstrumentationToSingleClass(file);
330        }
331
332        // TODO: Don't attempt to instrument a file if the outputFile already
333        //       exists and is newer than the input file, and the output and
334        //       input file are in different locations?
335        private void addInstrumentation(CoberturaFile coberturaFile)
336        {
337                if (coberturaFile.isClass() && classPattern.matches(coberturaFile.getPathname()))
338                {
339                        addInstrumentationToSingleClass(coberturaFile);
340                }
341                else if (coberturaFile.isDirectory())
342                {
343                        String[] contents = coberturaFile.list();
344                        for (int i = 0; i < contents.length; i++)
345                        {
346                                File relativeFile = new File(coberturaFile.getPathname(), contents[i]);
347                                CoberturaFile relativeCoberturaFile = new CoberturaFile(coberturaFile.getBaseDir(),
348                                                relativeFile.toString());
349                                //recursion!
350                                addInstrumentation(relativeCoberturaFile);
351                        }
352                }
353        }
354
355        private void parseArguments(String[] args){
356                Collection<Pattern> ignoreRegexes = new Vector<Pattern>();
357                coberturaInstrumenter.setIgnoreRegexes(ignoreRegexes);          
358                
359                File dataFile = CoverageDataFileHandler.getDefaultDataFile();
360
361                // Parse our parameters
362                List<CoberturaFile> filePaths = new ArrayList<CoberturaFile>();
363                String baseDir = null;
364                
365                boolean threadsafeRigorous = false;
366                boolean ignoreTrivial = false;
367                boolean failOnError = false;
368                Set<String> ignoreMethodAnnotations = new HashSet<String>();
369                
370                for (int i = 0; i < args.length; i++)
371                {
372                        if (args[i].equals("--basedir"))
373                                baseDir = args[++i];
374                        else if (args[i].equals("--datafile"))
375                                dataFile = new File(args[++i]);
376                        else if (args[i].equals("--destination")) {
377                                destinationDirectory = new File(args[++i]);
378                                coberturaInstrumenter.setDestinationDirectory(destinationDirectory);
379                        } else if (args[i].equals("--ignore")) {
380                                RegexUtil.addRegex(ignoreRegexes, args[++i]);
381                        }
382                        /*else if (args[i].equals("--ignoreBranches"))
383                        {
384                                RegexUtil.addRegex(ignoreBranchesRegexes, args[++i]);
385                        }*/
386                        else if (args[i].equals("--ignoreMethodAnnotation")) {
387                            ignoreMethodAnnotations.add(args[++i]);
388                    } else if (args[i].equals("--ignoreTrivial")) {
389                ignoreTrivial = true;
390                        } else if (args[i].equals("--includeClasses")) {
391                                classPattern.addIncludeClassesRegex(args[++i]);
392                        } else if (args[i].equals("--excludeClasses")) {
393                                classPattern.addExcludeClassesRegex(args[++i]);
394                        } else if (args[i].equals("--failOnError")) {
395                                failOnError = true;
396                            logger.setFailOnError(true);
397                        } else if (args[i].equals("--threadsafeRigorous")) {
398                            threadsafeRigorous = true;
399                        } else {
400                                filePaths.add(new CoberturaFile(baseDir, args[i]));
401                        }
402                }
403                
404                coberturaInstrumenter.setIgnoreTrivial(ignoreTrivial);
405                coberturaInstrumenter.setIgnoreMethodAnnotations(ignoreMethodAnnotations);
406                coberturaInstrumenter.setThreadsafeRigorous(threadsafeRigorous);
407                coberturaInstrumenter.setFailOnError(failOnError);
408                
409                ProjectData projectData;
410
411                // Load previous coverage data (if exists)
412                projectData = dataFile.isFile() ?
413                    CoverageDataFileHandler.loadCoverageData(dataFile) : new ProjectData();
414        coberturaInstrumenter.setProjectData(projectData);
415                
416                // Instrument classes
417                logger.info("Instrumenting "    + filePaths.size() + " "
418                                + (filePaths.size() == 1 ? "file" : "files")
419                                + (destinationDirectory != null ? " to "
420                                                + destinationDirectory.getAbsoluteFile() : ""));
421
422                Iterator<CoberturaFile> iter = filePaths.iterator();
423                while (iter.hasNext()) {
424                        CoberturaFile coberturaFile = iter.next();
425                        if (coberturaFile.isArchive()) {
426                                addInstrumentationToArchive(coberturaFile);
427                        } else {
428                                addInstrumentation(coberturaFile);
429                        }
430                }
431                
432                // Save coverage data (ser file with list of touch points, but not hits registered). 
433                CoverageDataFileHandler.saveCoverageData(projectData, dataFile);
434        }
435
436        public static void main(String[] args)
437        {
438                Header.print(System.out);
439
440                long startTime = System.currentTimeMillis();
441
442                Main main = new Main();
443
444                try {
445                        args = CommandLineBuilder.preprocessCommandLineArguments( args);
446                } catch( Exception ex) {
447                        System.err.println( "Error: Cannot process arguments: " + ex.getMessage());
448                        System.exit(1);
449                }
450                main.parseArguments(args);
451
452                long stopTime = System.currentTimeMillis();
453                logger.info("Instrument time: " + (stopTime - startTime) + "ms");
454        }
455        
456        // TODO: Preserved current behaviour, but this code is failing on WARN, not error
457    private static class LoggerWrapper {
458      private final Logger logger = Logger.getLogger(Main.class);
459      private boolean failOnError = false;
460    
461      public void setFailOnError(boolean failOnError){
462        this.failOnError = failOnError;
463      }
464      
465      public void debug(String message) {
466        logger.debug(message);
467      }
468    
469      public void debug(String message, Throwable t){
470        logger.debug(message, t);
471      }
472      
473      public void info(String message){
474          logger.debug(message);
475      }
476      
477      public void warn(String message, Throwable t) {
478        logger.warn(message, t);
479        if (failOnError) {
480          throw new RuntimeException("Warning detected and failOnError is true", t);
481        }
482      }
483    }
484
485}