Link to home
Start Free TrialLog in
Avatar of dirku
dirkuFlag for Germany

asked on

Modify CLASSPATH on-the-fly / dynamically

I have an application which inspects JAR files and performs some action afterwards. The JAR file needs to be choosen via a JFileChoser dialog. How can I add this choosen JAR file to the CLASSPATH automatically after it has been chosen?

I tried to use
Properties props = System.getProperties();
String cp = props.getProperty("java.class.path");
...
System.setProperties(props);

but the CLASSPATH doesn't seem to be changed although using System.out.println( System.getProperties("java.class.path")); shows, that the JAR has been appended to the CLASSPATH (at least to System.properties). Do I need to explicitely flush the Proeprties to be set effectively in th eenvironment?

Thank you,
Dirk
Avatar of hoomanv
hoomanv
Flag of Canada image

The class-path is picked up bye the JVM when it starts so even if you modify the system property, I don't think you can actually tell the JVM (once it has started) to recognize a new class-path (it could also be dangerous as you might remove some of the JARs from the class-path which are already being used).
Avatar of dirku

ASKER

Well, mayankeagle, you're right. However, how else can I make my application being aware that there is a (new) JAR file to be used in the further run?
My application choses the JAR file at runtime (and there are a lot of JAR files which could be chosed). Thus, I don't want these "thousands" of JAR files to be put in the CLASSPATH before starting the application.
So I want the application to use the chosed JAR file on later actions and the application needs to know about this JAR file to access the packed classes in this JAR file.

Dirk
Avatar of dirku

ASKER

Hello, hoomanv.

I tried to use the code but it has no effect. The idea is great but I couldn't make it work until now. ...but I'm trying, hoping it's a mistake of mine.
ASKER CERTIFIED SOLUTION
Avatar of Webstorm
Webstorm

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of dirku

ASKER

I used the file obtained from the JFileChoser via File file = fc.getSelectedFile(); when calling ClassLoader urlCl = URLClassLoader.newInstance(new URL[]{file.toURL()}, prevCl);
The selected file is a JAR file in my case it is: D:\tmp\TTK\PRN_Printer.jar

The classes Vector of urlCl has no content and thus, the subsequent calls fail.
It had no content? What did you use to verify that? Did you print it?
Avatar of dirku

ASKER

Running it from within Eclipse it seems to work but starting the application with a batch file results in a ClassNotFoundException for a class from within the just selected JAR file.
Eclipse uses a different class-path than the system class-path.
Avatar of dirku

ASKER

It seems so. When Eclipse can do it should be possible to do this either. But: HOW? Of course, the application should run as a stand-alon-app in the end and not within Eclipse. ;-)
Avatar of Webstorm
Webstorm

At command prompt, try :

     jar -tf <thejarfile>

to check that class is in the jar file.

You can also try to directly load the class using the classloader :

ClassLoader urlCl =
    URLClassLoader.newInstance(new URL[]{file.toURL()}, prevCl);
Class cl=urlCl.loadClass("<classname>");
You have to add classpath used by eclipse to your batch file.

see https://parse-dot-classpath.dev.java.net/
Avatar of dirku

ASKER

