Java Forum / GUI / November 2005
Dynamically load laf
Daniel - 28 Nov 2005 07:01 GMT Hello everyone! I am writing an application and I would like to give the user the option of choosing Look and Feel. The look and feels that are part of standard java is not that exciting so I would like to give the user the option of "installing" new look and feels without recompile or anything. My idea was to have a directory say lib/laf where the user would put a the look and feel with the full name as filename say "net.sourceforge.mlf.metouia.MetouiaLookAndFeel.jar" Thus I would add that directory to my classpath and all would be peaches. Well not quite. It seems this is not really working. So I wrote my own classloader trying to solve this problem. But my classLoader does not seem to work. (code included last) so i wonder if anyone has any good ideas on how to solve this. Not neccerily using a custom classloader, any other ideas are welcome.
regards Daniel
ps. I searched for this on both google and deja, but got no good hits, so maybe my keywords were poorly chosen, so if anyone has any websites dealing with this problem i appreciate links.
code
public class LaFClassLoader extends ClassLoader { public LaFClassLoader() { } public Class findClass(String name) throws ClassNotFoundException { File dir= new File("lib/LaF"); if(dir.exists()){ File[] lafs= dir.listFiles(); for(int i=0;i<lafs.length;i++){ if (lafs[i].getName().endsWith(".jar")) { try{ ZipInputStream zin = new ZipInputStream(new FileInputStream(lafs[i])); ZipEntry entry; while ( (entry = zin.getNextEntry()) != null) { if (entry.isDirectory()) { continue; // since this entry is just a directory } String s=entry.getName(); String tname=name.replace('.','/')+".class"; if (entry.getName().equals(tname)) { long size = entry.getSize(); if (size != -1) { byte[] clas = new byte[ (int) size]; zin.read(clas, 0, clas.length); return super.defineClass(name, clas, 0, clas.length); } }
} }catch(Exception e){ throw new ClassNotFoundException(name,e); } } }
}
throw new ClassNotFoundException(name);
}
}
problem I get with this classloader is on the super.defineClass line saying "net/sourceforge/mlf/metouia/MetouiaLookAndFeel" contains invalid UTF-8 character.
NullBock - 28 Nov 2005 12:54 GMT First off, it's almost always a mistake to write your own class loader. URLClassLoader works great for 99.9% of all problems, yours falling into this range. Second, you're definitely going to have issues if you try to dynamically add a L&F jar to the classpath after the event thread has been created, since it will have been loaded by the system class loader, and won't have access to any class loaders added aftewards. Class loaders added after program start can only load classes that aren't already defined in the hierarchy above it, but ancestor class loaders don't check there children to see if they know anything about a class to load. Thus, you could do something like this:
File lnfJar; //.... URLClassLoader cl = new URLClassLoader(new URL[] { lnfJar.toURL() }); Class clazz = cl.loadClass("custom.LnF"); LookAndFeel laf = (LookAndFeel) clazz.newInstance(); UIManager.setLookAndFeel(laf); SwingUtilities.updateComponentTreeUI(frame);
Everything's hunky-dorey up to setLookAndFeel. The UI will attempt to load and instantiate various L&F classes dynamically, *using its classloader*. Since this class loader is parent to the L&F's class loader, the classes cannot be found.
Thus, you need to do one of two things. First, and most stable, would be to insist that the L&F jar be included on the classpath at start-up. You could actually write a script to scan pre-defined directories when starting up your program, adding all jars found therein to the classpath. (This is how Ant works--check out the Ant startup scripts for good examples of how to do this.) Second would be to dynamically add a L&F jar to the system class loader using reflection:
File lnfJar; //.... Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class }); method.setAccessible(true); method.invoke(ClassLoader.getSystemClassLoader(), new Object[] { lnfJar.toURL() }); UIManager.setLookAndFeel("custom.LnF"); SwingUtilities.updateComponentTreeUI(frame);
This works, though isn't guaranteed to be portable.
There are other possibilities, of course, but these are the quickest and dirtiest.
Walter Gildersleeve Freiburg, Germany
______________________________________________________ http://linkfrog.net URL Shortening Free and easy, small and green.
Daniel - 30 Nov 2005 00:11 GMT >First off, it's almost always a mistake to write your own class loader. Yeah, I noticed. I did try to use the URLClassLoader, but I had (I realize after reading your post) given it invalid URLS, so of course it did not work... After feeding it good urls it works.
>try to dynamically add a L&F jar to the classpath after the event >thread has been created, since it will have been loaded by the system I would do all this before the EDT is created. I.e in the constructor of my first class.
I have tried the advice you gave me and so far it seems to be working very nice. I will have to try it a bit more though.
Thank you very much! regards Daniel
Daniel - 30 Nov 2005 01:27 GMT Now that I've tried this a bit more there seems to be a major problem with this solution. I load the Look and feel class properly and I can set the look and feel, but it seems that the classes it depend on is causing problems. What happens is this: I set the laf to one from the "plugin directory", restart the application I get lots of exceptions when I restart the program. They are all very similar and this is one example:
UIDefaults.getUI() failed: no ComponentUI class for: javax.swing.JMenuBar[,0,0,0x0,invalid,alignmentX=null,alignmentY=null,border=,flags=0,maximumSize=,minimumSize=,preferredSize=,margin=,paintBorder=true] java.lang.Error at javax.swing.UIDefaults.getUIError(UIDefaults.java:689) at javax.swing.UIDefaults.getUI(UIDefaults.java:719) at javax.swing.UIManager.getUI(UIManager.java:784) at javax.swing.JMenuBar.updateUI(JMenuBar.java:125) at javax.swing.JMenuBar.<init>(JMenuBar.java:93) at se.wexiodisk.service.MainWindow.<init>(MainWindow.java:35) at se.wexiodisk.service.Diagnosetool.<init>(MyApp.java:142) at se.wexiodisk.service.Diagnosetool.main(MyApp.java:178)
What am I missing? I feel it's obvious but I can't think of it now.
I think I've realized it now. What I need to do is to add the desired jar to the classloaders search path. Unfortunatly this method has protected access on the URLClassLoader so I am a bit at loss how to solve this problem.
The look and feel appear in the list over installed look and feels, but as you can see I can not use it. I get this error for every component in my app..
regards Daniel
Thomas Hawtin - 30 Nov 2005 11:11 GMT > Now that I've tried this a bit more there seems to be a major problem > with this solution. [quoted text clipped - 17 lines] > at se.wexiodisk.service.Diagnosetool.<init>(MyApp.java:142) > at se.wexiodisk.service.Diagnosetool.main(MyApp.java:178) I wrote a short, complete example thingy:
import javax.swing.*;
class LoadPLAF { public static void main(String[] args) { java.awt.EventQueue.invokeLater(new Runnable() { public void run() { swing(); }}); } private static void swing() { try { ClassLoader loader = new java.net.URLClassLoader( new java.net.URL[] { new java.net.URL( new java.io.File("").toURI().toURL(), "lib/napkinlaf.jar" ) } ); Class<?> clazz = Class.forName( "napkin.NapkinLookAndFeel", true, loader ); java.lang.reflect.Constructor constructor = clazz.getConstructor(); LookAndFeel plaf = (LookAndFeel)constructor.newInstance(); UIManager.getDefaults().put("ClassLoader", loader); UIManager.setLookAndFeel(plaf); JFrame frame = new JFrame("LoadPLAF"); frame.add(new JLabel("Hello mum.")); frame.setSize(100, 50); frame.setVisible(true); } catch (Throwable exc) { exc.printStackTrace(); } } }
> What am I missing? I feel it's obvious but I can't think of it now. The magic ingredient is the line:
UIManager.getDefaults().put("ClassLoader", loader);
That doesn't appear to be documented anywhere. What a surprise. The UIDefaults documentation is just odd.
> I think I've realized it now. > What I need to do is to add the desired jar to the classloaders search > path. Unfortunatly this method has protected access on the > URLClassLoader so I am a bit at loss how to solve this problem. It doesn't really make sense to.
If a ClassLoader can't find a class, it shouldn't check ever again. You need to replace the class loader, rather than attempting to mutate it.
Tom Hawtin
 Signature Unemployed English Java programmer http://jroller.com/page/tackline/
Daniel - 30 Nov 2005 23:22 GMT >I wrote a short, complete example thingy: Thank you! That little pice of code set me straight! Now it works nicely!
>The magic ingredient is the line: > > UIManager.getDefaults().put("ClassLoader", loader); I must say I have been wondering about that, because I figured that the "default classloader" was the wrong one. But I couldn't find anything to help me fix that problem.
>If a ClassLoader can't find a class, it shouldn't check ever again. You >need to replace the class loader, rather than attempting to mutate it. I'm not sure i actually agree with you here. As the classloader does not try to find the class before I mutate it. Since I know it where it will find it I tell it where, rather than creating a whole new one. Are you saying it is desirable to replace it rather than mutate it?
again thanks for you advice!
regards Daniel
NullBock - 28 Nov 2005 13:38 GMT I seem to have spoken too soon re: Ant's handling of its libraries. As of 1.6, dynamic jar discovery seems to take place in Java, not in the startup scripts. Here's the interesting startup script bits from 1.5, though:
::ant.bat [snippet] for %%i in ("%ANT_HOME%\lib\*.jar") do call "%ANT_HOME%\bin\lcp.bat" %%i
::lcp.bat [complete] set _CLASSPATHCOMPONENT=%1 if ""%1""=="""" goto gotAllArgs shift
:argCheck if ""%1""=="""" goto gotAllArgs set _CLASSPATHCOMPONENT=%_CLASSPATHCOMPONENT% %1 shift goto argCheck
:gotAllArgs set LOCALCLASSPATH=%_CLASSPATHCOMPONENT%;%LOCALCLASSPATH%
Hope this helps,
Walter Gildersleeve Freiburg, Germany
______________________________________________________ http://linkfrog.net URL Shortening Free and easy, small and green.
Roedy Green - 28 Nov 2005 16:14 GMT On Mon, 28 Nov 2005 07:01:49 GMT, Daniel <daik.no-spam@mds.nospam.mdh.se> wrote, quoted or indirectly quoted someone who said :
>Thus I would add that directory to my classpath and all would be >peaches. Well not quite. It seems this is not really working. So I >wrote my own classloader trying to solve this problem. But my >classLoader does not seem to work. You only need to resort to a classloader when you don't know at jar-create time where the classes will becoming from.
I think you will find it easier to use the internal jar Class-Path entry to point at a directory or jars where your lafs live.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Java custom programming, consulting and coaching.
Thomas Hawtin - 30 Nov 2005 01:23 GMT > Thus I would add that directory to my classpath and all would be > peaches. Well not quite. It seems this is not really working. You realise you have to put the jars file names in the classpath, not just the directory they are in. I seem to remember a feature in 1.6 for allowing specifying the directory only, but don't quote me on that.
> So I > wrote my own classloader trying to solve this problem. But my > classLoader does not seem to work. (code included last) so i wonder if > anyone has any good ideas on how to solve this. Not neccerily using a > custom classloader, any other ideas are welcome. URLClassLoader should do just as well. I'm not certain what your problem with the classpath was, but it is probably worthwhile sorting that one out first.
Perhaps it might help if we saw the code of how you are attempting to use the class loader to install the PL&F.
> public Class findClass(String name) throws ClassNotFoundException { > File dir= new File("lib/LaF"); [quoted text clipped - 7 lines] > ZipEntry entry; > while ( (entry = zin.getNextEntry()) != null) { You want to do all this for every class loaded? For one thing, you are not closing your input streams, even in the normal case. Using a ZipFile would probably be better, too.
Tom Hawtin
 Signature Unemployed English Java programmer http://jroller.com/page/tackline/
Daniel - 30 Nov 2005 01:34 GMT >You realise you have to put the jars file names in the classpath, not >just the directory they are in. I seem to remember a feature in 1.6 for >allowing specifying the directory only, but don't quote me on that. yes, I have realized that, and that is basically the root of the problem here.
>URLClassLoader should do just as well. I'm not certain what your problem >with the classpath was, but it is probably worthwhile sorting that one >out first. yeah, I have realized that, and scrapped my own classloader.
>Perhaps it might help if we saw the code of how you are attempting to >use the class loader to install the PL&F. Currently I am doing this:
if(dir.exists()){ ClassLoader loader= ClassLoader.getSystemClassLoader(); java.net.URLClassLoader ul=null; if(loader instanceof java.net.URLClassLoader){ ul=(java.net.URLClassLoader)loader; }
File[] lafs= dir.listFiles(); int jarnum=0; for(int i=0;i<lafs.length;i++){ if (lafs[i].getName().endsWith(".jar")) { jarnum++; } } java.net.URL[] url= new java.net.URL[jarnum]; int j=0; for(int i=0;i<lafs.length;i++){ if(lafs[i].getName().endsWith(".jar")){ int ind=lafs[i].getName().lastIndexOf(".jar"); url[j]= lafs[i].toURL(); ul= new java.net.URLClassLoader(url); j++; Class c=ul.loadClass(lafs[i].getName().substring(0,ind));
UIManager.setLookAndFeel((javax.swing.LookAndFeel)c.newInstance()); } }
from what I understand the line ul= new URL.... is actually quite stupid, since I basically just change the local variable and not the instance being used at other places. Ideally I would just add the new url to the existing one, but as I said it is not possible. Some of this code is not very good, but I will clean it up and make it smarter and better when I understand what I am doing :)
>You want to do all this for every class loaded? For one thing, you are >not closing your input streams, even in the normal case. Using a ZipFile >would probably be better, too. actually this is pretty much a work in progress, once I got it working I would clean it up, and look at things like what you mention here. The same with the code pasted above. I want to solve the problem first, only then can I begin to optemize away things not needed, or done in a stupid way.
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|