GeoTools : SerializableFeatures

Serializable Features: How to Transfer Feature Objects

The following classes provide Serializable wrappers around Feature and type objects. There are some comments in Geotools data classes like "// TODO consider serialization". But just marking Feature Serializable would require a fair amount of work and might cause some problems. A Feature references a FeatureType, which references many AttributeTypes, which contain things like GeometryFactories. And do we really want to send a GeometryFactory object when we just want to transfer a Feature? The following classes crystalize what's important about each of these objects to make serialization nearly pain-free. (Note: GisException is a subclass of RuntimeException which allows the to ignore the particulars of failure. You can write one yourself or you can handle the various catch blocks however you like.)

GisSerialization.java
import java.io.*;
import java.net.URI;
import java.util.*;

import org.geotools.data.*;
import org.geotools.feature.*;
import org.geotools.referencing.FactoryFinder;
import org.geotools.referencing.crs.EngineeringCRS;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;


/**
 * Serializable classes to wrap Feature and FeatureType objects.
 */
public class GisSerialization {
    
    /**
     * Serializable wrapper around a FeatureCollection.  May only contain
     * Features of one FeatureType. 
     */
    public static class FeatureCollectionTransporter implements Serializable {
        FeatureTypeTransporter featureTypeTransporter;
        Collection featureTransporters = new LinkedList();
        
        /**
         * @param featureTypeTransporter The FeaturType for this collection.
         */
        public FeatureCollectionTransporter(FeatureTypeTransporter featureTypeTransporter) {
            this.featureTypeTransporter = featureTypeTransporter;
        }
        
        /**
         * Add a feature to this collection.  It will be wrapped in a FeatureTransporter.
         * @param feature
         */
        public void addFeature(Feature feature) {
            featureTransporters.add(new FeatureTransporter(feature));
        }
        
        /**
         * @param features Will all be added in FeatureTransporter wrappers.
         */
        public void addFeatures(FeatureCollection features) {
            for (FeatureIterator iter = features.features(); iter.hasNext();) {
                Feature element = iter.next();
                addFeature(element);
            }
        }
        
        /**
         * @param reader Features will all be added in FeatureTransporter wrappers.
         * @throws GisException if reader throws an exception.
         */
        public void addFeatures(FeatureReader reader) throws GisException {
            try {
                while (reader.hasNext()) {
                    addFeature(reader.next());
                }
            } catch (NoSuchElementException e) {
                throw new GisException(e);
            } catch (IOException e) {
                throw new GisException(e);
            } catch (IllegalAttributeException e) {
                throw new GisException(e);
            }
        }
        
        /**
         * @param results Features will all be added in FeatureTransporter wrappers.
         * @throws GisException if results throws an exception.
         */
        public void addFeatures(FeatureResults results) throws GisException {
            try {
                addFeatures(results.reader());
            } catch (GisException e) {
                throw new GisException(e);
            } catch (IOException e) {
                throw new GisException(e);
            }
        }
        
        /**
         * @return An unwrapped FeatureCollection.
         * @throws GisException if there's a bug.
         */
        public FeatureCollection toFeatureCollection() throws GisException {
            try {
                FeatureType featureType = featureTypeTransporter.toFeatureType();
                FeatureCollection collection = FeatureCollections.newCollection();
                for (Iterator iter = featureTransporters.iterator(); iter.hasNext();) {
                    FeatureTransporter element = (FeatureTransporter) iter.next();
                    collection.add(element.toFeature(featureType));
                }
                return collection;
            } catch (NoSuchElementException e) {
                throw new GisException(e);
            }
        }
    }
    
    /**
     * Serializable wrapper around a FeatureType.
     */
    public static class FeatureTypeTransporter implements Serializable {
        String name;
        URI namespaceURI;
        AttributeTypeTransporter[] attributeTypeTransporters;
        
        /**
         * @param featureType The type to wrap.
         */
        public FeatureTypeTransporter(FeatureType featureType) {
            name = featureType.getTypeName();
            namespaceURI = featureType.getNamespaceURI();
            AttributeType[] attributeTypes = featureType.getAttributeTypes();
            attributeTypeTransporters = new AttributeTypeTransporter[attributeTypes.length];
            for (int i = 0; i < attributeTypes.length; i++) {
                attributeTypeTransporters[i] = new AttributeTypeTransporter(attributeTypes[i]);
            }
        }
        
