View Javadoc

1   /*
2    * EL4J, the Extension Library for the J2EE, adds incremental enhancements to
3    * the spring framework, http://el4j.sf.net
4    * Copyright (C) 2005 by ELCA Informatique SA, Av. de la Harpe 22-24,
5    * 1000 Lausanne, Switzerland, http://www.elca.ch
6    *
7    * EL4J is published under the GNU Lesser General Public License (LGPL)
8    * Version 2.1. See http://www.gnu.org/licenses/
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   * GNU Lesser General Public License for more details.
14   *
15   * For alternative licensing, please contact info@elca.ch
16   */
17  
18  package ch.elca.el4j.util.classpath;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.net.JarURLConnection;
23  import java.net.MalformedURLException;
24  import java.net.URISyntaxException;
25  import java.net.URL;
26  import java.net.URLClassLoader;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.jar.JarEntry;
36  import java.util.jar.JarFile;
37  
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * A tool for looking up duplicate class definitions in the classpath.
43   * It can also be used to inspect all class definitions loaded.
44   *
45   * @svnLink $Revision: 3874 $;$Date: 2009-08-04 14:25:40 +0200 (Di, 04. Aug 2009) $;$Author: swismer $;$URL: https://el4j.svn.sourceforge.net/svnroot/el4j/branches/el4j_3_1/el4j/framework/modules/core/src/main/java/ch/elca/el4j/util/classpath/DuplicateClassFinder.java $
46   *
47   * @author David Bernhard (DBD)
48   */
49  public class DuplicateClassFinder {
50  
51  	/*
52  	 * m_classes maps a fully qualified class name to a list of definitions.
53  	 * Any time a class is redefined, its name is added to m_duplicates.
54  	 * Invariant : m_duplicates contains exactly those class names that have
55  	 * more than one definition.
56  	 */
57  	
58  	/** The logger. */
59  	private static final Logger s_log = LoggerFactory.getLogger("DuplicateClassFinder");
60  	
61  	/** Holds all class names seen so far. */
62  	private Map<String, List<String>> m_classes;
63  	
64  	/** Holds urls to search for class definitions. */
65  	private List<URL> m_urls;
66  	
67  	/** Holds the names of duplicated classes. */
68  	private Set<String> m_duplicates;
69  	
70  	/** Whether we have completed the search yet. */
71  	private boolean m_searched = false;
72  	
73  	/**
74  	 * Default constructor - initialize the members.
75  	 */
76  	public DuplicateClassFinder() {
77  		m_classes = new HashMap<String, List<String>>();
78  		m_urls = new LinkedList<URL>();
79  		m_duplicates = new HashSet<String>();
80  	}
81  	
82  	/**
83  	 * Helper to check that a search has been executed.
84  	 * @param value The value to ckeck against.
85  	 * @throws RuntimeException If the status is not correct.
86  	 */
87  	private void assertSearched(boolean value) throws RuntimeException {
88  		if (!m_searched && value) {
89  			throw new RuntimeException("You must execute search() before "
90  				+ "performing this operation.");
91  		}
92  		if (m_searched && !value) {
93  			throw new RuntimeException("You cannot perform this poeration "
94  				+ "once a search has been performed.");
95  		}
96  	}
97  	
98  	/**
99  	 * Output a warning when a duplicate is found.
100 	 * @param className The class that is duplicated.
101 	 * @param oldLoc The existing location.
102 	 * @param newLoc The new location.
103 	 */
104 	private void warnDuplicate(String className, String oldLoc, String newLoc) {
105 		s_log.warn("The class " + className + " is duplicated:");
106 		s_log.warn("Last seen at " + oldLoc + " , duplicated in " + newLoc);
107 	}
108 	
109 	/**
110 	 * Add an URL to the search path.
111 	 * @param url The URL to add.
112 	 */
113 	public void addUrl(URL url) {
114 		assertSearched(false);
115 		s_log.debug("Added URL " + url);
116 		m_urls.add(url);
117 	}
118 	
119 	/**
120 	 * Add all URLs of a given classloader to the search path.
121 	 * @param cl The classloader.
122 	 */
123 	public void addURLClassLoader(URLClassLoader cl) {
124 		for (URL url : cl.getURLs()) {
125 			addUrl(url);
126 		}
127 	}
128 	
129 	/**
130 	 * Add a class's classloader search path. If the class is loaded by an
131 	 * {@link URLClassLoader}, add its URLs. If not, add the system classpath.
132 	 * @param c The class to add.
133 	 */
134 	public void addClass(Class<?> c) {
135 		ClassLoader cl = c.getClassLoader();
136 		if (cl instanceof URLClassLoader) {
137 			addURLClassLoader((URLClassLoader) cl);
138 		} else {
139 			addSystemClassPath();
140 		}
141 	}
142 	
143 	/**
144 	 * Add the system class path to the search path.
145 	 */
146 	public void addSystemClassPath() {
147 		String cp = System.getProperty("java.class.path");
148 		String[] entries = cp.split(System.getProperty("path.separator"));
149 		for (String entry : entries) {
150 			try {
151 				addUrl(new URL(entry));
152 			} catch (MalformedURLException e) {
153 				throw new RuntimeException("Error adding system cp to urls.");
154 			}
155 		}
156 	}
157 	
158 	/**
159 	 * Search the added urls for classes.
160 	 */
161 	public void search() {
162 		assertSearched(false);
163 		m_searched = true;
164 
165 		s_log.info("Searching ...");
166 		Iterator<URL> i = m_urls.iterator();
167 		
168 		// Iterate over all urls. If a directory or jar file, recurse.
169 		// Anything else is "unhandled"
170 		while (i.hasNext()) {
171 			URL next = i.next();
172 			
173 			if (!next.getProtocol().equalsIgnoreCase("FILE")) {
174 				s_log.warn("The entry " + next + "is unhandled.");
175 				return;
176 			}
177 			
178 			if (next.toString().endsWith(".jar")) {
179 				searchJar(next);
180 			} else {
181 				searchDirectory(next);
182 			}
183 		}
184 		report();
185 	}
186 	
187 	/**
188 	 * Display the search results.
189 	 */
190 	public void report() {
191 		assertSearched(true);
192 		if (m_duplicates.isEmpty()) {
193 			s_log.info("The search was successful, no classes are duplicated.");
194 			return;
195 		}
196 	
197 		s_log.warn("The search found " + m_duplicates.size()
198 			+ " duplicated classes:");
199 		for (Iterator<String> i = m_duplicates.iterator(); i.hasNext();) {
200 			String current = i.next();
201 			List<String> theList = m_classes.get(current);
202 			s_log.warn("  Class " + current + " appears "
203 				+ theList.size() + " times, at:");
204 			for (String location : theList) {
205 				s_log.warn("    " + location);
206 			}
207 		}
208 	}
209 	
210 	/**
211 	 * Search a base directory from the classpath. Ensures it is a directory,
212 	 * then delegates to recurseDirectory.
213 	 * @param url The directory.
214 	 */
215 	private void searchDirectory(URL url) {
216 		s_log.info("Searching Base Directory: " + url);
217 		try {
218 			File baseDir = new File(url.toURI());
219 			if (!baseDir.isDirectory()) {
220 				s_log.warn("File " + url + " is not a directory.");
221 				return;
222 			}
223 			recurseDirectory(baseDir.getAbsolutePath(), "");
224 		} catch (URISyntaxException e) {
225 			throw new RuntimeException("URL syntax error", e);
226 		}
227 	}
228 	
229 	/**
230 	 * Recursively search a directory, adding all .class files and recursing
231 	 * into subdirectories.
232 	 * @param base The base directory (classpath entry)
233 	 * @param rel the current relative path and package prefix.
234 	 */
235 	private void recurseDirectory(String base, String rel) {
236 		if (!rel.equals("")) {
237 			s_log.info("Recursing into: " + rel);
238 		}
239 		File[] files = new File(base + rel).listFiles();
240 		for (File file : files) {
241 			if (file.isDirectory()) {
242 				recurseDirectory(base, rel + "/" + file.getName());
243 			} else if (file.getName().endsWith(".class")) {
244 				addClassFile(file, rel);
245 			}
246 		}
247 	}
248 	
249 	/**
250 	 * Add a .class file from the filesystem. Delegates to addClass.
251 	 * @param file The class file.
252 	 * @param rel The relative path and package name.
253 	 */
254 	private void addClassFile(File file, String rel) {
255 		String pkg = rel;
256 		pkg = pkg.replaceAll("/", ".");
257 		pkg = pkg.replaceAll("\\\\", ".");
258 		if (pkg.startsWith(".")) {
259 			pkg = pkg.substring(1);
260 		}
261 		String name = file.getName();
262 		name = name.substring(0, name.length() - ".class".length());
263 		addClass(pkg, name, file.getAbsolutePath());
264 	}
265 	
266 	/**
267 	 * The method that actually adds classes. Check if it exists;
268 	 * if not add it, if it does add it to duplicates and warn.
269 	 * @param pkg The package name
270 	 * @param name The class name
271 	 * @param location The location of this .class file.
272 	 */
273 	private void addClass(String pkg, String name, String location) {
274 		String className = pkg.equals("") ? name : pkg + "." + name;
275 		s_log.info("Adding class " + className + "  from " + location);
276 		if (m_classes.get(className) != null) {
277 			// OOPS! It exists already.
278 			// Warn, and link in to the list of duplicates.
279 			List<String> theList = m_classes.get(className);
280 			warnDuplicate(className, theList.get(theList.size() - 1), location);
281 			theList.add(location);
282 			m_duplicates.add(className);
283 		} else {
284 			// It's new. Create its list and add it.
285 			List <String> newList = new LinkedList<String>();
286 			newList.add(location);
287 			m_classes.put(className, newList);
288 		}
289 	}
290 	
291 	/**
292 	 * Search a .jar file and add all .class files in it.
293 	 * @param url The jar's url.
294 	 */
295 	private void searchJar(URL url) {
296 		s_log.info("Searching jar: " + url);
297 		try {
298 			URL jar = new URL("jar:" + url.toExternalForm() + "!/");
299 			JarURLConnection conn = (JarURLConnection) jar.openConnection();
300 			JarFile jarFile = conn.getJarFile();
301 			Enumeration<JarEntry> entries = jarFile.entries();
302 			while (entries.hasMoreElements()) {
303 				JarEntry e = entries.nextElement();
304 				if (e.getName().endsWith(".class")) {
305 					String path = e.getName();
306 					// For package name, strip ".class" and
307 					// change foo/bar to foo.bar
308 					String pkg
309 						= path.substring(0, path.length() - ".class".length())
310 							.replaceAll("/", ".");
311 					if (pkg.startsWith(".")) {
312 						pkg = pkg.substring(1);
313 					}
314 					int splitter = pkg.lastIndexOf(".");
315 					String name;
316 					if (splitter != -1) {
317 						name = pkg.substring(splitter + 1);
318 						pkg = pkg.substring(0, splitter);
319 					} else {
320 						// top-level package
321 						name = pkg;
322 						pkg = "";
323 					}
324 					addClass(pkg, name, jarFile.getName() + "!/" + path);
325 				}
326 			}
327 			
328 		} catch (IOException e) {
329 			throw new RuntimeException("IO Exception reading jar: " + url);
330 		}
331 	}
332 	
333 	/**
334 	 * @return An iterator over all found classes, for report purposes.
335 	 */
336 	Iterator<String> iterator() {
337 		assertSearched(true);
338 		return m_classes.keySet().iterator();
339 	}
340 	
341 	/**
342 	 * @return    all found classes
343 	 */
344 	public Set<String> getAllClasses() {
345 		return new HashSet<String>(m_classes.keySet());
346 	}
347 	
348 	/**
349 	 * Looks up whether a class name exists.
350 	 * @param name The fully qualified class name.
351 	 * @return boolean.
352 	 */
353 	public boolean hasClass(String name) {
354 		assertSearched(true);
355 		return (m_classes.get(name) != null);
356 	}
357 	
358 	/**
359 	 * Returns all locations a class is defined at.
360 	 * @param name The class name.
361 	 * @return A list of locations.
362 	 */
363 	public List<String> getLocations(String name) {
364 		if (!hasClass(name)) {
365 			return null;
366 		}
367 		return m_classes.get(name);
368 	}
369 	
370 	/**
371 	 * @return Whether any duplicates were found.
372 	 */
373 	public boolean duplicatesFound() {
374 		assertSearched(true);
375 		return (m_duplicates != null && m_duplicates.size() > 0);
376 	}
377 	
378 	/**
379 	 * @return A set of strings describing duplicated classes.
380 	 */
381 	public Set<String> getAllDuplicates() {
382 		assertSearched(true);
383 		return m_duplicates;
384 	}
385 }