Thursday, January 7, 2010

Extend GeoServer with customized OWS service :: part 2

In previous post of this series, I've created "ags-ows" project under group id "playground". It will be rebuilt when GeoServer is rebuilt and you can also debug it along with other core GeoServer source code. So start from this post I am going to add more elements in this project little by little.

Create application context (applicationContext.xml)

The first thing to add for "ags-ows" service is to create application context which is configured through applicationContext.xml file in your project's src folder (e.g. \trunk\src\playground\ows-arcgisserver\src\main\java). applicationContext.xml is very important because service url mapping, services and operations, KvpParser, KvpRequestReader and Response almost everything I will create later must be registered as Java beans in it so that they can be loaded when GeoServer starts.
For now, you can start with an empty applicationContext.xml in your ows-arcgisserver project's root source code folder (\src\main\java)
1: <?xml version="1.0" encoding="UTF-8"?>
2: <beans>
3: </beans>
Reuse org.geoserver.ows.Dispatcher to dispatch the request

Class org.geoserver.ows.Dispatcher in GeoServer ows project is like a central hub which dispatches all ows service requests (e.g. WMS, WFS or WCS request) to appropriate OGC server implementation (wms, wfs or wcs GeoServer project) for further processing based a request's "service" and "version" parameters (e.g. if "service=WMS" then the request will be dispatched to wms implementation). If you look at the Spring application context in GeoServer "main" project you will find following servlet url mapping:
1: <bean id="dispatcherMapping" 
2:     class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
3:   <property name="alwaysUseFullPath" value="true"/>
4:   <property name="mappings">
5:     <props>
6:       <prop key="/ows">dispatcher</prop>
7:       <prop key="/ows/**">dispatcher</prop>
8:       <prop key="/styles/**">filePublisher</prop>
9:       <prop key="/www/**">filePublisher</prop>
10:     </props>
11:   </property>
12: </bean>
So all the requests matching pattern ".../geoserver/ows?..." or ".../geoserver/ows/*?..." (e.g. .../geoserver/ows/wms?) will be preprocessed and dispatched by ows dispatcher. This makes thing much easier for me because "ags-ows" service is suppose to be a fake OWS service that expects request like below:

http://localhost:8080/geoserver/ows/agsows?request=export&bbox=-115.8,30.4,-85.5,50.5&bboxSR=4326&layers=show:0,1,2&size=800,600&imageSR=4326&transparent=true
Or
http://localhost:8080/geoserver/ows?service=agsows&request=export&bbox=-115.8,30.4,-85.5,50.5&bboxSR=4326&layers=show:0,1,2&size=800,600&imageSR=4326&transparent=true

Basically I can just reuse the ows dispatcher without changing even one line of code. But now if you send the requests above, it won't work because the dispatcher does not know the existence of "ags-ows" service. Two things must be done before dispatcher correctly dispatches the request when it contains "service=agsows":

1. Create a class representing "ags-ows" service which declares all the supported operations as its methods; (DefaultWebCoverageService111 class in wcs1_1 project is a good example of such class), in my case, I created one class called ArcGISServerOWSService:
1: public class ArcGISServerOWSService {
2:     public ArcGISServerOWSService(...) {
3:         //... 
4:     }
5:     public AgsOwsExportResponse export(AgsOwsExportRequest request) {
6:     //...
7:     }
8: }
9: 
For now don't worry about where class AgsOwsExportResponse and AgsOwsExportRequest come from, and other details which you can find out in attached sample code.

2. Register "ags-ows" service class in application context as below:
1: <?xml version="1.0" encoding="UTF-8"?>
2: <beans>
3:   <bean id="ags-ows" 
4:            class="org.geoserver.ows.arcgisserver.ArcGISServerOWSService">
5:           <constructor-arg ref="geoServer"/>
6:   </bean>
7:   <bean id="ags-ows-1.0.0" class="org.geoserver.platform.Service">
8:     <constructor-arg index="0" value="agsows"/>
9:     <constructor-arg index="1" ref="ags-ows"/>
10:     <constructor-arg index="2" value="1.0.0"/>
11:     <constructor-arg index="3">
12:       <list>
13:         <value>Export</value>
14:       </list>
15:     </constructor-arg>
16:   </bean>
17: </beans>
As you can see, you first need to register ArcGISServerOWSService (geoserver bean is input parameter of the constructor) as a bean in application context. But what's more important is to add another bean in type org.geoserver.platform.Service, in which you must give "service" (agsows), the reference to the ArcGISServerOWSService bean, "version" (1.0.0), and list of supported operations (Export) as constructor input parameters. Once you save the application context and restart GeoServer, OWS Dispatcher will recognize the request with "service=agsows" and redirect to ArcGISServerOWSService implementation.

Extend org.geoserver.ows.KvpParser to parse request parameter

A subclass of org.geoserver.ows.KvpParser is used by ows dispatcher to parse a particular request parameter value in HTTP Get request. The reason to parse parameter value is that those values are coming in as String (could be UTF-8 encoded or other) but before they can be correctly recognized and used by GeoServer they must be converted to appropriate type of Java object or primitive types. For example, WMS request parameter 'bbox' needs to be converted as an JTS Envelope object, or WMS request parameter 'bgcolor' must be converted into java.awt.Color object, and a subclass of KvpParser does exactly that job.

To extend KvpParser, you need to give a key which is the target request parameter it parses and a Class type in KvpParser subclass's constructor. The Class type is the type into which string request parameter value will be converted.
1: public abstract class KvpParser {
2:     ...
3:    /**
4:     * The key.
5:     */
6:     String key;
7: 
8:    /**
9:     * The class of parsed objects.
10:     */
11:     Class binding;
12: 
13:     public KvpParser(String key, Class binding) {
14:         this.key = key;
15:         this.binding = binding;
16:     }
17:     ...
18: }
You also need to override method "parse(String value)" that contains the actual logic to do the converting; for example the parse method in a WMS bbox kvp parser will create and return an JTS Envelope from a string in pattern "-minx, miny, maxx, maxy".
1: ...
2: /**
3: * Parses the string representation into the object representation.
4: *
5: * @param value The string value.
6: *
7: * @return The parsed object, or null if it could not be parsed.
8: *
9: * @throws Exception In the event of an unsuccesful parse.
10: */
11: public abstract Object parse(String value) throws Exception;
12: ...
13: 
In "ags-ows" service, one kvp parser is needed for each request parameter. But I don't have to actually create 5 different KvpParser subclasses because I can reuse some existing ones, for example org.geoserver.ows.kvp.BooleanKvpParser can be used for parameter "transparent", and org.geoserver.wfs.kvp.BBoxKvpParser can be used for parameter "bbox". For the rest, I am going to create my own. First I need a new "AgsOwsKvpLayersParser" to convert comma separated layer list into an array of GeoServer LayerInfo. Below is a sample implmentation:
1: public class AgsOwsKvpLayersParser extends KvpParser 
2:   implements IArcGISServerOWSServiceLogger {
3:   
4:   private GeoServer geoServer;
5:   
6:   public AgsOwsKvpLayersParser(String service, String version, GeoServer geoServer) {
7:     super("layers", List.class);  
8:     setService(service);
9:     setVersion(new Version(version));
10:     this.geoServer = geoServer;
11:   }
12: 
13:   @Override
14:   public Object parse(String value) throws Exception {    
15:     AGSOWSLOGGER.info("layers parsed by AgsOwsKvpLayersParser");                
16:     /*
17:      * Syntax: [show | hide | include | exclude]:layerId1,layerId2
18:      */        
19:     Tokenizer outter_delimeter = new Tokenizer(":");
20:     List unparsed = KvpUtils.readFlat(value, outter_delimeter);
21:     boolean isInclude = true;
22:     if(unparsed.size() == 2) {
23:       if(((String)unparsed.get(0)).equalsIgnoreCase("show") == true) {
24:         isInclude = true;
25:       } else if(((String)unparsed.get(0)).equalsIgnoreCase("hide") == true) {
26:         isInclude = false;
27:       } else if(((String)unparsed.get(0)).equalsIgnoreCase("include") == true) {
28:         // TODO: implement later
29:       } else if(((String)unparsed.get(0)).equalsIgnoreCase("exclude") == true) {
30:         // TODO: implement later
31:       }
32:     }    
33:     List<LayerInfo> layers = null;
34:     List requestedLayers = KvpUtils.readFlat((String)unparsed.get(1), KvpUtils.INNER_DELIMETER);
35:     if(isInclude == true) {
36:       layers = new ArrayList<LayerInfo>();
37:     } else {
38:       layers = this.geoServer.getCatalog().getLayers();
39:     }        
40:     for(int i=0; i<requestedLayers.size(); i++) {
41:       String layerName = (String)requestedLayers.get(i);      
42:       if(layerName != null) {
43:         LayerInfo layerInfo = this.geoServer.getCatalog().getLayerByName(layerName);    
44:         if(layerInfo != null) {
45:           if(isInclude == true) {
46:             // include layers being requested
47:             layers.add(layerInfo);
48:           } else {
49:             // exclude layers being requested
50:             layers.remove(layerInfo);
51:           }          
52:         }
53:       }
54:     }    
55:     return layers;    
56:   }
57: }
Second I need another kvp parser to parse parameter "size" which is in syntax "width,height". Since there is no such "size" class in JTS or GeoTools library I will create a simple class called "AgsOwsExportSize" to hold the width and height of a map and have kvp parser return an instance of that class. Below is a sample implementation of "AgsOwsKvpSizeParser":
1: public class AgsOwsKvpSizeParser extends KvpParser {
2:     public AgsOwsKvpSizeParser(String service, String version) {
3:         super("size", AgsOwsExportSize.class); 
4:         setService(service);
5:         setVersion(new Version(version));
6:     }
7: 
8:     @Override
9:     public Object parse(String value) throws Exception {  
10:         List unparsed = KvpUtils.readFlat(value, KvpUtils.INNER_DELIMETER);
11:         if(unparsed.size() != 2) {   
12:             return new AgsOwsExportSize(1024,768);
13:         } else {
14:             int width = Integer.parseInt((String)unparsed.get(0));
15:             int height = Integer.parseInt((String)unparsed.get(1));
16:             return new AgsOwsExportSize(width, height);
17:         }
18:     }
19: }
20: 
Finally I need one more kvp parser AgsOwsKvpSRParser to parse coordinate system wkid for request parameters "imageSR", below is the sample implementation:
1: public class AgsOwsKvpSRParser extends KvpParser 
2:   implements IArcGISServerOWSServiceLogger {
3:   
4:   public AgsOwsKvpSRParser(String key, String service, String version) {
5:     super(key, CoordinateReferenceSystem.class);  
6:     setService(service);
7:     setVersion(new Version(version));
8:   }
9: 
10:   @Override
11:   public Object parse(String value) throws Exception {    
12:     AGSOWSLOGGER.info(this.getKey() + " parsed by AgsOwsKvpSRParser");
13:     CoordinateReferenceSystem crs = null;
14:     if(value!=null && "".equalsIgnoreCase(value)==false) {
15:             try {                
16:                 // create CoordinateReferenceSystem based on wkid
17:               crs = CRS.decode("EPSG:" + value);                
18:             } catch (Exception e) {
19:               AGSOWSLOGGER.warning("invalid parameter");
20:               e.printStackTrace();
21:             }
22:         }    
23:     return crs;    
24:   }
25: }
Two optional attributes you can set when you construct the kvp parser are "service" and "version", which you can either pass in into constructor or set them through parser's setter method. By setting "service" and "version", the kvp parser will only be applied on those requests with specific service type (e.g. service=WMS) and version number (e.g. version=1.3.0). Another important reason for you set service and version of kvp parser is to increase the its priority. Since only be one parser will be picked up to parse one particular request parameter, a matching service and version value will register a parser ahead of the rest if all of them have the same request parameter key.

Register kvp parsers in application context

Kvp parsers must be registered in the application context so that they will be instantiated and be functional when GeoServer starts. To register a kvp parser, simply add a in a project's applicationContext.xml and also give the input parameters for the constructor of parser class.
1: <bean class="kvpparser_class_name" id="kvpparser_id">
2:   <!-- list of constructor input parameters -->
3:   ...
4:         <constructor-arg index="0" value="agsows"/>
5:   <constructor-arg index="1" value="1.0.0"/>
6:         ...
7: </bean>
Up to this point, you've created the application context for "ags-ows" service with beans representing it so that dispatcher will redirect "ags-ows" request to it and you've also registered a few different kvp parsers, once you restart the GeoServer and send the following request:

http://localhost:8080/geoserver/ows?service=agsows&request=export&bbox=-115.8,30.4,-85.5,50.5&layers=show:0,1,2&size=800,600&imageSR=4326&transparent=true

Those request parameters will be parsed by your kvp parsers. To confirm that you can either set break point within the parse() method of any of those parsers or simply have parse() method print out something to the console.

In next post of the series, I will focusing on KvpRequestReader and request bean of "ags-ows" service.

1 comment:

  1. Thanks, Yingqi. It help a lot. since I am new to GeoServer and GIS.

    BTW, where is the source code?

    ReplyDelete