Skip to content

Workspace

tfr42 edited this page May 15, 2022 · 20 revisions

This page is the central entry point for documentation about the deegree workspace concept and implementation.

Shortcomings of old workspace concept (deegree 2.x)

The old workspace concept had a couple of shortcomings, which I'll describe here to give an understanding why a new implementation has been started.

Dependencies

The workspace has a concept of dependencies, but dependencies are only possible on a resource type level. At first glance, this is sufficient in most cases, but not all.

The first resource we came across where this was not sufficient any more was the caching tile store, which naturally depends on another tile store.

Thinking about it, a tile store implementation that automatically tiles a layer resource on the fly/whatever would be a natural thing. But since there's also a tile layer implementation, the layer subsystem already depends on the tile subsystem, introducing a dependency the other way round would introduce a cycle.

Restartability

Restarting a single resource is possible, but often has unknown/bad side effects on other resources. So if you restart your feature store (possibly changing the configuration) affects eg. the WFS, some feature layer, the WMS...

The only workable solution has been to just restart the whole workspace. While that's sufficient in many cases, it can be slow and time consuming, especially if many resources are configured.

If the exact dependency chains between resources were known, only affected resources could be restarted.

Static is bad

Some resource managers do things in a static context. While this is often sufficient in a webapp (where only a single workspace is active anyway), it's still bad practice and should be avoided. Looking at the infamous ConnectionManager, a rewrite is badly needed.

XML

The workspace is tightly bound to the XML formats which are used to configure resources. While this is not necessarily a bad thing, other scenarios still seem useful, where configurations are created by some other means (such as reading the config from a database). This is just not possible to do with the current workspace concepts.

Basic workspace concepts (deegree 3.x)

Resources

So, let's have a look at the basics. The workspace revolves around resources. A resource is an instance of a specific class of object deegree works with. Example: a feature store is a resource, a WFS is a resource, a layer is a resource, a JDBC connection is a resource.

Resources are grouped together by their type. So all feature stores are in a group, all services are in a group, all layers, all JDBC connections etc. The workspace is responsible to manage these resources, and provide access to them.

The workspace

The workspace manages resources, and provides access to them. That means that the lifecycle of resources is controlled by the workspace.

Typically your interaction with the workspace will be to initialize it, and to access resources contained within.

Resources are identified by ResourceIdentifier objects. This is a two part or qualified identifier, consisting of a string ID (usually the basename of the .xml configuration file) and a provider class.

We'll have a look at the provider classes later.

The lifecycle

In order to understand how the workspace works internally, you'll need to understand the lifecycle a resource goes through. We've got a couple of phases:

  • scanning
  • preparing
  • building
  • initializing

In the scanning phase, the workspace finds resources. This results in a bunch of ResourceMetadata objects. These metadata objects are uninitialized.

In the preparing phase, which operates on ResourceMetadata objects, the resources determine what they need to be built. This includes finding out what other resources they depend on. This results in the ResourceMetadata to be initialized, and a bunch of ResourceBuilder objects. After this phase the ResourceMetadata can be ordered in a graph, taking into account their dependencies.

In the building phase, the ResourceBuilder objects are used actually build Resource objects, the result is a bunch of uninitialized Resource objects.

In the initialization phase, the Resource objects are being initialized. After this, they can be properly used.

The bigger picture

This section introduces a couple of interfaces, and explains what implementations are responsible for.

Some interfaces have been introduced above in the lifecycle section.

ResourceLocation objects are an abstraction of where the configuration files live. Instead of URL or File objects they are used to obtain the actual configuration file content. This allows for other implementations that eg. pull the files off the net or so.

ResourceManager objects are responsible for initially creating ResourceMetadata objects during the scanning phase, and for knowing which ResourceProvider implementations are available.

ResourceProvider objects are used by the manager to actually create a ResourceMetadata object for a resource. For each type of resource, there must be an abstract class or interface that all concrete providers implement. This serves as an SPI extension point (so the resource manager knows who can create metadata objects for a given location). The abstract ResourceProvider for a specific type of resource is also used to qualify the ResourceIdentfier objects. While the providers are usually not very big, they're still very central to how the workspace works.

Static initialization

There are two new SPI interfaces, Initializable and Destroyable. They define #init(Workspace) and #destroy(Workspace) respectively. They can be used to initialize things that need to be done statically on a JVM global basis, such as initialization of the proxy settings (which need to be VM global).

Their use is somewhat discouraged (use proper resource managers where possible!), but sometimes unavoidable (like the proxy settings) or hard to fix (like the CRS subsystem which needs a rewrite anyway).

Initializables are initialized upon workspace startup (before any resources are initialized), Destroyables are destroyed upon workspace destruction (after all resources are destroyed).

Since there was no need yet, there is no dependency concept for these extension points and they may be executed in any random order.

Initializable and Destroyable can be used independently from each other, the SPI extension points are org.deegree.workspace.Initializable and org.deegree.workspace.Destroyable respectively.

How to work with the workspace

This section describes how you can work with the workspace. The first subsection shows how you can work with resources. The second subsection describes how a new resource can be implemented, the third subsection describes how a new resource type can be implemented. The last subsection shows you how to use the ResourceGraph.

