Skip to Content

This blog is about Property Facets, which is a very common way of adding metadata to the metadata of an OData service metadata.
Apart from metadating it is also about coding. But only the necessary coding. The rest is done by the framework, our friend, the SAP Cloud Platform SDK for service provisioning.
See here for the backgrounding of this blogging

 

Property Facets

Nice color…
And sounds nice…
But what is it?
It is like adding an annotation to a property and that annotation adds more information about that property. About how the value of that property has to look like.
It allows to add constraints how the value might look like.

Let’s look at our model.
Let’s see what we have.
We have a person.
Our person has a name.
Can you imagine that a user uses our service to create a person which doesn’t have a name?
No, right?
So we need to ensure, that this cannot happen.
Of course, in our code we can check if the CREATE operation receives a Name property with empty value. And we can return an error response with status 400 (Bad Request)
Yes, we could do that.

But:
we should already let the user know in advance that he has to provide always a value for Name. That this is mandatory. So he doesn’t need to experience the error response.

With other words: we should declare Name property as “not nullable”
Like this:

<Property Name="FirstName" Type="Edm.String" Nullable="false" />

 

After we’ve added this meta information to our model, what do we expect now?

Example:
If the user of our service attempts to create a person with no first name, then we expect that our service fails. Because our contract (metadata) says that FirstName is not nullable.

Furthermore, since we define this knowledge about such constraints already in our model, we expect that we don’t need to implement the checks in our code…. because we know that there’s a powerful framework which can handle it for us.
Yesssssssssssss – however….. hummmmm … we need to have a closer look….. continue reading…. to know if we need to implement or maybe not…..

 

Property Facets in detail

Let’s quickly go through our properties with facets

First of all: all facets are optional.
Facets are declared in the edmx, as attribute of a property, so they belong to one property only.

 

Nullable

This is maybe the most common facet.

Explanation

If we declare a property in our edmx file and add the attribute Nullable=”False”, then we ensure that this property always has a value.
And if it doesn’t, then the OData service call fails.

Code

The FWK partly takes care of verifying this facet.

“Partly” means:

Example:
We have declared a property as Nullable=False
The user invokes a QUERY or READ operation
Our service implementation sends that property with value null
In that case, the FWK validates what we send and fails due to that null value

“Partly” does not mean:

Example:
We have declared a property as Nullable=False
The user invokes a CREATE or UPDATE operation
The user sends that property with value null
In that case, the FWK will NOT check what the user sends
Reason: exactly that might be desired because some values are typically generated by the server, by our service, by the database, etc

So, in this case, the responsibility is on our side.
Either we generate a value
-> then it is ok to send null
Or we really require the user to send a value
-> then we have to respond with an error if the property is null

 

Note:
Usually, when doing a POST request (used for CREATE operation), the user gets the newly created entity in the response body.
We as service implementors have to set the data in the response object.
So, when sending the response of the POST request and if a non-nullable property is null, then the FWK will throw an error. And the status code will be 500 (internal server error), because wrong implementation on our side. Yes, shame on us…

 

Implementation

Sample code 1:

<Property Name="FirstName" Type="Edm.String" Nullable="false"/>

In our example, we’ve declared the property “FirstName” as Nullable=False.
If the user attempts to CREATE a new person without first name, then we respond with an error.
The StatusCode is 400 (bad request), because the user didn’t follow the contract:
The metadata document clearly states that Nullable=False

String firstName = (String) requestBody.get("FirstName");
if(firstName == null) {
   return CreateResponse.setError(
                    ErrorResponse.getBuilder()
                    .setMessage("Error: property FirstName must not be null")
                    .setStatusCode(400)
                    .response());
}

 

Sample code 2:

<Key>
    <PropertyRef Name="PersonGuid" />
</Key>
<Property Name="PersonGuid" Type="Edm.Guid" Nullable="false"/>

In our sample, we’ve declared the property “PersonGuid” as Nullable=False, because it is the key property

A property which is used as key can never be defined as Nullable=True (see here)

