/* * Cobertura - http://cobertura.sourceforge.net/ * * Copyright (C) 2005 Jeremy Thomerson * Copyright (C) 2005 Grzegorz Lukasik * Copyright (C) 2009 Charlie Squires * Copyright (C) 2009 John Lewis * * Cobertura is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * Cobertura is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Cobertura; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ package net.sourceforge.cobertura.util; import org.apache.log4j.Logger; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * Maps source file names to existing files. After adding description * of places files can be found in, it can be used to localize * the files. * <p/> * <p/> * FileFinder supports two types of source files locations: * <ul> * <li>source root directory, defines the directory under * which source files are located,</li> * <li>pair (base directory, file path relative to base directory).</li> * </ul> * The difference between these two is that in case of the first you add all * source files under the specified root directory, and in the second you add * exactly one file. In both cases file to be found has to be located under * subdirectory that maps to package definition provided with the source file name. * * @author Jeremy Thomerson */ public class FileFinder { private static Logger LOGGER = Logger.getLogger(FileFinder.class); // Contains Strings with directory paths private Set sourceDirectories = new HashSet(); // Contains pairs (String directoryRoot, Set fileNamesRelativeToRoot) private Map sourceFilesMap = new HashMap(); /** * Adds directory that is a root of sources. A source file * that is under this directory will be found if relative * path to the file from root matches package name. * <p> * Example: * <pre> * fileFinder.addSourceDirectory( "C:/MyProject/src/main"); * fileFinder.addSourceDirectory( "C:/MyProject/src/test"); * </pre> * In path both / and \ can be used. * </p> * * @param directory The root of source files * * @throws NullPointerException if <code>directory</code> is <code>null</code> */ public void addSourceDirectory(String directory) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Adding sourceDirectory=[" + directory + "]"); // Change \ to / in case of Windows users directory = getCorrectedPath(directory); sourceDirectories.add(directory); } /** * Adds file by specifying root directory and relative path to the * file in it. Adds exactly one file, relative path should match * package that the source file is in, otherwise it will be not * found later. * <p> * Example: * <pre> * fileFinder.addSourceFile( "C:/MyProject/src/main", "com/app/MyClass.java"); * fileFinder.addSourceFile( "C:/MyProject/src/test", "com/app/MyClassTest.java"); * </pre> * In paths both / and \ can be used. * </p> * * @param baseDir sources root directory * @param file path to source file relative to <code>baseDir</code> * * @throws NullPointerException if either <code>baseDir</code> or <code>file</code> is <code>null</code> */ public void addSourceFile(String baseDir, String file) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Adding sourceFile baseDir=[" + baseDir + "] file=[" + file + "]"); if (baseDir == null || file == null) throw new NullPointerException(); // Change \ to / in case of Windows users file = getCorrectedPath(file); baseDir = getCorrectedPath(baseDir); // Add file to sourceFilesMap Set container = (Set) sourceFilesMap.get(baseDir); if (container == null) { container = new HashSet(); sourceFilesMap.put(baseDir, container); } container.add(file); } /** * Maps source file name to existing file. * When mapping file name first values that were added with * {@link #addSourceDirectory} and later added with {@link #addSourceFile} are checked. * * @param fileName source file to be mapped * * @return existing file that maps to passed sourceFile * * @throws IOException if cannot map source file to existing file * @throws NullPointerException if fileName is null */ public File getFileForSource(String fileName) throws IOException { // Correct file name if (LOGGER.isDebugEnabled()) LOGGER.debug("Searching for file, name=[" + fileName + "]"); fileName = getCorrectedPath(fileName); // Check inside sourceDirectories for (Iterator it = sourceDirectories.iterator(); it.hasNext();) { String directory = (String) it.next(); File file = new File(directory, fileName); if (file.isFile()) { LOGGER.debug("Found inside sourceDirectories"); return file; } } // Check inside sourceFilesMap for (Iterator it = sourceFilesMap.keySet().iterator(); it.hasNext();) { String directory = (String) it.next(); Set container = (Set) sourceFilesMap.get(directory); if (!container.contains(fileName)) continue; File file = new File(directory, fileName); if (file.isFile()) { LOGGER.debug("Found inside sourceFilesMap"); return file; } } // Have not found? Throw an error. LOGGER.debug("File not found"); throw new IOException("Cannot find source file, name=[" + fileName + "]"); } /** * Maps source file name to existing file or source archive. * When mapping file name first values that were added with * {@link #addSourceDirectory} and later added with {@link #addSourceFile} are checked. * * @param fileName source file to be mapped * * @return Source that maps to passed sourceFile or null if it can't be found * * @throws NullPointerException if fileName is null */ public Source getSource(String fileName) { File file = null; try { file = getFileForSource(fileName); return new Source(new FileInputStream(file), file); } catch (IOException e) { //Source file wasn't found. Try searching archives. return searchJarsForSource(fileName); } } /** * Gets a BufferedReader for a file within a jar. * * @param fileName source file to get an input stream for * * @return Source for existing file inside a jar that maps to passed sourceFile * or null if cannot map source file to existing file */ private Source searchJarsForSource(String fileName) { //Check inside jars in sourceDirectories for (Iterator it = sourceDirectories.iterator(); it.hasNext();) { String directory = (String) it.next(); File file = new File(directory); //Get a list of jars and zips in the directory String[] jars = file.list(new JarZipFilter()); if (jars != null) { for (String jar : jars) { try { LOGGER.debug("Looking for: " + fileName + " in " + jar); JarFile jf = new JarFile(directory + "/" + jar); //Get a list of files in the jar Enumeration<JarEntry> files = jf.entries(); //See if the jar has the class we need while (files.hasMoreElements()) { JarEntry entry = files.nextElement(); if (entry.getName().equals(fileName)) { return new Source(jf.getInputStream(entry), jf); } } } catch (Throwable t) { LOGGER.warn("Error while reading " + jar, t); } } } } return null; } /** * Returns a list with string for all source directories. * Example: <code>[C:/MyProject/src/main,C:/MyProject/src/test]</code> * * @return list with Strings for all source roots, or empty list if no source roots were specified */ public List getSourceDirectoryList() { // Get names from sourceDirectories List result = new ArrayList(); for (Iterator it = sourceDirectories.iterator(); it.hasNext();) { result.add(it.next()); } // Get names from sourceFilesMap for (Iterator it = sourceFilesMap.keySet().iterator(); it.hasNext();) { result.add(it.next()); } // Return combined names return result; } private String getCorrectedPath(String path) { return path.replace('\\', '/'); } /** * Returns string representation of FileFinder. */ public String toString() { return "FileFinder, source directories: " + getSourceDirectoryList().toString(); } /** * A filter that accepts files that end in .jar or .zip */ private class JarZipFilter implements FilenameFilter { public boolean accept(File dir, String name) { return (name.endsWith(".jar") || name.endsWith(".zip")); } } }