Sunday, May 6, 2007

Loading files with RCP

Keeping my efforts to post about interesting things instead of just complaining most of the time like I use to do, today, I will write about how discover the file path of certain files from within an RCP application.

First of all, the problem of loading a file in that situation must be explained. It sounds a bit stupid to explain something if it seams there is no problem to solve, right?
So, loading a file when you are coding an RCP plug-in is a problem because you can never know where your application will be relatively to the plug-in. You could hack some way out of this by assuming some things about your platform since eRCP, by default, generates always the same structure. But the fact is that this could be broken by just a few minor changes on your platform. So if we want to solve the problem correctly, we have to rely on the main application to load things correctly since it knows exactly where it is and where the plugin is.

Now that the problem should be clear, let's understand what is eRCP solution to it. The first simple solution presented is implemented in the default Activator class eclipse generates for each plugin if required. This class contains a static method findImageDescriptor that loads an image with a relative path. This is very useful since most plug-ins have some kind of image that they wish to load (icons, images, etc...) but it obviously does not solve the problem of loading any file. This operation is a bit trickier because it is pretty well decoupled so that people can load things at the level they desire.

Before version 3.2, eclipse (and eRCP in general) suggested to use the method "find" implemented in the Activator class reachable from Activator.getDefault(). Since version 3.2, this has changed and that old method is now deprecated. I don't know the reason for sure but I would guess that they found out that the Activator class shouldn't contain knowledge about how to load a file. The fact is that the static class FileLocator was created to encapsulate that knowledge.

Now that class is quite simple to use and it is probably not a problem to find out how to use the FileLocator.find(Bundle, IPath, Map) method. Most of the time, you will get the bundle from you Activator class (getDefault().getBundle()) and the Map will be empty because you probably want to load a file that has no dynamic info (otherwise, check the javadoc to know how to use it). Most commons uses of this method will just instantiate a new Path with a String argument that is the path to the file relative to the plug-in's root folder. This will return you an URL instance containing the info about your bundle and the file's path. If you check it, you will notice it is not a normal path but something like "bundleentry://bundle_number/path_to_your_file". As you can guess, there is no way you can use that to instantiate a File or a FileInputStream to read it.

And that is the hard part: You will find two static methods in FileLocator that can help you:
FileLocator.resolve(URL)
and
FileLocator.toFileUrl(URL).

In most of the cases, both method will return the same correct path. But what is the difference about them? I am not sure so please do not take this info for the truth before looking for more information. From my viewpoint, the difference regards only the path you will get at the end. The first method will return URL with http, ftp, jar, etc.. protocols while the second one will only return file protocols which is, in most cases, what you desire. Having tried to explain all this, here is the code you will get to load a file from your RCP plug-in:


Bundle bundle = Activator.getDefault().getBundle();
Path path = new Path("path/to/your/file.extension"); //$NON-NLS-1$
URL url = FileLocator.find(bundle, path, Collections.EMPTY_MAP);
URL fileUrl = null;
try {
fileUrl = FileLocator.toFileURL(url);
}
catch (IOException e) {
// Will happen if the file cannot be read for some reason
e.printStackTrace();
}
File file = new File(fileUrl.getPath());


I hope this helped you guys and please feel free to ask any question if I wasn't clear on something. See you guys later.

12 comments:

Anonymous said...

Thanks a lot for this! It was exactly what I was looking for. Perhaps one should note, that the class "Activator" stands for your plugin class and could be called differently. Or is it a requirement to call it Activator? (I'm new to Plugins)

Hugo "NighT" Corbucci said...

Hi.
The "Activator" class doesn't need to be called Activator but it must be registered has your activator class in your Manifest.mf file. The following line specifies the class to be used as an Activator (it must extend AbstractUIPlugin):
Bundle-Activator: com.project.YourActivator

You are very welcome. :)
Friendly,
Hugo

Anonymous said...

Hi, Hugo,