...but is the class available in another class than in the class which loads it?
I mean, I have a class which creates the UI in which I can invoke a JFileChoser dialog. I chose a file and (that's what I intended to do) the selected JAR file is added to the existing CLASSPATH.

The JAR file contains some classes (in particular junit testcases) as well as an XML file (a configuration file) which I need later.

In a different class the XML file needs to be get from th eJAR file. In my testcase (which in turn is in the same JAR file) I use the path to this XML file (String) as a parameter for a method call. This called method does not know anything from the existence of the JAR file and performs the following with the path to the XML file:

URL url = this.getClass().getResource(configFile);
System.out.println(url == null ? "url == null" : url.toString());
assertTrue(deviceManager.start(url.openStream()));

Hence, the class extending the CLASSPATH is not the class using the extended CLASSPATH. So loading the class directly does not work, does it?
>> ...but is the class available in another class than in the class which loads it?
yes, if you called addClassesFrom before

But calling   Thread.currentThread().setContextClassLoader(urlCl);
only work for the current thread, and threads created from that thread.

I think you have to explicitly call  urlCl.loadClass("<classname>");  and use reflection to create new instance of the class.

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/package-frame.html
http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Class.html

because eclipse compile your application with Jarfile in the classpath (hardcoding direct class access), and you try to run it without the Jarfile in the classpath, which generates error because JVM try to load a class it cannot found, before your class can do anything.
Avatar of dirku

ASKER

Here's what I coded:

protected JButton createJarButton() {
      final JButton jarChoose= new JButton("...");
      jarChoose.addActionListener(
            new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                        JFileChooser fc = new JFileChooser();
                        fc.setCurrentDirectory(new File("."));
                        fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
                        fc.setFileFilter(new DBSJarFileFilter());
                        int returnVal = fc.showOpenDialog(jarChoose.getParent());
                        if (returnVal == JFileChooser.APPROVE_OPTION) {
                                  File file = fc.getSelectedFile();
                                  try
                                  {
                                        //jFile = new JarFile(file.getName());
                                        jFile = new JarFile(file.getAbsolutePath());
                                        browseSectionButton.setEnabled(true);
                                        if (fFrame.getTitle().indexOf(file.getName()) < 0)
                                        {
                                              fFrame.setTitle(fFrame.getTitle() + " - " + file.getName());
                                        }
                                        jarFileText.setText(file.getName());
                                        
                                        StringTokenizer st = new StringTokenizer(cp, ";");
                                        boolean isInCp = false;
                                        while ( st.hasMoreTokens() )
                                        {
                                              String cpEntry = st.nextToken();
                //System.out.println(">>> " + cpEntry);
                                              if ( cpEntry.equalsIgnoreCase(file.getName()) )
                                              {
                                                    isInCp = true;
                                                    break;
                                              }
                                        }//end while
                                        
                                        if ( isInCp == false )
                                        {
                                              String newPath = //"." +
                                                    //System.getProperty("file.separator") +
                                                    //file.getName();
                                                    file.getAbsolutePath();
                                              System.out.println("Selected file: " + file.getAbsolutePath());
                                              ClassLoader prevCl = Thread.currentThread().getContextClassLoader();

                                               //Create the class loader by using the given URL
                                               //Use prevCl as parent to maintain current visibility
                                              ClassLoader urlCl =
                                                  URLClassLoader.newInstance(new URL[]{file.toURL()}, prevCl);
                                        Thread.currentThread().setContextClassLoader(urlCl);
                                        }//end if ( isInCp == false )
                                  } catch (IOException ioEx)
                                  {
                                        ioEx.printStackTrace();
                                  }
                              }
                  }
            }
      );
      return jarChoose;
}
Avatar of dirku

ASKER

I removed to much statements. The following to line of code are coded right befor StringTokenizer instantiation:
Properties props = System.getProperties();
String cp = props.getProperty("java.class.path");
Can you post the code where you use classes from the selected jar file ?
I think you'll have to modify it to use reflection as I explained above.
Avatar of dirku

ASKER

I have extended the functionality of JUnit classes by subclassing to have more flexibility in chosing the testcases or even test methods.

Here's the class which is for inspecting the JAR file to find TestCases to setup the GUI which means to select the directories in which useful TestCases are located:

package junit.runner;

import java.io.File;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import de.bahn.ae.tests.AbstractDBSTestCase;


public class DBSJarInspectTestSectionCollector extends SimpleTestCollector
{
      private JarFile jarFile = null;
      //=========================================================================
      
      /**
       *
       * @param jFile
       */
      public DBSJarInspectTestSectionCollector(JarFile jFile)
      {
            jarFile = jFile;
      }//end constructor
      //-------------------------------------------------------------------------

