1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
2 *
3 * This program and the accompanying materials are made available under
4 * the terms of the Common Public License v1.0 which accompanies this distribution,
5 * and is available at http://www.eclipse.org/legal/cpl-v10.html
6 *
7 * $Id: InstrClassLoader.java,v 1.1.1.1.2.2 2004/07/16 23:32:03 vlad_r Exp $
8 */
9package com.vladium.emma.rt;
10
11import java.io.File;
12import java.io.IOException;
13import java.io.InputStream;
14import java.io.PrintWriter;
15import java.net.MalformedURLException;
16import java.net.URL;
17import java.net.URLClassLoader;
18import java.security.CodeSource;
19import java.security.cert.Certificate;
20import java.util.Map;
21
22import com.vladium.logging.Logger;
23import com.vladium.util.ByteArrayOStream;
24import com.vladium.util.asserts.$assert;
25import com.vladium.emma.IAppConstants;
26import com.vladium.emma.filter.IInclExclFilter;
27
28// ----------------------------------------------------------------------------
29/**
30 * @author Vlad Roubtsov, (C) 2003
31 */
32public
33final class InstrClassLoader extends URLClassLoader
34{
35    // public: ................................................................
36
37    // TODO: proper security [use PrivilegedAction as needed]
38    // TODO: [see above as well] need to keep track of res URLs to support [path] exclusion patterns
39    // TODO: improve error handling so it is clear when errors come from buggy instrumentation
40
41
42    public static final String PROPERTY_FORCED_DELEGATION_FILTER  = "clsload.forced_delegation_filter";
43    public static final String PROPERTY_THROUGH_DELEGATION_FILTER  = "clsload.through_delegation_filter";
44
45
46    public InstrClassLoader (final ClassLoader parent, final File [] classpath,
47                             final IInclExclFilter forcedDelegationFilter,
48                             final IInclExclFilter throughDelegationFilter,
49                             final IClassLoadHook hook, final Map cache)
50        throws MalformedURLException
51    {
52        // setting ClassLoader.parent to null disables the standard delegation
53        // behavior in a few places, including URLClassLoader.getResource():
54
55        super (filesToURLs (classpath), null);
56
57        // TODO: arg validation
58
59        m_hook = hook;
60        m_cache = cache; // can be null
61
62        m_forcedDelegationFilter = forcedDelegationFilter;
63        m_throughDelegationFilter = throughDelegationFilter;
64
65        m_parent = parent;
66        m_bufPool = new PoolEntry [BAOS_POOL_SIZE];
67
68        m_log = Logger.getLogger ();
69    }
70
71    /**
72     * Overrides java.lang.ClassLoader.loadClass() to change the usual parent-child
73     * delegation rules just enough to be able to 'replace' the parent loader. This
74     * also has the effect of detecting 'system' classes without doing any class
75     * name-based matching.
76     */
77    public synchronized final Class loadClass (final String name, final boolean resolve)
78        throws ClassNotFoundException
79    {
80        final boolean trace1 = m_log.atTRACE1 ();
81
82        if (trace1) m_log.trace1 ("loadClass",  "(" + name + ", " + resolve + "): nest level " + m_nestLevel);
83
84        Class c = null;
85
86        // first, check if this class has already been defined by this classloader
87        // instance:
88        c = findLoadedClass (name);
89
90        if (c == null)
91        {
92            Class parentsVersion = null;
93            if (m_parent != null)
94            {
95                try
96                {
97                    parentsVersion = m_parent.loadClass (name); // note: it is important that this does not init the class
98
99                    if ((parentsVersion.getClassLoader () != m_parent) ||
100                        ((m_forcedDelegationFilter == null) || m_forcedDelegationFilter.included (name)))
101                    {
102                        // (a) m_parent itself decided to delegate: use parent's version
103                        // (b) the class was on the forced delegation list: use parent's version
104                        c = parentsVersion;
105                        if (trace1) m_log.trace1 ("loadClass", "using parent's version for [" + name + "]");
106                    }
107                }
108                catch (ClassNotFoundException cnfe)
109                {
110                    // if the class was on the forced delegation list, error out:
111                    if ((m_forcedDelegationFilter == null) || m_forcedDelegationFilter.included (name))
112                        throw cnfe;
113                }
114            }
115
116            if (c == null)
117            {
118                try
119                {
120                    // either (a) m_parent was null or (b) it could not load 'c'
121                    // or (c) it will define 'c' itself if allowed to. In any
122                    // of these cases I attempt to define my own version:
123                    c = findClass (name);
124                }
125                catch (ClassNotFoundException cnfe)
126                {
127                    // this is a difficult design point unless I resurrect the -lx option
128                    // and document how to use it [which will confuse most users anyway]
129
130                    // another alternative would be to see if parent's version is included by
131                    // the filter and print a warning; still, it does not help with JAXP etc
132
133                    if (parentsVersion != null)
134                    {
135                        final boolean delegate = (m_throughDelegationFilter == null) || m_throughDelegationFilter.included (name);
136
137                        if (delegate)
138                        {
139                            c = parentsVersion;
140                            if (trace1) m_log.trace1 ("loadClass", "[delegation filter] using parent's version for [" + name + "]");
141                        }
142                        else
143                            throw cnfe;
144                    }
145                    else
146                      throw cnfe;
147                }
148            }
149        }
150
151        if (c == null) throw new ClassNotFoundException (name);
152
153        if (resolve) resolveClass (c); // this never happens in J2SE JVMs
154        return c;
155    }
156
157    // TODO: remove this in the release build
158
159    public final URL getResource (final String name)
160    {
161        final boolean trace1 = m_log.atTRACE1 ();
162
163        if (trace1) m_log.trace1 ("getResource",  "(" + name + "): nest level " + m_nestLevel);
164
165        final URL result = super.getResource (name);
166        if (trace1 && (result != null)) m_log.trace1 ("loadClass",  "[" + name + "] found in " + result);
167
168        return result;
169    }
170
171    // protected: .............................................................
172
173
174    protected final Class findClass (final String name)
175        throws ClassNotFoundException
176    {
177        final boolean trace1 = m_log.atTRACE1 ();
178
179        if (trace1) m_log.trace1 ("findClass",  "(" + name + "): nest level " + m_nestLevel);
180
181        final boolean useClassCache = (m_cache != null);
182        final ClassPathCacheEntry entry = useClassCache ? (ClassPathCacheEntry) m_cache.remove (name) : null;
183
184        byte [] bytes;
185        int length;
186        URL classURL = null;
187
188        if (entry != null) // cache hit
189        {
190            ++ m_cacheHits;
191
192            // used cached class def bytes, no need to repeat disk I/O:
193
194            try
195            {
196                classURL = new URL (entry.m_srcURL);
197            }
198            catch (MalformedURLException murle) // this should never happen
199            {
200                if ($assert.ENABLED)
201                {
202                    murle.printStackTrace (System.out);
203                }
204            }
205
206            PoolEntry buf = null;
207            try
208            {
209                buf = acquirePoolEntry ();
210                final ByteArrayOStream baos = buf.m_baos; // reset() has been called on this
211
212                // the original class definition:
213                bytes = entry.m_bytes;
214                length = bytes.length;
215
216                if ((m_hook != null) && m_hook.processClassDef (name, bytes, length, baos)) // note: this can overwrite 'bytes'
217                {
218                    // the instrumented class definition:
219                    bytes = baos.getByteArray ();
220                    length = baos.size ();
221
222                    if (trace1) m_log.trace1 ("findClass",  "defining [cached] instrumented [" + name + "] {" + length + " bytes }");
223                }
224                else
225                {
226                    if (trace1) m_log.trace1 ("findClass",  "defining [cached] [" + name + "] {" + length + " bytes }");
227                }
228
229                return defineClass (name, bytes, length, classURL);
230            }
231            catch (IOException ioe)
232            {
233                throw new ClassNotFoundException (name);
234            }
235            finally
236            {
237                if (buf != null) releasePoolEntry (buf);
238            }
239        }
240        else // cache miss
241        {
242            if (useClassCache) ++ m_cacheMisses;
243
244            // .class files are not guaranteed to be loadable as resources;
245            // but if Sun's code does it...
246            final String classResource = name.replace ('.', '/') + ".class";
247
248            // even thought normal delegation is disabled, this will find bootstrap classes:
249            classURL = getResource (classResource); // important to hook into URLClassLoader's overload of this so that Class-Path manifest attributes are processed etc
250
251            if (trace1 && (classURL != null)) m_log.trace1 ("findClass",  "[" + name + "] found in " + classURL);
252
253            if (classURL == null)
254                throw new ClassNotFoundException (name);
255            else
256            {
257                InputStream in = null;
258                PoolEntry buf = null;
259                try
260                {
261                    in = classURL.openStream ();
262
263                    buf = acquirePoolEntry ();
264                    final ByteArrayOStream baos = buf.m_baos; // reset() has been called on this
265
266                    readFully (in, baos, buf.m_buf);
267                    in.close (); // don't keep the file handle across reentrant calls
268                    in = null;
269
270                    // the original class definition:
271                    bytes = baos.getByteArray ();
272                    length = baos.size ();
273
274                    baos.reset (); // reuse this for processClassDef below
275
276                    if ((m_hook != null) && m_hook.processClassDef (name, bytes, length, baos)) // note: this can overwrite 'bytes'
277                    {
278                        // the instrumented class definition:
279                        bytes = baos.getByteArray ();
280                        length = baos.size ();
281
282                        if (trace1) m_log.trace1 ("findClass",  "defining instrumented [" + name + "] {" + length + " bytes }");
283                    }
284                    else
285                    {
286                        if (trace1) m_log.trace1 ("findClass",  "defining [" + name + "] {" + length + " bytes }");
287                    }
288
289                    return defineClass (name, bytes, length, classURL);
290                }
291                catch (IOException ioe)
292                {
293                    throw new ClassNotFoundException (name);
294                }
295                finally
296                {
297                    if (buf != null) releasePoolEntry (buf);
298                    if (in != null) try { in.close (); } catch (Exception ignore) {}
299                }
300            }
301        }
302    }
303
304    public void debugDump (final PrintWriter out)
305    {
306        if (out != null)
307        {
308            out.println (this + ": " + m_cacheHits + " class cache hits, " + m_cacheMisses + " misses");
309        }
310    }
311
312    // package: ...............................................................
313
314    // private: ...............................................................
315
316
317    private static final class PoolEntry
318    {
319        PoolEntry (final int baosCapacity, final int bufSize)
320        {
321            m_baos = new ByteArrayOStream (baosCapacity);
322            m_buf = new byte [bufSize];
323        }
324
325        void trim (final int baosCapacity, final int baosMaxCapacity)
326        {
327            if (m_baos.capacity () > baosMaxCapacity)
328            {
329                m_baos = new ByteArrayOStream (baosCapacity);
330            }
331        }
332
333        ByteArrayOStream m_baos;
334        final byte [] m_buf;
335
336    } // end of nested class
337
338
339    /*
340     * 'srcURL' may be null
341     */
342    private Class defineClass (final String className, final byte [] bytes, final int length, final URL srcURL)
343    {
344        // support ProtectionDomains with non-null class source URLs:
345        // [however, disable anything related to sealing or signing]
346
347        final CodeSource csrc = new CodeSource (srcURL, (Certificate[])null);
348
349        // allow getPackage() to return non-null on the class we are about to
350        // define (however, don't bother emulating the original manifest info since
351        // we may be altering manifest content anyway):
352
353        final int lastDot = className.lastIndexOf ('.');
354        if (lastDot >= 0)
355        {
356            final String packageName = className.substring (0, lastDot);
357
358            final Package pkg = getPackage (packageName);
359            if (pkg == null)
360            {
361                definePackage (packageName,
362                               IAppConstants.APP_NAME, IAppConstants.APP_VERSION, IAppConstants.APP_COPYRIGHT,
363                               IAppConstants.APP_NAME, IAppConstants.APP_VERSION, IAppConstants.APP_COPYRIGHT,
364                               srcURL);
365            }
366        }
367
368        return defineClass (className, bytes, 0, length, csrc);
369    }
370
371
372    private static URL [] filesToURLs (final File [] classpath)
373        throws MalformedURLException
374    {
375        if ((classpath == null) || (classpath.length == 0))
376            return EMPTY_URL_ARRAY;
377
378        final URL [] result = new URL [classpath.length];
379
380        for (int f = 0; f < result.length ; ++ f)
381        {
382            result [f] = classpath [f].toURL (); // note: this does proper dir encoding
383        }
384
385        return result;
386    }
387
388    /**
389     * Reads the entire contents of a given stream into a flat byte array.
390     */
391    private static void readFully (final InputStream in, final ByteArrayOStream out, final byte [] buf)
392        throws IOException
393    {
394        for (int read; (read = in.read (buf)) >= 0; )
395        {
396            out.write (buf, 0, read);
397        }
398    }
399
400    /*
401     * not MT-safe; must be called from loadClass() only
402     */
403    private PoolEntry acquirePoolEntry ()
404    {
405        PoolEntry result;
406
407        if (m_nestLevel >= BAOS_POOL_SIZE)
408        {
409            result = new PoolEntry (BAOS_INIT_SIZE, BAOS_INIT_SIZE);
410        }
411        else
412        {
413            result = m_bufPool [m_nestLevel];
414            if (result == null)
415            {
416                result = new PoolEntry (BAOS_INIT_SIZE, BAOS_INIT_SIZE);
417                m_bufPool [m_nestLevel] = result;
418            }
419            else
420            {
421                result.m_baos.reset ();
422            }
423        }
424
425        ++ m_nestLevel;
426
427        return result;
428    }
429
430    /*
431     * not MT-safe; must be called from loadClass() only
432     */
433    private void releasePoolEntry (final PoolEntry buf)
434    {
435        if (-- m_nestLevel < BAOS_POOL_SIZE)
436        {
437            buf.trim (BAOS_INIT_SIZE, BAOS_MAX_SIZE);
438        }
439    }
440
441
442    private final ClassLoader m_parent;
443
444    private final IInclExclFilter m_forcedDelegationFilter;
445    private final IInclExclFilter m_throughDelegationFilter;
446
447    private final Map /* classJavaName:String -> ClassPathCacheEntry */ m_cache; // can be null
448    private final IClassLoadHook m_hook;
449    private final PoolEntry [] m_bufPool;
450
451    private final Logger m_log; // a loader instance is used concurrently but cached its log config at construction time
452
453    private int m_nestLevel;
454
455    private int m_cacheHits, m_cacheMisses;
456
457    private static final int BAOS_INIT_SIZE = 32 * 1024;
458    private static final int BAOS_MAX_SIZE = 1024 * 1024;
459    private static final int BAOS_POOL_SIZE = 8;
460    private static final URL [] EMPTY_URL_ARRAY = new URL [0];
461
462} // end of class
463// ----------------------------------------------------------------------------
464