Plugin development

This tutorial will provide an overview of how plugins for LessEntropy can be developed, bundled and deployed. During this tutorial we will develop a simple plugin that adds the original file name (the file name used during indexing) to all files. The plugin provides an editor that displays this file name.

The source code for this plugin is available as a zip file.

It is assumed that you are familiar with Maven and have a Maven 2.0.x installed on your computer.

Plugins, tags and meta information

In order to build plugins it is important to understand the ideas behind the plugin interface.

A plugin is just a container for tags (discussed below). A plugin is a class that implements IPlugin but as plugins are described declaratively you will never need to create a class that implements IPlugin directly. Instead you will rely on support classes. Once the plugin is instantiated by the application it provides access to the contained tags.

A tag is the type of meta information that are linked to a file. You can think of the tag being a class and the meta information being the object. A tag provides methods for instantiating meta information for a given file. A tag is only instantiated once per application run. A tag is implemented using a java class that either implements ITag directly or extends one of the support classes (like TagSupport).

A tag can instantiate meta information which are objects of a class implementing IMetaInformation. Typically there is one meta information class that corresponds to one tag class but there are several instances (one for each file) of the meta information class that correspond to one instance of the tag class.

Keeping this in mind we will now start to build the plugin.

Structure of a plugin

The following figure shows a sample directory structure for a plugin. Since LessEntropy is build using Maven it is recommended that you build your plugin using maven as well. The plugin directory structure therefore obeys to the maven project layout.

-demoplugin
 |
 +-src
 | |
 | +-main
 |   |
 |   +-java
 |   |
 |   +-resources
 |     |
 |     +-localStrings
 |     |
 |     +-plugin.xml
 +-pom.xml

The src/main/java folder will contain all java source files for the pluging. The src/main/resources folder will contain the resources. In addition, it stores the very important plugin.xml which describes the plugin.

The pom.xml is the maven build descriptor. One sample descriptor is given below.

<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.sf.lessentropy</groupId>
        <artifactId>lessentropy-demo-plugin</artifactId>
        <packaging>jar</packaging>
        <version>1.0-SNAPSHOT</version>
        <name>LessEntropy Demo Plugin</name>
        <url>http://lessentropy.sourceforge.net</url>
        <dependencies>
                <dependency>
                        <groupId>net.sf.lessentropy</groupId>
                        <artifactId>lessentropy-sdk</artifactId>
                        <version>2.0.2</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
                        <artifactId>junit</artifactId>
                        <version>3.8.1</version>
                        <scope>test</scope>
                </dependency>
        </dependencies>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <configuration>
                                        <source>1.5</source>
                                        <target>1.5</target>
                                </configuration>
                        </plugin>
                </plugins>
                <resources>
                        <resource>
                                <directory>src/main/resources</directory>
                                <filtering>true</filtering>
                                <includes>
                                        <include>plugin.xml</include>
                                        <include>localString/**/*.xml</include>
                                </includes>
                        </resource>
                </resources>
        </build>
</project>

It does nothing special but saying that we are using java 5. It also says that we have one important dependency: The lessentropy sdk which contains the interfaces and classes we need to build a plugin. If you don't know maven 2, it's recommended that you take a look at it. Maven is not only suitable for plugins. It can greatly improve the build process for any (java) project you are running.

The plugin descriptor plugin.xml

Every plugin is bundled as a single jar file. This jar file is deployed to the plugins folder of the application. When LessEntropy loads plugins it reads all jar files from the plugins folder and extracts a file named plugin.xml from the root of the jar file. If such a file is not found, the plugin can not be loaded.

The plugin.xml describes the plugin and all tags it provides (remember that a plugin can provied more than one tag, although most plugins will provide only one tag).

The plugin descriptor for the demo plugin is shown below.

<?xml version="1.0" encoding="UTF-8"?>

<plugin sdk-version="1.0" id="net.sf.lessentropy.plugin.demo">
        <name>DemoPlugin</name>
        <author>Alexander Metzner</author>
        <version>1.0-SNAPSHOT</version>
        <url>http://lessentropy.sourceforge.net/plugins/demo</url>
        <plugin-class>net.sf.lessentropy.impl.plugin.PluginSupport</plugin-class>
        <tags>
                <tag id="net.sf.lessentryop.plugin.demo.demotag">
                        <name>DemoTag</name>
                        <author>Alexander Metzner</author>
                        <version>1.0</version>
                        <tag-class>net.sf.lessentropy.plugin.demo.DemoTag</tag-class>
                </tag>
        </tags>
</plugin>