      /**
       * Sammelt die Eintr&auml;ge f&uuml;r den {@link DBSTestSectionSelector}
       * zusammen.
       *
       * @return      Eine Aufz&auml;hlung der gesammelten Elemente.
       */
      public Enumeration collectTests()
      {
            Hashtable result = new Hashtable();
            Enumeration jFileEnum = jarFile.entries();

            while (jFileEnum.hasMoreElements())
            {
                  JarEntry jEntry = (JarEntry)jFileEnum.nextElement();

                  if (!jEntry.getName().endsWith(".class"))
                  {
                        continue;
                  }

                  File f = new File(jEntry.getName());
                  if (this.isTestClass(f.getName()))
                  {
//System.err.println(jEntry.getName());
                        String fName = jEntry.getName().replaceAll("/", ".")
                                    .substring(0, jEntry.getName().replaceAll("/", ".")
                                                .lastIndexOf("."));
//System.err.println(fName);
                        Class cl = null;

                        try
                        {
                              cl = Class.forName(fName);
                        } catch (ClassNotFoundException e1)
                        {
                              e1.printStackTrace();
                        }
                        
                        if (cl != null)
                        {
                              Class superClass = cl;
                              while((superClass = superClass.getSuperclass()) != null )
                              {
                                    if (superClass == AbstractDBSTestCase.class)
                                    {
                                          // Auf einem File Objekt zu arbeiten funktioniert
                                          // nicht, da das JarEntry lediglich im JarFile
                                          // vorliegt, aber nicht als Datei im Dateisystem!
                                          
                                          // eintrag des JarFiles vorbereiten...
                                          String s = jEntry.getName().replaceAll("/", "\\\\");
                                          String parentEntry = s.substring(0, s.lastIndexOf("\\"));
                                          
                                          // Sektionsnamen und kompletten Pfad zum Parent
                                          // der Sektion ermitteln.
                                          int dirIdx = parentEntry.lastIndexOf("\\");
                                          String grandParentEntry = parentEntry.substring(dirIdx+1, parentEntry.length());
                                          if ( grandParentEntry.startsWith("section_") && 
                                                 result.containsKey(grandParentEntry) == false )
                                          {
                                                result.put(grandParentEntry, parentEntry); //section, pfad
                                                break;
                                          }
                                    }//end if (superClass == AbstractDBSTestCase.class)
                              }//end while
                        }//end if (cl != null)

                  }//end if (this.isTestClass(f.getName()))
            }//end while (jFileEnum.hasMoreElements())
            return result.elements();
      }//end collectTests
      //-------------------------------------------------------------------------
      
      /**
       * Korrigiert einen Fehler aus {@link SimpleTestCollector#isTestClass}.
       * dort wird geprüft auf <code>classFileName.indexOf("Test") > 0</code>.
       * <code>String.indexOf(String)</code> liefert aber <b>-1</b>, wenn der
       * gesuchte String nicht gefunden werden konnte. Wird der gesuchte String
       * gleich am Anfang gefunden, wird <b>0</b> zur&uuml;ck geliefert.
       * Die Implementierung aus SimpleTestCollector liefert also immer
       * <code>FALSE</code>, wenn die Testmethode mit "Test" beginnt.  
       */
      protected boolean isTestClass(String classFileName) {
            /*
            boolean res = classFileName.endsWith(".class");
            res = classFileName.indexOf('$') < 0;
            res = classFileName.indexOf("Test") >= 0;
            return res;
            */
            return
                  classFileName.endsWith(".class") && 
                  classFileName.indexOf('$') < 0 &&
                  classFileName.indexOf("Test") >= 0;
      }//end isTestClass
      //-------------------------------------------------------------------------
}

At the command prompt I get this excerpt of hte StackTrace:
Selected file: D:\tmp\TTK\PRN_Printer.jar
java.lang.ClassNotFoundException: de.bahn.ae.tests.spi.sut.PRN_MT2Printer.section_004_Print.TestCase_001_Print

        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClassInternal(Unknown Source)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Unknown Source)
        at junit.runner.DBSJarInspectTestSectionCollector.collectTests(DBSJarInspectTestSectionCollector.java: 59)

which is infact: cl = Class.forName(fName);

So, the JAR file is given into DBSJarInspectTestSectionCollector properly but when I try to instantiate a class using Class.forName it fails.

Here's the constructor of one TestCase I intend to use:

public TestCase_001_Print(String testName)
{
      super(testName);
      configFile =
//"\\de\\bahn\\ae\\tests\\spi\\sut\\PRN_MT2Printer\\section_004_Print\\ServiceProviderConfig.xml";
"/de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/ServiceProviderConfig.xml";
//"de.bahn.ae.tests.spi.sut.PRN_MT2Printer.section_004_Print.ServiceProviderConfig.xml";
}//end constructor
//-------------------------------------------------------------------------
Replace:
    cl = Class.forName(fName);