However, we don’t force the user to send a guid in the CREATE operation, because we want to generate it. That’s more convenient for the user and also for us.
We even don’t want him to send this property. Smaller request body means less network traffic and better performance.
In any case, we just ignore if it is in the request body, or not. We even don’t read it from the body. We just generate it.

UUID personGuid = UUID.randomUUID(); // generated value

 

Note:
As you know, we’re using mock data, we’re simulating a database or whatever data source.
In a productive scenario, if possible, it would be better to let the database generate a guid, instead of generating it in our service implementation.

 

 

DefaultValue

This one is easier to explain.

Explanation

In the metadata, we can declare that a property always has a default value, and also which value.
So in case that the user of our service doesn’t send a value for it, he can just rely on the default.

Code

The FWK doesn’t provide any support at all for this case.

Implementation

Sample:
In order to give an example, I’ve thought of our people playing a game and scoring points and storing them. In order to distinguish, who of our users is participating in that game, there’s a property “IsMember” which can be true or false.
So when creating a Person with our OData service, this field can be omitted, in case that the person doesn’t know if it will participate.
In such a case, the default is always false, means not playing.
Do you like this example? – (Rhetorical question) – I know it is silly, like all others…

We can define like this

<Property Name="IsMember" Type="Edm.Boolean" DefaultValue="false"/>

and implement like that:

Boolean isMember = (Boolean) requestBody.get("IsMember");
if (isMember == null) {
   isMember = Boolean.FALSE; // default value
}

 

MaxLength

Explanation

Another easy facet: we can determine the allowed length for the value of that property (See here)

Code

The FWK takes care of checking it.

E.g. if the user specifies a too long value in the request body of a POST request, then the FWK will take care of validating the request body against the facet and the request will fail with a proper error message.

Implementation

Sample:
I thought of the following simple scenario:
We want to have information about gender of our people.
In order to reduce the network traffic, we decide that it is enough to use abbreviation: “f” for female and “m” for male. And whatever else which you can think of.
To ensure that the user pf our service doesn’t enter the full text, we specify a facet MaxLength=1 for the property Gender.

<Property Name="Gender" Type="Edm.String" MaxLength="1"/>

That’s it. We don’t need to add anything to our code.
As we know, the FWK will check if the given string is longer than 1 character.

However, if we like, we can check if the value which is given by the user, is correct. It could be an empty string, which we don’t like. Or it could be any character which doesn’t make sense…
But – today we aren’t in the mood of checking that…

Note:
If a UI application is built using our service, then the UI developer will check the metadata, will see that the Gender has a length constraint, and he will design his “Gender” – input field in a manner that doesn’t allow the user to enter more than 1 character.

 

Precision

I don’t know how many times in my life I’ve googled the meaning of this term – because for me it isn’t very intuitive, so I used to forget it and after some time forget again…

Explanation

It can be used together with e.g. decimal data type.
A simple explanation could be: it is used to somehow specify how long a box in the UI needs to be.
Precision is the total count of digits that form the number, it doesn’t care about the dot of a decimal number.
(See here for professional definition)

E.g.
Defining a facet like Precision=5
allows numbers like 123.45
or 1234.5
or 11005

Code

The FWK takes care of checking it, we don’t need to implement anything.

Implementation

Sample:
In our example, I’ve thought of adding a property called AverageGamePoints.
Maybe our people participate in a game where points can be collected, and we store the average. Come on…. it is just an example why we need a decimal data type. And the value won’t be too high. So we add the Precision=5

 

Scale

Similar like Precision and often used in conjunction with Precision and often confused and mixed with it…

Explanation

A decimal number can look like this:
0.5
or also like this
23.94753598201

The difference is: the first one makes sense, the second one is fantasy.
No.
The real difference is in the “Scale”: the first one has Scale=1, the second number has Scale=11

Scale = the maximum number of digits allowed to the right of the decimal point.
(such nice definition can only be quoted from the spec)
Of course, this also means, that also less than the maximum is allowed.

Code

The FWK takes care of checking it, we don’t need to implement anything.

Implementation

Sample:
In our example, you already know about the AverageGamePoints.
So in addition to the Precision, we can add to this property the facet Scale=2
Like that we ensure that no ridiculous numbers will appear, as we don’t need to precise average – it is only a game…

