Thursday, March 20, 2008

Platform scheme URI

Wow... It has been more than six months since the last time I took a few minutes to write something here. Things have been a bit crazy lately. A lot has kept me occupied, like, for example, writing the 2nd version of the EMF book (which is "already" available as a rough cut), working on a few bugzillas, and supporting the EMF community. On a more personal note, in October we moved to our new house right when our son was entering the "terrible twos" days. Actually, since then, my wife and I have been working as maniacs to finish the basement and prepare the kids' rooms for the arrival of the new member of the Paternostro family, who is supposed to be here by the end of April.

But I am sure you are not reading this to hear about what I've been doing ;-) So, let's move on to the subject of this post. Quite frequently people ask me how to "locate" files in Eclipse. In 80% of the cases, the conversation is about things like IFile and IWorkspace. Sometimes it digresses to files available inside bundles and, rarely, it involves the "state location" of a bundle (see the Plugin.getStateLocation() javadoc for more details). Obviously you can use IResources or java.io.File to work with files in these places. There is another way, though: platform URI. Although this scheme has been around since the beginning of time, I decided to ramble about it here because I've never seem its uses described in one single place.

There are a few ways to work with the "platform" scheme:

platform:/resourceIt is used to identify a resource located in the workspace. The next path segment after "resource" should be the name of a project, which can be followed by the folder and/or file we want to locate.
platform:/pluginIt is used to locate a resource available in a plug-in (I know, I know, bundle). One really cool thing about this one is that it doesn't really matter if this resource is available in a directory or in a jar file. It also doesn't matter if the bundle is installed in a link folder or in the default directory.

The path segment after "plugin" should be the identifier of the bundle, which can be followed by the path of the resource in the bundle.
platform:/fragmentThis one is quite similar to "platform:/plugin", being to used to locate fragment resources instead of bundle resources. As you are probably guessing, the segment after "fragment" should be the fragment's identifier.
platform:/metaWe can use this to access a bundle's stage location. The path segment after "meta" should be the bundle's identifier, followed by the path of the resource we want to refer to.
platform:/configThe "config" segment causes the platform URI to refer to the configuration area of the running Eclipse (usually the eclipse/configuration directory). This can be useful to read the config.ini file, for example.
platform:/baseThis always refers to the directory of the Eclipse being executed.

It is interesting to note that, for example, platform:/base/plugins/org.eclipse.emf/plugin.xml and platform:/plugin/org.eclipse.emf/plugin.xml don't necessarily refer to the same resource. The former is a "pointer" to a plugin.xml file located in a directory plugins/org.eclipse.emf under the directory that Eclipse is installed. The latter points to the plugin.xml of the "org.eclipse.emf.ecore" bundle regardless of where it is installed and whether it is jarred or not.

For the URI-savvy people, you should see "resource", "plugin", "fragment", "meta", "config", and "base" as authorities. Perhaps they could become authorities in e4 (yey! Now this is an e4 related post ;-)

Since we all like actual code...
IProject project = 
ResourcesPlugin.getWorkspace().getRoot().getProject("myproject");
if (!project.exists())
{
project.create(new NullProgressMonitor());
}

System.out.println("\n==== platform:/resource ====");
{
URI uri = URI.createPlatformResourceURI("myproject", true);
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));

uri = uri.appendSegments(new String[]{"folder", "file.txt"});
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));
}

System.out.println("\n==== platform:/plugin ====");
{
// Just for fun, choose a bundle that is not in the default location
URI uri = URI.createPlatformPluginURI("org.eclipse.emf.ecore", true);
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));

uri = uri.appendSegments(new String[]{"model", "Ecore.ecore"});
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));

// Choose a bundle that is in the default location (<eclipse-dir>/plugins)
uri = URI.createPlatformPluginURI("org.eclipse.core.resources", true);
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));

uri = uri.appendSegments(new String[]{"META-INF", "MANIFEST.MF"});
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));
}

System.out.println("\n==== platform:/fragment ====");
{
URI uri = URI.createURI("platform:/fragment/org.eclipse.swt");
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));

uri = uri.appendSegments(new String[]{"META-INF", "MANIFEST.MF"});
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));
}

System.out.println("\n==== platform:/config ====");
{
URI uri = URI.createURI("platform:/config/");
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));
}

System.out.println("\n==== platform:/base ====");
{
URI uri = URI.createURI("platform:/base/");
System.out.println(uri);
System.out.println(CommonPlugin.resolve(uri));
}
The call to CommonPlugin.resolve(URI) returns a URI that uses a protocol which is native to the Java class library (file, jar, http, etc).