by:
    cl=urlCl.loadClass(fName);
    // so you need to pass urlCl to your DBSJarInspectTestSectionCollector class
Avatar of dirku

ASKER

Well, although not very nice: it works. At least for setting up the GUI.
However, when it's time to let the TestCases run there is some strange behaviour. I have a JAR file for testing printers and I have a JAR file for testing cardreaders.
The TestCases derive from a class called SpiTestCase which in turn derives form AbstractDBSTestCase which finally derives from JUnit's TestCase class.

In the setUp() (you remember, this method is called for each test method of a testcase) method from SpiTestCase a so called DeviceManger is started:

protected void startSPs(String configFile)
      {
            try
            {
            System.out.println("startSPs(" + configFile + ")");
                  URL url = this.getClass().getResource(configFile);
            System.out.println("url == " + (url == null ? "null" : url.toString()));
            if (url == null) return;
                  long timeVorher = System.currentTimeMillis();
                  assertTrue(deviceManager.start(url.openStream()));
                  //assertTrue(deviceManager.start(configFile));
                  long timeElapsed = System.currentTimeMillis()-timeVorher;
                  assertTrue("Startvorgang des DeviceManagers dauert länger als 2500ms.",
                              (timeElapsed >= 0) && (timeElapsed <= 2500));
            }
            catch (SPIParameterException e)
            {
                  if (e.getParameterName().equalsIgnoreCase("File"))
                  {
                        System.err.println(e.getMessage());
                        assertTrue("Falscher Parameter: " + e.getParameterName() +
                                    " = " + configFile, false);
                  }
                  else
                  {
                        assertTrue("Falscher Parameter: " + e.getParameterName(), false);
                  }
                  System.err.println(e.toString());
            }
            catch (SPIServiceProviderAlreadyStartedException e)
            {
                  System.err.println(e.getMessage());
                  //deviceManager.reset();
                  assertTrue("ServiceProvider sind bereits gestartet.", false);
            } catch (IOException e)
            {
                  // TODO Auto-generated catch block
                  System.err.println("Konnte Datei " + configFile + " nicht lesen.");
                  e.printStackTrace();
            }
      }//end startSPs


When executing: URL url = this.getClass().getResource(configFile); the URL for a cardreader is null. Executing it for a printer I get a valid URL to work upon.

The JAR files are identicall except for the TestCase class. Especially the configuration file which is used in the statement URL url = this.getClass().getResource(configFile); has the same content for both, the cardreader and the pritner.
I have created the JAR files for printer and cardreader the same way using Eclipse.
How can it be that the XML/configuration file can be found using the PRN_printer.jar but not when using CRD_Cardreader.jar although the structur of both JARs is identical?
Avatar of dirku

ASKER

BTW:
Here's the output of the first two System.out.println() statements of startSPs(configFile):

1. for CRD_Cardreader.jar
startSPs(/de/bahn/ae/tests/spi/sut/CRD_MT2Cardreader/section_001_Cardhandling/ServiceProviderConfig.xml)
url == null

2. for PRN_Printer.jar
startSPs(/de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/ServiceProviderConfig.xml)
url == jar:file:/D:/tmp/TTK/PRN_Printer.jar!/de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/Service
ProviderConfig.xml
1.  /de/bahn/ae/tests/spi/sut/CRD_MT2Cardreader/section_001_Cardhandling/ServiceProviderConfig.xml
    Check this file is located in the jar you loaded.

As you load 2 jars, you can create 2 class loader, ->  urlCl1 urlCl2
or create only one (URLClassLoader accepting an array of URL) -> urlCl

In both cases, you have to use the class loader
    urlCl.getResource(configFile);
instead of
   this.getClass().getResource(configFile);
Avatar of dirku

ASKER

This behaviour occurs even if I solely try to load CRD_Cardreader.jar!!!
ServiceProviderConfig.xml is located in the JAR I loaded.

