001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: *
005: * Licensed under the Educational Community License, Version 1.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.opensource.org/licenses/ecl1.php
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package edu.iu.uis.eden.plugin;
018:
019: import java.io.BufferedInputStream;
020: import java.io.BufferedOutputStream;
021: import java.io.ByteArrayInputStream;
022: import java.io.ByteArrayOutputStream;
023: import java.io.File;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.net.MalformedURLException;
027: import java.net.URI;
028: import java.net.URISyntaxException;
029: import java.net.URL;
030: import java.net.URLConnection;
031: import java.net.URLStreamHandler;
032: import java.util.ArrayList;
033: import java.util.Enumeration;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.Map;
038: import java.util.jar.JarEntry;
039: import java.util.jar.JarFile;
040: import java.util.zip.ZipEntry;
041: import java.util.zip.ZipInputStream;
042:
043: import edu.iu.uis.eden.exception.WorkflowRuntimeException;
044: import edu.iu.uis.eden.util.SimpleEnumeration;
045:
046: /**
047: * A ClassLoader implementation which loads a KEW plugin from the classpath. The plugin could
048: * be embedded within a directory location in the parent classloader's classpath or within a jar
049: * on the parent classloader's classpath.
050: *
051: * <p>Because of the method by which the embedded jars are stored (an archive within an archive) this
052: * implementation must decompress all of the jars in the embedded plugin "lib" directory and then
053: * read all of the classes and resources from those jars into memory. This means that using this
054: * class will result in a decent amount of memory use (depending on the number of jars in the embedded
055: * plugin) in order to allow for the performance of access to those resources to be acceptiable.
056: *
057: * @author ewestfal
058: */
059: public class EmbeddedPluginClassLoader extends PluginClassLoader {
060:
061: private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
062: .getLogger(EmbeddedPluginClassLoader.class);
063:
064: private static final int BUFFER_SIZE = 2048;
065:
066: /**
067: * A cache of all the bytes from all of the embedded jars.
068: */
069: private Map<URL, Map<String, byte[]>> byteCache = new HashMap<URL, Map<String, byte[]>>();
070:
071: /**
072: * List of urls to embedded jars to include in the classpath of this classloader.
073: */
074: private List<URL> embeddedJarUrls = new ArrayList<URL>();
075:
076: /**
077: * Constucts the classloader to read the embedded plugin from the given path on the classpath.
078: */
079: public EmbeddedPluginClassLoader(String pathToEmbeddedPlugin) {
080: initialize(pathToEmbeddedPlugin);
081: }
082:
083: /**
084: * Constucts the classloader to read the embedded plugin from the given path on the classpath.
085: * The specified classloader will be the parent of this classloader.
086: */
087: public EmbeddedPluginClassLoader(ClassLoader parent,
088: String pathToEmbeddedPlugin) {
089: super (parent);
090: initialize(pathToEmbeddedPlugin);
091: }
092:
093: /**
094: * Initializes the classloader from the gven embedded plugin path. This process includes searching the
095: * classpath for any .jar files inside of the embedded plugin. Also, in order for this classloader to
096: * be responsive enough for real world use, all of the classes in resources in those jars are loaded into
097: * memory and cached for future access.
098: */
099: protected void initialize(String pathToEmbeddedPlugin) {
100: try {
101: establishEmbeddedUrls(pathToEmbeddedPlugin);
102: cacheBytes();
103: } catch (IOException e) {
104: throw new WorkflowRuntimeException(e);
105: } catch (URISyntaxException e) {
106: throw new WorkflowRuntimeException(e);
107: }
108: }
109:
110: /**
111: * Establishes the set of URLs to resources in this classloader's classpath. The classpath of this classloader is
112: * composed of the classes directory and all of the jars within the lib directory. The classes directory is
113: * added as a standard URL to the parent classloader.
114: * <p>
115: * The jars have to be handled differently because they may be embedded within the classpath inside of a jar as
116: * opposed to the standard jar loading model where they are located on the filesystem.
117: */
118: protected void establishEmbeddedUrls(String pathToEmbeddedPlugin)
119: throws IOException, URISyntaxException {
120: URL embeddedUrl = getParent().getResource(pathToEmbeddedPlugin);
121: if (embeddedUrl == null) {
122: throw new WorkflowRuntimeException(
123: "Could not locate embedded plugin on the classpath at: "
124: + pathToEmbeddedPlugin);
125: }
126: URL embeddedClasses = getParent().getResource(
127: pathToEmbeddedPlugin + "/" + CLASSES_DIR + "/");
128: if (embeddedClasses != null) {
129: addURL(embeddedClasses);
130: }
131: URL libUrl = getParent().getResource(
132: pathToEmbeddedPlugin + "/" + LIB_DIR);
133: // 1) if the lib directory is located inside of a jar, we need to search the jar to locate all embedded jars
134: // 2) if the lib directory is on the filesystem, we can load the jars using default URL classloading behavior
135: if (libUrl.getProtocol().equals("jar")) {
136: String jarPath = getJarPath(libUrl);
137: JarFile jarFile = new JarFile(new File(new URI(jarPath)));
138: Enumeration enumeration = jarFile.entries();
139: String libPath = pathToEmbeddedPlugin + "/" + LIB_DIR + "/";
140: while (enumeration.hasMoreElements()) {
141: JarEntry entry = (JarEntry) enumeration.nextElement();
142: String name = entry.getName();
143: if (name.startsWith(libPath)) {
144: String fileName = name.substring(libPath.length());
145: if (!fileName.contains("/")
146: && fileName.endsWith(".jar")) {
147: addEmbeddedJarURL(getParent().getResource(
148: pathToEmbeddedPlugin + "/" + LIB_DIR
149: + "/" + fileName));
150: }
151: }
152: }
153: } else if (libUrl.getProtocol().equals("file")) {
154: File libDirectory = new File(libUrl.toURI());
155: addLibDirectory(libDirectory);
156: }
157: }
158:
159: /**
160: * Reads all the data from the embedded jars into memory and caches the bytes of all their resources.
161: */
162: protected void cacheBytes() {
163: byte[] data = new byte[BUFFER_SIZE];
164: for (Iterator iterator = embeddedJarUrls.iterator(); iterator
165: .hasNext();) {
166: URL jarUrl = (URL) iterator.next();
167: ZipInputStream inputStream = openStreamToEmbeddedJar(jarUrl);
168: Map<String, byte[]> byteMap = byteCache.get(jarUrl);
169: if (byteMap == null) {
170: byteMap = new HashMap<String, byte[]>();
171: byteCache.put(jarUrl, byteMap);
172: }
173: try {
174: ZipEntry entry;
175: while ((entry = inputStream.getNextEntry()) != null) {
176: ByteArrayOutputStream baos = new ByteArrayOutputStream();
177: BufferedOutputStream os = new BufferedOutputStream(
178: baos, BUFFER_SIZE);
179: try {
180: int count;
181: while ((count = inputStream.read(data, 0,
182: BUFFER_SIZE)) != -1) {
183: os.write(data, 0, count);
184: }
185: os.flush();
186: byteMap
187: .put(entry.getName(), baos
188: .toByteArray());
189: } finally {
190: os.close();
191: }
192: }
193: } catch (IOException e) {
194: throw new WorkflowRuntimeException(e);
195: } finally {
196: try {
197: inputStream.close();
198: } catch (IOException e) {
199: throw new WorkflowRuntimeException(e);
200: }
201: }
202: }
203: }
204:
205: /**
206: * Add a URL to an embedded JAR on the classpath. This URL should be of a form
207: * similar to the following:
208: * <pre>jar:file:/path/to/mainjar/mainjar.jar!/pathToEmbeddedPlugin/lib/myEmbeddedJar.jar</pre>
209: */
210: protected void addEmbeddedJarURL(URL embeddedJarURL)
211: throws MalformedURLException {
212: embeddedJarUrls.add(embeddedJarURL);
213: }
214:
215: /**
216: * Attempts to locate the class with the given name. It accomplishes this by first looking in the list of
217: * standard URLs for the given class (this will search the plugin's "classes" directory). If the class
218: * cannot be located there it will then search in the embedded jars (this wil search the plugin's
219: * "lib" directory).
220: */
221: protected Class<?> findClass(String name)
222: throws ClassNotFoundException {
223: Class foundClass = null;
224: try {
225: foundClass = super .findClass(name);
226: } catch (ClassNotFoundException e) {
227: // now look in the embedded jars
228: foundClass = findClassInEmbeddedJars(name);
229: }
230: if (foundClass != null) {
231: return foundClass;
232: }
233: throw new ClassNotFoundException(name);
234: }
235:
236: /**
237: * Searches all of the embedded JARs for the Class with the given name.
238: */
239: protected Class findClassInEmbeddedJars(String className) {
240: String classNamePath = classNameToPath(className);
241: byte[] classBytes = null;
242: for (URL jarURLKey : byteCache.keySet()) {
243: Map<String, byte[]> byteMap = byteCache.get(jarURLKey);
244: byte[] bytes = byteMap.get(classNamePath);
245: if (bytes != null) {
246: classBytes = bytes;
247: }
248: }
249: if (classBytes != null) {
250: return defineClass(className, classBytes, 0,
251: classBytes.length);
252: }
253: return null;
254: }
255:
256: /**
257: * Return the cached bytes for the given jar and resource.
258: */
259: protected byte[] getCachedBytes(String jarPath, String resourceName) {
260: try {
261: Map<String, byte[]> byteMap = byteCache
262: .get(new URL(jarPath));
263: if (byteMap != null) {
264: return byteMap.get(resourceName);
265: }
266: } catch (MalformedURLException e) {
267: throw new WorkflowRuntimeException(e);
268: }
269: return null;
270: }
271:
272: /**
273: * Opens a ZipInputStream to the embedded jar represented by the given URL.
274: */
275: protected ZipInputStream openStreamToEmbeddedJar(URL jarUrl) {
276: int sepIndex = jarUrl.getPath().indexOf("!");
277: if (sepIndex == -1) {
278: throw new WorkflowRuntimeException(
279: "Invalid embedded jar url found: "
280: + jarUrl.toString());
281: }
282: // extract the path to the embedded jar within the classpath, given above example, this will be /pathToEmbeddedPlugin/lib/axis.jar
283: String jarPath = jarUrl.getPath().substring(sepIndex + 2);
284: return new ZipInputStream(new BufferedInputStream(getParent()
285: .getResourceAsStream(jarPath), BUFFER_SIZE));
286: }
287:
288: /**
289: * Finds the resource with the given resourceName by first searching the embedded plugin's "classes" directory and
290: * then the embedded JARs in the "lib" directory. It accomplishes the location of embedded jar resources by
291: * querying the internal resource cache to fetch the resource's bytes.
292: */
293: public URL findResource(String resourceName) {
294: resourceName = normalizeResourceName(resourceName);
295: URL resource = super .findResource(resourceName);
296: if (resource == null) {
297: try {
298: for (URL jarUrl : byteCache.keySet()) {
299: Map<String, byte[]> byteMap = byteCache.get(jarUrl);
300: if (byteMap.get(resourceName) != null) {
301: resource = new URL(null, jarUrl.toString()
302: + "!/" + resourceName,
303: new ByteURLStreamHandler(this ));
304: break;
305: }
306: }
307: } catch (MalformedURLException e) {
308: throw new WorkflowRuntimeException(e);
309: }
310: }
311: return resource;
312: }
313:
314: /**
315: * Finds resources by first looking in the superclass, and then looking at
316: */
317: public Enumeration<URL> findResources(String name)
318: throws IOException {
319: // TODO should this implementation somehow search for duplicates resources on the classpath?
320: return new SimpleEnumeration<URL>(findResource(name));
321: }
322:
323: /**
324: * A toString implementation which prints a list of all standard and embedded URLs in the classpath of this
325: * classloader.
326: */
327: public String toString() {
328: StringBuffer sb = new StringBuffer(
329: "[EmbeddedPluginClassLoader: urls=");
330: URL[] urls = getURLs();
331: if (urls == null) {
332: sb.append("null");
333: } else {
334: for (int i = 0; i < urls.length; i++) {
335: sb.append(urls[i]);
336: sb.append(",");
337: }
338: for (Iterator iterator = embeddedJarUrls.iterator(); iterator
339: .hasNext();) {
340: URL jarUrl = (URL) iterator.next();
341: sb.append(jarUrl);
342: sb.append(",");
343: }
344: // remove trailing comma
345: if (urls.length > 1) {
346: sb.setLength(sb.length() - 1);
347: }
348: }
349: sb.append("]");
350: return sb.toString();
351: }
352:
353: /**
354: * Extract the path to the classpath resources within the parent classloader where the
355: * embedded jar represented by the given URL is located.
356: *
357: * @param url The URL to the embedded jar
358: * @return the path to embedded jar resources, should be of the form /pathToEmbeddedPlugin/lib/myEmbeddedJar.jar
359: */
360: private String getJarPath(URL url) {
361: String path = url.getPath();
362: return path.substring(0, path.indexOf("!"));
363: }
364:
365: /**
366: * Converts the given classname to a resource path. This is accomplished by replacing all periods (.) with
367: * forward slashes (/).
368: */
369: private String classNameToPath(String className) {
370: className = className.replace('.', '/');
371: return normalizeResourceName(className) + ".class";
372: }
373:
374: /**
375: * Normalizes the given resource name by removing any leading forward slashes (/).
376: */
377: private String normalizeResourceName(String resourceName) {
378: while (resourceName.startsWith("/")) {
379: resourceName = resourceName.substring(1);
380: }
381: return resourceName;
382: }
383:
384: /**
385: * A simple URLStreamHandler implementation which is backed by a connection to an array of bytes.
386: *
387: * @author Eric Westfall
388: */
389: private static class ByteURLStreamHandler extends URLStreamHandler {
390:
391: private EmbeddedPluginClassLoader classLoader;
392:
393: public ByteURLStreamHandler(
394: EmbeddedPluginClassLoader classLoader) {
395: this .classLoader = classLoader;
396: }
397:
398: protected URLConnection openConnection(URL url)
399: throws IOException {
400: return new ByteURLConnection(url, classLoader);
401: }
402:
403: }
404:
405: /**
406: * A simple URLConnection implementation which represents a connection to an array of bytes
407: * via a URL to a resource within an embedded jar
408: *
409: * @author Eric Westfall
410: */
411: private static class ByteURLConnection extends URLConnection {
412:
413: private String jarPath;
414: private String resourceName;
415: private EmbeddedPluginClassLoader classLoader;
416: private byte[] bytes;
417:
418: public ByteURLConnection(URL url,
419: EmbeddedPluginClassLoader classLoader) {
420: super (url);
421: this .classLoader = classLoader;
422: String urlString = url.toString();
423: jarPath = urlString
424: .substring(0, urlString.lastIndexOf("!"));
425: resourceName = urlString.substring(urlString
426: .lastIndexOf("!") + 2);
427: }
428:
429: public void connect() throws IOException {
430: bytes = classLoader.getCachedBytes(jarPath, resourceName);
431: if (bytes == null) {
432: throw new IOException(
433: "Could not access byte data for the given URL: "
434: + url.toString());
435: }
436: }
437:
438: public InputStream getInputStream() throws IOException {
439: if (bytes == null) {
440: connect();
441: }
442: return new BufferedInputStream(new ByteArrayInputStream(
443: bytes), BUFFER_SIZE);
444: }
445:
446: public int getContentLength() {
447: return bytes.length;
448: }
449:
450: }
451:
452: public void stop() {
453: LOG
454: .info("Stopping the EmbeddedPluginClassLoader, clearing byte cache.");
455: byteCache.clear();
456: embeddedJarUrls.clear();
457: super.stop();
458: }
459:
460: }
|