Eclipse File Content Types

Version 3

    The Big Picture

    soup.jpgWhen you pick up a can of soup and the label reads “TOMATO SOUP”, you expect tomato soup to be inside, right? What if soup manufacturers all decided not to label their products and simply used generic containers with the label “SOUP”? One could argue that even if the soup cans were clearly labeled, who's to say that what's inside really is TOMATO SOUP? The only way to be absolutely certain is to open the can and peek inside!

     

    Since the dawn of MS-DOS, file name extensions have been used, much like soup can labels, to identify file contents and, typically which application is associated with that file. Eclipse has been following this same tried-and-true philosophy to launch the proper editor or viewer plugin when you double-click on a resource in the eclipse Navigator. But with the proliferation of XML formats that describe everything from graffiti to DNA patterns, the file name extension “XML” has become somewhat ambiguous. OK, we know it's SOUP but what kind of SOUP?

     

    That's essentially the idea behind the eclipse content-type extension point. When the eclipse workbench opens a folder resource for the first time, it looks at the folder's contained IFile resources and then all plugins that contribute to this extension point are allowed to “peek inside the can” to see if it contains a SOUP that they can consume. The extension point then provides an ID which is is associated with the IFile and cached by the workbench. If the name of the IFile changes, the workbench goes through the same content type checking again.

     

    As you can imagine, the time it takes for the workbench to run through this process increases (roughly) with the number of plugins that contribute to content-type multiplied by the number of files in your projects. So, as a plugin developer you need to ensure that content type checking is as fast as possible: peek inside the can, take a quick sniff and move on; don't grab a spoon and try to take a taste!

    The Details

    If you have been around eclipse long enough you probably already know that the SDK documentation was written by Captain Obvious and is of little help when you're trying figure out how to do something that should be so...simple. Case-in-point:

    Package org.eclipse.core.runtime.content

    Provides core support for content types.

    See:
              Description

    Interface Summary

    IContentDescriber

    Content describers describe contents.

    IContentDescription

    A description for content.

    IContentTypeManager

    Content type managers manage content types.

     

    Class Summary

    BinarySignatureDescriber

    A describer for binary signatures.

    XMLRootElementContentDescriber

    A content describer for XML root elements.

     

    Luckily there are a lot of folks out there doing amazing work with eclipse and publishing their experiences. I just wanted to give something back by describing the steps I took to add this feature to the eclipse BPEL editor.

    The “org.eclipse.ui.editors” extension point in your plugin.xml probably looks something like this:

    <extension

         point="org.eclipse.ui.editors">

       <editor

           class="org.eclipse.bpel.ui.BPELMultipageEditorPart"

           default="true"

           extensions="bpel"

           icon="icons/obj16/bpel.gif"

           id="org.eclipse.bpel.ui.bpeleditor"

           name="%BPEL_EDITOR_NAME">

       </editor>

    </extension>

     

    There are a couple of things wrong with this if you intend to do content type checking: first, do not specify file extensions here since this defeats the purpose of the content-type extension point; second, do not specify an icon for your editor here either, since the workbench will slap this decorator on all files based on their file extension instead of their content. Instead, replace these attributes with the following:

     

    <extension

        point="org.eclipse.ui.editors">

       <editor

           class="org.eclipse.bpel.ui.BPELMultipageEditorPart"

           default="true"

           id="org.eclipse.bpel.ui.bpeleditor"

           name="%BPEL_EDITOR_NAME">

         <contentTypeBinding

             contentTypeId="org.eclipse.bpel.contenttype">

         </contentTypeBinding>

       </editor>

    </extension>

     

    The contentTypeBinding element references a type ID which is defined in the content-type extension point:

     

    <extension

        point="org.eclipse.core.contenttype.contentTypes">

      <content-type

          base-type="org.eclipse.core.runtime.xml"

          id="org.eclipse.bpel.contenttype"

          name="BPEL Editor File"

          file-extensions="bpel,bpel2,xml"

          priority="normal">

        <describer

            class="org.eclipse.core.runtime.content.XMLRootElementContentDescriber2">

          <parameter

              name="element"

              value="{http://docs.oasis-open.org/wsbpel/2.0/process/executable}process"/>

        </describer>

      </content-type>

    </extension>

     

    I'm not going to repeat the eclipse reference documentation here, but I did want to make a few comments.

     

    1. Notice the file-extensions attribute is specified here, and you will still need to provide at least one file name extension for your content type. As of eclipse 3.5.2 file name extensions are still required – you can not, for example, create an XML file that has no file name extension and expect the XML editor to correctly recognize it based on content.

    2. The eclipse framework already provides an XML root element content describer and there are lots of examples out there that demonstrate how to use it. Note however, that a lot fo these examples refer to the deprecated class XMLRootElementContentDescriber - you should use XMLRootElementContentDescriber2 defined in the org.eclipse.core.runtime.content package instead.

    3. Whenever possible, provide a namespace for your root element. This will avoid possible conflicts with other plugins. For example, I discovered (after much gnashing of teeth) that JBoss Drools Flow editor files (*.rf files) also have a “process” root element.

     

    The icon attribute in the org.eclipse.editors extension point should be removed and specified in an org.eclipse.ui.decorators extension point instead, like so:

     

    <extension

        point="org.eclipse.ui.decorators">

      <decorator

          adaptable="true"

          icon="icons/obj16/bpel.gif"

          id="org.eclipse.bpel.ui.icondecorator"

          label="BPEL Resource Decorator"

          lightweight="true"

          location="TOP_LEFT"

          state="true">

        <enablement>

          <and>

             <objectClass

                name="org.eclipse.core.resources.IFile">

             </objectClass>

             <objectState

               name="contentTypeId"

               value="org.eclipse.bpel.contenttype">

             </objectState>

           </and>

         </enablement>

       </decorator>

    </extension>

     

    This will ensure that your editor's files will always display the correct icon regardless of what the file name extension is, as long as it's one of the extensions declared by your content-type.

     

    The last point I wanted to discuss was EMF support for content types. As far as I can tell this has been around since EMF 2.2.2 (possibly earlier). If your editor uses an EMF model, you also need to contribute an EMF content parser extension point:

     

    <extension point="org.eclipse.emf.ecore.content_parser">

      <parser

        class="org.eclipse.bpel.model.resource.BPELResourceFactoryImpl"

        contentTypeIdentifier="org.eclipse.bpel.contenttype">

      </parser>

    </extension>

     

    The resource factory is then responsible for constructing the correct XMI parser for your EMF model.

    Finally, don't forget to go through your editor code and replace all of the file name extension checking with content type checking! For example, stuff like this:

     

        IResource res;

        if (res.getType() == IResource.FILE) {

            if ("bpel".equalsIgnoreCase(((IFile)res).getFileExtension())) {

                // load it...

             }

        }

     

    should be replaced with this:

     

        IResource res;

        if (res.getType() == IResource.FILE) {

            IContentDescription desc = ((IFile)res).getContentDescription();

            if (desc != null) {

                IContentType type = desc.getContentType();

                if ("org.eclipse.bpel.contenttype".equals(type.getId())) {

                    // load it...

                }

            }

         }

    Going Further

    The XMLRootElementContentDescriber2 provided by eclipse should fill most of your content type checking needs. But, if you need to drill down deeper into the XML document, you may need to write your own custom content describer class. Keep in mind though that your implementation should only do the absolute bare minimum required to make its determination – for instance don't read the entire document using a DOM parser if you only need to check a couple of elements!

     

    Techniques that work well are to use either an XML pull parser or use the SAX parser and throw exceptions to terminate parsing after the first element.

     

    I've provided an example of my BPEL content describer using a SAX parser for your reading enjoyment:

     

    public class BPELContentDescriber implements ITextContentDescriber {


      private static final String WSBPEL_2_NAMESPACE = "http://docs.oasis-open.org/wsbpel/2.0/process/executable"; //$NON-NLS-1$

      private static final String ROOT_ELEMENT = "process"; //$NON-NLS-1$


      private RootElementParser parser = new RootElementParser();


      public BPELContentDescriber() {

      }


      public int describe(Reader contents, IContentDescription description) throws IOException {

        return doDescribe(contents) == null ? INVALID : VALID;

      }


      public int describe(InputStream contents, IContentDescription description) throws IOException {

        return describe(new InputStreamReader(contents), description);

      }


      private String doDescribe(Reader contents) throws IOException {

        InputSource source = new InputSource(contents);

        try {

          parser.parse(source);

        } catch (SAXException e) {

          return null;

        } catch (AcceptedException e) {

          return e.acceptedRootElement;

        } catch (RejectedException e) {

          return null;

        }


        return null;

      }


      public QualifiedName[] getSupportedOptions() {

        return null;

      }


      private class RootElementParser extends SAXParser {

        public void startElement(QName qName, XMLAttributes attributes, Augmentations augmentations) throws XNIException {

          super.startElement(qName, attributes, augmentations);


          if (ROOT_ELEMENT.equals(qName.localpart)) {

            String namespace = fNamespaceContext.getURI(qName.prefix);

            if (namespace != null && WSBPEL_2_NAMESPACE.equals(namespace))

              throw new AcceptedException(qName.localpart);

            else

              throw new RejectedException();

          } else

            throw new RejectedException();

        }

      }


      private class AcceptedException extends RuntimeException {

        public String acceptedRootElement;


        public AcceptedException(String acceptedRootElement) {

          this.acceptedRootElement = acceptedRootElement;

        }


        private static final long serialVersionUID = 1L;

      }


      private class RejectedException extends RuntimeException {

        private static final long serialVersionUID = 1L;

      }

    }

    References

    Contenttypes and describers

    Eclipse Tip: Define Custom Content Types to Identify Your Data Files

    Lessons from Behind The Curtain

    EMF Bits

    Eclipse Zone