001    /*
002      Copyright (C) 2001-2003 Renaud Pawlak, Lionel Seinturier, Fabrice
003      Legond-Aubry.
004    
005      This program is free software; you can redistribute it and/or modify
006      it under the terms of the GNU Lesser General Public License as
007      published by the Free Software Foundation; either version 2 of the
008      License, or (at your option) any later version.
009    
010      This program is distributed in the hope that it will be useful,
011      but WITHOUT ANY WARRANTY; without even the implied warranty of
012      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013      GNU Lesser General Public License for more details.
014    
015      You should have received a copy of the GNU Lesser General Public
016      License along with this program; if not, write to the Free Software
017      Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
018      USA */
019    
020    package org.objectweb.jac.core;
021    
022    import java.io.File;
023    import java.io.FileInputStream;
024    import java.io.FileOutputStream;
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.io.InvalidClassException;
028    import java.io.ObjectInputStream;
029    import java.io.ObjectOutputStream;
030    import java.lang.reflect.Field;
031    import java.lang.reflect.Method;
032    import java.net.URL;
033    import java.net.URLClassLoader;
034    import java.security.MessageDigest;
035    import java.security.NoSuchAlgorithmException;
036    import java.util.Arrays;
037    import java.util.HashSet;
038    import java.util.Hashtable;
039    import java.util.Iterator;
040    import java.util.Properties;
041    import org.apache.log4j.Logger;
042    import org.objectweb.jac.core.rtti.ClassInfo;
043    import org.objectweb.jac.core.rtti.LoadtimeRTTI;
044    import org.objectweb.jac.util.ExtArrays;
045    import org.objectweb.jac.util.Streams;
046    import org.objectweb.jac.util.Strings;
047    
048    /**
049     * The JAC specific class loader for JAC objects.
050     *
051     * <p>This loader does the following things :</p>
052     * <ul>
053     *   <li>load a set of Properties from all the found "jac.prop" files
054     *   <li>get from them the choosen wrappeeTranslator
055     *   <li>instanciate the choosen wrappeTranslator
056     *   <li>replace the system class loader.
057     *   <li>then for each loaded class it can delegates the translation of
058     *    JAC objects to the choosen <code>WrappeeTranslator</code>.
059     * </ul>
060     * @see org.objectweb.jac.core.WrappeeTranslator */
061    public class JacLoader extends ClassLoader {
062        static Logger logger = Logger.getLogger("loader");
063        static Logger loggerRes = Logger.getLogger("loader.resource");
064    
065        /** 
066         * Stores the bytecode of the loaded and translated classes.
067         * 
068         * @see isLoaded(String)
069         * @see getLoadedBytecode(String) */
070        private Hashtable classes = new Hashtable();
071        
072        /** If true, caches the tranlated classes on disk. */
073        private boolean write = false;
074        
075        /** If true, clears the disk cache. */
076        private boolean clean = false;
077    
078        private ClassLoader parentLoader = ClassLoader.getSystemClassLoader();
079    
080        private WrappeeTranslator wt = null;
081        private String bytecodeModifier;
082        
083        /* Packages whose loading we delegate */
084        private String[] ignoredPackages = {
085            "java.", "javax.", "sun.", "org.xml.sax.", "org.w3c.dom.", "org.apache.log4j"
086        };
087    
088        /* Classes whose loading we delegate. Those classes are thus shared
089           by all Jac loaders, and untranslated */
090        private HashSet ignoredClasses = new HashSet();
091    
092    
093        private static ClassLoader otherClassLoader;
094    
095        LoadtimeRTTI rtti;
096    
097        /**
098         * Create a JacLoader.
099         *
100         * @param write if true, caches the tranlated classes on disk
101         * @param clean if true, clears the disk cache
102         * @param otherClassLoader if not null, indicates another
103         * ClassLoader where to search for classes not in original
104         * ClassPath
105         */
106        public JacLoader(boolean write,
107                         boolean clean,
108                         ClassLoader otherClassLoader) 
109            throws Exception
110        {
111            logger.info("building JacLoader...");
112            //super (ClassLoader.getSystemClassLoader());
113            this.write = write;
114            this.clean = clean;
115            if (otherClassLoader != null)
116                JacLoader.otherClassLoader = otherClassLoader;
117    
118            ignoredClasses.add("org.objectweb.jac.util.Log");
119            ignoredClasses.add("org.objectweb.jac.core.Jac");
120            ignoredClasses.add("org.objectweb.jac.core.rtti.LoadtimeRTTI");
121            ignoredClasses.add("org.objectweb.jac.core.rtti.ClassInfo");
122            ignoredClasses.add("org.objectweb.jac.core.rtti.MethodInfo");
123            ignoredClasses.add("org.objectweb.jac.core.rtti.InvokeInfo");
124            // load the properties
125            JacPropLoader.loadProps();
126            logger.info("JacPropLoader = "+
127                      Strings.hex(JacPropLoader.class));
128                            
129            // get the bytecodeModifier string
130            bytecodeModifier = org.objectweb.jac.core.JacPropLoader.bytecodeModifier;
131            logger.info("jacloader: Selected bytecode modifier = '" +
132                      bytecodeModifier +"'");
133          
134            // instanciate the bytecode translator 
135            Class c = Class.forName("org.objectweb.jac.core.translators.WrappeeTranslator_"+
136                                    bytecodeModifier);
137            Class repositoryClass = loadClass("org.objectweb.jac.core.rtti.ClassRepository",true);
138            rtti = (LoadtimeRTTI)
139                repositoryClass.getMethod("get",ExtArrays.emptyClassArray).invoke(null,ExtArrays.emptyObjectArray);
140            wt = (WrappeeTranslator)c.getConstructor(
141                new Class[] {LoadtimeRTTI.class}).newInstance(
142                    new Object[] {rtti});
143          
144            logger.info("Instanciated bytecode modifier is "+wt);
145        }
146    
147    
148        /**
149         * Create a JacLoader. Same as
150         * <code>JacLoader(boolean,boolean,null)</code>.
151         *
152         * @param write if true, caches the tranlated classes on disk
153         * @param clean if true, clears the disk cache
154         */
155        public JacLoader(boolean write,
156                         boolean clean) 
157            throws Exception
158        {
159            this(write, clean, null);
160        }
161    
162    
163        public void setWrappeeTranslator(WrappeeTranslator wt) {
164            this.wt = wt;
165        }
166    
167        private void addIgnoredPkgs(String[] ignoredPackages) {
168            String[] new_p = new String[ignoredPackages.length + 
169                                       this.ignoredPackages.length];
170          
171            System.arraycopy(this.ignoredPackages, 0, new_p, 0, 
172                             this.ignoredPackages.length);
173            System.arraycopy(ignoredPackages, 0, new_p, this.ignoredPackages.length,
174                             ignoredPackages.length);
175            this.ignoredPackages = new_p;
176        }
177    
178        /**
179         * Tells wether to defer loading of a class to the parent ClassLoader
180         * @param classname name of the class
181         * @return true if we should defer the loading of that class
182         */ 
183        public boolean deferClass(String classname) {
184            if (ignoredClasses.contains(classname))
185                return true;
186            for(int i=0; i<ignoredPackages.length; i++) {
187                if (classname.startsWith(ignoredPackages[i])) {
188                    return true;
189                }
190            }
191            return false;
192        }
193    
194        /**
195         * Tells wether a defered class's bytecode should be analyzed
196         * @param classname the name of the class
197         */
198        protected boolean analyzeClass(String classname) {
199            return false;
200        }
201    
202        protected Class loadClass(String class_name, boolean resolve) 
203            throws ClassNotFoundException
204        {
205            logger.debug("loadClass("+class_name+","+resolve+")");
206            Class cl = null;
207    
208            cl = findLoadedClass(class_name);
209            if (cl!=null) {
210                logger.debug("already loaded "+class_name);
211                return cl;
212            }
213            cl = (Class)classes.get(class_name);
214            if (cl==null) {
215                if (deferClass(class_name) && !JacPropLoader.adaptClass(class_name)) {
216                    logger.debug("defer "+class_name);
217                    byte[] bytes = null;
218                    if (analyzeClass(class_name)) {
219                        try {
220                            logger.debug("fill RTTI for "+class_name);                   
221                            bytes = wt.fillClassRTTI(class_name);
222                        } catch (Exception e) {
223                            logger.error("Failed to fill RTTI for "+class_name,e);
224                            // TODO: we should throw another exception
225                            throw new ClassNotFoundException(
226                                "jacloader: cannot find class "+
227                                class_name+" on disk: "+e);
228                        }
229                    }
230                    cl = parentLoader.loadClass(class_name);
231                }
232    
233                if (cl == null) {
234                    byte[] bytes;
235                    String resourcePath = "/" + class_name.replace('.', '/') + ".class";
236                    logger.debug("resourcePath = "+resourcePath);
237                    bytes = loadResource(resourcePath);
238    
239                    if (wt!=null && classIsToBeAdapted(class_name)) {
240                        logger.info("adapting "+class_name);
241                        String baseName = 
242                            Jac.getJacRoot()+"classes_"+bytecodeModifier+"/"+
243                            class_name.replace('.','/');
244                        File cacheFile = new File(baseName+".class");
245                        MessageDigest digest;
246                        boolean useCache = false;
247                        File md5File =  new File(baseName+".md5");
248                        File rttiFile =  new File(baseName+".rtti");
249                        byte[] orig_md5 = null;
250                        try {
251                            digest = MessageDigest.getInstance("MD5");
252                            digest.update(bytes);
253                            orig_md5 = digest.digest();
254                            if (md5File.exists()) {
255                                byte[] stored_md5 = loadFile(md5File);
256                                useCache = Arrays.equals(orig_md5,stored_md5);
257                            }
258                        } catch (NoSuchAlgorithmException e) {
259                            logger.warn("Could not get an MD5 digest: "+e);
260                        }
261                        boolean loaded = false;
262                        if (useCache && cacheFile.exists() && rttiFile.exists()) {
263                            logger.info("loading class from cache "+cacheFile);
264                            bytes = loadFile(cacheFile);
265                            try {
266                                FileInputStream rttiStream = new FileInputStream(rttiFile);
267                                ObjectInputStream objStream = new ObjectInputStream(rttiStream);
268                                ClassInfo classInfo = (ClassInfo)objStream.readObject();
269                                rttiStream.close();
270                                objStream.close();
271                                rtti.setClassInfo(class_name,classInfo);
272                                loaded = true;
273                            } catch (InvalidClassException e) {
274                                // ignored
275                                logger.info("Rtti format must have changed: "+rttiFile);
276                            } catch (Exception e) {
277                                logger.error("Failed to read rtti cache file "+rttiFile+": "+e);
278                            }
279                        }
280                        if (!loaded) {
281                            try {
282                                //bytes = wt.translateClass(defineClass(class_name, bytes, 0, bytes.length));
283                                bytes = wt.translateClass(class_name);
284                            } catch (Exception e) {
285                                logger.error("Failed to translate class "+class_name,e);
286                                throw new ClassNotFoundException(
287                                    "jacloader: cannot find class "+
288                                    class_name+" on disk: "+e);
289                            }
290                            if (bytes!=null && write) {
291                                logger.info("writing "+cacheFile);
292                                try {
293                                    cacheFile.getParentFile().mkdirs();
294                                    FileOutputStream out = new FileOutputStream(cacheFile);
295                                    try {
296                                        out.write(bytes);
297                                    } finally {
298                                        out.close();
299                                    }
300                                } catch (Exception e) {
301                                    logger.warn("Failed to write class file "+cacheFile+": "+e);
302                                }
303                                if (orig_md5!=null) {
304                                    logger.info("writing "+md5File);
305                                    try {
306                                        md5File.getParentFile().mkdirs();
307                                        FileOutputStream out = new FileOutputStream(md5File);
308                                        try {
309                                            out.write(orig_md5);
310                                        } finally {
311                                            out.close();
312                                        }
313                                    } catch (Exception e) {
314                                        logger.warn("Failed to write class file "+md5File+": "+e);
315                                    }
316                                }
317                                try {
318                                    FileOutputStream rttiStream = new FileOutputStream(rttiFile);
319                                    ObjectOutputStream objStream = new ObjectOutputStream(rttiStream);
320                                    objStream.writeObject(rtti.getClassInfo(class_name));
321                                    objStream.flush();
322                                    rttiStream.close();
323                                    objStream.close();
324                                } catch (Exception e) {
325                                    logger.error("Failed to write rtti cache file "+rttiFile+": "+e);
326                                }
327                            }
328                        }
329                    }
330                    logger.debug("defineClass "+class_name);
331                    if (bytes==null) {
332                        logger.debug("defer "+class_name);
333                        try {
334                            if (otherClassLoader!=null)
335                                cl = otherClassLoader.loadClass(class_name);
336                            else
337                                cl = parentLoader.loadClass(class_name);
338                        } catch(Exception e) {
339                            logger.error("Failed to load class "+class_name,e);
340                        }
341                    } else {
342                        try {
343                            cl = defineClass(class_name, bytes, 0, bytes.length);
344                        } catch(Exception e1) {
345                            logger.error("Failed to define class "+class_name,e1);
346                            // we are not authorized to load this class, we defer
347                            logger.debug("defer "+class_name);
348                            bytes = null;
349                            if (analyzeClass(class_name))
350                                try {
351                                    logger.debug("fill RTTI for "+class_name);                   
352                                    bytes = wt.fillClassRTTI(class_name);
353                                } catch (Exception e) {
354                                    logger.error("Failed to fill RTTI for "+class_name,e);
355                                    throw new ClassNotFoundException(
356                                        "jacloader: cannot find class "+
357                                        class_name+" on disk: "+e);
358                                }
359                            cl = parentLoader.loadClass(class_name);
360                        }
361                        //cl = defineClass(class_name, bytes, 0, bytes.length);
362                    }
363                }
364          
365                if (resolve)
366                    resolveClass(cl);
367            }
368    
369            if (cl.getClassLoader()!=null)
370                logger.info(class_name+"@"+Integer.toHexString(cl.hashCode())+
371                            " ["+cl.getClassLoader()+"]");
372    
373            logger.debug("----------");
374            classes.put(class_name, cl);
375    
376            return cl;
377        }
378    
379        public InputStream getResourceAsStream(String name)
380        {
381            loggerRes.debug("getResourceAsStream: "+name);
382    
383            if ((name == null) || (name.length() == 0))
384                return null;
385    
386            InputStream result = null;
387          
388            result = super.getResourceAsStream(name);
389            if ((result == null) && (otherClassLoader != null)) {
390                loggerRes.debug("  resource not found with system classloader, trying with: "+otherClassLoader);
391                result = otherClassLoader.getResourceAsStream(name);
392            }
393            return result;
394        }
395    
396        public URL getResource(String name)
397        {
398            loggerRes.debug("getResource: "+name+" (parent="+getParent()+")");
399    
400            URL result = null;
401    
402            //result = super.getResource(name);
403            if (getParent() != null) {
404                result = getParent().getResource(name);
405            } 
406            if (result == null) {
407                result = findResource(name);
408            }
409    
410            // if not in usual CLASSPATH, search in additionnal CLASSPATH
411            if ((result == null) && (otherClassLoader != null)) {
412                loggerRes.debug("  resource not found with "+getParent()+", trying with: "+otherClassLoader);
413                if (otherClassLoader instanceof URLClassLoader)
414                    loggerRes.debug("  classpath="+Arrays.asList(((URLClassLoader)otherClassLoader).getURLs()));
415                result = otherClassLoader.getResource(name);
416            }
417    
418            return result;
419        }
420    
421        byte[]  loadResource(String resourcePath) throws ClassNotFoundException {
422            InputStream in = null;
423    
424            logger.debug( "loadResource: " + resourcePath);
425    
426            in = super.getClass().getResourceAsStream(resourcePath);
427    
428            // if not in usual CLASSPATH, search in additionnal CLASSPATH
429            if ((in == null) && (otherClassLoader != null))
430            {
431                logger.debug(
432                          "loadResource: searching in otherClassLoader ...");
433                in = otherClassLoader.getResourceAsStream(resourcePath.substring(1));
434            }
435    
436            if (in == null) 
437            {
438                throw new ClassNotFoundException("jacloader: Can not find resource "+resourcePath);
439            }
440            try {
441                return Streams.readStream(in);
442            } catch (IOException e) {
443                throw new ClassNotFoundException(
444                    "jacloader: failed to load resource "+resourcePath+" : "+e);
445            }
446          
447        }
448    
449        byte[]  loadFile(File file) throws ClassNotFoundException {
450            try {
451                InputStream in = new FileInputStream(file);
452                if (in == null) 
453                    throw new ClassNotFoundException("jacloader: Can not find resource "+file);
454                return Streams.readStream(in);
455            } catch (IOException e) {
456                throw new ClassNotFoundException(
457                    "jacloader: failed to load resource "+file+" : "+e);
458            }
459          
460        }
461    
462        /**
463         * Tell wether a class is to be adapted or not. All data
464         * are extracted from the jac.prop files located a various
465         * place.
466         * 
467         *
468         * @see org.objectweb.jac.core.JacPropLoader
469         * @see org.objectweb.jac.core.JacPropTools
470         * @param name a <code>String</code> value
471         * @return a <code>boolean</code> value (true if to be adpated).
472         *
473         */
474        public static boolean classIsToBeAdapted(String name) {
475            //      System.out.println("classIsToBeAdapted "+name+"? "+Strings.hex(JacPropLoader.class));
476            int index = name.lastIndexOf(".");
477            String packagename = "";
478            if (index!=-1)
479                packagename = name.substring(0,index);
480            Iterator it;
481            if (JacPropLoader.adaptClass(name))
482                return true;
483    
484            // do not translate classes that follow
485            if ( name.endsWith(".Run") || 
486                 name.endsWith(".Main") ||
487                 name.endsWith("AC") ||
488                 name.endsWith("Wrapper") ||
489                 (name.indexOf('$') != -1) ||
490                 name.endsWith("Exception") ||
491                 name.startsWith("org.objectweb.jac.core") ||
492                 name.startsWith("org.objectweb.jac.util") ||
493                 name.startsWith("org.objectweb.jac.aspects"))
494                return false;
495          
496            if (JacPropLoader.doNotAdaptClass(name))
497                return false;
498    
499            return true; 
500        }
501    
502        /**
503         * display some information about a class<br>
504         * Only use for debug purpose to verify that the class
505         * has been patched correctly.
506         *
507         * @see java.lang.ClassLoader
508         * @param name the class to display
509         *
510         */
511        public static void displayClassInfo(String name)
512        {
513            System.out.println ("jacloader: Displaying data on class " + name);
514            Class cl = null;
515            try {
516                cl = Class.forName (name);
517            }
518            catch (Exception e)
519            {
520                System.out.println ("\tClass is not available from the loader.");
521                return;
522            }
523            Field[] fl = cl.getDeclaredFields();
524            for (int i=0; i<fl.length; i++)
525            {
526                System.out.println ("\tField "+i+" : name("+
527                                    fl[i].getName()+"), type("+
528                                    fl[i].getType().getName()
529                                    +"), attr("+fl[i].getModifiers()+")");
530            }
531            Method[] fm = cl.getDeclaredMethods();
532            for (int i=0; i<fm.length; i++)
533            {
534                System.out.println ("\tMethod "+i+" ("+
535                                    fm[i].getName()+"), return type is "+
536                                    fm[i].getReturnType().getName());
537                Class[] clexcep = fm[i].getExceptionTypes();
538                for (int j=0; j<clexcep.length;j++)
539                    System.out.println ("\t\tThrow Exception : "+clexcep[j].getName());
540                Class[] clpar = fm[i].getParameterTypes();
541                for (int j=0; j<clpar.length;j++)
542                    System.out.println ("\t\tParameter number "+j+" is a "+clpar[j].getName());     
543            }
544        }
545       
546    
547        /**
548         * Returns true if the class is already loaded.
549         * 
550         * @param classname the class name to test
551         * @return true if already loaded
552         */
553        public boolean isLoaded (String classname) {
554            return (classes.containsKey(classname));
555        }
556            
557        /**
558         * Loads a class and calls <code>main()</code> in that class.
559         * 
560         * <p>This method was extracted "as is" from javassist 1.0 from
561         * Shigeru Chiba.
562         *
563         * <p><a href="www.csg.is.titech.ac.jp/~chiba/javassist/">Javassist
564         * Homepage</a>
565         *
566         * @param classname         the loaded class.
567         * @param args parameters passed to <code>main()</code>.  */
568            
569        public void run(String classname, String[] args) throws Throwable {
570            Class c = this.loadClass(classname);
571            ClassLoader saved = Thread.currentThread().getContextClassLoader();
572            try {
573                Thread.currentThread().setContextClassLoader(this); 
574                c.getDeclaredMethod("main", new Class[] { String[].class })
575                    .invoke(null, new Object[] { args });
576            }
577            catch (java.lang.reflect.InvocationTargetException e) {
578                throw e.getTargetException();
579            }
580            finally {
581                Thread.currentThread().setContextClassLoader(saved); 
582            }
583        }
584    
585        /**
586         * Use JacPropLoader to read some JAC properties
587         * @param props the properties to read
588         */
589        public void readProperties(Properties props) {
590            JacPropLoader.addProps(props);
591        }
592    
593        /**
594         * Usage: java org.objectweb.jac.core.JacLoader [class]
595         */
596        public static void main(String[] args) throws Exception {
597            JacLoader loader = new JacLoader(true,true, otherClassLoader);
598            for (int i=0; i<args.length; i++) {
599                JacPropLoader.packagesToAdapt.add(args[i]);
600                loader.loadClass(args[i]);
601            }
602        }
603    }