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    }