<Property Name="AverageGamePoints" Type="Edm.Decimal" Precision="5" Scale="2"/>

Note:
Do always provide a Scale Facet

Note:
When using the datatype Edm.Decimal, make sure to specify at least the facet “Scale”.
Because, if you don’t specify, it defaults to 0
This means, that normal decimal numbers, which I would call normal, like 9.99, would fail and only 9 would be accepted by your service.
This is specified in the CSDL spec, here:
“If no value is specified, the Scale facet defaults to zero.”

 

 

Intro

After all that theoretical and boring detail knowledge, we finally have reached…. the Intro.

This blog is part of a series of blogs for beginners, which aims to teach how to write OData services easily with the help of SAP Cloud Platform SDK for service development (Intro).

Project

How to create a project and prerequisites for following these blogs.

Model

As usual, we keep everything simple, so is the model of the present blog.
Just a few properties which are suitable for demoing some property facets.

 

<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
	<edmx:DataServices>
		<Schema Namespace="demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
			<EntityType Name="Person">
				<Key>
					<PropertyRef Name="PersonGuid" />
				</Key>
				<Property Name="PersonGuid" Type="Edm.Guid" Nullable="false"/>
				<Property Name="FirstName" Type="Edm.String" Nullable="false"/>
				<Property Name="LastName" Type="Edm.String"/>
				<Property Name="Gender" Type="Edm.String" MaxLength="1"/>
				<Property Name="AverageGamePoints" Type="Edm.Decimal" Scale="2" Precision="5"/>
				<Property Name="IsMember" Type="Edm.Boolean" DefaultValue="false"/>
			</EntityType>
			<EntityContainer Name="container">
				<EntitySet Name="People" EntityType="demo.Person"/>
			</EntityContainer>
		</Schema>
	</edmx:DataServices>
</edmx:Edmx>

 

Code

I think the suitable way for understanding how the facets work and which effect they have is using the CREATE operation.

The user executes a POST request and sends data in the request body.
The FWK helps us in some parts, others have to be handled by us.

So, we’re going to implement and focus on the CREATE operation.
Furthermore, to see the results, we also need the QUERY and READ, but here only minimal required code.

Create

We’ve already discussed the interesting parts in the section above, so let’s only have a quick view on the implementation of the CREATE implementation.

First, we get the request body containing the data which is sent by the user.
We obtain the data for the normal properties.
The values can be null, but that’s no prob.

Map<String, Object> requestBody = request.getMapData();
// normal properties
String lastName = (String) requestBody.get("LastName"); 
BigDecimal points = (BigDecimal) requestBody.get("AverageGamePoints");
String gender = (String) requestBody.get("Gender"); 

 

Then we handle those properties which have facets.

// handling for non-nullable property: if null, then error
String firstName = (String) requestBody.get("FirstName"); 
if(firstName == null) {
    return CreateResponse.setError(
                 ErrorResponse.getBuilder()
                 .setMessage("Error: property FirstName must not be null")
                 .setStatusCode(400)
                 .response());
}
		
// handling for non-nullable key: ignore the value in request and generate it instead
UUID personGuid = UUID.randomUUID(); 
		
//handling for DefaultValue (Nullable=true): if user doesn't send a value, we set the default
Boolean isMember = (Boolean) requestBody.get("IsMember");
if (isMember == null) {
   isMember = Boolean.FALSE;
}

 

At the end, the actual creation is done. In our example it is our dummy mock-database.
That doesn’t matter, the point is that we have done special treatment on some parts of the sent data.

 

// do the actual creation in database
Map<String, Object> createdPerson = createPerson(personGuid, firstName, lastName, points, isMember, gender); 
// we have to explicitly set the response data, because some of the values are computed by us  
return CreateResponse.setSuccess().setData(createdPerson).response();  

 

The full code can be found at the end of this page.

 

Test

As mentioned above, in order to test the functionality, we’re sending POST request with valid and invalid data in the request body.

So, after build and deploy, use a REST client to fire POST requests to this URL:

http://<yourHost>/DemoService/odata/v4/DemoService/People

