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 */
024 package net.sourceforge.cobertura.util;
025
026 import java.io.File;
027 import java.io.FileInputStream;
028 import java.io.FilenameFilter;
029 import java.io.IOException;
030 import java.util.ArrayList;
031 import java.util.Enumeration;
032 import java.util.HashMap;
033 import java.util.HashSet;
034 import java.util.Iterator;
035 import java.util.List;
036 import java.util.Map;
037 import java.util.Set;
038 import java.util.jar.JarEntry;
039 import java.util.jar.JarFile;
040
041 import 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 */
063 public 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 }