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}