Showing posts with label Tutorial. Show all posts
Showing posts with label Tutorial. Show all posts

April 03, 2012

CSW Server Implementation (Part III)

Last time we laid the basis for a rough CSW server implementation. The main missing point to it is the ability to plug it to a database, which would make it much more useful…

But before doing this, I will take some time to polish our server a little bit so that it is easier to test, and more compliant to the CSW standard. These are hardly complicated tasks, but they are necessary and other posts are long enough already. So here we go:

  • Support the GET+XML protocol.
  • Complete the GetCapabilities operation.

The GET+XML protocol

What we need here is another service, set to another WCF entry point. As we will see, this service will be reusable as is for every OGC web service you wish to implement using GeoSIK. Thus we will call it Ows:

image

Add a reference to the System.ServiceModel.Web assembly, and just add a simple operation to the contract:

[ServiceContract]
public interface IOws {

[OperationContract]
[FaultContract(typeof(GeoSik.Ogc.Ows.V100.Types.ExceptionReport), Name="ExceptionReport")]
[WebGet(UriTemplate="?service={service}&version={version}&request={request}", BodyStyle=WebMessageBodyStyle.Bare, RequestFormat=WebMessageFormat.Xml, ResponseFormat=WebMessageFormat.Xml)]
Message Execute(string service, string version, string request);
}

This is quite simple, and you can recognize the 3 mandatory parameters that are common to all OGC web services (cf. specification §9.2.1). Now we have our contract, let’s implement it using the GeoSik.Ogc.Ows.ServiceLocator class. This class is able to find OWS implementations in an assembly and dynamically call its services. To make it work, just add an attribute to the Discovery service we implemented last time:

[GeoSik.Ogc.OwsDescription(Discovery.Service, Discovery.Version)]
public class Discovery:
GeoSik.Ogc.WebCatalog.Csw.V202.Discovery
{
// ...
}

Now use the ServiceLocator to find and call this service at runtime:

public class Ows:
IOws
{

public Message Execute(string service, string version, string request)
{
NameValueCollection parameters=WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters;

var locator=new GeoSik.Ogc.Ows.ServiceLocator(Assembly.GetExecutingAssembly());
IXmlSerializable response=locator.InvokeService(parameters);

return CreateMessage(response, OperationContext.Current.IncomingMessageVersion);
}

private static Message CreateMessage(IXmlSerializable response, MessageVersion version)
{
StringBuilder serialized=new StringBuilder();
using (var xw=XmlWriter.Create(serialized))
response.WriteXml(xw);
var xr=XmlReader.Create(new StringReader(serialized.ToString()));
return Message.CreateMessage(version, "ExecuteResponse", xr);
}
}

Our service is implemented. We just have to tie it to a proper WCF endpoint in the Web.config file:

<service name="Tutorial.Ows" behaviorConfiguration="ows100PoxBehavior">
<endpoint address="" contract="Tutorial.IOws" binding="webHttpBinding" behaviorConfiguration="poxBehavior" />
</service>

That’s it. Let’s try to start the application and call the GetCapabilities operation at http://localhost:6155/Ows.svc/?service=CSW&version=2.0.2&request=GetCapabilities (your port number may vary). Works on my machine


The GetCapabilities operation


Now you can see the result of a GetCapabilities call, you can see that no operation is defined. The main reason for this is that GeoSIK is unable to detect the address of our services. We will have to help it out by implementing our own GetCapabilitiesProcessor class. Just override the GetEndPoints method and provide the proper parameters for our 2 services:

public class GetCapabilitiesProcessor:
GeoSik.Ogc.WebCatalog.Csw.V202.Discovery.GetCapabilitiesProcessorBase
{

public GetCapabilitiesProcessor(Discovery service):
base(service)
{ }

protected override IEnumerable<GeoSik.Ogc.OwsEndPoint> GetEndPoints()
{
var ret=new List<GeoSik.Ogc.OwsEndPoint>();

Uri baseUri=new Uri(
string.Format(
CultureInfo.InvariantCulture,
"{0}://{1}/",
WebOperationContext.Current.IncomingRequest.UriTemplateMatch.BaseUri.Scheme,
WebOperationContext.Current.IncomingRequest.UriTemplateMatch.BaseUri.Authority
)
);

ret.Add(
new GeoSik.Ogc.OwsEndPoint() {
AddOperationName=false,
BaseUri=new Uri(baseUri, "Ows.svc/"),
Method=GeoSik.Ogc.OwsMethod.Get
}
);
ret.Add(
new GeoSik.Ogc.OwsEndPoint() {
AddOperationName=true,
BaseUri=new Uri(baseUri, "Csw.svc/xml/"),
Method=GeoSik.Ogc.OwsMethod.Xml
}
);

return ret;
}
}

Make it the processor to use in the Discovery service:

protected override Discovery.GetCapabilitiesProcessorBase CreateGetCapabilitiesProcessor()
{
return new GetCapabilitiesProcessor(this);
}

Try the GetCapabilities operation again: we are compliant! If you delve a bit more in the GeoSIK libraries you will notice that there are many other opportunities for customization for this operation and that every other operation can be customized in pretty much the same way we just customized GetCapabilities. This gives you (the developer) all the power you need to implement the CSW server you have been dreaming of all this time (I may be a bit carried away here…).


Enjoy your CSW server for now. As is now usual, the complete source code for this tutorial can be downloaded here. Next time we will plug our server to a proper database: get set for some Sql Server and Entity Framework adventures!…

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.