OBO-Edit: Creating a Data Adapter Plugin

From GO Wiki
Jump to: navigation, search

It's actually quite simple to write an OBO-Edit data adapter plugin. There are really only five steps:

Configure Your Development Environment

How you do this is highly dependent on how you want to develop code, and whether you want access to the OBO-Edit source code while developing. Here are a few common options...

(quickest & easiest) Eclipse with No Access to the OBO-Edit Sources

  1. *optional* If you haven't created an Eclipse project for your plugin, create it now
  2. Download any OBO-Edit release
  3. Install or unzip the release
  4. Copy the libraries runtime/org.geneontology.jar and runtime/oboedit.jar from the OBO-Edit installation into your eclipse project
  5. Put the libraries on your project build path:
    1. Select your plugin project in the Package View or Navigation View
    2. Select the "Project -> Properties" menu item
    3. Choose "Build path" in the popup window
    4. Select the "Libraries" tab
    5. Click the "Add jars" button
    6. Select the org.geneontology and oboedit.jar jar files that you just added to your plugin project
    7. Select "OBO-Edit" and "org.geneontology"
    8. Click "Ok" in all open windows

Eclipse with Access to the OBO-Edit Sources

  1. Use Eclipse to check out *both* the OBO-Edit source code *and* the org.geneontology toolkit from CVS using the instructions described in OBO-Edit: Getting the Source Code#Getting the Source Code with Eclipse
  2. *optional* If you haven't created a project for your plugin, create one now.
  3. Put the OBO-Edit and org.geneontology projects on your build path
    1. Select your plugin project in the Package View or Navigation View
    2. Select the "Project -> Properties" menu item
    3. Choose "Build path" in the popup window
    4. Select the "Projects" tab
    5. Click the "Add" button
    6. Select "OBO-Edit" and "org.geneontology"
    7. Click "Ok" in all open windows

Command Line Building

  1. Download any OBO-Edit release
  2. Install or unzip the release
  3. Copy the libraries runtime/org.geneontology.jar and runtime/oboedit.jar to your main project directory (or wherever you like to keep jar libraries when building a project)
  4. When compiling, include the org.geneontology.jar and oboedit.jar in your classpath

Write Your Code

A Data Adapter is created by implementing the org.geneontology.oboedit.dataadapter.OBOEditAdapter inferface. The easiest way to do this is by extending org.geneontology.oboedit.dataadapter.AbstractAdapter, which provides default implementations for some methods. These default implementations will be fine for 99.9% of data adapters.

If you extend AbstractAdapter, you are responsible for providing implementations for the following methods:

  • doOperation
  • getConfiguration
  • getID
  • getName
  • getSupportedOperations
  • getPreferredUI

The Easy Part

Some of the methods are trivial to implement:

String getID()
- This method should return a unique identifier for the data adapter. This unique identifier is used to identify the adapter when saving adapter configuration info.
String getName()
- This method should return a user-friendly name for this adapter. This is the name that will be displayed in the user interface.
IOOperation [] getSupportedOperations()
- Returns an array of operations supported by this adapter. This adapter will only be available when a user is attempting an operation supported by the adapter. The possible operations that an adapter can support are:
  • org.geneontology.dataadapter.IOOperation.READ - Data adapters that support this operation must be able to load an ontology
  • org.geneontology.dataadapter.IOOperation.READ - Data adapters that support this operation must be able to save an ontology
  • org.geneontology.oboedit.dataadapter.OBOEditAdapter.READ_HISTORY - Data adpters that support this operation must be able to load an OBO-Edit XML history file
  • org.geneontology.oboedit.dataadapter.OBOEditAdapter.WRITE_HISTORY - Data adapters that support this operation must be able to save an OBO-Edit XML history file

Providing a User Interface

It can be tricky to write a user interface for an OBO-Edit data adapter, especially if the data adapter allows complex configurations.

A user interface presents some kind of interface to the user (either a GUI, or some command-line options, or something else, depending on how the adapter will be used), and creates an implementation of AdapterConfiguration based on how the user has manipulated that interface. The AdapterConfiguration created by the interface is then passed to the doOperation() method of the adapter (see below).

This can get really complex. Luckily, if your data adapter reads/writes disk files (as opposed to a database, or something like that) org.geneontology provides default implementations of org.geneontology.dataadapter.DataAdapterUI and org.geneontology.dataadapter.AdapterConfiguration. These implementations are org.geneontology.datadapter.FileAdapterConfiguration and org.geneontology.dataadapter.FileAdapterUI.

