Skip to Content
Technical Articles

How to enable RESTful services for offline synchronization

During the last months I published some blogs about our new Mobile Back-End Generator tools, which lets you create nice OData services. Today, I want to show you an extraordinary use case for this mighty tool. I hope you’ll find it useful.

CONSUME REST SERVICES

SAPs strategy for accessing data is clearly to use OData. The protocol provides a lot advantages. Nevertheless it is not used everywhere. Often you’ll stumble upon own RESTful services. I want to present a nice way to convert any arbitrary RESTful service into an OData service using the Mobile Back-End Generator.

You could use this approach whenever you want to use a REST service with the Mobile Development Kit, SAP Cloud Platform SDK for iOS or SAP Cloud Platform SDK for Android in offline mode and you can’t implement a completely new REST service just for your mobile app.

The idea is to work top-down and start with the OData model you want to see on your device and map the publicly available petstore API from swagger.io as an example. So we are replacing the usual database calls to retrieve and modify the data with calls to the petstore API.

THE APPROACH

We will create an OData service without a DB binding and instead of the usual database SQL statements we will do actual REST calls against the petstore API, parse the response, convert them into proxy classes and let the framework handle the remaining steps. For the http calls, the parsing and converting we need to provide JAVA coding – but not for the aspects of OData Query parsing and OData service responses. This saves a lot of work for us and at the same time we also add additional features to the petstore API, like searching by name. Wrapping the petstore API with an OData layer makes the data available for Offline OData Sync as well.

Here’s what the solution will look like at the end:

GETTING THE WORK DONE

Let’s create a new project using the Mobile Services template for a new OData Service Project. This time, we specify the “No database” option.

In my example I have chosen Cloud Foundry, because it is very convenient to generate the service and simply deploy it to my Cloud Foundry trial. This let me also easily debug my code later on.

If you have already a model and want to generate without DB artifacts you can specify “db”:”nodb” in the .serviceProperties.json manually.

Now, let’s define the final service model using the graphical editor of the Mobile Back-End Generator:

I’d recommend to get yourself familiar with the perstore API here. It’s well documented and you can try it out here: https://petstore.swagger.io/

The resulting CSDL is as follows:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" xmlns:edmx4="http://docs.oasis-open.org/odata/ns/edmx" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <edmx:DataServices m:DataServiceVersion="2.0">
        <Schema Namespace="petstore.swagger.io" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
            <EntityType Name="Pet">
                <Key>
                    <PropertyRef Name="PetID"/>
                </Key>
                <Property Name="Name" Type="Edm.String" Nullable="true" MaxLength="100"/>
                <Property Name="PetID" Type="Edm.Int64" Nullable="false"/>
                <Property Name="Status" Type="Edm.String" Nullable="false" MaxLength="9"/>
            </EntityType>
            <EntityContainer Name="PetService" m:IsDefaultEntityContainer="true">
                <EntitySet Name="Pets" EntityType="Self.Pet"/>
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

Now, we can generate the JAVA service based on this definition.

The main customization point will be around the PetHandler.java class. This class is part of the generated framework packages and responsible for managing the customization points between an incoming OData Call and the respective back-end action.

QUERY FOR DATA

@Override public DataValue executeQuery(DataQuery query)

This method is called whenever there is an incoming GET request on the Pet entity (let’s, for a second, ignore the details about collections and entities). Our task here is to map this to the petstore APIs for retrieving a specific Pet object using /pet{KEY} and /pet/findByStatus?status={status}. Here we are ahead to do some design decision. Reading the service description, there is no way to search the pet store by the name property of the pet, so there won’t be a straightforward way to do this in our final OData service. Actually, it could be done, but this would be even more advanced modification of the service and at this point it would probably not make sense to enhance the service in this direction, since it seemed to be enough in the first place to just get either a particular pet or retrieve a list of pets by the status. The decision we need to make is to decide what a typical service call to our final service will respond when calling /Pets. Usually, this should return all available pets. In order to achieve this we need to consider two ways for our executeQuery() implementation.

  • If key is provided map GET
    /Pets(KEY)

    OData call to swagger API GET

    /pet{KEY}
  • else use
    /pet/findByStatus?status=available,sold,pending
    1. If $filter contains status information make sure only these values are passed to petstore api

And this is how it looks in code:

@Override
public DataValue executeQuery(DataQuery query) {
	PetList pets = new PetList();
	EntityKey entityKey = query.getEntityKey();
	if (entityKey != null) {
		queryByKey(entityKey, pets);
	} else {
		queryByStatus(query, pets);
	}
	EntityValueList result = pets.toEntityList().filterAndSort(query).skipAndTop(query);
	return result;
}

Here are the methods queryByKey and queryByStatus:

