001: /* QuiltClassLoader.java */
002: package org.quilt.cl;
003:
004: import java.io.ByteArrayInputStream;
005: import java.io.ByteArrayOutputStream;
006: import java.io.File;
007: import java.io.InputStream;
008: import java.io.IOException;
009: import java.lang.reflect.Constructor;
010: import java.net.MalformedURLException;
011: import java.net.URL;
012: import java.net.URLClassLoader;
013: import java.util.Hashtable;
014: import java.util.Iterator;
015: import java.util.List;
016: import java.util.Map;
017: import java.util.Vector;
018:
019: import org.apache.bcel.classfile.ClassParser;
020: import org.apache.bcel.classfile.JavaClass;
021:
022: // DEBUG
023: import org.apache.bcel.classfile.Field;
024: import org.apache.bcel.classfile.Method; // END
025:
026: import org.quilt.reg.QuiltRegistry;
027:
028: /**
029: * <p>Quilt's transforming class loader. Can be directed to instrument
030: * a set of classes, matching class names against a list of prefixes
031: * and another list excluding classes from instrumentation, the
032: * exclusion list taking priority. Will delegate loading to a parent
033: * class loader where explicitly directed to; otherwise will be the
034: * defining loader. By default the loading of classes whose names
035: * begin with <tt>java., javax., junit., org.apache.bcel.,
036: * org.apache.tools.ant.</tt> and <tt>org.quilt.</tt> is delegated.</p>
037: *
038: * <p>Classes whose names begin with a reserved prefix, currently
039: * <tt>test.data.Test</tt>, are synthesized instead of being
040: * loaded. This must be specifically enabled.</p>
041: *
042: *
043: *
044: * @author <a href="jddixon@users.sourceforge.net">Jim Dixon</a>
045: *
046: * @see ClassFactory
047: */
048: public class QuiltClassLoader extends URLClassLoader {
049:
050: /** Operating system specific */
051: public static final char FILE_PATH_DELIM_CHAR = File.separatorChar;
052: public static final String FILE_PATH_DELIM_STR = File.separator;
053: public static final char CLASSPATH_DELIM_CHAR = File.pathSeparatorChar;
054: public static final String CLASSPATH_DELIM_STR = File.pathSeparator;
055:
056: /**
057: * Names of classes which must be loaded by the parent. There is one
058: * exception to this list: org.quilt.QIC, which is not delegated and
059: * not instrumented.
060: */
061: public static final String[] DELEGATED = { "java.", "javax.",
062: "junit.", "org.apache.bcel.", "org.apache.tools.ant.",
063: "org.quilt.", "sun." };
064:
065: /** XXX This is misleading! What's wanted is a copy. */
066: private String[] dels = DELEGATED;
067:
068: private List delegated = new Vector();
069: /**
070: * Names of classes NOT to be instrumented. Names are matched
071: * as above. The excluded list is consulted first.
072: */
073: private List excluded = new Vector();
074:
075: /**
076: * Names of classes to be instrumented. At this time no
077: * wildcards are permitted. Any class whose name begins
078: * with a string in the array will be instrumented,
079: * unless it is on the excluded list.
080: */
081: private List included = new Vector();
082:
083: /**
084: * URLs in the order in which they are to be searched. Those
085: * ending in '/' are directories. Any others are jars.
086: */
087: private List classPath = new Vector();
088:
089: /** Delegation class loader. Unless a class is to be instrumented
090: * (is on the inclusion list and not on the exclusion list),
091: * loading will be delegated to this class loader.
092: */
093: private ClassLoader parent = null;
094:
095: /** Prefix indicating that the class should be synthesized. */
096: public static final String SYNTH_PREFIX = "test.data.Test";
097: private String synthPrefix = SYNTH_PREFIX;
098: private boolean synthEnabled = false;
099:
100: /** Responsible for instrumenting classes. */
101: public ClassTransformer xformer = null;
102: /** Configurable class transformers. */
103: List cxf = new Vector();
104: /** Configurable method transformers. */
105: List mxf = new Vector();
106: /** Configurable graph transformers. */
107: List gxf = new Vector();
108: /** QuiltRegistry list. */
109: List regList = new Vector();
110:
111: /** Constructor with abbreviated argument list. */
112: public QuiltClassLoader(URL[] cp, String[] inc) {
113: this (cp, null, null, inc, null);
114: }
115:
116: /**
117: * Constructor with full argument list.
118: *
119: * @param cp Class path, an array of paths
120: * @param parent Class loader which we delegate to.
121: * @param del String array, names of classes to be delegated
122: * @param inc String array, names of classes to be instrumented
123: * @param exc String array, names of classes not to be instrumented.
124: */
125: public QuiltClassLoader(URL[] cp, ClassLoader parent, String[] del,
126: String[] inc, String[] exc) {
127: super (cp == null ? new URL[0] : cp, parent);
128:
129: if (cp != null) {
130: for (int i = 0; i < cp.length; i++) {
131: classPath.add(cp[i]);
132: }
133: }
134: if (parent == null) {
135: this .parent = getSystemClassLoader();
136: } else {
137: this .parent = parent;
138: }
139: for (int i = 0; i < dels.length; i++) {
140: delegated.add(dels[i]);
141: }
142: if (del != null) {
143: for (int i = 0; i < del.length; i++) {
144: delegated.add(del[i]);
145: }
146: }
147: if (inc != null) {
148: for (int i = 0; i < inc.length; i++) {
149: included.add(inc[i]);
150: }
151: }
152: if (exc != null) {
153: for (int i = 0; i < exc.length; i++) {
154: excluded.add(exc[i]);
155: }
156: }
157: }
158:
159: /** Do we delegate loading this to the parent? */
160: private boolean delegateTheClass(final String name) {
161: if (name.equals("org.quilt.QIC")) {
162: return false;
163: }
164: for (int i = 0; i < delegated.size(); i++) {
165: if (name.startsWith((String) delegated.get(i))) {
166: return true;
167: }
168: }
169: return false;
170: }
171:
172: /** Should class be instrumented? */
173: private boolean instrumentTheClass(final String name) {
174: if (name.equals("org.quilt.QIC")) {
175: return false;
176: }
177: for (int i = 0; i < excluded.size(); i++) {
178: if (name.startsWith((String) excluded.get(i))) {
179: return false;
180: }
181: }
182: for (int i = 0; i < included.size(); i++) {
183: if (name.startsWith((String) included.get(i))) {
184: return true;
185: }
186: }
187: return false;
188: }
189:
190: /**
191: * Convert a class name into a file name by replacing dots with
192: * forward slashes and appending ".class".
193: */
194: public static String classFileName(final String className) {
195: return className.replace('.', FILE_PATH_DELIM_CHAR) + ".class";
196: }
197:
198: /**
199: * Class loader. Delegates the loading if specifically instructed
200: * to do so. Returns the class if it has already been loaded.
201: * Otherwise creates a class transformer if necessary and then
202: * passes the name to <code>findClass.</code>
203: */
204: public synchronized Class loadClass(String name)
205: throws ClassNotFoundException {
206: if (name == null) {
207: throw new IllegalArgumentException("null class name");
208: }
209: if (delegateTheClass(name)) {
210: // DEBUG
211: // System.out.println("QCL.loadClass: delegating " + name);
212: // END
213: return parent.loadClass(name);
214: }
215: Class c = findLoadedClass(name);
216: if (c != null) {
217: return c;
218: }
219: if (xformer == null) {
220: xformer = new ClassTransformer(cxf, mxf, gxf);
221: }
222: return findClass(name);
223: }
224:
225: /**
226: * Locate the class whose name is passed and define it. If the
227: * class name has the appropriate prefix and synthesizing it is
228: * enabled, it synthesizes it. Otherwise it searches for it
229: * along the class path. If indicated, it transforms (instruments)
230: * the class. Finally, it defines and returns the result.
231: *
232: * @param name Class name in embedded dot (.) form.
233: */
234: protected Class findClass(String name)
235: throws ClassNotFoundException {
236: // we only instrument the class if we have transformers at
237: // class, method, or graph level
238: boolean instIt = instrumentTheClass(name)
239: && (cxf.size() > 0 || mxf.size() > 0 || gxf.size() > 0);
240: byte[] b = null;
241: if (name.startsWith(synthPrefix)) {
242: JavaClass jc = ClassFactory.getInstance().makeClass(name,
243: classFileName(name)).getJavaClass();
244: if (instIt) {
245: jc = xformer.xform(jc);
246: }
247: b = jc.getBytes(); // convert it into a byte array
248: } else {
249: // DEBUG
250: //System.out.println("QCL.findClass: locating " + name);
251: // END
252: try {
253: b = getClassData(name);
254: if (instIt) {
255: // DEBUG
256: // System.out.println("QCL.findClass: instrumenting " + name);
257: // END
258: // convert to bcel JavaClass -
259: // throws IOException, ClassFormatException
260: JavaClass jc = new ClassParser(
261: new ByteArrayInputStream(b),
262: classFileName(name)).parse();
263: JavaClass temp = xformer.xform(jc);
264: // // DEBUG
265: // Field [] myFields = temp.getFields();
266: // StringBuffer fieldData = new StringBuffer();
267: // for (int k = 0; k < myFields.length; k++)
268: // fieldData.append(" ")
269: // .append(myFields[k]).append("\n");
270:
271: // Method[] myMethods = temp.getMethods();
272: // StringBuffer methodData = new StringBuffer();
273: // for (int k = 0; k < myMethods.length; k++)
274: // methodData.append(" ")
275: // .append(myMethods[k].getName()).append("\n");
276:
277: // System.out.println(
278: // "QCL.findClass after instrumenting JavaClass for "
279: // + name
280: // + "\nFIELDS (" + myFields.length + ") :\n"
281: // + fieldData.toString()
282: // + "METHODS (" + myMethods.length + ") :\n"
283: // + methodData.toString() );
284: // // END
285:
286: //b = xformer.xform (jc).getBytes();
287: b = temp.getBytes();
288: }
289: } catch (IOException e) {
290: e.printStackTrace(); // DEBUG
291: throw new ClassNotFoundException(name, e);
292: }
293: }
294:
295: // this can throw a ClassFormatError or IndexOutOfBoundsException
296: return defineClass(name, b, 0, b.length);
297: }
298:
299: /** @return Classpath as a newline-terminated String. */
300: public String urlsToString() {
301: StringBuffer sb = new StringBuffer().append("classpath:\n");
302: URL[] urls = getURLs();
303: for (int k = 0; k < urls.length; k++) {
304: sb.append(" ").append(k).append(" ").append(urls[k])
305: .append("\n");
306: }
307: return sb.toString();
308: }
309:
310: /** Find a class along the class path and load it as a byte array. */
311: protected byte[] getClassData(String className) throws IOException {
312: URL fileURL = findResource(classFileName(className));
313: // DEBUG XXX
314: if (fileURL == null) {
315: System.err.println("QCL.getClassData mapping " + className
316: + " to " + classFileName(className));
317: System.err.println(" findResource returned null\n"
318: + urlsToString());
319: }
320: // END
321: if (fileURL == null) {
322: // ClassNotFoundException();
323: throw new IOException("null fileURL for " + className);
324: }
325: InputStream ins = fileURL.openStream();
326: ByteArrayOutputStream outs = new ByteArrayOutputStream(65536);
327: byte[] buffer = new byte[4096];
328: int count;
329: while ((count = ins.read(buffer)) != -1) {
330: outs.write(buffer, 0, count);
331: }
332: return outs.toByteArray();
333: }
334:
335: // ADD/GET/SET METHODS //////////////////////////////////////////
336: /**
337: * Add a path to the class loader's classpath.
338: * @param url Path to be added.
339: */
340: public void addPath(URL url) {
341: classPath.add(url);
342: }
343:
344: /** @return The classpath used by this QuiltClassLoader. */
345: public URL[] getClassPath() {
346: URL[] myURLs = new URL[classPath.size()];
347: return (URL[]) (classPath.toArray(myURLs));
348: }
349:
350: /**
351: * Convert domain name in classpath to file name, allowing for
352: * initial dots. Need to cope with ../../target/big.jar and
353: * similar constructions.
354: */
355: public static final String THIS_DIR = "." + FILE_PATH_DELIM_STR;
356: public static final String UP_DIR = ".." + FILE_PATH_DELIM_STR;
357: public static final int THIS_DIR_LEN = THIS_DIR.length();
358: public static final int UP_DIR_LEN = UP_DIR.length();
359:
360: /**
361: * Convert a dotted domain name to its path form, allowing for
362: * leading ./ and ../ and terminating .jar
363: */
364: public static String domainToFileName(String name) {
365: // ignore any leading dots
366: int startNdx;
367: for (startNdx = 0; startNdx < name.length();) {
368: if (name.substring(startNdx).startsWith(THIS_DIR)) {
369: startNdx += THIS_DIR_LEN;
370: } else if (name.substring(startNdx).startsWith(UP_DIR)) {
371: startNdx += UP_DIR_LEN;
372: } else {
373: break;
374: }
375: }
376: // leave .jar intact
377: int endNdx;
378: if (name.endsWith(".jar")) {
379: endNdx = name.length() - 4;
380: } else {
381: endNdx = name.length();
382: }
383:
384: StringBuffer sb = new StringBuffer();
385: if (startNdx > 0) {
386: sb.append(name.substring(0, startNdx));
387: }
388: sb.append(name.substring(startNdx, endNdx).replace('.',
389: FILE_PATH_DELIM_CHAR));
390: if (endNdx != name.length()) {
391: sb.append(".jar");
392: }
393: return sb.toString();
394: }
395:
396: /**
397: * Convert classpath in normal form to URL[]
398: */
399: public static URL[] cpToURLs(String cp) {
400: URL[] urls;
401: if (cp == null) {
402: urls = new URL[0];
403: } else {
404: String[] elements = cp.split(":");
405: List urlList = new Vector();
406: int urlCount = 0;
407: for (int i = 0; i < elements.length; i++) {
408: String noDots = domainToFileName(elements[i]);
409: boolean foundJar = noDots.endsWith(".jar");
410: File file = new File(noDots);
411: String urlForm = "file://" + file.getAbsolutePath();
412: if (!foundJar && !urlForm.endsWith(FILE_PATH_DELIM_STR)) {
413: urlForm += FILE_PATH_DELIM_STR;
414: }
415: try {
416: URL candidate = new URL(urlForm);
417: urlCount++; // didn't throw exception
418: urlList.add(candidate);
419: } catch (MalformedURLException e) {
420: System.err
421: .println("WARNING: ignoring malformed URL "
422: + urlForm);
423: }
424: }
425: urls = new URL[urlCount];
426: for (int k = 0; k < urls.length; k++) {
427: urls[k] = (URL) urlList.get(k);
428: }
429: }
430: return urls;
431: }
432:
433: /**
434: * Convert classpath in normal form to URL[] and sets loader
435: * classpath to the corresponding value.
436: *
437: * @param cp Class path in colon- or semicolon-delimited form.
438: */
439: public void setClassPath(String cp) {
440: classPath.clear();
441: URL[] urls = cpToURLs(cp);
442: for (int i = 0; i < urls.length; i++) {
443: classPath.add(urls[i]);
444: addURL(urls[i]);
445: }
446: // // DEBUG
447: // System.out.println("after setting classpath, new classpath is:");
448: // URL[] currURLs = getURLs();
449: // for (int k = 0; k < currURLs.length; k++) {
450: // System.out.println(" " + k + " " + currURLs[k].getPath() );
451: // }
452: // // END
453: }
454:
455: /**
456: * Add a class name prefix to the list of those to be delegated
457: * to the parent.
458: * @param prefix Prefix to be added.
459: */
460: public void addDelegated(final String prefix) {
461: delegated.add(prefix);
462: }
463:
464: /**
465: * @return As a String array the list of class name prefixes
466: * whose loading is to be delegated to the parent.
467: */
468: public String[] getDelegated() {
469: String[] myDels = new String[delegated.size()];
470: return (String[]) (delegated.toArray(myDels));
471: }
472:
473: /**
474: * Add a class name prefix to the list of those to be excluded
475: * from instrumentation.
476: *
477: * @param prefix Prefix to be added.
478: */
479: public void addExcluded(final String prefix) {
480: excluded.add(prefix);
481: }
482:
483: /**
484: * @return As a String array the list of class name prefixes
485: * which are NOT to be instrumented.
486: */
487: public String[] getExcluded() {
488: String[] myExc = new String[excluded.size()];
489: return (String[]) (excluded.toArray(myExc));
490: }
491:
492: /**
493: * Sets the list of classes to be excluded from instrumentation.
494: *
495: * @param s List of classes in comma-separated String form.
496: */
497: public void setExcluded(String s) {
498: excluded.clear();
499: if (s != null) {
500: String[] newExc = s.split(",");
501: for (int i = 0; i < newExc.length; i++) {
502: excluded.add(newExc[i]);
503: }
504: }
505: }
506:
507: /**
508: * Add a class name prefix to the list of those to be
509: * instrumented.
510: *
511: * @param prefix Prefix to be added.
512: */
513: public void addIncluded(final String prefix) {
514: included.add(prefix);
515: }
516:
517: /**
518: * @return As a String array the list of class name prefixes
519: * which ARE to be instrumented.
520: */
521: public String[] getIncluded() {
522: String[] myInc = new String[included.size()];
523: return (String[]) (included.toArray(myInc));
524: }
525:
526: /**
527: * Sets the list of classes to be instrumented.
528: *
529: * @param s List of classes in comma-separated String form.
530: */
531: public void setIncluded(String s) {
532: included.clear();
533: if (s != null) {
534: String[] newInc = s.split(",");
535: for (int i = 0; i < newInc.length; i++) {
536: included.add(newInc[i]);
537: }
538: }
539: }
540:
541: /** Get synthesizing-enabled flag. */
542: public boolean getSynthEnabled() {
543: return synthEnabled;
544: }
545:
546: /** Enable class synthesizing. */
547: public void setSynthEnabled(boolean b) {
548: synthEnabled = b;
549: }
550:
551: /**
552: * @return The prefix signifying that a class is to be synthesized.
553: */
554: public String getSynthPrefix() {
555: return synthPrefix;
556: }
557:
558: /** Add a class transformer. */
559: public void addClassXformer(ClassXformer xf) {
560: cxf.add(xf);
561: }
562:
563: /** Add a method transformer. */
564: public void addMethodXformer(MethodXformer xf) {
565: mxf.add(xf);
566: }
567:
568: /** Add a graph transformer. */
569: public void addGraphXformer(GraphXformer xf) {
570: gxf.add(xf);
571: }
572:
573: /** Map of registries by String name. */
574: public Map regMap = new Hashtable();
575:
576: /** Get a reference to a Quilt registry. */
577: public QuiltRegistry getRegistry(String regName) {
578: QuiltRegistry qr = null;
579: if (regMap.containsKey(regName)) {
580: qr = (QuiltRegistry) regMap.get(regName);
581: }
582: return qr;
583: }
584:
585: /**
586: * Add a new QuiltRegistry to the list. An example of the
587: * argument is "org.quilt.cover.stmt.StmtRegistry".
588: *
589: * @param regName The domain name of the registry in dotted form.
590: */
591: public QuiltRegistry addQuiltRegistry(String regName) {
592: QuiltRegistry qr = null;
593: if (regMap.containsKey(regName)) {
594: qr = (QuiltRegistry) regMap.get(regName);
595: } else
596: try {
597: Class o = Class.forName(regName, false, parent);
598: Constructor con = o
599: .getConstructor(new Class[] { QuiltClassLoader.class });
600: qr = (QuiltRegistry) con
601: .newInstance(new Object[] { this });
602: regList.add(qr);
603: regMap.put(regName, qr);
604: } catch (Exception e) {
605: System.out
606: .println("\nQuiltClassLoader.addQuiltRegistry:"
607: + "\n EXCEPTION while trying to add "
608: + regName
609: + "\n Is it on the parent's CLASSPATH?"
610: + "\n Exception: " + e);
611: }
612: return qr;
613: }
614:
615: /**
616: * Get reports from any or all registries. XXX This should not
617: * be returning a String -- it might be huge.
618: */
619: public String getReport() {
620: StringBuffer sb = new StringBuffer();
621: if (!regList.isEmpty()) {
622: Iterator i = regList.iterator();
623: while (i.hasNext()) {
624: QuiltRegistry reg = (QuiltRegistry) i.next();
625: sb.append(reg.getReport());
626: }
627: }
628: return sb.toString();
629: }
630: }
|