How to write a new driver

From OpenJUMP Wiki
Jump to navigation Jump to search

Here are some explanations for developers who need to write a driver to read/write a new file format.

Old api vs new api

There is an old api in com.vividsolutions.jump.io, with the JUMPReader and JUMPWriter classes, but developpers are encouraged to use new new api, with two main classes :

  • com.vividsolutions.jump.io.datasource.DataSource
  • com.vividsolutions.jump.io.datasource.Connection

DataSource or DataStore?

  • com.vividsolutions.jump.io.datasource package is for file data sources, while
  • com.vividsolutions.jump.datastore is for database access.

Datasource related classes are spread across several packages

1 - The driver classes

In com.vividsolutions.jump.io.datasource package, you will find the main classes to write a driver :

  • DataSource will store the datasource properties (ex. file name) and return a Connection to this DataSource
  • Connection interface which has to be implemented by a class which will have the responsability to return FeatureCollections or to execute updates
  • ReaderWriterFileDataSource is a special DataSource created from a JUMPReader (and a JUMPWriter). It is used as a bridge between the old and the new api.
  • DataSourceQuery : a small wrapper including a string representing a query and the source to apply it against.

2 - The UI stuff

com.vividsolutions.jump.workbench.datasource contains all the UI elements :

  • The DataSourceQueryChooserDialog with its getCurrentChooser method returning an implementation of DataSourceQueryChooser.
  • DataSourceQueryChooser is the UI for picking datasets for a given format. It produces DataSourceQueries each of which encapsulates a query string and the DataSource to run it against. A partial implementation is FileDataSourceQueryChooser which has two subclasses, one for file loading (LoadFileDataSourceQueryChooser) and the other one for file saving (SaveFileDataSourceQueryChooser)

3 - PlugIns

The plugins related to driver installation are also in com.vividsolutions.jump.workbench.datasource package.

  • There is a plugin for the installation of all the standard drivers called InstallStandardDataSourceQueryChooserPlugIn.
  • There is a hierarchy of plugins which parent is AbstractLoadSaveDatasetPlugIn and which concrete implementations are LoadDatasetFromFilePlugIn and SaveDatasetAsFilePlugIn

And now let's write our first driver

In this paragraph, we'll write a very simple driver to load xyz data files. Main characteristics of the file format and the parser characteristics are as follows :

  • each line represents a point,
  • each line contains fields separated by a whitespace, a tabulation, a comma, a semi-column,
  • the x field is in one of the 6 first columns (always the same clumn),
  • the y field follows the x field
  • the line contains an optional z field
  • every field preceding the x field is kept as a string attribute,
  • every field following the y or the z field is ignored

1 - The driver installer PlugIn

import java.io.File;
import java.util.Map;
import java.util.HashMap;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JComboBox;
import com.vividsolutions.jump.workbench.datasource.*;
import com.vividsolutions.jump.util.Blackboard;
import com.vividsolutions.jump.workbench.plugin.PlugInContext;

/**
 * PlugIn installing a driver to read XYZ data files.
 * @author Michaël Michaud
 * @version 0.1 (2007-04-29)
 */
public class InstallXYZDataSourceQueryChooserPlugIn extends
             InstallStandardDataSourceQueryChoosersPlugIn {
   
   public static final String INDEX = "INDEX";
   
   /**
    * PlugIn initialization.
    */
    public void initialize(final PlugInContext context) throws Exception {
        Blackboard blackboard = context.getWorkbenchContext().getWorkbench().getBlackboard();
        final String description = "XYZ (plain text)";
        final JComboBox jcb = new JComboBox(new Object[]{"1","2","3","4","5","6"});
        
        DataSourceQueryChooserManager.get(blackboard).addLoadDataSourceQueryChooser(
            new LoadFileDataSourceQueryChooser(XYZDataSource.class,
                                               description,
                                               new String[] { "", "xyz", "txt", "asc" },
                                               context.getWorkbenchContext()) {
                
                // Put the x column index in the properties map
                protected Map toProperties(File file) {
                    Map properties = new HashMap(super.toProperties(file));
                    JPanel panel = getSouthComponent1();
                    properties.put(INDEX, ((JComboBox)panel.getComponent(1)).getSelectedItem().toString());
                    return properties;
                }
                
                // Create a component to select the comlumn index of xyz data
                protected JPanel getSouthComponent1() {
                    JPanel southComponent = new JPanel();
                    southComponent.add(new JLabel("Index of X column"));
                    southComponent.add(jcb);
                    return southComponent;
                }
            }
        );
    }

}

This class extends InstallStandardDataSourceQueryChoosersPlugIn, which is used to install shapefile driver, gml driver...

InstallXYZDataSourceQueryChooserPlugIn adds a special widget to the interface to select the index of the X column (the Y and Z columns are supposed to be just after the X column).

The widget is a final JComboBox initialized during the installer initialization, and is made visible in the southComponent of the file chooser dialog.

