Factory Tutorial

We are going to use FunctionFactory as an introduction to the GeoTools plug-in system.

In the Architecture we saw the difference between the core GeoTools library and plug-ins that contribute functionality. One thing that makes a plug-in work is the “Factory SPI” plug-ins system (this is actually a part of Java not something we made up).

Reference:

Review

Each plugin jar has:

  • META-INF/services folder

    The folder contains a list of files (one for each interface name)

  • The files contain a list of classes that implement that interface

This page is where we explain how this all works.

GeoTools is extended using the Factory and FactoryRegistry classes. The standard Factory Pattern gives us a clue about what is about to happen:

FACTORY PATTERN

a Factory is an object that creates other objects.

Here is where the fun begins …

Step 1 Interface

The first step is to define your interface; in this case we are going to use the Function interface already provided by GeoTools.

../_images/filter_function.PNG

GeoTools loves to work with interfaces, and this is the reason why (it is the first step of making a plug-in based library).

We are going to use Function as an example for this discussion:

public interface Function extends Expression {
    String getName();
    List<Expression> getParameters();
    Literal getFallbackValue();
}
public interface Expression {
    Object evaluate(Object object);
    <T> T evaluate(Object object, Class<T> context);
    Object accept(ExpressionVisitor visitor, Object extraData);
}
  1. Here is a quick implementation of snapping a point to a line:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.List;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.ExpressionVisitor;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.filter.capability.FunctionNameImpl;
    import org.geotools.util.Converters;
    import org.locationtech.jts.geom.Coordinate;
    import org.locationtech.jts.geom.Geometry;
    import org.locationtech.jts.geom.Point;
    import org.locationtech.jts.linearref.LinearLocation;
    import org.locationtech.jts.linearref.LocationIndexedLine;
    
    /**
     * Quick function that illustrates snapping to a line.
     *
     * @author jody
     */
    public class SnapFunction implements Function {
    
        static FunctionName NAME =
                new FunctionNameImpl(
                        "snap",
                        Point.class,
                        FunctionNameImpl.parameter("point", Point.class),
                        FunctionNameImpl.parameter("line", Geometry.class));
    
        private final List<Expression> parameters;
    
        private final Literal fallback;
    
        public SnapFunction(List<Expression> parameters, Literal fallback) {
            if (parameters == null) {
                throw new NullPointerException("parameters required");
            }
            if (parameters.size() != 2) {
                throw new IllegalArgumentException("snap( point, line) requires two parameters only");
            }
            this.parameters = parameters;
            this.fallback = fallback;
        }
    
        public Object evaluate(Object object) {
            return evaluate(object, Point.class);
        }
    
        public <T> T evaluate(Object object, Class<T> context) {
            Expression pointExpression = parameters.get(0);
            Point point = pointExpression.evaluate(object, Point.class);
    
            Expression lineExpression = parameters.get(1);
            Geometry line = lineExpression.evaluate(object, Geometry.class);
    
            LocationIndexedLine index = new LocationIndexedLine(line);
    
            LinearLocation location = index.project(point.getCoordinate());
    
            Coordinate snap = index.extractPoint(location);
    
            Point pt = point.getFactory().createPoint(snap);
    
            return Converters.convert(pt, context); // convert to requested format
        }
    
        public Object accept(ExpressionVisitor visitor, Object extraData) {
            return visitor.visit(this, extraData);
        }
    
        public String getName() {
            return NAME.getName();
        }
    
        public FunctionName getFunctionName() {
            return NAME;
        }
    
        public List<Expression> getParameters() {
            return parameters;
        }
    
        public Literal getFallbackValue() {
            return fallback;
        }
    }
    
  2. The mechanics of using a LocationIndexLine are covered in Snap a Point to a Line if you are interested.

  3. One thing to notice is the definition of FunctionName used to describe valid parameters to user of our new function.

    By convention we define this as a static final SnapFunction.NAME, this is however only a convention (which will help in implementing the next section).

Step 2 Factory

Interfaces are not allowed to have constructors, so anything that would been a constructor is defined as a Factory interface.

To continue with our example:

public interface FunctionFactory {
    List<FunctionName> getFunctionNames();
    Function function(String name, List<Expression> args, Literal fallback);
}

The factory above describes what functions are available, and allows for Function creation. So far everything looks normal. The above is exactly how the plain “Factory Pattern” usually works - hopefully it is familiar to you?

Note

Factories are named after the interface they are responsible for; thus FunctionFactory.

Variations:

  • Many factories just have a single create method (this is called the factory method) We have a couple of examples of this in GeoTools including DataStoreFactorySpi

  • Some factories have several create method allowing a compatible set of objects to be created together. We have a couple of examples of these abstract factories, such as FeatureTypeFactory.

Note