If you use these default implementations, you can use the following implementations for getPreferredUI() and getConfiguration(): protected AdapterConfiguration configuration;

public AdapterConfiguration getConfiguration() {

  return configuration;

}

public DataAdapterUI getPreferredUI() {

  FileAdapterUI ui = new FileAdapterUI();
  ui.setFont(Controller.getController().getDefaultFont());
  ui.setButtonColor(Preferences.defaultButtonColor(), Color.black);
  return ui;

}

More complex implementations are beyond the scope of this article. Contact John Day-Richter if you are developing a more complex data adapter.

The Hard Part - Implementing doOperation()

The real meat of the data adapter is the doOperation method:

public Object doOperation(IOOperation op, AdapterConfiguration config, Object input) throws DataAdapterException

The arguments are the following:

  • IOOperation op - The operation that the adapter should perform. Most adapters can do more than one operation (ie IOOperation.READ and IOOperation.WRITE), so this parameter is used to decide which operation to perform.
  • AdapterConfiguration config - The adapter configuration to use. The adapter configuration provides all the information about 'how' to perform the operation. For example, the FileAdapterConfiguration object contains information about the files that should be read or written.
  • Object input - The object to manipulate. The type of the object passed to this parameter will differ depending on the IOOperation. If the operation is IOOperation.READ, the "input" paramter will always be null. If the operation is IOOperation.WRITE, the "input" parameter will always be an OBOSession.

The return value of doOperation also differs depending on the operation. If the operation is IOOperation.WRITE, the return value is ignored (null is usually returned for this operation). If the operation is IOOperation.READ, the return value is the OBOSession that was read in.

Any error during doOperation should throw a DataAdapterException. DataAdapterException can wrap another exception if desired.

Note that most doOperation methods take a long time to run. Long-running doOperation method should periodically check to see if the cancel() method has been called by another thread. If cancel() has been called, the doOperation method should immediately throw an org.geneontology.dataadapter.CancelledAdapterException (a subclass of DataAdapter). AbstractAdapter's implementation of cancel() sets a property called "cancelled", so doOperation can periodically check the value of the "cancelled" property to determine whether the operation should be aborted.

The following is a toy implementation of doOperation (note that this implementation assumes that config is an instance of FileAdapterConfiguration):

public Object doOperation(IOOperation op, AdapterConfiguration config,
Object input) {
  this.configuration = configuration;
  if (!(configuration instanceof FileAdapterConfiguration)) {
	throw new DataAdapterException("Bad configuration; "
					+ "expected a FileAdapterConfiguration");
  }
  if (op.equals(IOOperation.READ)) {
	OBOSession session = new OBOSessionImpl();
		session.addObject(new OBOClassImpl(
			"Read a file from "+ ((FileAdapterConfiguration) configuration).getReadPaths(), "dummy:id"));
			return session;
  } else if (op.equals(IOOperation.WRITE)) {
	FileAdapterConfiguration fileConfig = (FileAdapterConfiguration) configuration;
	try {
	  OBOSession session = (OBOSession) input;
  	  FileWriter writer = new FileWriter(fileConfig.getWritePath());
	  writer.write("Wrote ontology with "+session.getObjects().size()+" objects to this dummy file.\n");
	  writer.close();
  	  return session;
	} catch (IOException e) {
	  throw new DataAdapterException("Write error", e);
	}
  } else {
	// this condition should never happen if the adapter is called
	// correctly
	throw new DataAdapterException("Unsupported operation " + op);
  }
}

Create a Manifest

OBO-Edit plugins are stored inside Java jar files. These jar files must contain a special manifest file that tells OBO-Edit the names of the Java classes to install as plugins. Note that this manifest file is a different file from the jar manifest file used by the Java jar utility.

The OBO-Edit plugin manifest must be stored in a file called resources/pluginlist.

The pluginlist file is a text file. The first line of the text file should declare how many data adapters are to be loaded using the following syntax:

adapters%adapterCount=<i>number-of-adapters</i>

After that, each line should declare the class name of a data adapter:

adapters%installAdapter<i>index</i>=<i>full.class.name</i>

The following is the manifest for a jar file that contains one data adapter plugin named org.myorganization.oboedit.MyAdapter:

