/*******************************************************************************
* Copyright (c) 2009, 2015 Spring IDE Developers
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Spring IDE Developers - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.core.java;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.xbean.classloader.NonLockingJarFileClassLoader;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IPathVariableManager;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.springframework.ide.eclipse.core.SpringCore;
/**
* Internal cache of classpath urls and corresponding classloaders.
* @author Christian Dupuis
* @author Martin Lippert
* @since 2.2.5
*/
@SuppressWarnings("deprecation")
public class ProjectClassLoaderCache {
private static final String FILE_SCHEME = "file";
private static final int CACHE_SIZE = 12;
private static final List<ClassLoaderCacheEntry> CLASSLOADER_CACHE = new ArrayList<ClassLoaderCacheEntry>(CACHE_SIZE);
private static final String DEBUG_OPTION = SpringCore.PLUGIN_ID + "/java/classloader/debug";
private static final boolean DEBUG_CLASSLOADER = SpringCore.isDebug(DEBUG_OPTION);
private static ClassLoader cachedParentClassLoader = null;
private static IPropertyChangeListener propertyChangeListener = null;
private static IResourceChangeListener resourceChangeListener = null;
private static ClassLoader addClassLoaderToCache(IProject project, List<URL> urls, ClassLoader parentClassLoader) {
synchronized (CLASSLOADER_CACHE) {
int nEntries = CLASSLOADER_CACHE.size();
if (nEntries >= CACHE_SIZE) {
// find obsolete entries or remove entry that was least recently accessed
ClassLoaderCacheEntry oldest = null;
List<ClassLoaderCacheEntry> obsoleteClassLoaders = new ArrayList<ClassLoaderCacheEntry>(CACHE_SIZE);
for (int i = 0; i < nEntries; i++) {
ClassLoaderCacheEntry entry = (ClassLoaderCacheEntry) CLASSLOADER_CACHE.get(i);
IProject curr = entry.getProject();
if (!curr.exists() || !curr.isAccessible() || !curr.isOpen()) {
obsoleteClassLoaders.add(entry);
}
else {
if (oldest == null || entry.getLastAccess() < oldest.getLastAccess()) {
oldest = entry;
}
}
}
if (!obsoleteClassLoaders.isEmpty()) {
for (int i = 0; i < obsoleteClassLoaders.size(); i++) {
removeClassLoaderEntryFromCache((ClassLoaderCacheEntry) obsoleteClassLoaders.get(i));
}
}
else if (oldest != null) {
removeClassLoaderEntryFromCache(oldest);
}
}
ClassLoaderCacheEntry newEntry = new ClassLoaderCacheEntry(project, urls, parentClassLoader);
CLASSLOADER_CACHE.add(newEntry);
return newEntry.getClassLoader();
}
}
/**
* Add {@link URL}s to the given set of <code>paths</code>.
*/
private static void addClassPathUrls(IProject project, List<URL> paths, Set<IProject> resolvedProjects) {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
// add project to local cache to prevent adding its classpaths multiple times
if (resolvedProjects.contains(project)) {
return;
}
else {
resolvedProjects.add(project);
}
try {
if (JdtUtils.isJavaProject(project)) {
IJavaProject jp = JavaCore.create(project);
// configured classpath
IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
// add class path entries
for (int i = 0; i < classpath.length; i++) {
IClasspathEntry path = classpath[i];
if (path.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
IPath entryPath = path.getPath();
File file = entryPath.toFile();
if (file.exists()) {
paths.add(file.toURI().toURL());
}
else {
// case for project relative links
String projectName = entryPath.segment(0);
IProject pathProject = root.getProject(projectName);
covertPathToUrl(pathProject, paths, entryPath);
}
}
else if (path.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
// add source path as well for non java resources
IPath sourcePath = path.getPath();
covertPathToUrl(project, paths, sourcePath);
// add source output locations for different source
// folders
IPath sourceOutputPath = path.getOutputLocation();
covertPathToUrl(project, paths, sourceOutputPath);
}
}
// add all depending java projects
for (IJavaProject p : JdtUtils.getAllDependingJavaProjects(jp)) {
addClassPathUrls(p.getProject(), paths, resolvedProjects);
}
// get default output directory
IPath outputPath = jp.getOutputLocation();
covertPathToUrl(project, paths, outputPath);
}
else {
for (IProject p : project.getReferencedProjects()) {
addClassPathUrls(p, paths, resolvedProjects);
}
}
}
catch (Exception e) {
// ignore
}
}
private static void addUri(List<URL> paths, URI uri) throws MalformedURLException {
File file = new File(uri);
// If we keep the following check, non-existing output folders will never be used for the lifetime of
// a classloader. this causes issues with clean projects with not-yet existing output folders.
// if (file.exists()) {
if (file.isDirectory()) {
paths.add(new URL(uri.toString() + File.separator));
}
else {
paths.add(uri.toURL());
}
// }
}
private static void covertPathToUrl(IProject project, List<URL> paths, IPath path) throws MalformedURLException {
if (path != null && project != null && path.removeFirstSegments(1) != null
&& project.findMember(path.removeFirstSegments(1)) != null) {
URI uri = project.findMember(path.removeFirstSegments(1)).getRawLocationURI();
if (uri != null) {
String scheme = uri.getScheme();
if (FILE_SCHEME.equalsIgnoreCase(scheme)) {
addUri(paths, uri);
}
else if ("sourcecontrol".equals(scheme)) {
// special case of Rational Team Concert
IPath sourceControlPath = project.findMember(path.removeFirstSegments(1)).getLocation();
File sourceControlFile = sourceControlPath.toFile();
if (sourceControlFile.exists()) {
addUri(paths, sourceControlFile.toURI());
}
}
else {
IPathVariableManager variableManager = ResourcesPlugin.getWorkspace().getPathVariableManager();
addUri(paths, variableManager.resolveURI(uri));
}
}
}
}
private static ClassLoader findClassLoaderInCache(IProject project, ClassLoader parentClassLoader) {
synchronized (CLASSLOADER_CACHE) {
for (int i = CLASSLOADER_CACHE.size() - 1; i >= 0; i--) {
ClassLoaderCacheEntry entry = (ClassLoaderCacheEntry) CLASSLOADER_CACHE.get(i);
IProject curr = entry.getProject();
if (curr == null || !curr.exists() || !curr.isAccessible() || !curr.isOpen()) {
removeClassLoaderEntryFromCache(entry);
if (DEBUG_CLASSLOADER) {
System.out.println(String.format("> removing classloader for '%s' : total %s",
entry.getProject(), CLASSLOADER_CACHE.size()));
}
}
else {
if (entry.matches(project, parentClassLoader)) {
entry.markAsAccessed();
return entry.getClassLoader();
}
}
}
}
return null;
}
/**
* Iterates all class path entries of the given <code>project</code> and all depending projects.
* <p>
* Note: if <code>useParentClassLoader</code> is true, the Spring, AspectJ, Commons Logging and ASM bundles are
* automatically added to the paths.
* @param project the {@link IProject}
* @param useParentClassLoader use the OSGi classloader as parent
* @return a set of {@link URL}s that can be used to construct a {@link URLClassLoader}
*/
public static List<URL> getClassPathUrls(IProject project, ClassLoader parentClassLoader) {
// needs to be linked to preserve ordering
List<URL> paths = new ArrayList<URL>();
Set<IProject> resolvedProjects = new HashSet<IProject>();
addClassPathUrls(project, paths, resolvedProjects);
return paths;
}
/**
* Registers internal listeners that listen to changes relevant to clear out stale cache entries.
*/
private static void registerListenersIfRequired() {
if (propertyChangeListener == null) {
propertyChangeListener = new EnablementPropertyChangeListener();
SpringCore.getDefault().getPluginPreferences().addPropertyChangeListener(propertyChangeListener);
}
if (resourceChangeListener == null) {
resourceChangeListener = new SourceAndOutputLocationResourceChangeListener();
ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener);
}
}
/**
* Removes the given {@link ClassLoaderCacheEntry} from the internal cache.
* @param entry the entry to remove
*/
private static void removeClassLoaderEntryFromCache(ClassLoaderCacheEntry entry) {
synchronized (CLASSLOADER_CACHE) {
if (DEBUG_CLASSLOADER) {
System.out.println(String.format("> removing classloader for '%s' : total %s", entry.getProject()
.getName(), CLASSLOADER_CACHE.size()));
}
entry.dispose();
CLASSLOADER_CACHE.remove(entry);
}
}
public static boolean shouldFilter(String name) {
if ("commons-logging.properties".equals(name)) return true;
if (name != null && name.startsWith("META-INF/services/")) {
return (name.indexOf('/', 18) == -1
&& !name.startsWith("org.springframework", 18));
}
return false;
}
private static boolean useNonLockingClassLoader() {
boolean useNonLockingClassloaderPreference = SpringCore.getDefault().getPluginPreferences().getBoolean(SpringCore.USE_NON_LOCKING_CLASSLOADER);
if (useNonLockingClassloaderPreference) {
NonLockingJarFileClassLoader.setCheckForUpdates(false);
}
return useNonLockingClassloaderPreference;
}
/**
* Returns a {@link ClassLoader} for the given project.
*/
protected static ClassLoader getClassLoader(IProject project, ClassLoader parentClassLoader) {
synchronized (ProjectClassLoaderCache.class) {
// Setup the root class loader to be used when no explicit parent class loader is given
if (parentClassLoader == null && cachedParentClassLoader == null) {
List<URL> paths = new ArrayList<URL>();
Enumeration<String> libs = SpringCore.getDefault().getBundle().getEntryPaths("/lib/");
while (libs.hasMoreElements()) {
String lib = libs.nextElement();
// Don't add the non locking classloader jar
if (!lib.contains("xbean-nonlocking-classloader")) {
paths.add(SpringCore.getDefault().getBundle().getEntry(lib));
}
}
paths.addAll(JdtUtils.getBundleClassPath("org.aspectj.runtime"));
paths.addAll(JdtUtils.getBundleClassPath("org.aspectj.weaver"));
paths.addAll(JdtUtils.getBundleClassPath("org.objectweb.asm"));
paths.addAll(JdtUtils.getBundleClassPath("org.aopalliance"));
cachedParentClassLoader = new URLClassLoader(paths.toArray(new URL[paths.size()]));
}
if (project == null) {
return cachedParentClassLoader;
}
registerListenersIfRequired();
}
ClassLoader classLoader = findClassLoaderInCache(project, parentClassLoader);
if (classLoader == null) {
List<URL> urls = getClassPathUrls(project, parentClassLoader);
classLoader = addClassLoaderToCache(project, urls, parentClassLoader);
if (DEBUG_CLASSLOADER) {
System.out.println(String.format("> creating new classloader for '%s' with parent '%s' : total %s",
project.getName(), parentClassLoader, CLASSLOADER_CACHE.size()));
}
}
return classLoader;
}
/**
* Removes any cached {@link ClassLoaderCacheEntry} for the given {@link IProject}.
* @param project the project to remove {@link ClassLoaderCacheEntry} for
*/
protected static void removeClassLoaderEntryFromCache(IProject project) {
synchronized (CLASSLOADER_CACHE) {
if (DEBUG_CLASSLOADER) {
System.out.println(String.format("> removing classloader for '%s' : total %s", project.getName(),
CLASSLOADER_CACHE.size()));
}
for (ClassLoaderCacheEntry entry : new ArrayList<ClassLoaderCacheEntry>(CLASSLOADER_CACHE)) {
if (project.equals(entry.getProject())) {
entry.dispose();
CLASSLOADER_CACHE.remove(entry);
}
}
}
}
/**
* Internal cache entry
*/
static class ClassLoaderCacheEntry implements IElementChangedListener {
private URL[] directories;
private ClassLoader jarClassLoader;
private long lastAccess;
private ClassLoader parentClassLoader;
private IProject project;
private URL[] urls;
public ClassLoaderCacheEntry(IProject project, List<URL> urls, ClassLoader parentClassLoader) {
this.project = project;
this.urls = urls.toArray(new URL[urls.size()]);
this.parentClassLoader = parentClassLoader;
markAsAccessed();
JavaCore.addElementChangedListener(this, ElementChangedEvent.POST_CHANGE);
}
public void dispose() {
JavaCore.removeElementChangedListener(this);
this.urls = null;
this.jarClassLoader = null;
}
public void elementChanged(ElementChangedEvent event) {
IJavaProject javaProject = JdtUtils.getJavaProject(project);
if (javaProject != null) {
for (IJavaElementDelta delta : event.getDelta().getAffectedChildren()) {
if ((delta.getFlags() & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0
|| (delta.getFlags() & IJavaElementDelta.F_CLASSPATH_CHANGED) != 0) {
if (javaProject.equals(delta.getElement()) || javaProject.isOnClasspath(delta.getElement())) {
removeClassLoaderEntryFromCache(this);
}
}
}
}
}
public ClassLoader getClassLoader() {
ClassLoader parent = getJarClassLoader();
if (useNonLockingClassLoader()) {
return new FilteringNonLockingJarFileClassLoader(String.format("ClassLoader for '%s'", project.getName()),
directories, parent);
}
else {
return new FilteringURLClassLoader(directories, parent);
}
}
public long getLastAccess() {
return lastAccess;
}
public IProject getProject() {
return this.project;
}
public void markAsAccessed() {
lastAccess = System.currentTimeMillis();
}
public boolean matches(IProject project, ClassLoader parentClassLoader) {
return this.project.equals(project)
&& ((parentClassLoader == null && this.parentClassLoader == null) || (parentClassLoader != null && parentClassLoader
.equals(this.parentClassLoader)));
}
private synchronized ClassLoader getJarClassLoader() {
if (jarClassLoader == null) {
Set<URL> jars = new LinkedHashSet<URL>();
List<URL> dirs = new ArrayList<URL>();
for (URL url : urls) {
if (shouldLoadFromParent(url)) {
jars.add(url);
}
else {
dirs.add(url);
}
}
if (parentClassLoader != null) {
// We use the parent class loader of the org.springframework.ide.eclipse.beans.core bundle
if (useNonLockingClassLoader()) {
jarClassLoader = new FilteringNonLockingJarFileClassLoader(String.format("ClassLoader for '%s'",
project.getName()), (URL[]) jars.toArray(new URL[jars.size()]), parentClassLoader);
}
else {
jarClassLoader = new FilteringURLClassLoader((URL[]) jars.toArray(new URL[jars.size()]),
parentClassLoader);
}
}
else {
if (useNonLockingClassLoader()) {
jarClassLoader = new FilteringNonLockingJarFileClassLoader(String.format("ClassLoader for '%s'",
project.getName()), (URL[]) jars.toArray(new URL[jars.size()]), cachedParentClassLoader);
}
else {
jarClassLoader = new FilteringURLClassLoader((URL[]) jars.toArray(new URL[jars.size()]),
cachedParentClassLoader);
}
}
directories = dirs.toArray(new URL[dirs.size()]);
}
return jarClassLoader;
}
private boolean shouldLoadFromParent(URL url) {
String path = url.getPath();
if (path.endsWith(".jar") || path.endsWith(".zip")) {
return true;
}
else if (path.contains("/org.eclipse.osgi/bundles/")) {
return true;
}
return false;
}
}
/**
* {@link IPropertyChangeListener} to clear the cache whenever the setting is changed.
* @since 2.5.0
*/
static class EnablementPropertyChangeListener implements IPropertyChangeListener {
/**
* {@inheritDoc}
*/
public void propertyChange(PropertyChangeEvent event) {
if (SpringCore.USE_NON_LOCKING_CLASSLOADER.equals(event.getProperty())) {
synchronized (CLASSLOADER_CACHE) {
CLASSLOADER_CACHE.clear();
}
}
}
}
/**
* {@link IResourceChangeListener} to clear the cache whenever new source or output folders are being added.
* @since 2.5.2
*/
static class SourceAndOutputLocationResourceChangeListener implements IResourceChangeListener {
private static final int VISITOR_FLAGS = IResourceDelta.ADDED | IResourceDelta.CHANGED;
/**
* {@inheritDoc}
*/
public void resourceChanged(IResourceChangeEvent event) {
if (event.getSource() instanceof IWorkspace) {
int eventType = event.getType();
switch (eventType) {
case IResourceChangeEvent.POST_CHANGE:
IResourceDelta delta = event.getDelta();
if (delta != null) {
try {
delta.accept(getVisitor(), VISITOR_FLAGS);
}
catch (CoreException e) {
SpringCore.log("Error while traversing resource change delta", e);
}
}
break;
}
}
else if (event.getSource() instanceof IProject) {
int eventType = event.getType();
switch (eventType) {
case IResourceChangeEvent.POST_CHANGE:
IResourceDelta delta = event.getDelta();
if (delta != null) {
try {
delta.accept(getVisitor(), VISITOR_FLAGS);
}
catch (CoreException e) {
SpringCore.log("Error while traversing resource change delta", e);
}
}
break;
}
}
}
protected IResourceDeltaVisitor getVisitor() {
return new SourceAndOutputLocationResourceVisitor();
}
/**
* Internal resource delta visitor.
*/
protected class SourceAndOutputLocationResourceVisitor implements IResourceDeltaVisitor {
public final boolean visit(IResourceDelta delta) throws CoreException {
IResource resource = delta.getResource();
switch (delta.getKind()) {
case IResourceDelta.ADDED:
return resourceAdded(resource);
}
return true;
}
protected boolean resourceAdded(IResource resource) {
if (resource instanceof IFolder && JdtUtils.isJavaProject(resource)) {
try {
IJavaProject javaProject = JdtUtils.getJavaProject(resource);
// Safe guard once again
if (javaProject == null) {
return false;
}
// Check the default output location
if (javaProject.getOutputLocation() != null
&& javaProject.getOutputLocation().equals(resource.getFullPath())) {
removeClassLoaderEntryFromCache(resource.getProject());
return false;
}
// Check any source and output folder location
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
if (resource.getFullPath() != null && resource.getFullPath().equals(entry.getPath())) {
removeClassLoaderEntryFromCache(resource.getProject());
return false;
}
else if (resource.getFullPath() != null
&& resource.getFullPath().equals(entry.getOutputLocation())) {
removeClassLoaderEntryFromCache(resource.getProject());
return false;
}
}
}
catch (JavaModelException e) {
SpringCore.log("Error traversing resource change delta", e);
}
}
return true;
}
}
}
}