I will leave it to you to run this code and see the results :-P Don't forget that it must be executed in an Eclipse shell. Probably the simplest way to do so is to paste these lines into a JUnit test located in a bundle and execute it as a "JUnit Plug-in Test".

So what can we do with platform URIs? For one, read the contents of the resources pointed by them. We may also be able to write to such resources or even delete or create them. A tip for EMF 2.4 users: URIConverter.INSTANCE allows easy access to methods that are extremely handy when dealing with URIs (createOutputStream(URI), createInputStream(URI), delete(URI), and exists(URI)).

Personally I consider URIs a good fit for APIs that would normally use "plain" paths. Take as an example the icon attribute of the extension point below. Because its value is handled as a URI, we are allowed to refer to an image located in a different bundle.
<extension point="org.eclipse.ui.editorActions">
<editorContribution ...>
<action
icon="platform:/plugin/com.myplugin/icons/me.gif"
...
/>
</editorContribution>
</extension>
BTW, if I were to implement the code to process such an extension point, I would probably do something like this:
IConfigurationElement configurationElement = ...
URI iconURI = URI.createURI(configurationElement.getAttribute("icon"));
if (iconURI.isRelative())
{
URI pluginURI =
URI.createPlatformPluginURI(
configurationElement.getContributor().getName() + "/", true);
iconURI = iconURI.resolve(pluginURI);
}

try
{
ImageDescriptor imageDescriptor =
ImageDescriptor.createFromURL(new URL(iconURI.toString()));
descriptorImpl.setIcon(imageDescriptor.createImage());
}
catch (Exception e)
{
e.printStackTrace();
}
This code assumes that when the value of the icon attribute is a relative URI (like icon/me.gif for example), the developer is indicating that the image is contained in the bundle that uses the extension point.

For obvious reasons, I wrote these examples using EMF APIs, including our URI class. It shouldn't be terribly difficult to rewrite the code in this post to use basic Eclipse and Java code (like, java.net.URI).

15 comments:

Chris Aniszczyk (zx) said...

Thanks Marcelo, this is a post to keep archived... as the documentation is a bit weak in this area traditionally.

Ian Bull said...

Thanks Marcelo, this is the kind of stuff that I'm always trying to figure out, and I keep finding ugly work-arounds...

As for Little Marcelo #2, that is pretty exciting! I don't get to Toronto enough to see the EMF team anymore :-(.

Marcelo Paternostro said...

Thanks for the kind words guys ;-)

Yeah... I am sure I will have lots of fun when Alyssa (== "Little Marcelo #2" :-) arrives. We are counting the days.

Ed Merks said...

Marcelo, this is excellent information that is extremely difficult to find, even via Google. Perhaps a link to your blog from the EMF FAQ or using it as an EMF recipe would make it easier for folks to find it in the future. Good stuff!

By the way, I hope Alyssa == "Little Helena #1" :-P

Marcelo Paternostro said...

Good idea, Ed. I've just added an FAQ entry with a link to the blog. It would take a bit of a time to remove the "funny and personal digressions" to make it a recipe :-P

Awesome point!!! I sure hope Alyssa inherits the qualities of my best half ;-)

sinleeh said...

Dear Marcelo,

is "platform:/resource" an EMF specific thing?

The reason I ask is

FileLocator.resolve(new URL("platform:/resource/ValidDir/validFile"));

FileLocator.toFileURL(new URL("platform:/resource/ValidDir/validFile"));

do not work but

CommonPlugin.asLocalURI(URI.createURI("platform:/resource/ValidDir/validFile"));
CommonPlugin.resolve(URI.createURI("platform:/resource/ValidDir/validFile"));

do work.

If so, may be we should be requesting that "platform:/resource" to be made standard for Eclipse. I can see its usefulness although I am unlikely to use it myself

Congratulations on your second child if she arrived, if not, good luck.

Marcelo Paternostro said...

Hi,

Sorry for not paying attention to the comments on this post. My daughter did arrive so things have been a bit erratic here ;-)

The platform scheme is defined in Eclipse. So if the code you are executing is running in an Eclipse shell, it should work.

Note that after the "resource" segment, you should enter the name of a registered project.

Finally, EMF has some tricks to allow the same application to run inside and outside Eclipse. Perhaps this could explain the difference you are seeing.

Marc said...

Hello Marcelo, I'm really interested in this post, and I want to ask you about can I iterate a URI in order to get all the files placed into a directory, for instance all the files placed into "platform:/plugin/org.eclipse.emf.ecore/model/". I only have the URI.

In the same line, I want to know if it's easy to convert this URI to a java.io.File, it's so easy to iterate and read it finding the files...

Thanks a lot Marcelo!
Marc

Marcelo Paternostro said...

Hi Mark,

