/************************************************************************************* * Copyright (c) 2008-2012 Red Hat, Inc. and others. * 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: * JBoss by Red Hat - Initial implementation. ************************************************************************************/ package org.jboss.tools.arquillian.core.internal.classpath; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.osgi.framework.adaptor.BundleClassLoader; import org.eclipse.wst.sse.core.utils.StringUtils; import org.jboss.tools.arquillian.core.ArquillianCoreActivator; import org.jboss.tools.arquillian.core.internal.util.ArquillianUtility; /** * * @author snjeza * */ public class ArquillianClassLoader extends ClassLoader implements PrivilegedAction { private static final int DEFAULT_READING_SIZE = 8192; private ClassLoader sourceLoader; private Set<URL> URLs = new HashSet<URL>(); private static final class Finder extends SecurityManager { public Class[] getClassContext() { return super.getClassContext(); } } private IJavaProject jProject; private List<IPath> jars; private List<IJavaProject> dependentProjects = new ArrayList<IJavaProject>(); private Map<String, URL> found = new HashMap<String, URL>(); private List<String> notFound = new ArrayList<String>(); private Set<IPath> outputLocations; static ClassLoader finderClassLoader; static Finder contextFinder; static { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { finderClassLoader = ArquillianClassLoader.class .getClassLoader(); contextFinder = new Finder(); return null; } }); } public void clear() { jars.clear(); found.clear(); notFound.clear(); URLs.clear(); dependentProjects.clear(); outputLocations.clear(); jProject = null; } public ArquillianClassLoader(ClassLoader contextClassLoader, IJavaProject jProject) { super(contextClassLoader); this.jProject = jProject; this.jars = getJarPaths(jProject); if (URLs.size() > 0) { URL[] urls = (URL[]) URLs.toArray(new URL[0]); sourceLoader = new URLClassLoader(urls,contextClassLoader); } this.outputLocations = getOutpuLocations(); try { outputLocations.add(jProject.getOutputLocation()); } catch (JavaModelException e) { ArquillianCoreActivator.log(e); } } // Return a list of all classloaders on the stack that are neither the // ContextFinder classloader nor the boot classloader. The last classloader // in the list is either a bundle classloader or the framework's classloader // We assume that the bootclassloader never uses the context classloader to // find classes in itself. ArrayList basicFindClassLoaders() { Class[] stack = contextFinder.getClassContext(); ArrayList result = new ArrayList(1); for (int i = 1; i < stack.length; i++) { ClassLoader tmp = stack[i].getClassLoader(); if (stack[i] != ArquillianClassLoader.class && tmp != null && tmp != this) { if (checkClassLoader(tmp)) result.add(tmp); // stop at the framework classloader or the first bundle // classloader if (tmp == finderClassLoader || tmp instanceof BundleClassLoader) break; } } return result; } // ensures that a classloader does not have the ContextFinder as part of the // parent hierachy. A classloader which has the ContextFinder as a parent // must // not be used as a delegate, otherwise we endup in endless recursion. private boolean checkClassLoader(ClassLoader classloader) { if (classloader == null || classloader == getParent()) return false; for (ClassLoader parent = classloader.getParent(); parent != null; parent = parent .getParent()) if (parent == this) return false; return true; } private ArrayList findClassLoaders() { if (System.getSecurityManager() == null) return basicFindClassLoaders(); return (ArrayList) AccessController.doPrivileged(this); } public Object run() { return basicFindClassLoaders(); } @Override protected Class findClass(String className) throws ClassNotFoundException { try { IType type = jProject.findType(className); int offset = -1; if (type == null && (offset = className.indexOf('$')) != -1) { // Internal classes from source files must be referenced by . instead of $ String cls = className.substring(0, offset) + className.substring(offset).replace('$', '.'); type = jProject.findType(cls); } if (type != null) { IPath path = null; IResource resource = type.getResource(); if (resource != null) path = resource.getLocation(); if (path == null) path = type.getPath(); // needs to be compiled before we can load it if ("class".equalsIgnoreCase(path.getFileExtension())) { IFile file = null; if (resource != null && resource.getType() == IResource.FILE) file = (IFile) resource; else file = ResourcesPlugin.getWorkspace().getRoot().getFile(path); if (file != null && file.isAccessible()) { byte[] bytes = loadBytes(file); return defineClass(className, bytes, 0, bytes.length); } } // Look up the class file based on the output location of the java project else if ("java".equalsIgnoreCase(path.getFileExtension()) && resource != null) { //$NON-NLS-1$ if (resource.getProject() != null) { IJavaProject jProject = JavaCore.create(resource.getProject()); String outputClass = StringUtils.replace(type.getFullyQualifiedName(), ".", "/").concat(".class"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ for (IPath outputLocation : outputLocations) { IPath classPath = outputLocation .append(outputClass); IFile file = ResourcesPlugin.getWorkspace() .getRoot().getFile(classPath); if (file != null && file.isAccessible()) { byte[] bytes = loadBytes(file); return defineClass(className, bytes, 0, bytes.length); } } } } else if ("jar".equalsIgnoreCase(path.getFileExtension())) { String expectedFileName = StringUtils.replace(className, ".", "/").concat(".class"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ byte[] bytes = getCachedInputStream(path.toOSString(), expectedFileName); return defineClass(className, bytes, 0, bytes.length); } } } catch (JavaModelException e) { ArquillianCoreActivator.log(e); } return super.findClass(className); } private byte[] getCachedInputStream(String jarFilename, String entryName) { ByteArrayOutputStream buffer = null; File testFile = new File(jarFilename); if (!testFile.exists()) return null; ZipFile jarfile = null; try { jarfile = new ZipFile(jarFilename); if (jarfile != null) { ZipEntry zentry = jarfile.getEntry(entryName); if (zentry != null) { InputStream entryInputStream = null; try { entryInputStream = jarfile.getInputStream(zentry); } catch (IOException e) { ArquillianCoreActivator.log(e); } if (entryInputStream != null) { int c; if (zentry.getSize() > 0) { buffer = new ByteArrayOutputStream((int) zentry.getSize()); } else { buffer = new ByteArrayOutputStream(); } // array dim restriction? byte bytes[] = new byte[2048]; try { while ((c = entryInputStream.read(bytes)) >= 0) { buffer.write(bytes, 0, c); } } catch (IOException ioe) { // no cleanup can be done } finally { try { entryInputStream.close(); } catch (IOException e) { } } } } } } catch (IOException e) { ArquillianCoreActivator.log(e); } finally { closeJarFile(jarfile); } if (buffer != null) { return buffer.toByteArray(); } return new byte[0]; } public void closeJarFile(ZipFile file) { if (file == null) return; try { file.close(); } catch (IOException e) { ArquillianCoreActivator.log(e); } } private byte[] loadBytes(IFile file) { ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = null; try { in = file.getContents(); byte[] buffer = new byte[4096]; int read = 0; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (CoreException e) { ArquillianCoreActivator.log(e); } catch (IOException e) { ArquillianCoreActivator.log(e); } finally { try { if (in != null) in.close(); } catch (IOException e) { } } return out.toByteArray(); } private Set<IPath> getOutpuLocations() { Set<IPath> paths = new HashSet<IPath>(); IClasspathEntry[] entries; try { entries = jProject.getRawClasspath(); } catch (JavaModelException e) { // ignore return paths; } for (IClasspathEntry entry:entries) { if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { IPath outputPath = entry.getOutputLocation(); if (outputPath != null && !outputPath.isEmpty()) { paths.add(outputPath); } } } return paths; } /* * partially copied from org.eclipse.jdt.internal.compiler.util.Util.getInputStreamAsByteArray */ public static byte[] getInputStreamAsByteArray(InputStream stream) throws IOException { byte[] contents; contents = new byte[0]; int contentsLength = 0; int amountRead = -1; do { int amountRequested = Math.max(stream.available(), DEFAULT_READING_SIZE); if (contentsLength + amountRequested > contents.length) { System.arraycopy(contents, 0, contents = new byte[contentsLength + amountRequested], 0, contentsLength); } amountRead = stream.read(contents, contentsLength, amountRequested); if (amountRead > 0) { contentsLength += amountRead; } } while (amountRead != -1); if (contentsLength < contents.length) { System.arraycopy(contents, 0, contents = new byte[contentsLength], 0, contentsLength); } return contents; } private byte[] getBytes(String clazz) throws JavaModelException { IType type = jProject.findType(clazz); int offset = -1; if (type == null && (offset = clazz.indexOf('$')) != -1) { // Internal classes from source files must be referenced by . instead of $ String cls = clazz.substring(0, offset) + clazz.substring(offset).replace('$', '.'); type = jProject.findType(cls); } if (type == null) { for (IJavaProject project:dependentProjects) { type = project.findType(clazz); if (type != null) break; } } IClassFile classFile = type.getClassFile(); if (classFile != null) { byte[] bytes = classFile.getBytes(); return bytes; } return null; } protected URL findResource(String name) { ArrayList toConsult = findClassLoaders(); for (Iterator loaders = toConsult.iterator(); loaders.hasNext();) { URL result = ((ClassLoader) loaders.next()).getResource(name); if (result != null) return result; // go to the next class loader } URL result = sourceLoader.getResource(name); if (result != null) return result; result = findURL(name); if (result != null) return result; return super.findResource(name); } protected Enumeration findResources(String name) throws IOException { ArrayList toConsult = findClassLoaders(); Enumeration result; for (Iterator loaders = toConsult.iterator(); loaders.hasNext();) { result = ((ClassLoader) loaders.next()) .getResources(name); if (result != null && result.hasMoreElements()) return result; // go to the next class loader } result = sourceLoader.getResources(name); if (result != null) return result; // FIXME find all resources URL resultURL = findURL(name); if (resultURL != null) { return new ArrayEnumeration(resultURL); } return super.findResources(name); } private URL findURL(String name) { if (found.containsKey(name)) return (URL) found.get(name); if (notFound.contains(name)) return null; for (Iterator iter = jars.iterator(); iter.hasNext();) { IPath path = (IPath) iter.next(); File file = getRawLocationFile(path); if (file.exists() && file.isDirectory()) { File resource = new File(file, name); if (resource.exists()) { try { URL url = file.toURI().toURL(); found.put(name, url); return url; } catch (MalformedURLException e) { // ignore } } } else if (file.exists()) { JarFile jarFile = null; try { jarFile = new JarFile(file); Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = (JarEntry) entries.nextElement(); if (jarEntry.getName().equals(name)) { URL url = creatFileResource(jarEntry, jarFile, file); found.put(name, url); return url; } } jarFile.close(); } catch (Exception e) { // ignore } finally { if (jarFile != null) { try { jarFile.close(); } catch (IOException e) { // ignore } } } } } notFound.add(name); return null; } private URL creatFileResource(JarEntry jarEntry, JarFile jarFile, File file) throws ClassNotFoundException { //File rootFile = ArquillianCoreActivator.getRootFile(); File projectDirectory = ArquillianCoreActivator.getLoaderDirectory(jProject.getProject()); projectDirectory = makeNewDirectory(projectDirectory); File dest = null; try { dest = getDestination(projectDirectory, file); } catch (IOException e) { return null; } createFileResource(jarEntry, dest, jarFile, file); URL url = null; try { url = new File(dest, jarEntry.getName()).toURI().toURL(); } catch (MalformedURLException e) { return null; } return url; } private void createFileResource(JarEntry jarEntry, File dest, JarFile jarFile, File file) throws ClassNotFoundException { File dir = dest; String separator = "/"; String name = jarEntry.getName(); while (name.indexOf(separator) > -1) { String dirString = name.substring(0, name.indexOf(separator)); File newDir = new File(dir, dirString); newDir = makeNewDirectory(newDir); name = name.substring(name.indexOf(separator) + 1); dir = newDir; } InputStream input = null; OutputStream output = null; try { input = jarFile.getInputStream(jarEntry); File outputFile = new File(dir, name); if (!outputFile.exists() || outputFile.lastModified() < file.lastModified()) { outputFile.delete(); if (!outputFile.createNewFile()) throw new ClassNotFoundException( "Could not create the " + file.getName() + " file"); output = new FileOutputStream(outputFile); int c; while (((c = input.read()) != -1)) output.write(c); } } catch (Exception e) { ArquillianCoreActivator.log(e); } finally { if (input != null) try { input.close(); } catch (IOException e) { } if (output != null) try { output.close(); } catch (IOException e) { } } } private File getDestination(File baseUnpackDir, File file) throws IOException { File dest = new File(baseUnpackDir, getOutputFile(file.getName())); if (dest.exists() || file.lastModified() < dest.lastModified()) return dest; ArquillianUtility.deleteFile(dest); dest.mkdirs(); return dest; } private String getOutputFile(String name) { if (name == null) return null; if (name.endsWith(".jar")) name = name.substring(0, name.length() - 4); String out = name.replace('.', '_'); return out; } private File makeNewDirectory(File dir) { if (!dir.isDirectory()) { dir.delete(); } if (!dir.exists()) dir.mkdirs(); return dir; } private File getRawLocationFile(IPath simplePath) { IResource resource = ResourcesPlugin.getWorkspace().getRoot() .findMember(simplePath); File file = null; if (resource != null) { file = ResourcesPlugin.getWorkspace().getRoot().findMember( simplePath).getLocation().toFile(); } else { file = simplePath.toFile(); } return file; } private List<IPath> getJarPaths(IJavaProject jProject) { List<IPath> classPaths = new ArrayList<IPath>(); List<IPath> sourcePaths = new ArrayList<IPath>(); if (jProject == null) return classPaths; try { IClasspathEntry[] entries = jProject.getRawClasspath(); for (int i = 0; i < entries.length; i++) { IClasspathEntry entry = entries[i]; if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { IPath path = entry.getOutputLocation(); if (path == null) { path = jProject.getOutputLocation(); } File file = getRawLocationFile(path); if (file.exists()) { URLs.add(file.toURI().toURL()); } sourcePaths.add(entry.getPath()); } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { IClasspathEntry resLib = JavaCore .getResolvedClasspathEntry(entry); addClassPath(classPaths, resLib); } else if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { IClasspathEntry projectEntry = JavaCore .getResolvedClasspathEntry(entry); IPath path = projectEntry.getPath(); String name = path.segment(0); IProject project = ResourcesPlugin.getWorkspace().getRoot() .getProject(name); if (project.exists()) { IJavaProject javaProject = JavaCore.create(project); if (javaProject.exists()) { dependentProjects.add(javaProject); classPaths.addAll(getJarPaths(javaProject)); } } } else if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { IClasspathEntry resLib = JavaCore .getResolvedClasspathEntry(entry); addClassPath(classPaths, resLib); } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { if (!entry.getPath().toString().endsWith( "JRE_CONTAINER")) { IClasspathEntry[] resLibs = JavaCore .getClasspathContainer(entry.getPath(), jProject).getClasspathEntries(); for (int j = 0; j < resLibs.length; j++) { addClassPath(classPaths, resLibs[j]); } } } } } catch (Exception e) { ArquillianCoreActivator.log(e); } for (IPath path:sourcePaths) { File file = getRawLocationFile(path); if (file.exists()) { try { URLs.add(file.toURI().toURL()); } catch (MalformedURLException e) { // ignore } } } return classPaths; } private void addClassPath(List<IPath> classPaths, IClasspathEntry resLibs) { IPath path = resLibs.getPath(); String ls = path.lastSegment(); if (ls != null && ls.length() > 0) { classPaths.add(path); } } class ArrayEnumeration implements Enumeration { private Object[] array; int cur = 0; public ArrayEnumeration(Object object) { this.array = new Object[1]; System.arraycopy(array, 0, this.array, 0, this.array.length); } public boolean hasMoreElements() { return cur < array.length; } public Object nextElement() { return array[cur++]; } } }