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.
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:
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:
<?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.
@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.
/Pets(KEY)
/pet{KEY}
/pet/findByStatus?status=available,sold,pending
@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;
}
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));
}
@Override public void createEntity(EntityValue entityValue)
@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);
}
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);
}
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);
}
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;
}
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;
}
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;
}
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);
}
}
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;
}
}
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);
}
}
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
17 | |
15 | |
11 | |
10 | |
8 | |
8 | |
8 | |
8 | |
7 | |
7 |