private void queryByKey(EntityKey entityKey, PetList pets) {
	DataValue petID = entityKey.getMap().getRequired("PetID");
	JsonObject responseObject = (JsonObject) RequestHelper.getResponse(HttpMethod.GET, "/pet/" + petID, null, true);
	pets.add(fromJSON(responseObject));
}

private void queryByStatus(DataQuery query, PetList pets) {
	StringList statusNames = new StringList();
	statusNames.add("available");
	statusNames.add("pending");
	statusNames.add("sold");
	QueryFilter filter = query.getQueryFilter();
	if (filter != null) {
		DataValue findByStatus = filter.find(Pet.status, QueryOperatorCode.EQUAL);
		if (findByStatus instanceof StringValue) {
			// Reduce the requested results to just one status.
			String status = findByStatus.toString();
			statusNames.clear();
			statusNames.add(status);
		}
	}
	JsonArray responseArray = (JsonArray) RequestHelper.getResponse(HttpMethod.GET,
			"/pet/findByStatus?status=" + statusNames.join(","), null, true);
	pets.addAll(fromJSON(responseArray));
}

 

HANDLE POST REQUEST TO CREATE ENTITIES

When it comes to entity creations the generated code provides a createEntity method:

@Override public void createEntity(EntityValue entityValue)

This method expects a valid OData POST command and should do the following:

  • Convert the incoming request to a Pet object of the generated entity class.
  • Validate the object (optional)
    1. You can for example check whether the status value is in the range of the expected values (“available”, “pending”, “sold”)
  • Convert Pet object to JSON representation as expected by the petstore API
  • Fire the actual REST call to create a PET using the petstore API
  • Parse the response
  • Create a Pet object based on the response object and overwrite values from the response
    1. This is needed because the REST service responds with a server key that needs to be passed back to the OData response of our service.
@Override
public void createEntity(EntityValue entityValue) {
	petstore.swagger.io.proxy.Pet entity = (petstore.swagger.io.proxy.Pet) entityValue;
	validate(entity);
	JsonObject requestObject = toJSON(entity);
	JsonObject responseObject = (JsonObject) RequestHelper.getResponse(HttpMethod.POST, "/pet", requestObject,true);
	Pet created = fromJSON(responseObject);
	EntityHelper.copyProperties(created, entity);
}

UPDATING ENTITIES

This is very similar to the create, instead here you use PUT to pass the updated values to the petstore API.

@Override
public void updateEntity(EntityValue entityValue) {
	petstore.swagger.io.proxy.Pet entity = (petstore.swagger.io.proxy.Pet) entityValue;
	validate(entity);
	JsonObject requestObject = toJSON(entity);
	JsonObject responseObject = (JsonObject) RequestHelper.getResponse(HttpMethod.PUT, "/pet", requestObject, true);
	Pet updated = fromJSON(responseObject);
	EntityHelper.copyProperties(updated, entity);
}

DELETING ENTITIES

Deletions is the easiest one. I think the code is self-explanatory:

@Override
public void deleteEntity(EntityValue entityValue) {
	petstore.swagger.io.proxy.Pet entity = (petstore.swagger.io.proxy.Pet) entityValue;
	RequestHelper.getResponse(HttpMethod.DELETE, "/pet/" + entity.getPetID(), null, false);
}

HELPER METHODS YOU WOULD NEED

This method copies a JSON representation of a pet to a proxy class of type Pet. I commented the category, since in the petstore API this category seems to have no purpose.

public static Pet fromJSON(JsonObject json) {
	Pet pet = new Pet();
	pet.setPetID(JsonValue.toLong(json.getRequired("id")));
	pet.setName(JsonValue.toNullableString(json.get("name")));
	pet.setStatus(JsonValue.toString(json.getRequired("status")));
	return pet;
}

Next method does the same as the above but for arrays of objects.

public static PetList fromJSON(JsonArray array) {
	PetList result = new PetList();
	int count = array.length();
	for (int index = 0; index < count; index++) {
		JsonObject item = array.getObject(index);
		result.add(fromJSON(item));
	}
	return result;
}

The method toJSON(Pet pet) is to prepare objects to be transmitted to the petstore API.

public static JsonObject toJSON(Pet pet) {
	JsonObject json = new JsonObject();
	json.set("id", JsonValue.fromLong(pet.getPetID()));
	json.set("name", JsonValue.fromNullableString(pet.getName()));
	json.set("status", JsonValue.fromString(pet.getStatus()));
	return json;
}

The validate method is about checking if the provided values are valid for the petstore API. These data validation methods are extremely important to make sure your service behaves correctly and is a proper citizen of the petstore API.

