March 10, 2012

CSW Server Implementation (Part II)

After last time introduction to GeoSIK and CSW, now is the time to implement our first CSW server. We will setup the basic infrastructure for our application and implement a basic service. A more complete (and useful) implementation will be shown in a future post.
For this tutorial you will need Visual Studio 2010 (Express Edition should be enough), NuGet Package Manager 1.6.1 and GeoSIK 0.9 Preview 1. This latest package is just a zip containing the GeoSIK binaries in a preview version that seems usable in a tutorial like this one. Proper releases will be delivered as NuGet packages.

The application

What we need first is an ASP .NET Application that will host our CSW service:

image
Then we will add the needed references. Add the following NuGet packages: LinqToXsd 2.0.2, Common.Logging 2.0.0 and Json.NET 4.0.8. Then add the following assemblies from the GeoSIK Preview package: GeoSik.dll, GeoSik.Services.dll, Irony.dll, Irony.Interpreter.dll, ProjNet.dll (you should recognize most of the dependencies I introduced in the previous post).

The service (I)

Then we need a WCF Service named Csw.svc:

image
Change the definition of the ICsw interface to the following:
[ServiceContract]
public interface ICsw:
GeoSik.Ogc.WebCatalog.Csw.V202.IDiscovery
{
}
Easy, right? Now implement the associated Csw class. You should have 5 methods: make them throw NotImplementedExceptions for now, we’ll implement them later.
public class Csw:
ICsw
{

public DescribeRecordResponse DescribeRecord(DescribeRecord request)
{
throw new NotImplementedException();
}

public Capabilities GetCapabilities(GetCapabilities request)
{
throw new NotImplementedException();
}

public GetDomainResponse GetDomain(GetDomain request)
{
throw new NotImplementedException();
}

public GetRecordByIdResponse GetRecordById(GetRecordById request)
{
throw new NotImplementedException();
}

public IGetRecordsResponse GetRecords(GetRecords request)
{
throw new NotImplementedException();
}
}
Now configure the web service by editing the Web.config file in a usual way. We will provide the CSW service in a POST+XML fashion (cf. specification §10.3.1). The GET+KVP encoding will be provided later. For this service to be fully CSW compliant, we will need to specify a custom behavior so that exceptions will be returned in XML too (cf. specification §10.3.5). This behavior is defined in the GeoSik.Services assembly, so the configuration reads as follows:
<system.serviceModel>
<services>
<service name="Tutorial.Csw" behaviorConfiguration="ows100PoxBehavior">
<endpoint address="xml" contract="Tutorial.ICsw" binding="webHttpBinding" behaviorConfiguration="poxBehavior" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="poxBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="ows100PoxBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="True" />
<ows100PoxFaultBehavior />
</behavior>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="ows100PoxFaultBehavior" type="GeoSik.Ogc.Ows.V100.PoxFaultBehaviorExtensionElement, GeoSik.Services" />
</behaviorExtensions>
</extensions>
</system.serviceModel>

Nothing fancy here if you are used to WCF services configurations: our service endpoint is defined.


The data source

Then we need to define a data source for our CSW server: our metadata store. This will usually be a database. In the context of this post though, we will define our metadata statically in memory. The link to a real database will be the subject of a following post.

What we need first is define a class that contains our metadata (a record, in cswspeak). To keep things simple, we will only support the basic, Dublin Core based, information like an identifier, a title… What we need to take into account, though, is that a CSW client can query our metadata in 2 ways:


  1. with an alias (a core queryable, cf. specification §6.3.2). These will be handled thanks to the GeoSik.Ogc.WebCatalog.Csw.V202.CoreQueryableAttribute that can be applied to any property.
  2. with an XPath expression (cf. specification §10.6.4.9). To that effect, our record class has to be serializable in XML with the XmlSerializer. Do not worry: it will never be serialized this way. But if it were, the GeoSik XPath resolver would be able to find the nodes according to the resulting XML. Thus we will use the usual XML serialization attributes to customize the serialization.
