001    /**
002     * $Id: ClasspathDigger.java,v 1.11 2012/03/20 18:46:23 oboehm Exp $
003     *
004     * Copyright (c) 2009 by Oliver Boehm
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     *
018     * (c)reated 15.05.2009 by oliver (ob@aosd.de)
019     */
020    package patterntesting.runtime.monitor;
021    
022    import java.io.File;
023    import java.lang.management.ManagementFactory;
024    import java.lang.reflect.*;
025    import java.net.*;
026    import java.util.*;
027    
028    import javax.management.*;
029    
030    import org.apache.commons.lang.StringUtils;
031    import org.slf4j.*;
032    
033    import patterntesting.runtime.util.*;
034    
035    /**
036     * This helper class digs into found classloader for information like
037     * used classpath and other things. It was extracted from ClasspathMonitor
038     * to separate the classloader specific part of it into its own class.
039     * If you want to support an unknown classloader you can subclass this class
040     * and together with ClasspathMonitor.
041     *
042     * @author <a href="boehm@javatux.de">oliver</a>
043     * @since 15.05.2009
044     * @version $Revision: 1.11 $
045     */
046    public final class ClasspathDigger {
047        
048        /** The ClasspathAgent as MBean. */
049        protected static final ObjectName AGENT_MBEAN ;
050    
051        private static final Logger log = LoggerFactory.getLogger(ClasspathDigger.class);
052        private final ClassLoader classLoader;
053        private static Boolean classLoaderSupported = null;
054        private static final String[] supportedClassLoaders = {
055                "sun.misc.Launcher$AppClassLoader",
056                "org.apache.catalina.loader.WebappClassLoader",
057                "weblogic.utils.classloaders.ChangeAwareClassLoader"
058        };
059        /** This will work of course only for the SunVM. */
060        private final String[] bootClassPath = ClasspathDigger.getClasspath("sun.boot.class.path");
061        private static final MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
062    
063        static {
064            try {
065                AGENT_MBEAN = new ObjectName("patterntesting.agent:type=ClasspathAgent");
066            } catch (MalformedObjectNameException e) {
067                throw new ExceptionInInitializerError(e);
068            }
069        }
070        
071        /**
072         * Instantiates a new classpath digger.
073         */
074        public ClasspathDigger() {
075            this(Environment.getClassLoader());
076        }
077        
078        /**
079         * Instantiates a new classpath digger.
080         *
081         * @param cloader the cloader
082         * @since 1.2
083         */
084        public ClasspathDigger(final ClassLoader cloader) {
085            this.classLoader = cloader;
086        }
087    
088        /**
089         * Gets the class loader.
090         *
091         * @return the classLoader
092         */
093        public ClassLoader getClassLoader() {
094            return classLoader;
095        }
096    
097        /**
098         * Checks if is classloader supported.
099         *
100         * @return true, if is classloader supported
101         */
102        public boolean isClassloaderSupported() {
103            if (classLoaderSupported != null) {
104                return classLoaderSupported;
105            }
106            String classloaderName = classLoader.getClass().getName();
107            for (int i = 0; i < supportedClassLoaders.length; i++) {
108                if(classloaderName.equals(supportedClassLoaders[i])) {
109                    return true;
110                }
111            }
112            return false;
113        }
114        
115        /**
116         * Checks if the ClasspathAgent is available as MBean.
117         * The ClasspathAgent is needed for classloaders which are not
118         * directly supported (e.g. IBM's classloader of their JDK).
119         *
120         * @return true, if is agent available
121         */
122        public static boolean isAgentAvailable() {
123            try {
124                return (mbeanServer.getObjectInstance(AGENT_MBEAN) != null);
125            } catch (InstanceNotFoundException e) {
126                log.debug("{} is not available.", AGENT_MBEAN, e);
127                return false;
128            }
129        }
130    
131        /**
132         * To get the boot classpath the sytem property "sun.boot.class.path" is
133         * used to get them. This will work of course only for the SunVM.
134         *
135         * @return the boot classpath as String array
136         */
137        public String[] getBootClasspath() {
138            return this.bootClassPath;
139        }
140    
141        /**
142         * We can use the system property "java.class.path" to get the classpath.
143         * But this works not inside an application server or servlet enginle (e.g.
144         * inside Tomcat) because they have their own classloader to load the
145         * classes. <br/>
146         * In the past we tried to use the private (and undocoumented) attribute
147         * "domains" of the classloader. This works for a normal application but
148         * Tomcat's WebappClassLoader listed also classes in the "domains"-Set. Now
149         * we will try to detect the different classloader to access some private
150         * and secret attributes of this classloader. <br/>
151         * At the moment only org.apache.catalina.loader.WebappClassLoader is
152         * supported. For all other classloaders the standard approach using the
153         * system property "java.class.path" is used.
154         *
155         * @return the classpath as String array
156         */
157        public String[] getClasspath() {
158            String classloaderName = classLoader.getClass().getName();
159            if (classloaderName
160                    .equals("org.apache.catalina.loader.WebappClassLoader")) {
161                return getTomcatClasspath();
162            } else if (classloaderName.startsWith("weblogic")) {
163                    return getWeblogicClasspath();
164            } else {
165                log.trace("using 'java.class.path' to get classpath...");
166                return ClasspathDigger.getClasspath("java.class.path");
167            }
168        }
169    
170        private String[] getTomcatClasspath() {
171            try {
172                Field field = ReflectionHelper.getField(classLoader.getClass(),
173                        "repositoryURLs");
174                URL[] repositoryURLs = (URL[]) field.get(classLoader);
175                String[] cp = new String[repositoryURLs.length];
176                for (int i = 0; i < cp.length; i++) {
177                    cp[i] = Converter.toAbsolutePath(Converter.toURI(repositoryURLs[i]));
178                }
179                return cp;
180            } catch (Exception e) {
181                log.warn("can't access field 'repositoryURLs'", e);
182                return getClasspath("java.class.path");
183            }
184        }
185    
186        private String[] getWeblogicClasspath() {
187            return getClasspathFromPackages();
188        }
189    
190            /**
191             * Gets the classpath.
192             *
193             * @param key the key
194             * @return the classpath as String array
195             */
196            protected static String[] getClasspath(final String key) {
197                String classpath = System.getProperty(key);
198                if (classpath == null) {
199                    log.info(key + " is not set (not a SunVM?)");
200                    return new String[0];
201                }
202                return splitClasspath(classpath);
203            }
204    
205            private static String[] splitClasspath(final String classpath) {
206                String separator = System.getProperty("path.separator", ":");
207                String[] cp = StringUtils.split(classpath, separator);
208                for (int i = 0; i < cp.length; i++) {
209                    if (cp[i].endsWith(File.separator)) {
210                        cp[i] = cp[i].substring(0, (cp[i].length() - 1));
211                    }
212                }
213                return cp;
214            }
215    
216        /**
217         * Returns the packages which were loaded by the classloader. <br/>
218         * Unfortunately ClassLoader.getPackages() is protected - ok, let's do the
219         * hard way using reflexion.
220         *
221         * @return array with the loaded packages
222         */
223        public Package[] getLoadedPackageArray() {
224            Method method;
225            Class<?> cloaderClass = classLoader.getClass();
226                    try {
227                            method = ReflectionHelper.getMethod(cloaderClass, "getPackages");
228                    } catch (NoSuchMethodException nsme) {
229                log.warn(cloaderClass + "#getPackages() not found", nsme);
230                return new Package[0];
231                } catch (SecurityException e) {
232                    log.warn("can't get method " + cloaderClass + "#getPackages()", e);
233                    return new Package[0];
234            }
235            try {
236                method.setAccessible(true);
237                return (Package[]) method.invoke(classLoader);
238            } catch (Exception e) {
239                log.warn("can't access " + method, e);
240                return new Package[0];
241            }
242        }
243    
244        /**
245         * Here we use the loaded packages to calculate the classpath. For each
246         * loaded package we will look from which jar file or directory this
247         * package is loaded.
248         *
249         * @return the found classpath as string array
250         * @since 27-Jul-2009
251         */
252        protected String[] getClasspathFromPackages() {
253            Set<URI> packageURIs = new LinkedHashSet<URI>();
254            Package[] packages = this.getLoadedPackageArray();
255            for (int i = 0; i < packages.length; i++) {
256                            String resource = Converter.toResource(packages[i]);
257                            URI uri = whichResource(resource, this.classLoader);
258                            if (uri != null) {
259                                    URI path = ClasspathHelper.getParent(uri, resource);
260                                    packageURIs.add(path);
261                            }
262                    }
263            return getClasspathFromPackages(packageURIs);
264        }
265    
266            private String[] getClasspathFromPackages(final Set<URI> packages) {
267                    String[] classpath = new String[packages.size()];
268                    Iterator<URI> iterator = packages.iterator();
269                    for (int i = 0; i < classpath.length; i++) {
270                            URI uri = iterator.next();
271                            classpath[i] = Converter.toAbsolutePath(uri);
272                    }
273                    return classpath;
274            }
275    
276        /**
277         * Returns the URI of the given resource and the given classloader.
278         * If the resource is not found it will be tried again with/without
279         * a leading "/" and with the parent classloader.
280         *
281         * @param name resource name (e.g. "log4j.properties")
282         * @return URI of the given resource (or null if resource was not found)
283         */
284        public URI whichResource(final String name) {
285            return whichResource(name, this.classLoader);
286        }
287    
288            /**
289             * Returns the URI of the given resource and the given classloader.
290             * If the resource is not found it will be tried again with/without
291             * a leading "/" and with the parent classloader.
292             *
293             * @param name resource name (e.g. "log4j.properties")
294             * @param cloader class loader
295             * @return URI of the given resource (or null if resource was not found)
296             */
297            //@ProfileMe
298            public static URI whichResource(final String name, final ClassLoader cloader) {
299                assert cloader != null : "no classloader given";
300                URL url = cloader.getResource(name);
301                if (url == null) {
302                    if (name.startsWith("/")) {
303                        url = cloader.getResource(name.substring(1));
304                    } else {
305                        url = cloader.getResource("/" + name);
306                    }
307                }
308                if (url == null) {
309                    ClassLoader parent = cloader.getParent();
310                    if (parent == null) {
311                        return null;
312                    } else {
313                        if (log.isTraceEnabled()) {
314                            log.trace(name + " not found with " + cloader
315                                    + ", will ask " + parent + "...");
316                        }
317                        return whichResource(name, parent);
318                    }
319                }
320                return Converter.toURI(url);
321            }
322    
323            /**
324             * Checks if the given classname is loaded.
325             * Why does we use not Class as parameter here? If you would allow a
326             * parameter of type "Class" this class will be problably loaded before
327             * and this method will return always true!
328             *
329             * @param classname name of the class
330             * @return true if class is loaded
331             */
332        public boolean isLoaded(final String classname) {
333            for (Iterator<Class<?>> iterator = getLoadedClassList().iterator(); iterator.hasNext();) {
334                Class<?> loaded = iterator.next();
335                if (classname.equals(loaded.getName())) {
336                    return true;
337                }
338            }
339            return false;
340        }
341    
342        /**
343         * Puts also the classloader in the toString representation.
344         *
345         * @return string containing the class laoder
346         * @see java.lang.Object#toString()
347         */
348        @Override
349        public String toString() {
350            return "ClasspathDigger for " + this.classLoader;
351        }
352    
353        /**
354         * Returns a list of classes which were loaded by the given classloader. <br/>
355         * Ok, we must do some hacks here: there is an undocumented attribute
356         * "classes" which contains the loaded classes. <br/>
357         * HANDLE WITH CARE (it's a hack and it depends on the used classloader)
358         *
359         * @return list of classes
360         */
361        //@ProfileMe
362        @SuppressWarnings("unchecked")
363        public List<Class<?>> getLoadedClassList() {
364            try {
365                Field field = ReflectionHelper.getField(classLoader.getClass(),
366                        "classes");
367                List<Class<?>> classList = (List<Class<?>>) field.get(classLoader);
368                return new ArrayList<Class<?>>(classList);
369            } catch (Exception e) {
370                log.debug("Unsupported classloader {} / has no field \"classes\".", classLoader, e);
371                return getLoadedClassListFromAgent();
372            }
373        }
374        
375        /**
376         * Gets the loaded class list from patterntesting-agent. For this method you
377         * must start the Java VM with PatternTesting Agent as Java agent
378         * (<tt>java -javaagent:patterntesting-agent-1.x.x.jar ...</tt>) because
379         * this MBean is needed for the loaded classes.
380         * <br/>
381         * This class is protected for test reason.
382         *
383         * @return the loaded class list from agent
384         */
385        protected List<Class<?>> getLoadedClassListFromAgent() {
386            try {
387                log.trace("Using \"{}\" as fallback for unsupported classloader {}.", AGENT_MBEAN, this.classLoader);
388                Class<?>[] classes = (Class<?>[]) mbeanServer.invoke(AGENT_MBEAN, "getLoadedClasses", new Object[] { this
389                        .getClass().getClassLoader() }, new String[] { ClassLoader.class.getName() });
390                return Arrays.asList(classes);
391            } catch (InstanceNotFoundException e) {
392                log.warn("MBean \"{}\" not found ({}) - be sure to call patterntesting as agent"
393                        + " ('java -javaagent:patterntesting-agent-1.x.x.jar...')", AGENT_MBEAN, e);
394                return new ArrayList<Class<?>>();
395            } catch (JMException e) {
396                log.warn("Cannot call 'getLoadedClasses(..)' from MBean \"{}\"", AGENT_MBEAN, e);
397                return new ArrayList<Class<?>>();
398            }
399        }
400    
401    }