001/* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2005 Jeremy Thomerson 005 * Copyright (C) 2005 Grzegorz Lukasik 006 * Copyright (C) 2009 Charlie Squires 007 * Copyright (C) 2009 John Lewis 008 * 009 * Cobertura is free software; you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published 011 * by the Free Software Foundation; either version 2 of the License, 012 * or (at your option) any later version. 013 * 014 * Cobertura is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 017 * General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with Cobertura; if not, write to the Free Software 021 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 022 * USA 023 */ 024package net.sourceforge.cobertura.util; 025 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.FilenameFilter; 029import java.io.IOException; 030import java.util.ArrayList; 031import java.util.Enumeration; 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.Iterator; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.jar.JarEntry; 039import java.util.jar.JarFile; 040 041import org.apache.log4j.Logger; 042 043 044/** 045 * Maps source file names to existing files. After adding description 046 * of places files can be found in, it can be used to localize 047 * the files. 048 * 049 * <p> 050 * FileFinder supports two types of source files locations: 051 * <ul> 052 * <li>source root directory, defines the directory under 053 * which source files are located,</li> 054 * <li>pair (base directory, file path relative to base directory).</li> 055 * </ul> 056 * The difference between these two is that in case of the first you add all 057 * source files under the specified root directory, and in the second you add 058 * exactly one file. In both cases file to be found has to be located under 059 * subdirectory that maps to package definition provided with the source file name. 060 * 061 * @author Jeremy Thomerson 062 */ 063public class FileFinder { 064 065 private static Logger LOGGER = Logger.getLogger(FileFinder.class); 066 067 // Contains Strings with directory paths 068 private Set sourceDirectories = new HashSet(); 069 070 // Contains pairs (String directoryRoot, Set fileNamesRelativeToRoot) 071 private Map sourceFilesMap = new HashMap(); 072 073 /** 074 * Adds directory that is a root of sources. A source file 075 * that is under this directory will be found if relative 076 * path to the file from root matches package name. 077 * <p> 078 * Example: 079 * <pre> 080 * fileFinder.addSourceDirectory( "C:/MyProject/src/main"); 081 * fileFinder.addSourceDirectory( "C:/MyProject/src/test"); 082 * </pre> 083 * In path both / and \ can be used. 084 * </p> 085 * 086 * @param directory The root of source files 087 * @throws NullPointerException if <code>directory</code> is <code>null</code> 088 */ 089 public void addSourceDirectory( String directory) { 090 if( LOGGER.isDebugEnabled()) 091 LOGGER.debug( "Adding sourceDirectory=[" + directory + "]"); 092 093 // Change \ to / in case of Windows users 094 directory = getCorrectedPath(directory); 095 sourceDirectories.add(directory); 096 } 097 098 /** 099 * Adds file by specifying root directory and relative path to the 100 * file in it. Adds exactly one file, relative path should match 101 * package that the source file is in, otherwise it will be not 102 * found later. 103 * <p> 104 * Example: 105 * <pre> 106 * fileFinder.addSourceFile( "C:/MyProject/src/main", "com/app/MyClass.java"); 107 * fileFinder.addSourceFile( "C:/MyProject/src/test", "com/app/MyClassTest.java"); 108 * </pre> 109 * In paths both / and \ can be used. 110 * </p> 111 * 112 * @param baseDir sources root directory 113 * @param file path to source file relative to <code>baseDir</code> 114 * @throws NullPointerException if either <code>baseDir</code> or <code>file</code> is <code>null</code> 115 */ 116 public void addSourceFile( String baseDir, String file) { 117 if( LOGGER.isDebugEnabled()) 118 LOGGER.debug( "Adding sourceFile baseDir=[" + baseDir + "] file=[" + file + "]"); 119 120 if( baseDir==null || file==null) 121 throw new NullPointerException(); 122 123 // Change \ to / in case of Windows users 124 file = getCorrectedPath( file); 125 baseDir = getCorrectedPath( baseDir); 126 127 // Add file to sourceFilesMap 128 Set container = (Set) sourceFilesMap.get(baseDir); 129 if( container==null) { 130 container = new HashSet(); 131 sourceFilesMap.put( baseDir, container); 132 } 133 container.add( file); 134 } 135 136 /** 137 * Maps source file name to existing file. 138 * When mapping file name first values that were added with 139 * {@link #addSourceDirectory} and later added with {@link #addSourceFile} are checked. 140 * 141 * @param fileName source file to be mapped 142 * @return existing file that maps to passed sourceFile 143 * @throws IOException if cannot map source file to existing file 144 * @throws NullPointerException if fileName is null 145 */ 146 public File getFileForSource(String fileName) throws IOException { 147 // Correct file name 148 if( LOGGER.isDebugEnabled()) 149 LOGGER.debug( "Searching for file, name=[" + fileName + "]"); 150 fileName = getCorrectedPath( fileName); 151 152 // Check inside sourceDirectories 153 for( Iterator it=sourceDirectories.iterator(); it.hasNext();) { 154 String directory = (String)it.next(); 155 File file = new File( directory, fileName); 156 if( file.isFile()) { 157 LOGGER.debug( "Found inside sourceDirectories"); 158 return file; 159 } 160 } 161 162 // Check inside sourceFilesMap 163 for( Iterator it=sourceFilesMap.keySet().iterator(); it.hasNext();) { 164 String directory = (String)it.next(); 165 Set container = (Set) sourceFilesMap.get(directory); 166 if( !container.contains( fileName)) 167 continue; 168 File file = new File( directory, fileName); 169 if( file.isFile()) { 170 LOGGER.debug( "Found inside sourceFilesMap"); 171 return file; 172 } 173 } 174 175 // Have not found? Throw an error. 176 LOGGER.debug( "File not found"); 177 throw new IOException( "Cannot find source file, name=["+fileName+"]"); 178 } 179 180 /** 181 * Maps source file name to existing file or source archive. 182 * When mapping file name first values that were added with 183 * {@link #addSourceDirectory} and later added with {@link #addSourceFile} are checked. 184 * 185 * @param fileName source file to be mapped 186 * @return Source that maps to passed sourceFile or null if it can't be found 187 * @throws NullPointerException if fileName is null 188 */ 189 public Source getSource(String fileName) { 190 File file = null; 191 try 192 { 193 file = getFileForSource(fileName); 194 return new Source(new FileInputStream(file), file); 195 } 196 catch (IOException e) 197 { 198 //Source file wasn't found. Try searching archives. 199 return searchJarsForSource(fileName); 200 } 201 202 } 203 204 /** 205 * Gets a BufferedReader for a file within a jar. 206 * 207 * @param fileName source file to get an input stream for 208 * @return Source for existing file inside a jar that maps to passed sourceFile 209 * or null if cannot map source file to existing file 210 */ 211 private Source searchJarsForSource(String fileName) { 212 //Check inside jars in sourceDirectories 213 for( Iterator it=sourceDirectories.iterator(); it.hasNext();) { 214 String directory = (String)it.next(); 215 File file = new File(directory); 216 //Get a list of jars and zips in the directory 217 String[] jars = file.list(new JarZipFilter()); 218 if(jars != null) { 219 for(String jar : jars) { 220 try 221 { 222 LOGGER.debug("Looking for: " + fileName + " in "+ jar); 223 JarFile jf = new JarFile(directory + "/" + jar); 224 225 //Get a list of files in the jar 226 Enumeration<JarEntry> files = jf.entries(); 227 //See if the jar has the class we need 228 while(files.hasMoreElements()) { 229 JarEntry entry = files.nextElement(); 230 if(entry.getName().equals(fileName)) { 231 return new Source(jf.getInputStream(entry), jf); 232 } 233 } 234 } 235 catch (Throwable t) 236 { 237 LOGGER.warn("Error while reading " + jar, t); 238 } 239 } 240 } 241 } 242 return null; 243 } 244 245 /** 246 * Returns a list with string for all source directories. 247 * Example: <code>[C:/MyProject/src/main,C:/MyProject/src/test]</code> 248 * 249 * @return list with Strings for all source roots, or empty list if no source roots were specified 250 */ 251 public List getSourceDirectoryList() { 252 // Get names from sourceDirectories 253 List result = new ArrayList(); 254 for( Iterator it=sourceDirectories.iterator(); it.hasNext();) { 255 result.add( it.next()); 256 } 257 258 // Get names from sourceFilesMap 259 for( Iterator it=sourceFilesMap.keySet().iterator(); it.hasNext();) { 260 result.add(it.next()); 261 } 262 263 // Return combined names 264 return result; 265 } 266 267 private String getCorrectedPath(String path) { 268 return path.replace('\\', '/'); 269 } 270 271 /** 272 * Returns string representation of FileFinder. 273 */ 274 public String toString() { 275 return "FileFinder, source directories: " + getSourceDirectoryList().toString(); 276 } 277 278 /** 279 * A filter that accepts files that end in .jar or .zip 280 */ 281 private class JarZipFilter implements FilenameFilter { 282 public boolean accept(File dir, String name) { 283 return(name.endsWith(".jar") || name.endsWith(".zip")); 284 } 285 } 286}