If the URI is either a file URI ("file://") or a URI that can be resolved into a file URI, you could use the java.io.File(URI) constructor to create a File object and use it normally.

That said, URIs are not mere replacements for File objects. Typically when dealing with URIs, there is a service to which you pass the URI and from which you receive some information - think about a regular interaction with a HTTP server. What I am trying to say is that "grabbing" a file URI and converting it a File object (which typically only has visibility of the local file system) may not be of any good if that URI was not meant for this purpose.

Moreover, it is up to this service to define what you can do with the URI. Apache, for example, can be configured to allow directory browsing, although that's enabled by default.

rhudson said...

Is there a contextual difference between trying to resolve a platform URI when running in a deployed instance versus running from within Eclipse itself? I seem to be able to resolve a platform plugin URI when I deploy my RCP product, but not when I run my application (still as an RCP application) from an Eclipse "Run Configuration".

In the working case, it gets nicely resolved to a jar URL in my exported product. In the failing case a FileNotFoundException gets thrown for the path portion of the URI.

Marcelo Paternostro said...

rhudson,

I definitely had a regular Eclipse execution in mind when I wrote this post. That said, I just tried locating the real paths for the some URIs with a code running on a runtime workspace, and it worked. Remember that PDE does a lot of magic to make our lives easier so some bundles may not be where you'd expect them to be.

I am pasting here the code I used. If you want to run it, add it to a bundle called 'Test' that depends on 'org.eclipse.core.runtime' and 'org.junit' (a 3.x version). Then right click the test and do 'Run As > JUnit Plug-in Test'. Oh, you will need to format it (I am sure it will look gross here ;-)

Cheers.

--------------

import java.io.File;
import java.net.URI;
import java.net.URL;

import junit.framework.TestCase;

import org.eclipse.core.runtime.FileLocator;

/**
* @author Marcelo Paternostro (mpaternostro@gmail.com)
*/
public class PlatformURITest extends TestCase {
public void testFileURL() throws Exception {
assertURI("platform:/plugin/org.eclipse.osgi", true);
assertURI("platform:/plugin/org.eclipse.core.runtime", true);

assertURI("platform:/config/", false);

// This bundle
assertURI("platform:/plugin/Test/build.properties", false);
}

private void assertURI(String uriString, boolean isBundleDirOrJar) throws Exception {
URI uri = new URI(uriString);
URL url = FileLocator.toFileURL(uri.toURL());
System.out.println("\nURI: " + uri + "\nFile URL: " + url);

File file = new File(url.toURI());
assertTrue(file.getAbsolutePath(), file.exists());

if (isBundleDirOrJar) {
if (file.isDirectory()) {
File manifestFile = new File(file, "META-INF/MANIFEST.MF");
assertTrue(manifestFile.getAbsolutePath(), manifestFile.isFile());
} else {
assertTrue(file.getAbsolutePath(), file.getName().endsWith(".jar"));
}
}
}
}

rhudson said...

After some careful observation using the debugger, the issue was really that the file layout of the plugin was different when it was jar'ed and deployed than when it was referenced during a launch from the IDE. The platform:/plugin resolution during the IDE launch resolved to a file URL that pointed to the file system location of the plugin project concatenated with the path portion of my platform URI. Since the resource I was trying to access was in a source directory, it had an additional path component during the ide launch that was removed during the packaging of the plugin into a jar. That's why it worked for the JAR, but not the IDE launch.

I guess I encountered this problem because I wasn't following the convention of putting my .ecore files in the 'model' directory. Or perhaps there is some plugin configuration magic that would have resolved my situation and I simply am missing that piece. In any event, I resolved the problem by moving my resource files to a location, the more conventional 'model' directory, that was the same for both the packaged JAR and the IDE launch.

Thanks for your help!

SeB said...

Hi,
can you tell me how to access a resource in a plugin for a given plugin version ?
something like the platform:/plugin/my.plugin.v1.00/resource/foo.jpg
is this even possible ?

Marcelo Paternostro said...

Hi,

AFAIK, the schemes I've described in this post do not allow specifying the version information. That said...

- Say for example that the bundle A uses C1 and bundle B uses C2, where C is a bundle and 1 and 2 different versions of it. I'd assume that a URI of a C artifact in A would resolve to C1 while it would resolve to C2 in B. I haven't tested this though.

- Eclipse 4 has introduced a new URI scheme: bundleclass. It may have support for versions (I have only used it to do the E4 tutorials so I can't say for sure).

Cheers.

Brian de Alwis said...

Just wanted to note that this information was incorporated into the platform documentation:

http://help.eclipse.org/juno/topic/org.eclipse.platform.doc.isv/reference/misc/platform-scheme-uri.html