Don’t forget the set the content-type header to application/json

  1. Test the non-nullable key property:
    Request body contains a value for the key

    {
       "PersonGuid": "7aa27a37-55dd-48ed-ab8b-90f51748d059",
       "FirstName": "Erik",
       "LastName": "Hendricks",
       "Gender": "m",
       "AverageGamePoints": 15.45,
       "IsMember": true
    }

    Result:
    Success
    a new PersonGuid is generated, sending it has no effect

  2. Test the non-nullable key property
    Request body doesn’t contain the key property

    {
       "FirstName": "Erika",
       "LastName": "Lenz",
       "Gender": "f",
       "AverageGamePoints": 14.87,
       "IsMember": true
    }

    Result:
    Sucess
    a new PersonGuid is generated, omitting it is not a problem

  3. Test the non-nullable property FirstName
    Request body doesn’t contain the property

    {
       "LastName": "Martinez",
       "Gender": "f",
       "AverageGamePoints": 19.99,
       "IsMember": true
    }

    Result:
    Error, status code 400
    This is the error which we have coded, it is expected

  4. Test the normal property LastName (nullable=true, which is the default)
    Request body doesn’t contain the property

    {
       "FirstName": "Michael",
       "Gender": "m",
       "AverageGamePoints": 16.66,
       "IsMember": true
    }

    Result:
    Success
    The response contains null as value for LastName. Which is expected.

  5. Test the property IsMember
    Request body doesn’t contain the property

    {
       "FirstName": "Martin",
       "LastName": "Melter",
       "Gender": "m",
       "AverageGamePoints": 8.12
    }

    Result:
    Success
    The response shows the IsMember has value false. Which is the default.

  6. Test the property “AverageGamePoints” which is of type decimal
    Request body contains normal decimal value

    {
       "FirstName": "Martin",
       "LastName": "Melter",
       "Gender": "m",
       "AverageGamePoints": 123.45,
       "IsMember": true
    }

    Result:
    Success

  7. Test the property “AverageGamePoints”
    Request body contains invalid scale

    {
       "FirstName": "Martin",
       "LastName": "Melter",
       "Gender": "m",
       "AverageGamePoints": 11.1234,
       "IsMember": true
    }

    Result:
    Error
    The error response indicates that the property has invalid value

  8. Test the property “AverageGamePoints”
    Request body contains invalid precision

    {
       "FirstName": "Martin",
       "LastName": "Melter",
       "Gender": "m",
       "AverageGamePoints": 1234.56,
       "IsMember": true
    }

    Result:
    Error
    The error response indicates that the property has invalid value

  9. Test the property Gender
    Request body contains a value which is longer than allowed

    {
       "FirstName": "Camille",
       "LastName": "Claudel",
       "Gender": "female",
       "AverageGamePoints": 0
    }

    Result:
    Error
    The error response indicates that the value is invalid

 

 

Summary

We’ve learned to understand the most important property facets and their impact on our OData service.
We’ve also learned what we have to take care about in the code.
And what is handled by the framework.

All what you’ve learned here you can use in any model and any OData service.

 

Links

Overview of blog series and link collection.

 

Appendix 1: Source code of Model file: DemoService.xml

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
	<edmx:DataServices>
		<Schema Namespace="demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
			<EntityType Name="Person">
				<Key>
					<PropertyRef Name="PersonGuid" />
				</Key>
				<Property Name="PersonGuid" Type="Edm.Guid" Nullable="false"/>
				<Property Name="FirstName" Type="Edm.String" Nullable="false"/>
				<Property Name="LastName" Type="Edm.String"/>
				<Property Name="Gender" Type="Edm.String" MaxLength="1"/>
				<Property Name="AverageGamePoints" Type="Edm.Decimal" Scale="2" Precision="5"/>
				<Property Name="IsMember" Type="Edm.Boolean" DefaultValue="false"/>
			</EntityType>
			<EntityContainer Name="container">
				<EntitySet Name="People" EntityType="demo.Person"/>
			</EntityContainer>
		</Schema>
	</edmx:DataServices>
</edmx:Edmx>

Appendix 2: Source code of Java class: ServiceImplementation.java