The tags are discussed below

  • plugin The root element. The element must declare an attribute named sdk-version which defines the plugin version this descriptor uses. It must also define the id for the plugin. The id must be unique for all plugins running in the application. It is therefore a good idea to use the pacakge name for the plugin's code as it's id.
  • name The plugin's name.
  • author The plugin's author.
  • version The plugin's version.
  • url A URL providing additional information.
  • plugin-class The full qualified name of the plugin class. This must be a class that implements IPlugin and it must have a default constructor. In this case we use the predefined support class PluginSupport which suits most of the needs.
  • tags Defines all the tags this plugin provides.
  • tag Defines a tag. It must have an id which must also be unique for all tags.
  • name, author and version are the same as for the plugin but only apply to the tag.
  • tag-class The full qualified name of the class implementing ITag.

    Once you have created the plugin descriptor we can now go on and talk about the java stuff.

The java side of the plugin

All java source files are located in the src/main/java folder. The demo plugin provides three classes:

- net.sf.lessentropy.plugin.demo.DemoTag

- net.sf.lessentropy.plugin.demo.DemoMetaInformation

- net.sf.lessentropy.plugin.demo.editor.DemoTagEditor

The tag class

The class net.sf.lessentropy.plugin.demo.DemoTag implements the java logic needed to make up the tag. The source code is shown below.

package net.sf.lessentropy.plugin.demo;

import java.util.ArrayList;
import java.util.Collection;

import net.sf.lessentropy.impl.plugin.TagSupport;
import net.sf.lessentropy.model.File;
import net.sf.lessentropy.model.IMetaInformation;
import net.sf.lessentropy.model.MimeType;
import net.sf.lessentropy.plugin.TagMismatchException;
import net.sf.lessentropy.plugin.demo.editor.DemoTagEditor;
import net.sf.lessentropy.plugin.editor.ITagEditor;

/**
 * A tag demonstrating the plugin interface of LessEntropy.
 * @author Alexander Metzner
 * @version 1.0
 */
public class DemoTag extends TagSupport {
        private DemoTagEditor editor = new DemoTagEditor();
        private String[] propertyNames = { "Demo" };

        public String[] getPropertyNames() {
                return propertyNames;
        }
        
        public String[] getDefaultPropertyNames() {
                return propertyNames;
        }
        
        public Collection<MimeType> getSupportedMimeType() {
                ArrayList<MimeType> ret = new ArrayList<MimeType>();
                ret.add(MimeType.ALL);
                return ret;
        }

        public boolean supportsCustomEditor() {
                return true;
        }

        public ITagEditor getCustomEditor() {
                return editor;
        }
        
        public IMetaInformation instantiate(File file, java.io.File physicalPath) throws TagMismatchException {
                DemoMetaInformation ret = new DemoMetaInformation();
                ret.setTag(this);
                ret.setAbsolutePath(physicalPath.getAbsolutePath());
                return ret;
        }

        public IMetaInformation recreate(String data) {
                DemoMetaInformation ret = new DemoMetaInformation();
                ret.setTag(this);
                ret.setAbsolutePath(data);
                return ret;
        }
}

The class extends TagSupport which is a helper class provided by the LessEntropy SDK. We will now talk about the methods this class implements:

  • getPropertyNames Returns a list of strings each representing one property that can be seached by LessEntropy. Note that this searching functionality has not been implemented in version 2.0.2 of LessEntropy.
  • getDefaultPropertyNames Returns a list of strings each representing one property that should be search be default.
  • getSupportedMimeTypes Returns a list of mime types this tag can be applied to. In our example the tag applied to all files.
  • supportsCustomEditor Returns true if the tag has a custom UI editor.
  • getCustomEditor Returns the editor (we will talk about the editor later) if the tag provides one. Returns null otherwise.
  • instantiate This is one of the key methods (along recreate). During indexing, this method is called to create an instance of IMetaInformation that will be attached to the indexed file. You are provided with the file index object as well as the physical file. In our tag we only extract the absolute path of the physical file. Note that the returned meta information will be attached to file by the application not by the tag.
  • recreate This method recreates the meta information from the local database. All meta information data must be serialized to a plain string which is stored in the db. This methods performs the deserialization. This method goes hand in hand with another method from IMetaInformation that will be described below.

The meta information class

The class net.sf.lessentropy.plugin.demo.DemoMetaInformation implements the meta information that will be attached to our files. The code is shown below.

package net.sf.lessentropy.plugin.demo;

import net.sf.lessentropy.model.IMetaInformation;
import net.sf.lessentropy.plugin.ITag;

/**
 * {@link IMetaInformation} for the demo plugin.
 * @author Alexander Metzner
 * @version 1.0
 */
public class DemoMetaInformation implements IMetaInformation {
        private String absolutePath;
        private ITag tag;

        public ITag getTag() {
                return tag;
        }

        public void setTag(ITag tag) {
                this.tag = tag;
        }

        public String getAbsolutePath() {
                return absolutePath;
        }

        public void setAbsolutePath(String absolutePath) {
                this.absolutePath = absolutePath;
        }

        public boolean matches(net.sf.lessentropy.logic.ISearchConstraints arg0) {
                return false;
        }
        
        public String getPersistentString() {
                return absolutePath;
        }
}