Some GeoTools factories extend the Factory interface, but this is optional. This Factory interface is useful only for factories that can be configured through a set of Hints.

To continue with our implementation we will define ExampleFunctionFactory:

  1. Create ExampleFunctionFactory implementing FunctionFactory

  2. Fill in the information as shown:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import org.geotools.api.feature.type.Name;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.feature.NameImpl;
    import org.geotools.filter.FunctionFactory;
    
    public class ExampleFunctionFactory implements FunctionFactory {
    
        public List<FunctionName> getFunctionNames() {
            List<FunctionName> functionList = new ArrayList<>();
            functionList.add(SnapFunction.NAME);
            return Collections.unmodifiableList(functionList);
        }
    
        public Function function(String name, List<Expression> args, Literal fallback) {
            return function(new NameImpl(name), args, fallback);
        }
    
        public Function function(Name name, List<Expression> args, Literal fallback) {
            if (SnapFunction.NAME.getFunctionName().equals(name)) {
                return new SnapFunction(args, fallback);
            }
            return null; // we do not implement that function
        }
    }
    
  3. We make reference to the static final SnapFunction.NAME.

    While we mentioned this as only a convention, you are free to create a new FunctionNameImpl("snap", "point", "line") as part of the getFunctionNames() method. This has the advantage of avoiding loading SnapFunction until a user requests it by name.

  4. We can now register our factory.

    Create the file:

    • META_INF/services/org.geotools.filter.FunctionFactory

  5. Fill in the following contents (one implementation class per line):

    org.geotools.tutorial.function.ExampleFunctionFactory
    
  6. That is it SnapFunction is now published!

Step 3 FactoryRegistry

GeoTools 2.2 uses javax.imageio.ServiceRegistry magic (where this plug-in system originated from). Please note that the FactoryRegistry will cache the factories already found. Since factories are stateless this should not be a problem.

Direct use of FactoryRegistry

  1. You can directly use FactoryRegistry in your own code:

    Set categories = Collections.singleton(new Class[] {FunctionFactory.class,});
    FactoryRegistry registry = new FactoryRegistry(categories);
    
    Iterator iterator = registry.getProviders(FunctionFactory.class);
    
  2. Internally The FactoryRegistry will look up key in System properties.

    • If key doesn’t exist or a SecurityException is thrown, fall through.

    • Otherwise attempt to instantiate the given class.

  3. Then FactoryRegistry will search the resource paths for the key in META-INF/services.

    • If the resource is found, the file is read and the class is instantiated.

    • If the resource does not exist, fall through.

  4. This means that FactoryRegistry will be able find any FunctionFactory that is provided on the CLASSPATH.

Note

GeoTools already has a FactoryRegistry for handling FunctionFactory, as part of CommonFactory finder. There is however nothing stopping you from using your own FactoryRegistry (other than wasting resources).

Defining your own FactoryFinder

It is noted that FactoryRegistry is not synchronized, to protect for this you can wrap the direct use up in a FactoryFinder, which also provide type-safety.

Note

Finders are named after the interface they are responsible for; thus FunctionFinder.

Here is an use of FactoryRegistry as part of FactoryFinder:

  1. Create the FactoryRegistry in a lazy fashion, listing the interfaces you are interested in obtaining (known as categories).

  2. GeoTools traditionally holds a FactoryRegistry in a “Finder” class:

    • Create ExampleFinder

  3. Fill in the following details:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.Set;
    import java.util.stream.Stream;
    import org.geotools.api.filter.FilterFactory;
    import org.geotools.filter.FunctionFactory;
    import org.geotools.util.LazySet;
    import org.geotools.util.factory.FactoryCreator;
    import org.geotools.util.factory.FactoryFinder;
    import org.geotools.util.factory.FactoryRegistry;
    import org.geotools.util.factory.Hints;
    
    public class ExampleFinder extends FactoryFinder {
    
        private static volatile FactoryCreator registry;
    
        private static FactoryRegistry getServiceRegistry() {
            assert Thread.holdsLock(ExampleFinder.class);
            if (registry == null) {
                Class<?>[] categories = new Class<?>[] {FunctionFactory.class};
                registry = new FactoryCreator(categories);
            }
            return registry;
        }
    
        /**
         * Returns a set of all available implementations for the {@link FilterFactory} interface.
         *
         * @param hints An optional map of hints, or {@code null} if none.
         * @return Set of available filter factory implementations.
         */
        public static synchronized Set<FunctionFactory> getFilterFactories(Hints hints) {
            hints = mergeSystemHints(hints);
            Stream<FunctionFactory> serviceProviders =
                    getServiceRegistry().getFactories(FunctionFactory.class, null, hints);
            return new LazySet<>(serviceProviders);
        }
    
        /** Allow the classpath to be rescanned */
        public static synchronized void scanForPlugins() {
            if (registry != null) {
                registry.scanForPlugins();
            }
        }
    }
    
  4. The above is an example only, please use FunctionFinder