        /**
         * @return An unwrapped FeatureType.
         * @throws GisException if there's a bug.
         */
        public FeatureType toFeatureType() throws GisException {
            try {
                FeatureTypeFactory factory = new DefaultFeatureTypeFactory();
                factory.setName(name);
                factory.setNamespaceURI(namespaceURI);
                for (int i = 0; i < attributeTypeTransporters.length; i++) {
                    AttributeType attributeType = attributeTypeTransporters[i].toAttributeType();
                    factory.addType(attributeType);
                    if (attributeType.isGeometry() && factory.getDefaultGeometry() == null) {
                        factory.setDefaultGeometry((GeometryAttributeType) attributeType);
                    }
                }
                return factory.getFeatureType();
            } catch (SchemaException e) {
                throw new GisException(e);
            } catch (NullPointerException e) {
                throw new GisException(e);
            } catch (IllegalArgumentException e) {
                throw new GisException(e);
            } catch (NoSuchElementException e) {
                throw new GisException(e);
            }
        }
    }
    
    /**
     * Serializable wrapper around an AttributeType.
     */
    public static class AttributeTypeTransporter implements Serializable {
        String name;
        String className;
        boolean isNillable;
        int fieldLength;
        Object defaultValue;
        String wkt;
        
        /**
         * @param attributeType The type to wrap.
         */
        public AttributeTypeTransporter(AttributeType attributeType) {
            name = attributeType.getName();
            className = attributeType.getType().getName();
            isNillable = attributeType.isNillable();
            fieldLength = attributeType.getFieldLength();
            defaultValue = attributeType.createDefaultValue();
            if (attributeType.isGeometry()) {
                GeometryAttributeType geomType = (GeometryAttributeType) attributeType;
                if (geomType.getCoordinateSystem() == null) {
                    wkt = "";
                } else {
                    wkt = ((GeometryAttributeType) attributeType).getCoordinateSystem().toWKT();
                }
            }
        }
        
        /**
         * @return An unwrapped AttributeType.
         * @throws GisException if there's a bug.
         */
        public AttributeType toAttributeType() throws GisException {
            try {
                if (wkt != null) {  // geometry attribute
                    CoordinateReferenceSystem crs;
                    if ("".equals(wkt)) {
                        crs = EngineeringCRS.GENERIC_2D;
                    } else {
                        crs = FactoryFinder.getCRSFactory().createFromWKT(wkt);
                    }
                    return AttributeTypeFactory.newAttributeType(name, Class.forName(className), isNillable, fieldLength, defaultValue, crs);
                }  // else it's an ordinary attribute
                return AttributeTypeFactory.newAttributeType(name, Class.forName(className), isNillable, fieldLength, defaultValue);
            } catch (FactoryException e) {
                throw new GisException(e);
            } catch (ClassNotFoundException e) {
                throw new GisException(e);
            }
        }
    }
    
    /**
     * Serialzable wrapper around Features.
     */
    public static class FeatureTransporter implements Serializable {
        String featureId;
        Object[] attributes;
        
        /**
         * @param feature Will be wrapped.
         */
        public FeatureTransporter(Feature feature) {
            featureId = feature.getID();
            attributes = feature.getAttributes(null);
        }
        
        /**
         * @param featureType The interpretation of this Feature.
         * @return An unwrapped feature.
         * @throws GisException If the FeatureType is incompatible with the data.
         */
        public Feature toFeature(FeatureType featureType) throws GisException {
            try {
                return featureType.create(attributes, featureId);
            } catch (IllegalAttributeException e) {
                throw new GisException(e);
            }
        }
    }

Usage

Suppose you have two programs which communicate via serialization (RMI, for instance). To transfer data from one to the other, all you have to do (assuming sendSerializedObject() serializes and sends to a receiveSerializedObject() method which calls receiveFeatures and assuming processFeatureCollection does something interesting) is:

Sender.java
public void sendFeatures(FeatureResults features) {
    GisSerialization.FeatureCollectionTransporter transporter = new GisSerialization.FeatureCollectionTransporter(features.getSchema());
    transporter.addFeatures(features);
    sendSerializedObject(transporter);
}
Receiver.java
public void receiveFeatures(GisSerialization.FeatureCollectionTransporter transporter) {
    FeatureCollection collection = transporter.toFeatureCollection();
    processFeatureCollection(collection);
}

You can also send individual Features, FeatureTypes, and AttributeTypes. So one program could ask another for a FeatureType independently of any features.

Some brief tests suggest that this methodology is up to 10 times faster than generating GML, saving it, and parsing it. (Which is admittedly slower than WFS, but can be done without a web container.)