Then our class has to implement the GeoSik.Ogc.WebCatalog.Csw.V202.IRecord interface. The only method in this interface resulting class must create a GeoSik.Ogc.WebCatalog.Csw.V202.IRecordConverter interface. This is because directly serializing our record in XML is not enough to have a fully compliant CSW server: a client can cherry pick the piece of data it wants returned. This is the role of a record converter. For now, just return an instance of the GeoSik.Ogc.WebCatalog.Csw.V202.RecordConverter class. Our class looks like the following:
[XmlRoot("Record", Namespace=Namespaces.OgcWebCatalogCswV202, IsNullable=false)]
public class Record:
IRecord
{

[XmlElement("identifier", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=0, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Identifier)]
public string Identifier { get; set; }

[XmlElement("title", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=1, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Title)]
public string Title { get; set; }

[XmlElement("subject", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=2, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Subject)]
public string Subject { get; set; }

[XmlElement("BoundingBox", Namespace=Namespaces.OgcOws, Order=3, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.BoundingBox)]
public ISimpleGeometry Coverage { get; set; }

[XmlElement("AnyText", Namespace=Namespaces.OgcWebCatalogCswV202, DataType="string", Order=4, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.AnyText)]
public string AnyText
{
get
{
return string.Join(" ", Identifier, Title, Subject);
}
}

public IRecordConverter GetConverter(XmlNamespaceManager namespaceManager)
{
return new RecordConverter(namespaceManager);
}
}
Notice how the virtual AnyText element is a simple combination of the text elements of our record.

Our data source for this tutorial will simply be a manually created list of Record instances. But, you may ask, how do I initialize the Coverage property, which is a ISimpleGeometry? First of all: congratulations if you noticed that! The handling of geometries in GeoSik is way beyond this tutorial, so for now let me just show you an easy way to create geometries from their WKT definitions:
public static ISimpleGeometry CreateGeometry(string wkt)
{
var builder=new GeoSik.Ogc.Gml.V311.GmlGeometryBuilder();
builder.Parse(wkt, ProjNet.CoordinateSystems.GeographicCoordinateSystem.WGS84);
return builder.ConstructedGeometry;
}
We will come back to this in another post. And I will let you initialize your Record instances as an exercise.


The service (II)

Let’s do this thing! What we need now is to plug our previous data source into the GeoSik CSW base implementation. For this we need to implement the abstract GeoSik.Ogc.WebCatalog.Csw.V202.Discovery class (the name of this class is a reference to the class defined in the specification §7.2.4). As you will see, there are only 2 methods to implement, and they are not overly difficult:
public class Discovery:
GeoSik.Ogc.WebCatalog.Csw.V202.Discovery
{

protected override IQueryable GetRecordsSource(Uri outputSchema)
{
return _RecordList.AsQueryable();
}

public override string ProviderName
{
get { return "CSW Tutorial"; }
}

private static List<Record> _RecordList=new List<Record>();
}
Obviously, you would have to properly initialize a list of records: this data source is pretty empty…

Now we can implement the Csw service class that we partially implemented sooner. I will show you all of it now:
public class Csw:
ICsw
{

public DescribeRecordResponse DescribeRecord(DescribeRecord request)
{
return _Implementation.DescribeRecord(request);
}

public Capabilities GetCapabilities(GetCapabilities request)
{
return _Implementation.GetCapabilities(request);
}

public GetDomainResponse GetDomain(GetDomain request)
{
return _Implementation.GetDomain(request);
}

public GetRecordByIdResponse GetRecordById(GetRecordById request)
{
return _Implementation.GetRecordById(request);
}

public IGetRecordsResponse GetRecords(GetRecords request)
{
return _Implementation.GetRecords(request);
}

private Discovery _Implementation=new Discovery();
}

The server

That’s it: our server is complete. Granted, there are a few drawbacks:


  • only the POST+XML protocol is supported, which makes it quite hard to test unless you have a tool like Fiddler (and you know how to use it).
  • if you manage to launch a GetCapabilities request (the address of the service is something like ~/Csw.svc/xml/GetCapabilities), you will notice that no operation has been defined.

image
This could be fixed quite easily, but it goes beyond the scope of this post. And this will give us some work to do in the next tutorial, besides the connection to a database.

Those of you readers who are not very accustomed to the intricacies of the CSW specifications (though I think you should be if you want to mess with a CSW server) may not have been very impressed by all the above, but I know I have been surprised by the few lines of code this tutorial comprises. And even though there is still a lot of work to do, this makes me think that the project heads on a good direction. But let me know what you think.

For those interested, the source code of this tutorial can be downloaded heredownloaded here.

No comments:

Post a Comment