The methods of this class are described below.

  • getTag Returns the instance of the tag this meta information corresponds to.
  • setTag Sets the tag this meta information corresponds to.
  • getAbsolutePath A custom method (not defined by IMetaInformation) to get the absolute path. This method is called by the editor (see below).
  • setAbsolutePath A custom setter. Called by the tag to instantiate the meta information.
  • matches This method is called during search and should return true if the meta information matches the search constraints given. Note that in 2.0.2 the search has not been implemented for plugins.
  • getPersistentString Returns a serialized version of the meta information data. This method corresponds to the recreate method of ITag. In our example the serialization is quite simple. For more complex tags the SDK provides helper classes to serialize Properties or encode byte data to base64.

    If the tag provides no editor we are ready to launch the plugin. As we want to show the absolute path to the user we need to create one additional class.

The tag editor

The class net.sf.lessentropy.plugin.demo.editor.DemoTagEditor defines the editor used to visualize the meta information. The source code is shown below.

package net.sf.lessentropy.plugin.demo.editor;

import java.awt.GridLayout;
import java.net.URL;

import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import net.sf.lessentropy.impl.util.stringmanager.StringManager;
import net.sf.lessentropy.impl.util.stringmanager.XmlContentProvider;
import net.sf.lessentropy.model.IMetaInformation;
import net.sf.lessentropy.plugin.demo.DemoMetaInformation;
import net.sf.lessentropy.plugin.editor.ITagEditor;

import org.dom4j.DocumentException;

/**
 * {@link ITagEditor} for the demo plugin's tag.
 * 
 * @author Alexander Metzner
 * @version $Revision$
 * 
 */
public class DemoTagEditor extends JPanel implements ITagEditor {
        private JTextField absolutePath;

        public DemoTagEditor() {
                try {
                        URL url = getClass().getClassLoader().getResource(
                                        "localStrings/demoplugin.xml");
                        StringManager.initialize(new XmlContentProvider(url));

                        StringManager sm = StringManager
                                        .getStringManager(DemoTagEditor.class);
                        absolutePath = new JTextField(30);

                        setLayout(new GridLayout(1, 2));
                        add(new JLabel(sm.getString("absolutePath")));
                        add(absolutePath);
                } catch (DocumentException e) {
                        throw new RuntimeException(e);
                }
        }

        public JComponent getEditorComponent() {
                return this;
        }

        public void updateData(IMetaInformation metaInformation) {
                absolutePath.setText(((DemoMetaInformation)metaInformation).getAbsolutePath());
        }
}

The editor implements the ITagEditor interface. It also extends from JPanel but that is not required. Here is an description of the methods that the editor implements:

  • getEditorComponent Returns an instance of JComponent that is added to a dialog and shown to the user. In our case we simple return this as our editor extends JPanel. If your editor is more complex than this very simple one you can choose to create a different class that is used as the editor component and return an instance of this class form this method.
  • updateDate This method is called just before an reference to the editor component is obtained via getEditorComponent. This method should be used to update the editor component's data. In our case we simply update our own text field.

    The constructor of this class initializes the text field and the layout. Notice the first six lines of the constructor. Here we instantiate a StringManager that provides access to localized messages. The string manager is another class from the LessEntropy SDK that provides an easy way of localizing your layout. It also manages the language the user has chosen from the application and returns appropriate translations if available.

    The string manager is configured with a URL. This url points to a resource that should also be located in the src/main/resources folder and is named localStrings/demoplugin.xml. It is good practice to put all your local messages into a folder named localStrings although it is up to you to choose another location as long as you can create a URL to it.

    The resource's contents is shown below.

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <!DOCTYPE stringTable SYSTEM "dtd/stringTable.dtd">
    
    <stringTable>
            <locale code="en" default="true">
                <package name="net.sf.lessentropy.plugin.demo.editor">
                    <class name="DemoTagEditor">
                            <key name="absolutePath">Absolute path during indexing:</key>
                    </class>
                    </package>
            </locale>
            <locale code="de" default="false">
                <package name="net.sf.lessentropy.plugin.demo.editor">
                    <class name="DemoTagEditor">
                            <key name="absolutePath">Absoluter Pfad bei der Indizierung:</key>
                    </class>
                    </package>
            </locale>
    </stringTable>
    

    This xml file defines one resource for one class: net.sf.lessentropy.plugin.demo.editor.DemoTagEditor. The resource is provided in english and german. The string manager selects the correct translation based on the user's choice.

Building the plugin

After having created all the files it's time to build the plugin. One a shell and issue the following command (assuming you have maven 2 installed on your machine).

mvn package

This will compile all the sources, collect the resources and bundle everything together to a single jar that is named lessentropy-demo-plugin-1.0-SNAPSHOT.jar. The file is located in the target folder.

Deploying the plugin

Copy the lessentropy-demo-plugin-1.0-SNAPSHOT.jar file from the target folder the the plugins folder of LessEntropy.

Testing the plugin

Start LessEntropy and click on the plugins menu. It should contain an entry named Demoplugin.

Congratulations! You successfully build your first plugin.