1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.resources.manager;
18 
19 import com.android.SdkConstants;
20 import com.android.ide.eclipse.adt.AdtPlugin;
21 import com.android.ide.eclipse.adt.internal.build.BuildHelper;
22 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
23 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
24 
25 import org.eclipse.core.resources.IProject;
26 import org.eclipse.core.resources.IResource;
27 import org.eclipse.core.resources.IWorkspaceRoot;
28 import org.eclipse.core.resources.ResourcesPlugin;
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.IPath;
31 import org.eclipse.jdt.core.IClasspathContainer;
32 import org.eclipse.jdt.core.IClasspathEntry;
33 import org.eclipse.jdt.core.IJavaProject;
34 import org.eclipse.jdt.core.JavaCore;
35 import org.eclipse.jdt.core.JavaModelException;
36 import org.objectweb.asm.ClassReader;
37 import org.objectweb.asm.ClassVisitor;
38 import org.objectweb.asm.ClassWriter;
39 import org.objectweb.asm.Opcodes;
40 
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.IOException;
45 import java.net.MalformedURLException;
46 import java.net.URL;
47 import java.net.URLClassLoader;
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /**
52  * ClassLoader able to load class from output of an Eclipse project.
53  */
54 public final class ProjectClassLoader extends ClassLoader {
55 
56     private final IJavaProject mJavaProject;
57     private URLClassLoader mJarClassLoader;
58     private boolean mInsideJarClassLoader = false;
59 
ProjectClassLoader(ClassLoader parentClassLoader, IProject project)60     public ProjectClassLoader(ClassLoader parentClassLoader, IProject project) {
61         super(parentClassLoader);
62         mJavaProject = JavaCore.create(project);
63     }
64 
65     @Override
findClass(String name)66     protected Class<?> findClass(String name) throws ClassNotFoundException {
67         // if we are here through a child classloader, throw an exception.
68         if (mInsideJarClassLoader) {
69             throw new ClassNotFoundException(name);
70         }
71 
72         // attempt to load the class from the main project
73         Class<?> clazz = loadFromProject(mJavaProject, name);
74 
75         if (clazz != null) {
76             return clazz;
77         }
78 
79         // attempt to load the class from the jar dependencies
80         clazz = loadClassFromJar(name);
81         if (clazz != null) {
82             return clazz;
83         }
84 
85         // attempt to load the class from the libraries
86         try {
87             // get the project info
88             ProjectState projectState = Sdk.getProjectState(mJavaProject.getProject());
89 
90             // this can happen if the project has no project.properties.
91             if (projectState != null) {
92 
93                 List<IProject> libProjects = projectState.getFullLibraryProjects();
94                 List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
95                         libProjects);
96 
97                 for (IJavaProject javaProject : referencedJavaProjects) {
98                     clazz = loadFromProject(javaProject, name);
99 
100                     if (clazz != null) {
101                         return clazz;
102                     }
103                 }
104             }
105         } catch (CoreException e) {
106             // log exception?
107         }
108 
109         throw new ClassNotFoundException(name);
110     }
111 
112     /**
113      * Attempts to load a class from a project output folder.
114      * @param project the project to load the class from
115      * @param name the name of the class
116      * @return a class object if found, null otherwise.
117      */
loadFromProject(IJavaProject project, String name)118     private Class<?> loadFromProject(IJavaProject project, String name) {
119         try {
120             // get the project output folder.
121             IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
122             IPath outputLocation = project.getOutputLocation();
123             IResource outRes = root.findMember(outputLocation);
124             if (outRes == null) {
125                 return null;
126             }
127 
128             File outFolder = new File(outRes.getLocation().toOSString());
129 
130             // get the class name segments
131             String[] segments = name.split("\\."); //$NON-NLS-1$
132 
133             // try to load the class from the bin folder of the project.
134             File classFile = getFile(outFolder, segments, 0);
135             if (classFile == null) {
136                 return null;
137             }
138 
139             // load the content of the file and create the class.
140             FileInputStream fis = new FileInputStream(classFile);
141             byte[] data = new byte[(int)classFile.length()];
142             int read = 0;
143             try {
144                 read = fis.read(data);
145             } catch (IOException e) {
146                 data = null;
147             }
148             fis.close();
149 
150             if (data != null) {
151                 try {
152                     Class<?> clazz = defineClass(null, data, 0, read);
153                     if (clazz != null) {
154                         return clazz;
155                     }
156                 } catch (UnsupportedClassVersionError e) {
157                     // Attempt to reload on lower version
158                     int maxVersion = 50; // JDK 1.6
159                     try {
160                         byte[] rewritten = rewriteClass(data, maxVersion, 0);
161                         return defineClass(null, rewritten, 0, rewritten.length);
162                     } catch (UnsupportedClassVersionError e2) {
163                         throw e; // throw *original* exception, not attempt to rewrite
164                     }
165                 }
166             }
167         } catch (Exception e) {
168             // log the exception?
169         }
170 
171         return null;
172     }
173 
174     /**
175      * Rewrites the given class to the given target class file version.
176      */
rewriteClass(byte[] classData, final int maxVersion, final int minVersion)177     public static byte[] rewriteClass(byte[] classData, final int maxVersion, final int minVersion) {
178         assert maxVersion >= minVersion;
179         ClassWriter classWriter = new ClassWriter(0);
180         ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
181             @Override
182             public void visit(int version, int access, String name, String signature,
183                     String superName, String[] interfaces) {
184                 if (version > maxVersion) {
185                     version = maxVersion;
186                 }
187                 if (version < minVersion) {
188                     version = minVersion;
189                 }
190                 super.visit(version, access, name, signature, superName, interfaces);
191             }
192         };
193         ClassReader reader = new ClassReader(classData);
194         reader.accept(classVisitor, 0);
195         return classWriter.toByteArray();
196     }
197 
198     /**
199      * Returns the File matching the a certain path from a root {@link File}.
200      * <p/>The methods checks that the file ends in .class even though the last segment
201      * does not.
202      * @param parent the root of the file.
203      * @param segments the segments containing the path of the file
204      * @param index the offset at which to start looking into segments.
205      * @throws FileNotFoundException
206      */
getFile(File parent, String[] segments, int index)207     private File getFile(File parent, String[] segments, int index)
208             throws FileNotFoundException {
209         // reached the end with no match?
210         if (index == segments.length) {
211             throw new FileNotFoundException();
212         }
213 
214         String toMatch = segments[index];
215         File[] files = parent.listFiles();
216 
217         // we're at the last segments. we look for a matching <file>.class
218         if (index == segments.length - 1) {
219             toMatch = toMatch + ".class";
220 
221             if (files != null) {
222                 for (File file : files) {
223                     if (file.isFile() && file.getName().equals(toMatch)) {
224                         return file;
225                     }
226                 }
227             }
228 
229             // no match? abort.
230             throw new FileNotFoundException();
231         }
232 
233         String innerClassName = null;
234 
235         if (files != null) {
236             for (File file : files) {
237                 if (file.isDirectory()) {
238                     if (toMatch.equals(file.getName())) {
239                         return getFile(file, segments, index+1);
240                     }
241                 } else if (file.getName().startsWith(toMatch)) {
242                     if (innerClassName == null) {
243                         StringBuilder sb = new StringBuilder(segments[index]);
244                         for (int i = index + 1 ; i < segments.length ; i++) {
245                             sb.append('$');
246                             sb.append(segments[i]);
247                         }
248                         sb.append(".class");
249 
250                         innerClassName = sb.toString();
251                     }
252 
253                     if (file.getName().equals(innerClassName)) {
254                         return file;
255                     }
256                 }
257             }
258         }
259 
260         return null;
261     }
262 
263     /**
264      * Loads a class from the 3rd party jar present in the project
265      *
266      * @return the class loader or null if not found.
267      */
loadClassFromJar(String name)268     private Class<?> loadClassFromJar(String name) {
269         if (mJarClassLoader == null) {
270             // get the OS path to all the external jars
271             URL[] jars = getExternalJars();
272 
273             mJarClassLoader = new URLClassLoader(jars, this /* parent */);
274         }
275 
276         try {
277             // because a class loader always look in its parent loader first, we need to know
278             // that we are querying the jar classloader. This will let us know to not query
279             // it again for classes we don't find, or this would create an infinite loop.
280             mInsideJarClassLoader = true;
281             return mJarClassLoader.loadClass(name);
282         } catch (ClassNotFoundException e) {
283             // not found? return null.
284             return null;
285         } finally {
286             mInsideJarClassLoader = false;
287         }
288     }
289 
290     /**
291      * Returns an array of external jar files used by the project.
292      * @return an array of OS-specific absolute file paths
293      */
getExternalJars()294     private final URL[] getExternalJars() {
295         // get a java project from it
296         IJavaProject javaProject = JavaCore.create(mJavaProject.getProject());
297 
298         ArrayList<URL> oslibraryList = new ArrayList<URL>();
299         IClasspathEntry[] classpaths = javaProject.readRawClasspath();
300         if (classpaths != null) {
301             for (IClasspathEntry e : classpaths) {
302                 if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY ||
303                         e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
304                     // if this is a classpath variable reference, we resolve it.
305                     if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
306                         e = JavaCore.getResolvedClasspathEntry(e);
307                     }
308 
309                     handleClassPathEntry(e, oslibraryList);
310                 } else if (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
311                     // get the container.
312                     try {
313                         IClasspathContainer container = JavaCore.getClasspathContainer(
314                                 e.getPath(), javaProject);
315                         // ignore the system and default_system types as they represent
316                         // libraries that are part of the runtime.
317                         if (container != null &&
318                                 container.getKind() == IClasspathContainer.K_APPLICATION) {
319                             IClasspathEntry[] entries = container.getClasspathEntries();
320                             for (IClasspathEntry entry : entries) {
321                                 // TODO: Xav -- is this necessary?
322                                 if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
323                                     entry = JavaCore.getResolvedClasspathEntry(entry);
324                                 }
325 
326                                 handleClassPathEntry(entry, oslibraryList);
327                             }
328                         }
329                     } catch (JavaModelException jme) {
330                         // can't resolve the container? ignore it.
331                         AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s",
332                                 e.getPath());
333                     }
334                 }
335             }
336         }
337 
338         return oslibraryList.toArray(new URL[oslibraryList.size()]);
339     }
340 
handleClassPathEntry(IClasspathEntry e, ArrayList<URL> oslibraryList)341     private void handleClassPathEntry(IClasspathEntry e, ArrayList<URL> oslibraryList) {
342         // get the IPath
343         IPath path = e.getPath();
344 
345         // check the name ends with .jar
346         if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
347             boolean local = false;
348             IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path);
349             if (resource != null && resource.exists() &&
350                     resource.getType() == IResource.FILE) {
351                 local = true;
352                 try {
353                     oslibraryList.add(new File(resource.getLocation().toOSString())
354                             .toURI().toURL());
355                 } catch (MalformedURLException mue) {
356                     // pass
357                 }
358             }
359 
360             if (local == false) {
361                 // if the jar path doesn't match a workspace resource,
362                 // then we get an OSString and check if this links to a valid file.
363                 String osFullPath = path.toOSString();
364 
365                 File f = new File(osFullPath);
366                 if (f.exists()) {
367                     try {
368                         oslibraryList.add(f.toURI().toURL());
369                     } catch (MalformedURLException mue) {
370                         // pass
371                     }
372                 }
373             }
374         }
375     }
376 }
377