Tips for implementing your own FactoryFinder:

  • The code example makes use of LazySet, this keeps us from having to check the classpath each time.

  • The utility method addDefaultHints is used to apply the global GeoTools configuration to the hints supplied by the user.

  • As shown above you can add some helper methods for client code. Often this is used to perform searches based on some criteria, or used to locate the “best” factory for a given task.

FactoryIteratorProviders

FactoryIteratorProviders is used to support other plugin mechanisms.

By default the “Factory SPI” mechanism is used to locate the Factories provided by a FactoryFinder (and FactoryRegistry). However in order to support other plugin mechanisms the factories has a method addFactoryIteratorProvider(...). This method allows a developer to add an iterator that knows how to process another extension mechanism. For example, in Eclipse one would add a FactoryIteratorProvider that returns a provider that knows how to process eclipse extension points and can create factories from the eclipse extensions.

Abstract

Now that we have helped client code make use of our interface, the next step is to provide an abstract class to help those developing an implementation.

Most GeoTools Factories are kind enough to give you an abstract super class to start your implementation efforts from. When making your own factories this is a good example to follow.

Note

By asking developers to extend an abstract class you can help protect them from any additional methods that are added to the interface in the future.

  1. Here is an example AbstractFunction to get a feel for what is involved.

    This is not part of GeoTools (yet) - it just shows the approach used:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.Collections;
    import java.util.List;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.ExpressionVisitor;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.util.Converters;
    
    public abstract class AbstractFunction implements Function {
        protected final FunctionName name;
        protected final List<Expression> params;
        protected final Literal fallback;
    
        protected AbstractFunction(FunctionName name, List<Expression> args, Literal fallback) {
            this.name = name;
            this.params = args;
            this.fallback = fallback;
        }
    
        public abstract Object evaluate(Object object);
    
        public <T> T evaluate(Object object, Class<T> context) {
            Object value = evaluate(object);
            return Converters.convert(value, context);
        }
    
        public Object accept(ExpressionVisitor visitor, Object extraData) {
            return visitor.visit(this, extraData);
        }
    
        public String getName() {
            return name.getName();
        }
    
        public FunctionName getFunctionName() {
            return name;
        }
    
        public List<Expression> getParameters() {
            return Collections.unmodifiableList(params);
        }
    
        public Literal getFallbackValue() {
            return fallback;
        }
        // helper methods
        <T> T eval(Object feature, int index, Class<T> type) {
            Expression expr = params.get(index);
            Object value = expr.evaluate(feature, type);
            return type.cast(value);
        }
    }
    
  2. Here is a sample use.

    Note we have cut down on the number of methods the developer needs to fill in, and we have provided a helper method to avoid some of the “boiler plate” cut and paste coding associated with evaluating a parameter:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import org.geotools.api.feature.type.Name;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.feature.NameImpl;
    import org.geotools.filter.FunctionFactory;
    import org.geotools.filter.capability.FunctionNameImpl;
    import org.locationtech.jts.geom.Coordinate;
    import org.locationtech.jts.geom.Geometry;
    
    public class ExampleFunctionFactory2 implements FunctionFactory {
        private ArrayList<FunctionName> functionList;
        private static FunctionName FIRST = new FunctionNameImpl("first", "geometry");
    
        public synchronized List<FunctionName> getFunctionNames() {
            if (functionList == null) {
                functionList = new ArrayList<>();
                functionList.add(FIRST);
            }
            return Collections.unmodifiableList(functionList);
        }
    
        public Function function(String name, List<Expression> args, Literal fallback) {
            return function(new NameImpl(name), args, fallback);
        }
    
        public Function function(Name name, List<Expression> args, Literal fallback) {
            if (new NameImpl("first").equals(name)) {
                return new AbstractFunction(FIRST, args, fallback) {
                    public Geometry evaluate(Object object) {
                        Geometry geom = eval(object, 0, Geometry.class);
                        Coordinate coordinate = geom.getCoordinate();
                        return geom.getFactory().createPoint(coordinate);
                    }
                };
            }
            return null; // we do not implement that function
        }
    }
    
  3. You can see how that would help in quickly banging out a set of functions.

Plugin Checklist

To allow clients to contribute a plugin

  1. Define an interface

    Example: Foo

  2. Define factory interface

    Example: FooFactory

  3. Define FactoryFinder

    Example: FooFactoryFinder

  4. Define an abstract class for implementers

    Example: AbstractFoo

To allow client code access to plug-ins

  1. Make your FactoryFinder public

    Example: FooFinder

When implementing a Plugin

  1. Create your implementation

    Example: MyFoo

  2. Create you extension factory

    Example: MyFooFactory

  3. Register with META-INF/services

    Example: META-INF/services/Foo