The toProperties method of the LoadFileDataSourceQueryChooser put this column index in the properties Map used by the special XYZDataSource.

2 - The XYZ DataSource

Now, let's write the class doing the job. XYZDataSource extends DataSource and has to return an implementation of the Connection interface.

For a read-only driver as our XYZ driver, the Connection method to averload is :

public FeatureCollection executeQuery(String query, Collection exceptions, TaskMonitor monitor)

Here is the piece of code which will return a Connection able to parse the XYZ file:

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.io.BufferedReader;
import java.io.FileReader;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jump.feature.*;
import com.vividsolutions.jump.io.datasource.*;
import com.vividsolutions.jump.task.TaskMonitor;

import org.apache.log4j.Logger;

/**
 * XYZ Data Source
 * @author Michael MICHAUD
 * @version 0.1 (2007-04-29)
 */
public class XYZDataSource extends DataSource {
    
    private static final Logger LOG = Logger.getLogger(XYZDataSource.class);
    
    /**
     * Creates a new Connection to this DataSource.
     */
    public Connection getConnection() {
        
        try {
            
            return new Connection() {
                
                public FeatureCollection executeQuery(String query, Collection exceptions, TaskMonitor monitor) {
                    BufferedReader br = null;
                    try {
                        File file = new File(getProperties().get(FILE_KEY).toString());
                        int index = new Integer(getProperties().get(InstallXYZDataSourceQueryChooserPlugIn.INDEX).toString()).intValue();
                        br = new BufferedReader(new FileReader(file));
                        String line;
                        FeatureSchema fs = new FeatureSchema();
                        fs.addAttribute("GEOMETRY", AttributeType.GEOMETRY);
                        for (int i = 0 ; i < index ; i++) {
                            fs.addAttribute("Attribute_"+i, AttributeType.STRING);
                        }
                        GeometryFactory gf = new GeometryFactory();
                        FeatureCollection coll = new FeatureDataset(fs);
                        while(null != (line = br.readLine())) {
                            if (line.trim().length()==0) continue;
                            String[] ss = line.split("(\\s|,|;|\\|)");
                            try {
                                double x = Double.parseDouble(ss[index-1]);
                                double y = Double.parseDouble(ss[index]);
                                double z = ss.length > (index+1) ? Double.parseDouble(ss[index+1]) : Double.NaN;
                                BasicFeature bf = new BasicFeature(fs);
                                bf.setGeometry(gf.createPoint(new Coordinate(x,y,z)));
                                for (int i = 0 ; i < index ; i++) {
                                    bf.setAttribute("Attribute_"+i, ss[i]);
                                }
                                coll.add(bf);
                            }
                            catch(Exception e) {
                                LOG.debug("Error reading XYZ file: " + line, e);
                            }
                        }
                        br.close();
                        return coll;
                    } catch (Exception e) {
                        LOG.warn("Error executing query \"" + query + "\"", e);
                        exceptions.add(e);
                        return null;
                    }
                }
                
                public void executeUpdate(String update,
                                          FeatureCollection featureCollection,
                                          TaskMonitor monitor) throws Exception {
                    throw new Exception("Update is not authorized for this DataSource");
                }
                
                public void close() {}
                
                public FeatureCollection executeQuery(String query, TaskMonitor monitor) throws Exception {
                    ArrayList exceptions = new ArrayList();
                    FeatureCollection featureCollection = executeQuery(query, exceptions, monitor);
                    if (!exceptions.isEmpty()) {
                        throw (Exception) exceptions.iterator().next();
                    }
                    return featureCollection;
                }
            };
        }
        catch(Exception e) {
            LOG.warn("Error trying to connect to a GeoConcept Data Source", e);
            return null;
        }
    }
    
    public boolean isWritable() {
        return false;
    }

    
}

If you want to add a 'save as' driver, you'll have to implement the _executeUpdate_ method of the Connection

For more complex drivers, it may be useful to create special classes as JUMPReaders and/or JUMPWriters.

3 - The Driver Extension

The following class, derived from Extension, will transform the PlugIn into an extension, which is like an external PlugIn :

import com.vividsolutions.jump.workbench.plugin.Extension;
import com.vividsolutions.jump.workbench.plugin.PlugInContext;

/**
 * Configuration file for xyz driver extension.
 * @version 0.1 (2007-04-29)
 */
public class XYZDriverConfiguration extends Extension {
    public void configure(PlugInContext context) throws Exception {
        new InstallXYZDataSourceQueryChooserPlugIn().initialize(context);
    }
    public String getName() {return XYZdriver(read-only)";}
    public String getVersion() {return "0.1 (2007-04-29)";}
}

Hope that example will help you to write your own driver. I wrote this small tutorial because it gave me headache to understand the api and write a driver for the GeoConcept format. If you find errors or want to improve this tutorial, please, feel free to do it.