Thank you very much for the poster! I have worked on this problem for days without luck. I greatly appreciate your help in advance!

I have tried two approaches:

First approach, without manually implementing Activator class:
In this approach, I didn't implement Activator class. Instead, I use "import org.eclipse.core.internal.runtime.Activator;"
I must specify the plug-in name in "getBundle()", like "Activator.getDefault.getBundle("myRCP-plugin-name").
It works fine when I run it as an application or product under Eclipse. However, it won't work after I export it to RCP! I guess the plug-in name will change after exporting to RCP? Anyway, I don't know how to fix it.

Second approach, implementing Activator class as in your poster:
The following is the source code:

import org.eclipse.ui.plugin.AbstractUIPlugin;
public class Activator extends AbstractUIPlugin {
private static Activator instance;
public Activator(){
super();
instance = this;
}
public static Activator getInstance() {return instance;}

I add this class to Bundle-Activator in MANIFEST.ME file and using the following code to invoke it:

Activator activator = new Activator();
Activator instance = activator.getInstance();
Bundle bundle = instance.getBundle();
URL url = FileLocator.find(instance.getBundle(), new Path("test.txt"),null);

(it won't work with or without the first line to initialize Activator; "test.txt" is a file under the root directory of the plug-in)
I can't use getDefault method, like "instance.getDefault().getBundle()". I can only call "getBundle()" on the Activator "instance".
However, this approach won't work either running under Eclipse or after exporting to RCP!

I use Eclipse 3.3.1.1 with the Eclipse Delta pack. I have really no idea what's going on here! RCP is totally different with common java project. Any help/hint will be greatly appreciated!

Sam

Anonymous said...

Hi, Hugo,

I forgot to mention that the error of second approach is "NullPointerException", occured on "instance.getBundle()". According to the following web page:
http://dev.eclipse.org/newslists/news.eclipse.platform.rcp/msg25802.html

It seems that I should call Plugin#start(BundleContext) before calling getBundle. However, the only way to get BundleContext is through calling getBundle as "BundleContext context = instance.getBundle().getBundleContext();" Do you happen to know how I can get BundleContext without calling getBundle? then I can try this approach.

Thanks!
Sam

Hugo "NighT" Corbucci said...

Hello Sam,
Sorry it took me so long to answer. Your question looked complicated and required time to answer (which I hadn't).
On your first attempt, I have a question: are you deploying your plugin as a jar file? If so, you might have some other troubles loading the file. Try deploying your plugin as directory.
Running from within Eclipse is really different from the normal deployed environment mainly because Eclipse usually runs your application pointing the working directory to the root of your project (which is not true when running on normal mode).

Regarding your second approach.
You shouldn't instantiate a new Activator. RCP instantiated it when loading your plugin. That's why I suggested using getDefault() or getInstance(). If you instantiate your own Activator, it won't receive the required information which is why you are receiving a NullPointerException on the Bundle. You should NOT call your constructor. Your code is good as long as you skip the first line:
Activator instance = activator.getInstance();
Bundle bundle = instance.getBundle();
URL url = FileLocator.find(bundle, new Path("test.txt"),null);

I know this may not help you anymore since so much time passed but it might be useful to other people. Good luck.

sh2x said...

It's great solution. Good source. Thanks!

Anonymous said...

Exactly what I was looking for. Clear and concise. Thanks!

Anonymous said...

Thank you Hugo!! You've saved my life!!

Anonymous said...

Just wanted to let you know this post is still helping people. This was the most useful explanation I found! Thanks for posting!

Unknown said...

big thx from me too. some things that are pretty easy in plain java aren't that trivial anymore when it comes to rcp and plugin-development. anyways, those few lines probably saved me hours of frustration. i like! ;-)

Anonymous said...

Thanks man, is really help!!!!

Campa said...

Thanks dude.
@seeAlso http://blog.vogella.com/2010/07/06/reading-resources-from-plugin/