package com.example.DemoService;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.sap.cloud.sdk.service.prov.api.operations.Create;
import com.sap.cloud.sdk.service.prov.api.operations.Query;
import com.sap.cloud.sdk.service.prov.api.operations.Read;
import com.sap.cloud.sdk.service.prov.api.request.CreateRequest;
import com.sap.cloud.sdk.service.prov.api.request.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.request.ReadRequest;
import com.sap.cloud.sdk.service.prov.api.response.CreateResponse;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;

public class ServiceImplementation {
	
	private static final List<Map<String, Object>> peopleList = new ArrayList<Map<String, Object>>();
	
	@Query(serviceName="DemoService", entity="People")
	public QueryResponse getPeople(QueryRequest request) {
		List<Map<String, Object>> peopleList = getAllPeople();
		return QueryResponse.setSuccess().setDataAsMap(peopleList).response();
	}

	@Read(serviceName="DemoService", entity="People")
	public ReadResponse getPerson(ReadRequest request) {
		UUID guid = (UUID) request.getKeys().get("PersonGuid");
		Map<String, Object> personMap = findPerson(guid);
		return ReadResponse.setSuccess().setData(personMap).response();
	}	
	
	
	@Create(serviceName = "DemoService", entity = "People")
	public CreateResponse createPerson(CreateRequest request) {

		// extract the data for creation 
		Map<String, Object> requestBody = request.getMapData();
		// normal properties
		String lastName = (String) requestBody.get("LastName"); 
		BigDecimal points = (BigDecimal) requestBody.get("AverageGamePoints");
		String gender = (String) requestBody.get("Gender"); 
		
		// handling for non-nullable property: if null, then error
		String firstName = (String) requestBody.get("FirstName"); 
		if(firstName == null) {
			return CreateResponse.setError(ErrorResponse.getBuilder().setMessage("Error: property FirstName must not be null").setStatusCode(400).response());
		}
		
		// handling for non-nullable key: ignore the value in request and generate it instead
		UUID personGuid = UUID.randomUUID(); 
		
		//handling for DefaultValue (Nullable=true): if user doesn't send a value, we set the default
		Boolean isMember = (Boolean) requestBody.get("IsMember");
		if (isMember == null) {
			isMember = Boolean.FALSE;
		}

		// do the actual creation in database
		Map<String, Object> createdPerson = createPerson(personGuid, firstName, lastName, points, isMember, gender);
		// we have to set the response, because we compute some of the fields in our code
		return CreateResponse.setSuccess().setData(createdPerson).response();  
	}		
	
	
	/* Dummy Database */
	
	private List<Map<String, Object>> getAllPeople(){
		
		// init the "database"
		if(peopleList.isEmpty()) {
			createPerson(UUID.randomUUID(), "Anna", "Martinez", BigDecimal.valueOf(11.11), false, "f");
			createPerson(UUID.randomUUID(), "Berta", "Hughes", BigDecimal.valueOf(18.88), false, "f");
			createPerson(UUID.randomUUID(), "Claude", "Forrester", BigDecimal.valueOf(12.34), true, "m");
			createPerson(UUID.randomUUID(), "Detlev", "Huffington", BigDecimal.valueOf(19.19), true, "m");
		}
		
		return peopleList;
	}
	
	
	// used for READ operation. Go through the list and find the person with requested PersonId
	private Map<String, Object> findPerson(UUID requiredPersonId){
		List<Map<String,Object>> peopleList = getAllPeople();
		for(Map<String, Object> personMap : peopleList) {
			if(((UUID)personMap.get("PersonGuid")).equals(requiredPersonId)) {
				return personMap;
			}
		}
		return null;
	}
	
	private Map<String, Object> createPerson(UUID guid, String firstName, String lastName, BigDecimal points, Boolean isMember, String gender){
		Map<String, Object> personMap = new HashMap<String, Object>();

		personMap.put("PersonGuid", guid);
		personMap.put("FirstName", firstName);
		personMap.put("LastName", lastName);
		personMap.put("AverageGamePoints", points);
		personMap.put("Gender", gender);
		personMap.put("IsMember", isMember);

		peopleList.add(personMap);
		
		return personMap;
	}
}
To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply