Smallworld GeoSpatial Server (GSS) is an industrial-strength application that allows Smallworld GIS to communicate with other systems in an industry-standard, open manner.
It comes equipped with a variety of services out-of-the-box, but it’s real strength is that it can be customized to fit an enterprise’s exact needs.
Unfortunately most custom implementations are tightly coupled to GSS and that can increase administration efforts as well as make it more difficult to enhance and maintain the system — which inevitably leads to higher costs.
What we really want is a way to abstract away all the boilerplate and low-level GSS code so we can concentrate on writing only business logic.
Fortunately, with very little effort, custom service providers can be implemented in a loosely-coupled manner that realize the benefits of GSS and lowers administration, maintenance and enhancement efforts.
Let’s look at how a typical GSS service provider is implemented.
A Typical GSS Service
The usual way developers write services is to first define a service provider by editing two XML configuration files (a routes file and a service definition file).
Second, the custom service provider is subclassed from the service_provider class and methods are written on the subclass. Further, since the majority of functionality is written on the custom subclass, services tend to be monolithic and become even more so over time.
Third, each method must handle errors itself and code reuse is minimal at best. As new custom service provider subclasses are written, boilerplate (e.g. invocation and error handling) code is copied and pasted from previous classes thus polluting the codebase.
In addition, the code that implements the service is tightly coupled to the service_provider class and thus tightly coupled to GSS. It would be better to decouple the code and decompose it into loosely coupled components so they can be reused, not only in further GSS applications, but in non-GSS applications as well.
So how can we make things better?
A Better Way
Well, right off the bat, let’s remove the need for multiple custom service providers inheriting from the service_provider class. Doing this means we only need one service configured in the service definition file.
We’ll call it dispatch_service and define it, in the beeble_service module’s resources folder, as follows.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<service>
<!-- this service MUST be defined. It is the only one required. -->
<method name="dispatch_service">
<comment>Beeble GSS interfaces</comment>
<request>
<element name="name" required="false" type="string" />
</request>
<response formats="application/json" custom_format="true"/>
</method>
</service>
Once we’ve defined the service for GSS, we need to implement it. I’m going to implement a very simple service (beeble_service) and do the main work in another method named dispatch() — invoked in line 17 below.
#-------------------------------------------------------
# EXEMPLAR:
#-------------------------------------------------------
def_slotted_exemplar(:beeble_service,
{ },
{:service_provider})
$
#-------------------------------------------------------
# DISPATCH_SERVICE:
#-------------------------------------------------------
_method beeble_service.dispatch_service(p_service_call)
_constant PROC_NAME << _self.get_proc_name(p_service_call)
_return _self.dispatch(PROC_NAME, p_service_call)
_endmethod
$
Note dispatch_service() returns whatever dispatch() returns.
You can combine the two methods if you’d like, but I wanted to split out the proc_name and explicitly pass it to dispatch(). The proc name is retrieved from the service call using the helper method, get_proc_name(), defined below.
#-------------------------------------------------------
# GET_PROC_NAME:
#-------------------------------------------------------
_method beeble_service.get_proc_name(p_service_call)
_try
# convert the full path + service name to a proc name.
# For example: "/beeble-api/zaphod" --> :|beeble_api_zaphod()|
_return write_string(p_service_call.path.replace_all_instances_of("/","_").replace_all_instances_of("-","_").slice_to_end(2), "()").as_symbol()
_when error
_return :shut_er_down_clancy_she_is_pumpin_mud|()|
_endtry
_endmethod
$
get_proc_name() is responsible for converting the path and service name to a procedure name. So if the path is “/beeble-api/zaphod” then the proc name returned would be :|beeble_api_zaphod()|.
To do this it uses another helper method named path, defined on service_call. This method is shown below.
#-------------------------------------------------------
# PATH:
#-------------------------------------------------------
_method service_call.path
# return the path from the request (e.g. /beeble-api/zaphod).
_return .request.path
_endmethod
$
Once we have the proc name, we call dispatch() to actually invoke the proc. Following is the dispatch method.
#-------------------------------------------------------
# DISPATCH:
#-------------------------------------------------------
_method beeble_service.dispatch(p_proc_name, p_service_call)
# alias to standard library.
_constant B << FP.fns.lib
_constant S << FP.custom.beeble.services
_local l_args << beeble_object.new()
_try _with cond
l_args.service_call << p_service_call
l_args.service_response << service_call_response.new(p_service_call)
_local l_resp << S.perform(p_proc_name, l_args)
_if l_resp.class_name _is :beeble_object
_then
# get the specific response if it's available, otherwise keep the default response.
l_resp << l_resp.response.default(l_resp)
_endif
# ensure return type is valid, otherwise raise an error.
l_resp << _self.check_type(l_resp)
# L_RESP is a string.
_return l_resp
_when error
condition.raise(:http_request_processing_client_error, :response, write_string("Internal Server Error: ", cond.report_string))
_endtry
_endmethod
$
The dispatch method creates a beeble_object (line 10) to use as the argument to the procedure that will implement that actual service and then invokes the procedure in line 17. Normally a beeble_object with the response is returned, but it is legal to return a string, a rope or a simple_vector.
If a beeble_object is returned, it’s checked to see if there is a response property defined. If so, this is used as the response sent back to the consumer. It allows services that have other properties on the beeble_object the ability to return only a subset of these (i.e. the properties defined in the response property).
If there is no response property, then the entire beeble_object is returned. But before that’s done, return values must be serialized to JSON strings which is done in the check_type() method.
Here’s what check_type() looks like.
#-------------------------------------------------------
# CHECK_TYPE:
#-------------------------------------------------------
_method beeble_service.check_type(p_value)
# alias to standard library.
_constant B << FP.fns.lib
_if (cn << p_value.class_name) _is :beeble_object _orif cn _is :rope _orif cn _is :simple_vector
_then
_return _self.return_json(p_value)
_elif cn _is :char16_vector
_then
_return p_value
_else
condition.raise(:error, :string, write_string("Invalid response type: ", cn))
_endif
_endmethod
$
If the type is already a string, it simply returns it (line 15).
If the type is a beeble_object, rope or simple_vector, it serializes that type to a JSON string, in line 11, using return_json(), otherwise it raises an invalid type error (line 18).
Here’s the return_json() method.
#-------------------------------------------------------
# RETURN_JSON:
#-------------------------------------------------------
_method beeble_service.return_json(p_obj)
_constant JSON_STREAM << internal_text_output_stream.new()
p_obj.as_json(JSON_STREAM)
_return JSON_STREAM.string
_endmethod
$
return_json() uses the built-in as_json() methods defined on beeble_object, rope and simple_vector to serialize the object to the appropriate JSON format. These JSON methods are part of the beeble_object module.
Once the object has been serialized to a JSON string (or an explicit string was returned from the service), the stack unwinds and the return string is returned to dispatch_service(), GSS and then back to the consumer.
Everything written up to this point takes care of the underlying, low-level details necessary to invoke a GSS service and return a response. With these details out of the way, we can focus on simply defining service routes and writing services. We no longer have to worry about how to interact with GSS.
Writing a Better Service
Since our dispatch_service() code relieves us from having to interact with GSS, writing a service is as simple as defining the route and writing the associated procedure.
Routes are defined in the route configuration file (an example from the Cambdb vertx application, located at …\gss\geospatial_server\modules\gss_basic_camdb_vertx_application\resources\base\data\gss_basic_camdb_vertx_config.xml, is shown below).
<?xml version="1.0" encoding="UTF-8"?>
<!-- Configuration of the GeoSpatial Server test application on CAMDB -->
<!-- This application includes only GSS http services and only http plugin -->
<config base="gss_basic_vertx_application.gss_basic_vertx_config">
<plugins>
<plugin name="http" class_name="http_service_request_handler_plugin">
<property name="port_number" value="7771"/>
<property name="http_server" value="http_service_request_handler"/>
<property name="load_balancer_url" value="${BIFROST_URL}"/>
<property name="resources_root_dir" value="${EIS_RESOURCES_PATH}"/>
<paths>
<path string="beeble-api">
<service string="trillian" method="get" provider="beeble_service.dispatch_service" db_spec_name=""/>
<service string="zaphod" method="get" provider="beeble_service.dispatch_service" db_spec_name=""/>
</path>
</plugin>
</plugins>
</config>
Note we’ve defined two services on the beeble-api path: zaphod and trillian (lines 14 and 15). Therefore we’ll name our implementing procedures beeble_api_zaphod() and beeble_api_trillian(). Including the path in the name ensures the procedure name is unique (for example if we had two paths pointing to services with the same service name, we can differentiate between them).
Also note both services list their provider as beeble_service.dispatch_service. This is important because we want all services to run through the dispatcher. Therefore all services we define will always use beeble_service.dispatch_service as their provider.
And with that, we now have a transparent way to invoke our procedures by simply defining a path and service name in the route configuration file.
The benefit is we no longer have to define services in our module’s service configuration file, we don’t have to define new service_provider subclasses and don’t have to handle multiple return cases using boilerplate code. All requests and responses go through one method (i.e. dispatch_service).
This allows us to focus on writing business logic without worrying about boilerplate and infrastructure-related code.
Now let’s look at our service procedures. Services are wrapped in procedures. Since a procedure can do practically anything that can be done in Magik, they form a flexible, loosely-coupled container where code can be implemented directly or various methods and other procedures can be invoked.
However you decide to implement the service, the important concept to remember is the procedure must return a beeble_object (preferred because it provides the most flexibility), a rope, a simple_vector or a string.
I generally use the MagikFP functional programming library to write code because it offers a number of benefits. You can read about them in articles I’ve written about Functional Programming, the Need for Functional Programming and Error Handling with Functional Programming.
Here’s a sample service written in a functional manner using the MagikFP library (but of course you can use standard OO techniques within the procedure).
#-------------------------------------------------------
# ZAPHOD SERVICE:
#-------------------------------------------------------
FNS.beeble_api_zaphod <<
_proc @beeble_api_zaphod(p_obj)
# set up aliases to the appropriate objects.
_constant B << FP.fns.lib
_constant C << FP.custom.beeble.lib
_constant F << FP.custom.beeble.fns
_constant REQUEST << p_obj.service_call
_constant RESPONSE << p_obj.service_call_response
# mhdebug:
write("Service ZAPHOD invoked with: ", p_obj)
# call the relevant functions in a safe_pipe to perform the necessary actions.
# if processing is successful, return the results,
# otherwise handle the error and raise an error condition that will be sent back to the consumer.
_return FP.fns.safe_pipe(F.get_data,
F.filter_data,
F.extract_required_fields,
F.process_fields)(p_obj).or_else(F.handle_error).get_or_else_throw("Zaphod Function Failed.")
_endproc
$
Making a Service Request
And just like that, we can make a REST API request to the endpoint and GSS will invoke our service procedure and return its response to the client.

This is a partial result returned from the Zaphod service request (with the work done by the beeble_api_zaphod() procedure).
As you can see, we can now focus solely on writing business logic because the underlying GSS-specific details are abstracted away. Our code is not tied to GSS, it’s simply a procedure that either does the work itself or invokes other methods and procedures. Errors are handled automatically so there’s no need to explicitly handle them — if an error needs to be returned to the client, simply raise an error condition and it will be automatically returned.
We can also reuse the code in other places, not necessarily GSS-based, because it’s loosely-coupled to GSS.
And of course we create new services by simply defining their routes and writing the associated procedures without mucking about with GSS-specific classes.
I’m sure you can envision additional benefits to this architecture, but you can wring out even more value by implementing services in a functional manner. Browse through this site for the latest information about how to implement functional programming in Magik.