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:
http://download.oracle.com/javase/tutorial/sound/SPI-intro.html
http://gsraj.tripod.com/design/creational/factory/factory.html
http://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html
Review¶
Each plugin jar has:
META-INF/services
folderThe 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.
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);
}
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; } }
The mechanics of using a
LocationIndexLine
are covered in Snap a Point to a Line if you are interested.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
:
Create
ExampleFunctionFactory
implementingFunctionFactory
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 } }
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 thegetFunctionNames()
method. This has the advantage of avoiding loadingSnapFunction
until a user requests it by name.We can now register our factory.
Create the file:
META_INF/services/org.geotools.filter.FunctionFactory
Fill in the following contents (one implementation class per line):
org.geotools.tutorial.function.ExampleFunctionFactory
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¶
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);
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.
Then
FactoryRegistry
will search the resource paths for the key inMETA-INF/services
.If the resource is found, the file is read and the class is instantiated.
If the resource does not exist, fall through.
This means that
FactoryRegistry
will be able find anyFunctionFactory
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
:
Create the
FactoryRegistry
in a lazy fashion, listing the interfaces you are interested in obtaining (known as categories).GeoTools traditionally holds a
FactoryRegistry
in a “Finder” class:Create
ExampleFinder
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(); } } }
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.
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); } }
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 } }
You can see how that would help in quickly banging out a set of functions.
Plugin Checklist¶
To allow clients to contribute a plugin
Define an interface
Example:
Foo
Define factory interface
Example:
FooFactory
Define
FactoryFinder
Example:
FooFactoryFinder
Define an abstract class for implementers
Example:
AbstractFoo
To allow client code access to plug-ins
Make your
FactoryFinder
publicExample:
FooFinder
When implementing a Plugin
Create your implementation
Example:
MyFoo
Create you extension factory
Example:
MyFooFactory
Register with
META-INF/services
Example:
META-INF/services/Foo