adapters%adapterCount=1
adapters%installAdapter0=org.myorganization.oboedit.MyAdapter

Package Your Code

Use your favorite utility to create a jar file containing the compiled class files and the "releases/pluginlist" manifest file.

Install the Plugin

To install the plugin, place the jar file into the extensions/ directory of your OBO-Edit installation, and restart OBO-Edit.

Examples

A Toy Example

This toy example creates a data adapter that creates the same tiny ontology no matter what file is loaded, and creates a dummy save file:

Code

package org.myorganization.oboedit;

import java.awt.Color;
import java.io.FileWriter;
import java.io.IOException;

import org.geneontology.dataadapter.AdapterConfiguration;
import org.geneontology.dataadapter.DataAdapterException;
import org.geneontology.dataadapter.DataAdapterUI;
import org.geneontology.dataadapter.FileAdapterConfiguration;
import org.geneontology.dataadapter.FileAdapterUI;
import org.geneontology.dataadapter.IOOperation;
import org.geneontology.oboedit.dataadapter.AbstractAdapter;
import org.geneontology.oboedit.dataadapter.OBOEditAdapter;
import org.geneontology.oboedit.datamodel.OBOSession;
import org.geneontology.oboedit.datamodel.impl.OBOClassImpl;
import org.geneontology.oboedit.datamodel.impl.OBOSessionImpl;
import org.geneontology.oboedit.gui.Controller;
import org.geneontology.oboedit.gui.Preferences;

public class MyAdapter extends AbstractAdapter implements OBOEditAdapter {

	protected AdapterConfiguration configuration;

	public Object doOperation(IOOperation op,
			AdapterConfiguration configuration, Object input)
			throws DataAdapterException {
		this.configuration = configuration;
		if (!(configuration instanceof FileAdapterConfiguration)) {
			throw new DataAdapterException("Bad configuration; "
					+ "expected a FileAdapterConfiguration");
		}
		if (op.equals(IOOperation.READ)) {
			OBOSession session = new OBOSessionImpl();
			session.addObject(new OBOClassImpl(
					"Read a file from "
							+ ((FileAdapterConfiguration) configuration)
									.getReadPaths(), "dummy:id"));
			return session;
		} else if (op.equals(IOOperation.WRITE)) {
			FileAdapterConfiguration fileConfig = (FileAdapterConfiguration) configuration;
			try {
				OBOSession session = (OBOSession) input;
				FileWriter writer = new FileWriter(fileConfig.getWritePath());
				writer.write("Wrote ontology with "+session.getObjects().size()+" objects to this dummy file.\n");
				writer.close();
				return session;
			} catch (IOException e) {
				throw new DataAdapterException("Write error", e);
			}
		} else {
			// this condition should never happen if the adapter is called
			// correctly
			throw new DataAdapterException("Unsupported operation " + op);
		}
	}

	public AdapterConfiguration getConfiguration() {
		return configuration;
	}

	public String getID() {
		return "MYPACKAGE:my_adapter";
	}

	public String getName() {
		return "My Data Adapter";
	}

	public DataAdapterUI getPreferredUI() {
		FileAdapterUI ui = new FileAdapterUI();
		ui.setFont(Controller.getController().getDefaultFont());
		ui.setButtonColor(Preferences.defaultButtonColor(), Color.black);
		return ui;
	}

	public IOOperation[] getSupportedOperations() {
		IOOperation[] ops = { IOOperation.READ, IOOperation.WRITE };
		return ops;
	}
}

Manifest File

adapters%adapterCount=1
adapters%installAdapter0=org.myorganization.oboedit.MyAdapter

Download

Download toy_adapter_plugin.jar.

A Real, Simple Data Adapter

This is the code for the OBO Serial File Adapter, included with OBO-Edit. This adapter reads and writes compressed Java serial files:

package org.geneontology.oboedit.dataadapter;

import java.util.*;
import java.util.zip.*;
import org.geneontology.oboedit.datamodel.*;
import org.geneontology.oboedit.gui.*;
import org.geneontology.dataadapter.*;
import java.io.*;
import java.awt.Color;
import org.geneontology.util.*;
import org.geneontology.io.ProgressableInputStream;
import org.geneontology.io.IOUtil;

public class SerialAdapter implements OBOEditAdapter {