If you're looking for random bits check out the WorkspaceUtils class (in the org.deegree.workspace package). While it contains some useful shortcut methods to achieve simple tasks, its code also demonstrates how to manually work with the workspace.

Working with resources

First, you'll need a workspace:

Workspace workspace = new DefaultWorkspace( "/path/to/workspace" );

Then you'll need to initialize the resources:

workspace.initAll();

Now you're ready to go:

FeatureStore fs = workspace.getResource( NewFeatureStoreManager.class, "myfeature" ); ConnectionProvider prov = workspace.getResource( ConnectionProviderProvider.class, "postgresonsecundum" );

As you can see, the workspace doesn't explicitly want a ResourceIdentifier, but still requests the two identifier parts. Just give him the base provider class of the resource you'd like, and the actual ID.

How to implement a new resource (e.g. a new feature store or a new OGC web service)

To implement a new resource, it's advisable to browse through existing implementations of the same resource, e.g. if you want to implement a new feature store, check out the SimpleSQLFeatureStore implementation (module deegree-featurestore-simplesql).

You typically need at least four new classes to implement a new resource that automatically plugs into the workspace:

  1. A ResourceProvider implementation: Invoked by the Workspace to create ResourceMetadata objects
  2. A ResourceMetadata implementation: Provides information on the required dependencies (depending on the configuration)
  3. A ResourceBuilder for the new Resource: Creates a Resource instances from a JAXB object (the XML configuration)
  4. The actual Resource class that implements the respective Resource subinterface (e.g. FeatureStore)

The first is the implementation of your ResourceProvider. This should be easy enough. When implementing #createFromLocation you'll notice that you'll need a ResourceMetadata implementation. There's really not much besides the metadata instantiation that needs to be done here. Don't forget to add file META-INF/services/my.resource.provider.package.MyResourceProvider with the fully qualified class name of the provider in it (the workspace learns about available providers using the classic Java SPI mechanism).

The ResourceMetadata implementation should also not provide a lot of trouble. The only thing to keep in mind is that you'll need to figure out the dependencies of your resource during #prepare. In order to make life easy, just extend the AbstractResourceMetadata class and add your dependencies to the dependencies field. If you have dependencies that need to be initialized before your resource but are not crucial, you can add them to the softDependencies field. This will make sure they are initialized before your resource, but allows your resource to be initialized even if they fail. Make sure to check for null before using them!

While implementing #prepare, you'll realize you also need a ResourceBuilder. At this stage, you'll probably still have the JAXB parsed configuration object. The idea is that you deconstruct the JAXB stuff and construct a proper Resource object during #build. Make sure you pass the ResourceMetadata object to the resource.

Last but not least, of course you'll need a Resource implementation. What that means exactly, is defined by the respective resource type (e.g. FeatureStore) and your concrete implementation. Just make sure the #getMetadata returns the proper metadata object (passed down during initialization), and you clean up after yourself in #destroy.

How to implement a new resource type (e.g. a new kind of datastore)

To implement a new resource type, it's generally advisable to have a look at existing code. For a new resource type, you typically need two classes, a new ResourceManager and a new abstract ResourceProvider.

In order to make life simple, just extend DefaultResourceManager. Add a default constructor, in which you call the super constructor like this:

public class MyResourceManager extends DefaultResourceManager<MyResource> {

  public MyResourceManager() {
        super( new DefaultResourceManagerMetadata<MyResource>( MyResourceProvider.class, "my resources",
                                                               "datasources/myresources" ) );
  }

}

Extend the AbstractResourceProvider to have the marker provider class:

public abstract class MyResourceProvider extends AbstractResourceProvider<MyResource> {
}

Don't forget to actually add your Resource interface:

public interface MyResource extends Resource {
  // add fancy methods
}

Then don't forget to add a META-INF/services/org.deegree.workspace.ResourceManager file containing your fully qualified ResourceManager class name (the workspace finds out about resource types via Java SPI).

That's all you need! If you have implementations of your resource provider on the class path, they'll get loaded automatically, and any resources your provider wants to handle will be initialized automatically.

The resource graph

If you have some metadata objects and want to see how they are ordered dependency wise, you can construct a ResourceGraph:

ResourceGraph graph = new ResourceGraph();

You can also construct a graph with a bunch of ResourceMetadata objects:

List<ResourceMetadata<? extends Resource>> list = new ArrayList<ResourceMetadata<?>>();
list.add( md1 );
list.add( md2 );
// ... add more
ResourceGraph graph = new ResourceGraph( list );

You can then examine the graph, or add more nodes:

ResourceMetadata<? extends Resource> metadata = ...;
graph.insertNode( metadata );
ResourceNode<? extends Resource> node = graph.getNode( metadata.getIdentifier() );

A ResourceNode provides access to its dependencies, soft dependencies and nodes which directly depend on this resource, the dependents.

A ResourceGraph can be flattened by using the #toSortedList method:

List<ResourceMetadata<? extends Resource>> sorted = graph.toSortedList();

This gives you an ordered list, where resources without dependencies are in the front. Every metadata entry is always preceded by its dependencies.

Further development (deegree 3.5/4.0)

About the further development of the workspace, see WorkspaceNG.

Clone this wiki locally