Using urlCl.getResource(configFile) would be difficult to do since I have no chance to priovide the TestCase with urlCl because:
DBSTestRunner e.g. invokes JarInspectTestSectionCollector. This works fine giving urlCl as parameter.
Finally, however, JUnit is used to run the TestCases which is not in my hands. Thus, I don't have a chance to provide the TestCase with urlCl, do I?

It's weird that the PRN_Printer.jar works fine but CRD_Cardreader does not although they are identical in its structure.
Running the app with PRN_Printer.jar succeeds every time even running after CRD_Cardreader.jar has failed already.
Running the app with CRD_Cardreader.jar fails every time even if CRD_Cardreader.jar is the only JAR file to load.

Here's the structure of CRD_Cardreader:
META-INF/MANIFEST.MF    
de/                      
de/bahn/                
de/bahn/ae/              
de/bahn/ae/tests/        
de/bahn/ae/tests/spi/    
de/bahn/ae/tests/spi/sut/  
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/  
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/section_001_Cardhandling/  
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/section_001_Cardhandling/TestCase_001_CardhandlingContactlessCardreader.class  
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/section_001_Cardhandling/TestCase_002_Cardhandling.class  
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/section_001_Cardhandling/ServiceProviderConfig.xml  
de/bahn/ae/tests/SpiTestCase.class

Here's the structure of PRN_Printer.jar:
META-INF/MANIFEST.MF    
de/                      
de/bahn/                
de/bahn/ae/              
de/bahn/ae/tests/        
de/bahn/ae/tests/spi/    
de/bahn/ae/tests/spi/sut/  
de/bahn/ae/tests/spi/sut/PRN_MT2Printer/  
de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/  
de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/TestCase_001_Print.class  
de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/TestCase_002_PrintLine.class  
de/bahn/ae/tests/spi/sut/PRN_MT2Printer/section_004_Print/ServiceProviderConfig.xml  
de/bahn/ae/tests/SpiTestCase.class

You can create one URLClassLoader :

    // file1 : new File("CRD_Cardreader.jar");
    // file2 : new File("PRN_Printer.jar");
    urlCl=URLClassLoader.newInstance(new URL[]{  file1.toURL() , file2.toURL() }, prevCl);
and use urlCl to load the class

or create a different class loader for each jar file :
    urlCl1=URLClassLoader.newInstance(new URL[]{  file1.toURL() }, prevCl);
    urlCl2=URLClassLoader.newInstance(new URL[]{  file2.toURL() }, prevCl);
and use the right loader to load the class

you can also chain the 2 classloaders :
    urlCl=URLClassLoader.newInstance(new URL[]{  file1.toURL() }, prevCl);
    urlCl=URLClassLoader.newInstance(new URL[]{  file2.toURL() }, urlCl); // urlCl instead of prevCl
in this case you'll make a function like this one :

    ClassLoader prevCl=null;
    ClassLoader addJarFile(File f)
    {
           return prevCl=URLClassLoader.newInstance(new URL[]{  file.toURL() },
              (prevCl==null)? Thread.currentThread().getContextClassLoader()  :prevCl  );
    }
Avatar of dirku

ASKER

OK. But I don't that this is the point.
The prior soultion works fine (maybe it's prettier to implement the way you suggested with your last posting and so I did) but the fact is that the ServiceProviderConfig.xml cannot be found within the JAR file.

I printed out which JAR the URLClassLoader has 'loaded' after chosing in the JFileChoser and the CRD_Cardreader.jar has been loaded.
The question is now:
Why does my cardreader testcase cannot find the XML file within the JAR file although the printer testcase can do? (Looking for this XML file is performed in a method of the super class so both testcases call the same method.)

BTW: I increased point from 50 to 75 since you helped me so much so far.
I found the error :

You're looking for a config file in
de/bahn/ae/tests/spi/sut/CRD_MT2Cardreader/

But in Jar it is
de/bahn/ae/tests/spi/sut/CRD_Mt2Cardreader/

Difference is letters 'T' and 't'  in  CRD_MT2Cardreader
Avatar of dirku

ASKER

Oh no!!!!

I knew it must have been something really obvious. It was TOO trivial!! :-)

Thanks a lot, Webstorm. Next thing I will do is looking for an optivian (hoping I will find any as blind as I am). :-))

However, I learned a lot about class loading which I will keep in mind for future tasks.

Best regards,
Dirk