private void validate(Pet pet) {
	String status = pet.getStatus();
	if (!(status.equals("available") || status.equals("pending") || status.equals("sold"))) {
		throw DataServiceException.validationError("Invalid status: " + status);
	}
}

COMMUNICATING WITH THE PETSTORE API

In order to do the actual calls to the petstore API I used a helper class called RequestHelper to encapsulate the actual communication code. It handles the HTTP calls, prepares the headers and bodies, executes the actual calls and provides basic error handling.

package petstore.swagger.io.handler;

import com.sap.cloud.server.odata.*;
import com.sap.cloud.server.odata.http.*;
import com.sap.cloud.server.odata.json.*;

public abstract class RequestHelper {
	private static final String PETSTORE_URL_BASE = "https://petstore.swagger.io/v2";

	public static JsonElement getResponse(String method, String pathAndQuery, JsonElement requestBody,
			boolean responseBody) {
		String url = PETSTORE_URL_BASE + pathAndQuery;
		HttpRequest request = new HttpRequest();
		boolean logTrace = petstore.swagger.io.LogSettings.LOG_TRACE;
		boolean logDebug = petstore.swagger.io.LogSettings.LOG_DEBUG || logTrace;
		boolean logPretty = petstore.swagger.io.LogSettings.PRETTY_TRACING;
		request.enableTrace("petstore.openapi", logDebug, logTrace, logTrace, logPretty);
		request.open(method, url);
		if (requestBody != null) {
			request.setRequestHeader("Content-Type", "application/json");
			request.setRequestText(requestBody.toString());
		}
		if (responseBody) {
			request.setRequestHeader("Accept", "application/json");
		}
		request.send();
		int status = request.getStatus();
		String responseText = request.getResponseText();
		request.close();
		JsonElement responseElement = null;
		if (responseText.length() > 0) {
			responseElement = JsonElement.parse(responseText);
		}
		if (status < 200 || status > 299) {
			ErrorResponse errorResponse = null;
			if (responseElement instanceof JsonObject) {
				JsonObject responseObject = (JsonObject) responseElement;
				Object responseType = responseObject.get("type");
				if (responseType instanceof JsonString && ((JsonString) responseType).getValue().equals("error")) {
					// Convert petstore error content to OData error response.
					errorResponse = new ErrorResponse();
					Integer code = JsonValue.toNullableInt(responseObject.get("code"));
					String message = JsonValue.toNullableString(responseObject.get("message"));
					if (code == null)
						code = 0;
					if (message == null)
						message = responseText;
					errorResponse.setCode(code.toString());
					errorResponse.setMessage(message);
				}
			}
			throw DataServiceException.withResponse(status, null, errorResponse).safe();
		}
		return responseBody ? responseElement : null;
	}
}

There is one more class that I used which is very generic and we plan to pull it into our framework as well, so that you don’t need to code for this yourself.

It’s called EntityHelper and provides a single static method called copyProperties, which copies all values from Pet object A to Pet object B. There is no return value needed, since the objects are passed by reference.

Let’s quickly discuss an incoming OData request. It will be parsed through the framework and eventually end in some generated methods. For example, we look at the flow of the OData POST for a Pet, we will first have to examine the code in the PetListener.java

PetListener.beforeCreate() > PetListener.beforeSave() > PetHandler.createEntity() >PetListener.afterSave() > PetListeners.afterCreate()

Similar flows will happen for update and delete as well. As you can see the Object that is passed through the methods is of type EntityValue and this is where our EntityHelper kicks in.

package petstore.swagger.io.handler;

import com.sap.cloud.server.odata.*;

public abstract class EntityHelper {
	public static void copyProperties(EntityValue from, EntityValue to) {
		EntityType type = from.getEntityType();
		for (Property property : type.getStructuralProperties()) {
			if (from.hasDataValue(property)) {
				to.setDataValue(property, from.getDataValue(property));
			} else {
				to.unsetDataValue(property);
			}
		}
	}
}

 

SUMMARY

Connecting to a RESTful API from a generated Mobile Back-End project isn’t really rocket science. The code presented within this blog is very straightforward and can easily be adapted to your own use cases. Yet it is extremely powerful, since you can now use the offline sync features and you have magically improved the petstore API by features like paging, sort, top and skip which were not available in the original API.

If we think about it, we could use the presented approach to use it to fetch data using JCP/RFC or SOAP or JDBC for existing databases.

 

Have Fun,

Martin

 

APPENDIX

Petstore Swagger API documentation (Link)

Server OData API (Link)

Logging in your generated service (Link)

Mobile Back-End Generator Documentation (Link)

CREDITS

The idea and the code is based on the work of Evan Ireland

Be the first to leave a comment
You must be Logged on to comment or reply to a post.