You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:21:44 UTC

[sling-org-apache-sling-commons-fsclassloader] 02/05: SLING-5980: Deduplicating the FSClassLoader cache clearing functionality and adding a JMX interface

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.commons.fsclassloader-1.0.6
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-commons-fsclassloader.git

commit a5f873258b6519e702a044a80f093d4a945da7f0
Author: Dan Klco <dk...@apache.org>
AuthorDate: Fri Feb 17 19:06:12 2017 +0000

    SLING-5980: Deduplicating the FSClassLoader cache clearing functionality and adding a JMX interface
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/commons/fsclassloader@1783452 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |   8 +-
 .../commons/fsclassloader/FSClassLoaderMBean.java  |  59 ++
 .../fsclassloader/impl/FSClassLoaderMBeanImpl.java | 112 ++++
 .../fsclassloader/impl/FSClassLoaderProvider.java  | 549 +++++++++-------
 .../impl/FSClassLoaderWebConsole.java              | 691 +++++++++------------
 .../commons/fsclassloader/impl/ScriptFiles.java    |  89 +++
 6 files changed, 883 insertions(+), 625 deletions(-)

diff --git a/pom.xml b/pom.xml
index d79e248..f75ddc3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,7 +97,13 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.commons.classloader</artifactId>
-            <version>1.3.0</version>
+            <version>1.3.9-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.1.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/commons/fsclassloader/FSClassLoaderMBean.java b/src/main/java/org/apache/sling/commons/fsclassloader/FSClassLoaderMBean.java
new file mode 100644
index 0000000..be49971
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/fsclassloader/FSClassLoaderMBean.java
@@ -0,0 +1,59 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2017 - Adobe
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package org.apache.sling.commons.fsclassloader;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * MBean interface for interacting with the FSClassLoader
+ */
+public interface FSClassLoaderMBean {
+
+	/**
+	 * Clears the cache of compiled scripts from the FSClassLoader
+	 */
+	void clearCache();
+
+	/**
+	 * Gets the root directory at which the FSClassloader is creating it's
+	 * cached files
+	 * 
+	 * @return the file system classloader root
+	 */
+	String getFSClassLoaderRoot();
+
+	/**
+	 * Get the total count of scripts in the FSClassLoader cache
+	 * 
+	 * @return the total number of scripts
+	 * @throws IOException
+	 */
+	int getCachedScriptCount() throws IOException;
+
+	/**
+	 * Gets the scripts in the FSClassLoaderCache
+	 * 
+	 * @return the scripts from the FSClassLoaderCache
+	 * @throws IOException
+	 */
+	List<String> getCachedScripts() throws IOException;
+
+}
diff --git a/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderMBeanImpl.java b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderMBeanImpl.java
new file mode 100644
index 0000000..d9afe3c
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderMBeanImpl.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.commons.fsclassloader.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.commons.fsclassloader.FSClassLoaderMBean;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of the FSClassLoaderMBean interface
+ */
+public class FSClassLoaderMBeanImpl implements FSClassLoaderMBean {
+	private final BundleContext context;
+	private final FSClassLoaderProvider fsClassLoaderProvider;
+	private static final Logger log = LoggerFactory.getLogger(FSClassLoaderMBeanImpl.class);
+
+	public FSClassLoaderMBeanImpl(final FSClassLoaderProvider fsClassLoaderProvider, final BundleContext context) {
+		this.fsClassLoaderProvider = fsClassLoaderProvider;
+		this.context = context;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see org.apache.sling.commons.fsclassloader.FSClassLoaderMBean#
+	 * getCachedScriptCount()
+	 */
+	@Override
+	public int getCachedScriptCount() throws IOException {
+		return getScripts().size();
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see org.apache.sling.commons.fsclassloader.FSClassLoaderMBean#
+	 * getCachedScripts()
+	 */
+	@Override
+	public List<String> getCachedScripts() {
+		List<String> scripts = new ArrayList<String>();
+		scripts.addAll(getScripts());
+		Collections.sort(scripts);
+		return scripts;
+	}
+
+	private Collection<String> getScripts() {
+		Collection<String> scripts = new HashSet<String>();
+		try {
+			Map<String, ScriptFiles> s = new LinkedHashMap<String, ScriptFiles>();
+			File root = new File(context.getDataFile(""), "classes");
+			if (root != null) {
+				FSClassLoaderWebConsole.readFiles(root, root, s);
+			}
+			scripts = s.keySet();
+		} catch (Exception e) {
+			log.warn("Exception retrieving scripts from FSClassLoader", e);
+		}
+		return scripts;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * org.apache.sling.commons.fsclassloader.FSClassLoaderMBean#clearCache()
+	 */
+	@Override
+	public void clearCache() {
+		fsClassLoaderProvider.delete("");
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see org.apache.sling.commons.fsclassloader.FSClassLoaderMBean#
+	 * getFSClassLoaderRoot()
+	 */
+	@Override
+	public String getFSClassLoaderRoot() {
+		return new File(context.getDataFile(""), "classes").getAbsolutePath();
+	}
+
+}
diff --git a/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderProvider.java b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderProvider.java
index ee6b6bc..2b63f8d 100644
--- a/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderProvider.java
+++ b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderProvider.java
@@ -28,7 +28,13 @@ import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -37,10 +43,17 @@ import org.apache.felix.scr.annotations.Property;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.Service;
 import org.apache.sling.commons.classloader.ClassLoaderWriter;
+import org.apache.sling.commons.classloader.ClassLoaderWriterListener;
 import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.commons.fsclassloader.FSClassLoaderMBean;
+import org.apache.sling.commons.osgi.PropertiesUtil;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
 import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,240 +64,304 @@ import org.slf4j.LoggerFactory;
  *
  */
 @Component
-@Service(value={ClassLoaderWriter.class}, serviceFactory = true)
-@Property( name=Constants.SERVICE_RANKING, intValue=100)
-public class FSClassLoaderProvider
-    implements ClassLoaderWriter {
-
-    /** File root */
-    private File root;
-
-    /** File root URL */
-    private URL rootURL;
-
-    /** Current class loader */
-    private FSDynamicClassLoader loader;
-
-    private final Logger logger = LoggerFactory.getLogger(this.getClass());
-
-    @Reference(
-            referenceInterface = DynamicClassLoaderManager.class,
-            bind = "bindDynamicClassLoaderManager",
-            unbind = "unbindDynamicClassLoaderManager")
-    private ServiceReference dynamicClassLoaderManager;
-
-    /** The bundle asking for this service instance */
-    private Bundle callerBundle;
-
-    /**
-     * Activate this component.
-     * Create the root directory.
-     * @param componentContext
-     * @throws MalformedURLException
-     */
-    @Activate
-    protected void activate(final ComponentContext componentContext) throws MalformedURLException {
-        // get the file root
-        this.root = new File(componentContext.getBundleContext().getDataFile(""), "classes");
-        this.root.mkdirs();
-        this.rootURL = this.root.toURI().toURL();
-        this.callerBundle = componentContext.getUsingBundle();
-    }
-
-    /**
-     * Deactivate this component.
-     * Create the root directory.
-     */
-    @Deactivate
-    protected void deactivate() {
-        this.root = null;
-        this.rootURL = null;
-        this.destroyClassLoader();
-    }
-
-    /**
-     * Called to handle binding the DynamicClassLoaderManager service
-     * reference
-     */
-    @SuppressWarnings("unused")
-    private void bindDynamicClassLoaderManager(final ServiceReference ref) {
-        this.dynamicClassLoaderManager = ref;
-    }
-
-    /**
-     * Called to handle unbinding of the DynamicClassLoaderManager service
-     * reference
-     */
-    @SuppressWarnings("unused")
-    private void unbindDynamicClassLoaderManager(final ServiceReference ref) {
-        if (this.dynamicClassLoaderManager == ref) {
-            this.dynamicClassLoaderManager = null;
-        }
-    }
-
-    private void destroyClassLoader() {
-        final ClassLoader rcl = this.loader;
-        if (rcl != null) {
-            this.loader = null;
-
-            final ServiceReference localDynamicClassLoaderManager = this.dynamicClassLoaderManager;
-            final Bundle localCallerBundle = this.callerBundle;
-            if ( localDynamicClassLoaderManager != null && localCallerBundle != null ) {
-                localCallerBundle.getBundleContext().ungetService(localDynamicClassLoaderManager);
-            }
-        }
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getClassLoader()
-     */
-    public ClassLoader getClassLoader() {
-        synchronized ( this ) {
-            if ( loader == null || !loader.isLive() ) {
-                this.destroyClassLoader();
-                // get the dynamic class loader for the bundle using this
-                // class loader writer
-                final DynamicClassLoaderManager dclm = (DynamicClassLoaderManager) this.callerBundle.getBundleContext().getService(
-                    this.dynamicClassLoaderManager);
-
-                loader = new FSDynamicClassLoader(new URL[] {this.rootURL}, dclm.getDynamicClassLoader());
-            }
-            return this.loader;
-        }
-    }
-
-    private void checkClassLoader(final String filePath) {
-        if ( filePath.endsWith(".class") ) {
-            // remove store directory and .class
-            final String path = filePath.substring(this.root.getAbsolutePath().length() + 1, filePath.length() - 6);
-            // convert to a class name
-            final String className = path.replace(File.separatorChar, '.');
-
-            synchronized ( this ) {
-                final FSDynamicClassLoader currentLoader = this.loader;
-                if ( currentLoader != null ) {
-                    currentLoader.check(className);
-                }
-            }
-        }
-    }
-
-    //---------- SCR Integration ----------------------------------------------
-
-    private boolean deleteRecursive(final File f, final List<String> names) {
-        if ( f.isDirectory() ) {
-            for(final File c : f.listFiles()) {
-                if ( !deleteRecursive(c, names) ) {
-                    return false;
-                }
-            }
-        }
-        names.add(f.getAbsolutePath());
-        return f.delete();
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#delete(java.lang.String)
-     */
-    public boolean delete(final String name) {
-        final String path = cleanPath(name);
-        final File file = new File(path);
-        if ( file.exists() ) {
-            final List<String> names = new ArrayList<String>();
-            final boolean result = deleteRecursive(file, names);
-            logger.debug("Deleted {} : {}", name, result);
-            if ( result ) {
-                for(final String n : names ) {
-                    this.checkClassLoader(n);
-                }
-            }
-
-            return result;
-        }
-        // file does not exist so we return false
-        return false;
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getOutputStream(java.lang.String)
-     */
-    public OutputStream getOutputStream(final String name) {
-        logger.debug("Get stream for {}", name);
-        final String path = cleanPath(name);
-        final File file = new File(path);
-        final File parentDir = file.getParentFile();
-        if ( !parentDir.exists() ) {
-            parentDir.mkdirs();
-        }
-        try {
-            if ( file.exists() ) {
-                this.checkClassLoader(path);
-            }
-            return new FileOutputStream(path);
-        } catch (FileNotFoundException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#rename(java.lang.String, java.lang.String)
-     */
-    public boolean rename(final String oldName, final String newName) {
-        logger.debug("Rename {} to {}", oldName, newName);
-        final String oldPath = cleanPath(oldName);
-        final String newPath = cleanPath(newName);
-        final File old = new File(oldPath);
-        final boolean result = old.renameTo(new File(newPath));
-        if ( result ) {
-            this.checkClassLoader(oldPath);
-            this.checkClassLoader(newPath);
-        }
-        return result;
-    }
-
-    /**
-     * Clean the path by converting slashes to the correct format
-     * and prefixing the root directory.
-     * @param path The path
-     * @return The file path
-     */
-    private String cleanPath(String path) {
-        // replace backslash by slash
-        path = path.replace('\\', '/');
-
-        // cut off trailing slash
-        while (path.endsWith("/")) {
-            path = path.substring(0, path.length() - 1);
-        }
-        if ( File.separatorChar != '/') {
-            path = path.replace('/', File.separatorChar);
-        }
-        return this.root.getAbsolutePath() + path;
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getInputStream(java.lang.String)
-     */
-    public InputStream getInputStream(final String name)
-    throws IOException {
-        logger.debug("Get input stream of {}", name);
-        final String path = cleanPath(name);
-        final File file = new File(path);
-        return new FileInputStream(file);
-    }
-
-    /**
-     * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getLastModified(java.lang.String)
-     */
-    public long getLastModified(final String name) {
-        logger.debug("Get last modified of {}", name);
-        final String path = cleanPath(name);
-        final File file = new File(path);
-        if ( file.exists() ) {
-            return file.lastModified();
-        }
-
-        // fallback to "non-existant" in case of problems
-        return -1;
-    }
+@Service(value = { ClassLoaderWriter.class }, serviceFactory = true)
+@Property(name = Constants.SERVICE_RANKING, intValue = 100)
+public class FSClassLoaderProvider implements ClassLoaderWriter {
+
+	private static final String LISTENER_FILTER = "(" + Constants.OBJECTCLASS + "="
+			+ ClassLoaderWriterListener.class.getName() + ")";
+
+	/** File root */
+	private File root;
+
+	/** File root URL */
+	private URL rootURL;
+
+	/** Current class loader */
+	private FSDynamicClassLoader loader;
+
+	private static ServiceListener classLoaderWriterServiceListener;
+
+	private Map<Long, ServiceReference<ClassLoaderWriterListener>> classLoaderWriterListeners = new HashMap<Long, ServiceReference<ClassLoaderWriterListener>>();
+
+	private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+	@Reference(referenceInterface = DynamicClassLoaderManager.class, bind = "bindDynamicClassLoaderManager", unbind = "unbindDynamicClassLoaderManager")
+	private ServiceReference dynamicClassLoaderManager;
+
+	/** The bundle asking for this service instance */
+	private Bundle callerBundle;
+
+	private static ServiceRegistration<?> mbeanRegistration;
+
+	/**
+	 * Activate this component. Create the root directory.
+	 * 
+	 * @param componentContext
+	 * @throws MalformedURLException
+	 * @throws InvalidSyntaxException
+	 * @throws MalformedObjectNameException
+	 */
+	@Activate
+	protected void activate(final ComponentContext componentContext)
+			throws MalformedURLException, InvalidSyntaxException, MalformedObjectNameException {
+		// get the file root
+		this.root = new File(componentContext.getBundleContext().getDataFile(""), "classes");
+		this.root.mkdirs();
+		this.rootURL = this.root.toURI().toURL();
+		this.callerBundle = componentContext.getUsingBundle();
+
+		classLoaderWriterListeners.clear();
+		if (this.classLoaderWriterServiceListener != null) {
+			componentContext.getBundleContext().removeServiceListener(classLoaderWriterServiceListener);
+			classLoaderWriterServiceListener = null;
+		}
+		classLoaderWriterServiceListener = new ServiceListener() {
+			@Override
+			public void serviceChanged(ServiceEvent event) {
+				ServiceReference<ClassLoaderWriterListener> reference = (ServiceReference<ClassLoaderWriterListener>) event
+						.getServiceReference();
+				if (event.getType() == ServiceEvent.MODIFIED || event.getType() == ServiceEvent.REGISTERED) {
+					classLoaderWriterListeners.put(getId(reference), reference);
+				} else {
+					classLoaderWriterListeners.remove(getId(reference));
+				}
+			}
+
+			private Long getId(ServiceReference<ClassLoaderWriterListener> reference) {
+				return PropertiesUtil.toLong(reference.getProperty(Constants.SERVICE_ID), -1);
+			}
+		};
+		componentContext.getBundleContext().addServiceListener(classLoaderWriterServiceListener, LISTENER_FILTER);
+
+		// handle the MBean Installation
+		if (mbeanRegistration != null) {
+			mbeanRegistration.unregister();
+			mbeanRegistration = null;
+		}
+		Hashtable<String, String> jmxProps = new Hashtable<String, String>();
+		jmxProps.put("type", "ClassLoader");
+		jmxProps.put("name", "FSClassLoader");
+
+		final Hashtable<String, Object> mbeanProps = new Hashtable<String, Object>();
+		mbeanProps.put(Constants.SERVICE_DESCRIPTION, "Apache Sling FSClassLoader Controller Service");
+		mbeanProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+		mbeanProps.put("jmx.objectname", new ObjectName("org.apache.sling.classloader", jmxProps));
+		mbeanRegistration = componentContext.getBundleContext().registerService(FSClassLoaderMBean.class.getName(),
+				new FSClassLoaderMBeanImpl(this, componentContext.getBundleContext()), mbeanProps);
+	}
+
+	/**
+	 * Deactivate this component. Create the root directory.
+	 */
+	@Deactivate
+	protected void deactivate(ComponentContext componentContext) {
+		this.root = null;
+		this.rootURL = null;
+		this.destroyClassLoader();
+		if (this.classLoaderWriterServiceListener != null) {
+			componentContext.getBundleContext().removeServiceListener(classLoaderWriterServiceListener);
+		}
+		if (mbeanRegistration != null) {
+			mbeanRegistration.unregister();
+			mbeanRegistration = null;
+		}
+	}
+
+	/**
+	 * Called to handle binding the DynamicClassLoaderManager service reference
+	 */
+	@SuppressWarnings("unused")
+	private void bindDynamicClassLoaderManager(final ServiceReference ref) {
+		this.dynamicClassLoaderManager = ref;
+	}
+
+	/**
+	 * Called to handle unbinding of the DynamicClassLoaderManager service
+	 * reference
+	 */
+	@SuppressWarnings("unused")
+	private void unbindDynamicClassLoaderManager(final ServiceReference ref) {
+		if (this.dynamicClassLoaderManager == ref) {
+			this.dynamicClassLoaderManager = null;
+		}
+	}
+
+	private void destroyClassLoader() {
+		final ClassLoader rcl = this.loader;
+		if (rcl != null) {
+			this.loader = null;
+
+			final ServiceReference localDynamicClassLoaderManager = this.dynamicClassLoaderManager;
+			final Bundle localCallerBundle = this.callerBundle;
+			if (localDynamicClassLoaderManager != null && localCallerBundle != null) {
+				localCallerBundle.getBundleContext().ungetService(localDynamicClassLoaderManager);
+			}
+		}
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getClassLoader()
+	 */
+	public ClassLoader getClassLoader() {
+		synchronized (this) {
+			if (loader == null || !loader.isLive()) {
+				this.destroyClassLoader();
+				// get the dynamic class loader for the bundle using this
+				// class loader writer
+				final DynamicClassLoaderManager dclm = (DynamicClassLoaderManager) this.callerBundle.getBundleContext()
+						.getService(this.dynamicClassLoaderManager);
+
+				loader = new FSDynamicClassLoader(new URL[] { this.rootURL }, dclm.getDynamicClassLoader());
+			}
+			return this.loader;
+		}
+	}
+
+	private void checkClassLoader(final String filePath) {
+		if (filePath.endsWith(".class")) {
+			// remove store directory and .class
+			final String path = filePath.substring(this.root.getAbsolutePath().length() + 1, filePath.length() - 6);
+			// convert to a class name
+			final String className = path.replace(File.separatorChar, '.');
+
+			synchronized (this) {
+				final FSDynamicClassLoader currentLoader = this.loader;
+				if (currentLoader != null) {
+					currentLoader.check(className);
+				}
+			}
+		}
+	}
+
+	// ---------- SCR Integration ----------------------------------------------
+
+	private boolean deleteRecursive(final File f, final List<String> names) {
+		if (f.isDirectory()) {
+			for (final File c : f.listFiles()) {
+				if (!deleteRecursive(c, names)) {
+					return false;
+				}
+			}
+		}
+		names.add(f.getAbsolutePath());
+		return f.delete();
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#delete(java.lang.String)
+	 */
+	public boolean delete(final String name) {
+		final String path = cleanPath(name);
+		final File file = new File(path);
+		if (file.exists()) {
+			final List<String> names = new ArrayList<String>();
+			final boolean result = deleteRecursive(file, names);
+			logger.debug("Deleted {} : {}", name, result);
+			if (result) {
+				for (final String n : names) {
+					this.checkClassLoader(n);
+				}
+				for (ServiceReference<ClassLoaderWriterListener> reference : classLoaderWriterListeners.values()) {
+					if (reference != null) {
+						ClassLoaderWriterListener listener = callerBundle.getBundleContext().getService(reference);
+						if (listener != null) {
+							listener.onClassLoaderClear(name);
+						} else {
+							logger.warn("Found ClassLoaderWriterListener Service reference with no service bound");
+						}
+					}
+				}
+			}
+
+			return result;
+		}
+		// file does not exist so we return false
+		return false;
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getOutputStream(java.lang.String)
+	 */
+	public OutputStream getOutputStream(final String name) {
+		logger.debug("Get stream for {}", name);
+		final String path = cleanPath(name);
+		final File file = new File(path);
+		final File parentDir = file.getParentFile();
+		if (!parentDir.exists()) {
+			parentDir.mkdirs();
+		}
+		try {
+			if (file.exists()) {
+				this.checkClassLoader(path);
+			}
+			return new FileOutputStream(path);
+		} catch (FileNotFoundException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#rename(java.lang.String,
+	 *      java.lang.String)
+	 */
+	public boolean rename(final String oldName, final String newName) {
+		logger.debug("Rename {} to {}", oldName, newName);
+		final String oldPath = cleanPath(oldName);
+		final String newPath = cleanPath(newName);
+		final File old = new File(oldPath);
+		final boolean result = old.renameTo(new File(newPath));
+		if (result) {
+			this.checkClassLoader(oldPath);
+			this.checkClassLoader(newPath);
+		}
+		return result;
+	}
+
+	/**
+	 * Clean the path by converting slashes to the correct format and prefixing
+	 * the root directory.
+	 * 
+	 * @param path
+	 *            The path
+	 * @return The file path
+	 */
+	private String cleanPath(String path) {
+		// replace backslash by slash
+		path = path.replace('\\', '/');
+
+		// cut off trailing slash
+		while (path.endsWith("/")) {
+			path = path.substring(0, path.length() - 1);
+		}
+		if (File.separatorChar != '/') {
+			path = path.replace('/', File.separatorChar);
+		}
+		return this.root.getAbsolutePath() + path;
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getInputStream(java.lang.String)
+	 */
+	public InputStream getInputStream(final String name) throws IOException {
+		logger.debug("Get input stream of {}", name);
+		final String path = cleanPath(name);
+		final File file = new File(path);
+		return new FileInputStream(file);
+	}
+
+	/**
+	 * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getLastModified(java.lang.String)
+	 */
+	public long getLastModified(final String name) {
+		logger.debug("Get last modified of {}", name);
+		final String path = cleanPath(name);
+		final File file = new File(path);
+		if (file.exists()) {
+			return file.lastModified();
+		}
+
+		// fallback to "non-existant" in case of problems
+		return -1;
+	}
 }
diff --git a/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderWebConsole.java b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderWebConsole.java
index eccbc29..0affacc 100644
--- a/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderWebConsole.java
+++ b/src/main/java/org/apache/sling/commons/fsclassloader/impl/FSClassLoaderWebConsole.java
@@ -53,394 +53,309 @@ import org.slf4j.LoggerFactory;
  */
 @Component
 @Service
-@Properties({
-        @Property(name = "service.description", value = "Web Console for the FileSystem Class Loader"),
-        @Property(name = "service.vendor", value = "The Apache Software Foundation"),
-        @Property(name = "felix.webconsole.label", value = FSClassLoaderWebConsole.APP_ROOT),
-        @Property(name = "felix.webconsole.title", value = "File System Class Loader"),
-        @Property(name = "felix.webconsole.css", value = { FSClassLoaderWebConsole.RES_LOC
-                + "/prettify.css" }),
-        @Property(name = "felix.webconsole.category", value = "Sling") })
+@Properties({ @Property(name = "service.description", value = "Web Console for the FileSystem Class Loader"),
+		@Property(name = "service.vendor", value = "The Apache Software Foundation"),
+		@Property(name = "felix.webconsole.label", value = FSClassLoaderWebConsole.APP_ROOT),
+		@Property(name = "felix.webconsole.title", value = "File System Class Loader"),
+		@Property(name = "felix.webconsole.css", value = { FSClassLoaderWebConsole.RES_LOC + "/prettify.css" }),
+		@Property(name = "felix.webconsole.category", value = "Sling") })
 public class FSClassLoaderWebConsole extends AbstractWebConsolePlugin {
 
-    static final String APP_ROOT = "fsclassloader";
-
-    static final String RES_LOC = APP_ROOT + "/res/ui";
-    static final String POST_PARAM_CLEAR_CLASSLOADER = "clear";
-
-    private static final Logger LOG = LoggerFactory.getLogger(FSClassLoaderWebConsole.class);
-
-    @Reference(target = "(component.name=org.apache.sling.commons.fsclassloader.impl.FSClassLoaderProvider)")
-    private ClassLoaderWriter classLoaderWriter;
-
-    /**
-     * Represents a set of class, java and deps files for a script.
-     */
-    private static class ScriptFiles {
-
-        /**
-         * Gets the script associated with the file.
-         *
-         * @param file
-         *            the file to find the associate script
-         * @return the associated script
-         */
-        public static String getScript(File file) {
-            String relative = file.getAbsolutePath().substring(
-                    root.getAbsolutePath().length());
-            String script = remove(relative, "/org/apache/jsp");
-            script = remove(script, ".class");
-            script = remove(script, ".java");
-            script = remove(script, ".deps");
-            if (File.separatorChar == '\\') {
-                script = script.replace(File.separatorChar, '/');
-            }
-            return StringUtils.substringBeforeLast(script, "_") + "."
-                    + StringUtils.substringAfterLast(script, "_");
-        }
-
-        private static String remove(String orig, String rem) {
-            return orig.replace(rem, "");
-        }
-
-        private final String classFile;
-        private final String depsFile;
-
-        private final String javaFile;
-
-        private final String script;
-
-        public ScriptFiles(File file) {
-            script = getScript(file);
-
-            String relative = file.getAbsolutePath().substring(
-                    root.getAbsolutePath().length());
-
-            relative = remove(relative, ".class");
-            relative = remove(relative, ".deps");
-            relative = remove(relative, ".java");
-            classFile = relative + ".class";
-            depsFile = relative + ".deps";
-            javaFile = relative + ".java";
-        }
-
-        public String getClassFile() {
-            return classFile;
-        }
-
-        public String getDepsFile() {
-            return depsFile;
-        }
-
-        public String getJavaFile() {
-            return javaFile;
-        }
-
-        public String getScript() {
-            return script;
-        }
-
-    }
-
-    /**
-     * The root under which the class files are under
-     */
-    private static File root;
-
-    /**
-     * The serialization UID
-     */
-    private static final long serialVersionUID = -5728679635644481848L;
-
-    /**
-     * The servlet configuration
-     */
-    private ServletConfig config;
-
-    /**
-     * Activate this component. Create the root directory.
-     *
-     * @param componentContext the component context
-     * @throws MalformedURLException
-     */
-    @Activate
-    @SuppressWarnings("unused")
-    protected void activate(final ComponentContext componentContext)
-            throws MalformedURLException {
-        // get the file root
-        root = new File(componentContext.getBundleContext().getDataFile(""),
-                "classes");
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see javax.servlet.Servlet#destroy()
-     */
-    public void destroy() {
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest,
-     * javax.servlet.ServletResponse)
-     */
-    protected void doGet(HttpServletRequest request,
-            HttpServletResponse response) throws ServletException, IOException {
-        String file = request.getParameter("download");
-        File toDownload = new File(root + file);
-        if (!StringUtils.isEmpty(file)) {
-            if (isValid(toDownload)) {
-                InputStream is = null;
-                try {
-                    is = new FileInputStream(toDownload);
-                    response.setHeader("Content-disposition",
-                            "attachment; filename=" + toDownload.getName());
-                    IOUtils.copy(is, response.getOutputStream());
-                } finally {
-                    IOUtils.closeQuietly(is);
-                    IOUtils.closeQuietly(response.getOutputStream());
-                }
-            } else {
-                response.sendError(404, "File " + file + " not found");
-            }
-        } else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.css")) {
-            response.setContentType("text/css");
-            IOUtils.copy(
-                    getClass().getClassLoader().getResourceAsStream(
-                            "/res/ui/prettify.css"), response.getOutputStream());
-        } else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.js")) {
-            response.setContentType("application/javascript");
-            IOUtils.copy(
-                    getClass().getClassLoader().getResourceAsStream(
-                            "/res/ui/prettify.js"), response.getOutputStream());
-        } else if (request.getRequestURI().endsWith(RES_LOC + "/fsclassloader.js")) {
-            response.setContentType("application/javascript");
-            IOUtils.copy(
-                    getClass().getClassLoader().getResourceAsStream(
-                            "/res/ui/fsclassloader.js"), response.getOutputStream());
-        }
-        else {
-            super.doGet(request, response);
-        }
-    }
-
-    @Override
-    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-        String clear = req.getParameter(POST_PARAM_CLEAR_CLASSLOADER);
-        boolean shouldClear = Boolean.parseBoolean(clear);
-        if (shouldClear) {
-            if (classLoaderWriter != null) {
-                boolean result = classLoaderWriter.delete("");
-                if (result) {
-                    resp.getWriter().write("{ \"status\" : \"success\" }");
-                    resp.setStatus(HttpServletResponse.SC_OK);
-                } else {
-                    resp.getWriter().write("{ \"status\" : \"failure\", \"message\" : \"unable to clear classloader; check server log\" }");
-                    resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-                }
-            } else {
-                LOG.error("Cannot get a reference to org.apache.sling.commons.fsclassloader.impl.FSClassLoaderProvider");
-                resp.getWriter().write("{ \"status\" : \"failure\", \"message\" : \"unable to clear classloader; check server log\" }");
-                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-            }
-        } else {
-            resp.getWriter().write("{ \"status\" : \"failure\", \"message\" : \"invalid command\" }");
-            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-        }
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#getLabel()
-     */
-    @Override
-    public String getLabel() {
-        return "fsclassloader";
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see javax.servlet.Servlet#getServletConfig()
-     */
-    public ServletConfig getServletConfig() {
-        return this.config;
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see javax.servlet.Servlet#getServletInfo()
-     */
-    public String getServletInfo() {
-        return "";
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#getTitle()
-     */
-    @Override
-    public String getTitle() {
-        return "File System Class Loader";
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
-     */
-    public void init(ServletConfig config) throws ServletException {
-        this.config = config;
-    }
-
-    /**
-     * Checks whether the specified file is a file and is underneath the root
-     * directory.
-     *
-     * @param file
-     *            the file to check
-     * @return false if not a file or not under the root directory, true
-     *         otherwise
-     * @throws IOException
-     */
-    private boolean isValid(File file) throws IOException {
-        if (file.isFile()) {
-            File parent = file.getCanonicalFile().getAbsoluteFile()
-                    .getParentFile();
-            while (parent != null) {
-                if (parent.getCanonicalPath().equals(root.getCanonicalPath())) {
-                    return true;
-                }
-                parent = parent.getParentFile();
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Reads all of the files under the current file.
-     *
-     * @param file
-     *            the root file
-     * @param scripts
-     *            the map of scripts
-     * @throws IOException
-     *             an exception occurs reading the files
-     */
-    private void readFiles(File file, Map<String, ScriptFiles> scripts)
-            throws IOException {
-        if (file.isDirectory()) {
-            File[] children = file.listFiles();
-            if (children != null) {
-                for (File f : children) {
-                    readFiles(f, scripts);
-                }
-            }
-        } else {
-            String script = ScriptFiles.getScript(file);
-            if (!scripts.containsKey(script)
-                    && file.getName().endsWith(".java")) {
-                scripts.put(script, new ScriptFiles(file));
-            }
-        }
-    }
-
-    /*
-     * (non-Javadoc)
-     *
-     * @see
-     * org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax
-     * .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
-     */
-    @Override
-    protected void renderContent(HttpServletRequest request,
-            HttpServletResponse response) throws ServletException, IOException {
-        Map<String, ScriptFiles> scripts = new LinkedHashMap<String, ScriptFiles>();
-        readFiles(root, scripts);
-
-        Writer w = response.getWriter();
-
-        w.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + RES_LOC
-                + "/prettify.css\"></link>");
-        w.write("<script type=\"text/javascript\" src=\"" + RES_LOC
-                + "/prettify.js\"></script>");
-        w.write("<script type=\"text/javascript\" src=\"" + RES_LOC
-                + "/fsclassloader.js\"></script>");
-        w.write("<script>$(document).ready(prettyPrint);</script>");
-        w.write("<style>.prettyprint ol.linenums > li { list-style-type: decimal; } pre.prettyprint { white-space: pre-wrap; }</style>");
-        String file = request.getParameter("view");
-        File toView = new File(root + file);
-        w.write("<div id=\"classes\">");
-        if (!StringUtils.isEmpty(file)) {
-            if (isValid(toView)) {
-
-                w.write("<p class=\"statline ui-state-highlight\">Viewing Script: "
-                        + root + file + "</p><br/><br/>");
-
-                ScriptFiles scriptFiles = new ScriptFiles(toView);
-
-                w.write("<table class=\"nicetable ui-widget\">");
-                w.write("<tr class=\"header ui-widget-header\">");
-                w.write("<th>Script</th>");
-                w.write("<th>Class</th>");
-                w.write("<th>Deps</th>");
-                w.write("<th>Java</th>");
-                w.write("</tr>");
-                w.write("<tr class=\"ui-state-default\">");
-                w.write("<td>" + scriptFiles.getScript() + "</td>");
-                w.write("<td>[<a href=\"?download="
-                        + scriptFiles.getClassFile()
-                        + "\" target=\"_blank\">download</a>]</td>");
-                w.write("<td>[<a href=\"?download="
-                        + scriptFiles.getDepsFile()
-                        + "\" target=\"_blank\">download</a>]</td>");
-                w.write("<td>[<a href=\"?download="
-                        + scriptFiles.getJavaFile()
-                        + "\" target=\"_blank\">download</a>]</td>");
-                w.write("</tr>");
-                w.write("</table><br/><br/>");
-                InputStream is = null;
-                try {
-                    is = new FileInputStream(toView);
-                    String contents = IOUtils.toString(is, "UTF-8");
-                    w.write("<pre class=\"prettyprint linenums\">");
-                    w.write(StringEscapeUtils.escapeHtml4(contents));
-                    w.write("</pre>");
-                } finally {
-                    IOUtils.closeQuietly(is);
-                }
-            } else {
-                response.sendError(404, "File " + file + " not found");
-            }
-        } else {
-            w.write("<p class=\"statline ui-state-highlight\">File System ClassLoader Root: "
-                    + root + " <span style=\"float: right\"><button type='button' id='clear'>Clear Class Loader</button></span></p>");
-            if (scripts.values().size() > 0 ) {
-                w.write("<table class=\"nicetable ui-widget fsclassloader-has-classes\">");
-            } else {
-                w.write("<table class=\"nicetable ui-widget\">");
-            }
-            w.write("<tr class=\"header ui-widget-header\">");
-            w.write("<th>View</th>");
-            w.write("<th>Script</th>");
-            w.write("</tr>");
-            int i = 0;
-            for (ScriptFiles scriptFiles : scripts.values()) {
-                w.write("<tr class=\"" + (i % 2 == 0 ? "even" : "odd")
-                        + " ui-state-default\">");
-                w.write("<td>[<a href=\"?view=" + scriptFiles.getJavaFile()
-                        + "\">view</a>]</td>");
-                w.write("<td>" + scriptFiles.getScript() + "</td>");
-                w.write("</tr>");
-                i++;
-            }
-            w.write("</table>");
-        }
-        w.write("</div>");
-    }
+	static final String APP_ROOT = "fsclassloader";
+
+	static final String RES_LOC = APP_ROOT + "/res/ui";
+	static final String POST_PARAM_CLEAR_CLASSLOADER = "clear";
+
+	private static final Logger LOG = LoggerFactory.getLogger(FSClassLoaderWebConsole.class);
+
+	@Reference(target = "(component.name=org.apache.sling.commons.fsclassloader.impl.FSClassLoaderProvider)")
+	private ClassLoaderWriter classLoaderWriter;
+
+	/**
+	 * The root under which the class files are under
+	 */
+	private File root;
+
+	/**
+	 * The serialization UID
+	 */
+	private static final long serialVersionUID = -5728679635644481848L;
+
+	/**
+	 * The servlet configuration
+	 */
+	private ServletConfig config;
+
+	/**
+	 * Activate this component. Create the root directory.
+	 *
+	 * @param componentContext
+	 *            the component context
+	 * @throws MalformedURLException
+	 */
+	@Activate
+	protected void activate(final ComponentContext componentContext) throws MalformedURLException {
+		// get the file root
+		root = new File(componentContext.getBundleContext().getDataFile(""), "classes");
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see javax.servlet.Servlet#destroy()
+	 */
+	public void destroy() {
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest,
+	 * javax.servlet.ServletResponse)
+	 */
+	protected void doGet(HttpServletRequest request, HttpServletResponse response)
+			throws ServletException, IOException {
+		String file = request.getParameter("download");
+		File toDownload = new File(root + file);
+		if (!StringUtils.isEmpty(file)) {
+			if (isValid(toDownload)) {
+				InputStream is = null;
+				try {
+					is = new FileInputStream(toDownload);
+					response.setHeader("Content-disposition", "attachment; filename=" + toDownload.getName());
+					IOUtils.copy(is, response.getOutputStream());
+				} finally {
+					IOUtils.closeQuietly(is);
+					IOUtils.closeQuietly(response.getOutputStream());
+				}
+			} else {
+				response.sendError(404, "File " + file + " not found");
+			}
+		} else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.css")) {
+			response.setContentType("text/css");
+			IOUtils.copy(getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.css"),
+					response.getOutputStream());
+		} else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.js")) {
+			response.setContentType("application/javascript");
+			IOUtils.copy(getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.js"),
+					response.getOutputStream());
+		} else if (request.getRequestURI().endsWith(RES_LOC + "/fsclassloader.js")) {
+			response.setContentType("application/javascript");
+			IOUtils.copy(getClass().getClassLoader().getResourceAsStream("/res/ui/fsclassloader.js"),
+					response.getOutputStream());
+		} else {
+			super.doGet(request, response);
+		}
+	}
+
+	@Override
+	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+		String clear = req.getParameter(POST_PARAM_CLEAR_CLASSLOADER);
+		boolean shouldClear = Boolean.parseBoolean(clear);
+		if (shouldClear) {
+			if (classLoaderWriter != null) {
+				boolean result = classLoaderWriter.delete("");
+				if (result) {
+					resp.getWriter().write("{ \"status\" : \"success\" }");
+					resp.setStatus(HttpServletResponse.SC_OK);
+				} else {
+					resp.getWriter().write(
+							"{ \"status\" : \"failure\", \"message\" : \"unable to clear classloader; check server log\" }");
+					resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+				}
+			} else {
+				LOG.error(
+						"Cannot get a reference to org.apache.sling.commons.fsclassloader.impl.FSClassLoaderProvider");
+				resp.getWriter().write(
+						"{ \"status\" : \"failure\", \"message\" : \"unable to clear classloader; check server log\" }");
+				resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+			}
+		} else {
+			resp.getWriter().write("{ \"status\" : \"failure\", \"message\" : \"invalid command\" }");
+			resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+		}
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#getLabel()
+	 */
+	@Override
+	public String getLabel() {
+		return "fsclassloader";
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see javax.servlet.Servlet#getServletConfig()
+	 */
+	public ServletConfig getServletConfig() {
+		return this.config;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see javax.servlet.Servlet#getServletInfo()
+	 */
+	public String getServletInfo() {
+		return "";
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#getTitle()
+	 */
+	@Override
+	public String getTitle() {
+		return "File System Class Loader";
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+	 */
+	public void init(ServletConfig config) throws ServletException {
+		this.config = config;
+	}
+
+	/**
+	 * Checks whether the specified file is a file and is underneath the root
+	 * directory.
+	 *
+	 * @param file
+	 *            the file to check
+	 * @return false if not a file or not under the root directory, true
+	 *         otherwise
+	 * @throws IOException
+	 */
+	private boolean isValid(File file) throws IOException {
+		if (file.isFile()) {
+			File parent = file.getCanonicalFile().getAbsoluteFile().getParentFile();
+			while (parent != null) {
+				if (parent.getCanonicalPath().equals(root.getCanonicalPath())) {
+					return true;
+				}
+				parent = parent.getParentFile();
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Reads all of the files under the current file.
+	 *
+	 * @param current
+	 *            the current file
+	 * @param root
+	 *            the root file
+	 * @param scripts
+	 *            the map of scripts
+	 * @throws IOException
+	 *             an exception occurs reading the files
+	 */
+	protected static void readFiles(File current, File root, Map<String, ScriptFiles> scripts) throws IOException {
+		if (current.isDirectory()) {
+			File[] children = current.listFiles();
+			if (children != null) {
+				for (File f : children) {
+					readFiles(f, root, scripts);
+				}
+			}
+		} else {
+			String script = ScriptFiles.getScript(root, current);
+			if (!scripts.containsKey(script) && current.getName().endsWith(".java")) {
+				scripts.put(script, new ScriptFiles(root, current));
+			}
+		}
+	}
+
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see
+	 * org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax
+	 * .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+	 */
+	@Override
+	protected void renderContent(HttpServletRequest request, HttpServletResponse response)
+			throws ServletException, IOException {
+		Map<String, ScriptFiles> scripts = new LinkedHashMap<String, ScriptFiles>();
+		readFiles(root, root, scripts);
+
+		Writer w = response.getWriter();
+
+		w.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + RES_LOC + "/prettify.css\"></link>");
+		w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/prettify.js\"></script>");
+		w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/fsclassloader.js\"></script>");
+		w.write("<script>$(document).ready(prettyPrint);</script>");
+		w.write("<style>.prettyprint ol.linenums > li { list-style-type: decimal; } pre.prettyprint { white-space: pre-wrap; }</style>");
+		String file = request.getParameter("view");
+		File toView = new File(root + file);
+		w.write("<div id=\"classes\">");
+		if (!StringUtils.isEmpty(file)) {
+			if (isValid(toView)) {
+
+				w.write("<p class=\"statline ui-state-highlight\">Viewing Script: " + root + file + "</p><br/><br/>");
+
+				ScriptFiles scriptFiles = new ScriptFiles(root, toView);
+
+				w.write("<table class=\"nicetable ui-widget\">");
+				w.write("<tr class=\"header ui-widget-header\">");
+				w.write("<th>Script</th>");
+				w.write("<th>Class</th>");
+				w.write("<th>Deps</th>");
+				w.write("<th>Java</th>");
+				w.write("</tr>");
+				w.write("<tr class=\"ui-state-default\">");
+				w.write("<td>" + scriptFiles.getScript() + "</td>");
+				w.write("<td>[<a href=\"?download=" + scriptFiles.getClassFile()
+						+ "\" target=\"_blank\">download</a>]</td>");
+				w.write("<td>[<a href=\"?download=" + scriptFiles.getDepsFile()
+						+ "\" target=\"_blank\">download</a>]</td>");
+				w.write("<td>[<a href=\"?download=" + scriptFiles.getJavaFile()
+						+ "\" target=\"_blank\">download</a>]</td>");
+				w.write("</tr>");
+				w.write("</table><br/><br/>");
+				InputStream is = null;
+				try {
+					is = new FileInputStream(toView);
+					String contents = IOUtils.toString(is, "UTF-8");
+					w.write("<pre class=\"prettyprint linenums\">");
+					w.write(StringEscapeUtils.escapeHtml4(contents));
+					w.write("</pre>");
+				} finally {
+					IOUtils.closeQuietly(is);
+				}
+			} else {
+				response.sendError(404, "File " + file + " not found");
+			}
+		} else {
+			w.write("<p class=\"statline ui-state-highlight\">File System ClassLoader Root: " + root
+					+ " <span style=\"float: right\"><button type='button' id='clear'>Clear Class Loader</button></span></p>");
+			if (scripts.values().size() > 0) {
+				w.write("<table class=\"nicetable ui-widget fsclassloader-has-classes\">");
+			} else {
+				w.write("<table class=\"nicetable ui-widget\">");
+			}
+			w.write("<tr class=\"header ui-widget-header\">");
+			w.write("<th>View</th>");
+			w.write("<th>Script</th>");
+			w.write("</tr>");
+			int i = 0;
+			for (ScriptFiles scriptFiles : scripts.values()) {
+				w.write("<tr class=\"" + (i % 2 == 0 ? "even" : "odd") + " ui-state-default\">");
+				w.write("<td>[<a href=\"?view=" + scriptFiles.getJavaFile() + "\">view</a>]</td>");
+				w.write("<td>" + scriptFiles.getScript() + "</td>");
+				w.write("</tr>");
+				i++;
+			}
+			w.write("</table>");
+		}
+		w.write("</div>");
+	}
 }
diff --git a/src/main/java/org/apache/sling/commons/fsclassloader/impl/ScriptFiles.java b/src/main/java/org/apache/sling/commons/fsclassloader/impl/ScriptFiles.java
new file mode 100644
index 0000000..80bde37
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/fsclassloader/impl/ScriptFiles.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.fsclassloader.impl;
+
+import java.io.File;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Represents a set of class, java and deps files for a script.
+ */
+public class ScriptFiles {
+
+	/**
+	 * Gets the script associated with the file.
+	 *
+	 * @param file
+	 *            the file to find the associate script
+	 * @return the associated script
+	 */
+	public static String getScript(File root, File file) {
+		String relative = file.getAbsolutePath().substring(root.getAbsolutePath().length());
+		String script = remove(relative, "/org/apache/jsp");
+		script = remove(script, ".class");
+		script = remove(script, ".java");
+		script = remove(script, ".deps");
+		if (File.separatorChar == '\\') {
+			script = script.replace(File.separatorChar, '/');
+		}
+		return StringUtils.substringBeforeLast(script, "_") + "." + StringUtils.substringAfterLast(script, "_");
+	}
+
+	private static String remove(String orig, String rem) {
+		return orig.replace(rem, "");
+	}
+
+	private final String classFile;
+	private final String depsFile;
+
+	private final String javaFile;
+
+	private final String script;
+
+	public ScriptFiles(final File root, final File file) {
+		script = getScript(root, file);
+
+		String relative = file.getAbsolutePath().substring(root.getAbsolutePath().length());
+
+		relative = remove(relative, ".class");
+		relative = remove(relative, ".deps");
+		relative = remove(relative, ".java");
+		classFile = relative + ".class";
+		depsFile = relative + ".deps";
+		javaFile = relative + ".java";
+	}
+
+	public String getClassFile() {
+		return classFile;
+	}
+
+	public String getDepsFile() {
+		return depsFile;
+	}
+
+	public String getJavaFile() {
+		return javaFile;
+	}
+
+	public String getScript() {
+		return script;
+	}
+
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.