	protected String path;
	protected AdapterConfiguration config;
	protected ProgressableInputStream pfis;
	protected boolean cancelled = false;
	protected List listeners = new Vector();

	public void addProgressListener(ProgressListener listener) {
		listeners.add(listener);
	}

	public void removeProgressListener(ProgressListener listener) {
		listeners.remove(listener);
	}

	public void fireProgressEvent(ProgressEvent e) {
		for (int i = 0; i < listeners.size(); i++) {
			ProgressListener pl = (ProgressListener) listeners.get(i);
			pl.progressMade(e);
		}
	}

	public DataAdapterUI getPreferredUI() {
		FileAdapterUI ui = new FileAdapterUI();
		ui.setFont(Controller.getController().getDefaultFont());
		ui.setButtonColor(Preferences.defaultButtonColor(), Color.black);
		return ui;
	}

	public void cancel() {
		try {
			cancelled = true;
			if (pfis != null)
				pfis.close();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	public AdapterConfiguration getConfiguration() {
		return config;
	}

	public Object doOperation(IOOperation op, AdapterConfiguration oldconfig,
			Object o) throws DataAdapterException {
		this.config = oldconfig;
		cancelled = false;
		if (op.equals(IOOperation.READ)) {
			if (oldconfig instanceof FileAdapterConfiguration) {
				FileAdapterConfiguration config = (FileAdapterConfiguration) oldconfig;
				if (config.getReadPaths().size() == 1) {
					path = (String) config.getReadPaths().iterator().next();
					try {
						return getRoot();
					} catch (DataAdapterException ex) {
						if (cancelled)
							throw new CancelledAdapterException();
						else
							throw ex;
					}
				}
			}
			throw new DataAdapterException("Bad configuration");
		} else if (op.equals(IOOperation.WRITE)) {
			if (oldconfig instanceof FileAdapterConfiguration) {
				FileAdapterConfiguration config = (FileAdapterConfiguration) oldconfig;
				path = config.getWritePath();
				write((OBOSession) o);
				return o;
			} else
				throw new DataAdapterException("Bad configuration");
			// write((OBOSession) o);
		}
		return null;
	}

	public String getID() {
		return "OBOEDIT:Serial";
	}

	public String getName() {
		return "OBO-Edit Serial Adapter";
	}

	public IOOperation[] getSupportedOperations() {
		IOOperation[] supported = { IOOperation.WRITE, IOOperation.READ };
		return supported;
	}

	public OBOSession getRoot() throws DataAdapterException {
		try {
			pfis = IOUtil.getProgressableStream(path);
			pfis.addProgressListener(new ProgressListener() {
				public void progressMade(ProgressEvent e) {
					SerialAdapter.this.fireProgressEvent(e);
				}
			});
			ZipInputStream zipstream = new ZipInputStream(
					new BufferedInputStream(pfis));
			zipstream.getNextEntry();
			ObjectInputStream stream = new ObjectInputStream(zipstream);
			OBOSession history = (OBOSession) stream.readObject();
			history.setNeedsSave(false);
			history.setLoadRemark(IOUtil.getShortName(path));
			return history;
		} catch (Exception e) {
			throw new DataAdapterException(e, "Load error");
		}
	}

	public OBOSession write(OBOSession history) throws DataAdapterException {
		try {
			ZipOutputStream zipstream = new ZipOutputStream(
					new BufferedOutputStream(new FileOutputStream(path)));
			ZipEntry entry = new ZipEntry("main");
			zipstream.putNextEntry(entry);
			zipstream.setLevel(5);
			ReusableProgressEvent rpe = new ReusableProgressEvent(this);
			rpe.setFastVal(-1);
			rpe.setDescription("Writing file...");
			fireProgressEvent(rpe);
			ObjectOutputStream stream = new ObjectOutputStream(zipstream);
			stream.writeObject(history);
			stream.close();
			history.setNeedsSave(false);
			return history;
		} catch (Exception e) {
			throw new DataAdapterException(e, "Write error");
		}
	}

	public String getTermText(IdentifiedObject term)
			throws DataAdapterException {
		final StringBuffer buffer = new StringBuffer();
		OutputStream os = new OutputStream() {
			public void write(int b) {
				buffer.append((char) b);
			}
		};
		try {
			ObjectOutputStream stream = new ObjectOutputStream(os);
			stream.writeObject(os);
		} catch (IOException ex) {
		}
		return buffer.toString();
	}
}