SAPOData

Use the SAPOData component to interact with an OData endpoint. SAPOData parses OData payloads, produces OData requests, and handles responses for OData versions 2 and 4.

Dependencies

The SAPOData component depends on the following component:

  • SAPCommon
  • SAPFoundation

Installation

Add SAPCommon.framework, SAPFoundation.framework and SAPOData.framework to your XCode Project. Make sure Copy Items is selected. Your app is now linked to the Frameworks and they should appear under Link Binary with Libraries in the Build Phases tab of your app’s XCode project settings.

Proxy class generation

The SAP Cloud Platform SDK for iOS includes tooling to generate Swift classes that help consume a specified OData service. You can use the OData service’s metadata document and generate your data model from it. The generated Swift code is based on the SAPOData framework and provides strongly typed functions to perform OData operations on the data objects.

The sapcpsdk-proxygenerator tool can be installed via SAP Cloud Platform SDK for iOS Assistant: select the main menu of Assistant and click Install Tools. Administration right will be needed to install the class-generator tool. By default it is installed into usr/local/bin folder so that it can be used easily from the Terminal.

Restriction! sapcpsdk-proxygenerator is only available on macOS.

Use the sapcpsdk-proxygenerator to generate proxy class Swift files based on OData metadata.

Dependencies and Installation

  • Dependencies: sapcpsdk-proxygenerator has no external dependencies.
  • Installation: since sapcpsdk-proxygenerator is a command line tool no installation is required

OData Proxy Class Generator Usage

  1. If you have placed the proxy generator resources outside of /usr/local/bin (default folder if installed via Assistant), set the environment variable <$ODATAPROXY_HOME> to point to the directory where sapcpsdk-proxygenerator and resources were placed, for example ./Tools/ProxyGenerator/.

  2. Download the desired metadata document (for example, MyMetadata.xml)

  3. Enter the following command in terminal to generate the Swift proxy class: $ODATAPROXY_HOME/sapcpsdk-proxygenerator -m <path to: MyMetadata.xml> -s <MyServiceName> -cp <Generated Proxy Class Prefix> -d <Generated Proxy Destination> where

  4. MyMetadata.xml : is the downloaded metadata document (required)

  5. MyServiceName : is the generated service class name (optional)

Note: if you are using the tool outside of /usr/local/bin, you have to provide the generator resource path as well using the -p option.

  1. Find the generated .swift file(s) by default in the $ODATAPROXY_HOME/public subfolder or in the <Generated Proxy Destination>/public subfolder if it was set using -d option.

Configuration Parameters

  • -m, –metadata The path of the metadata. (required from file or piped)
  • -p, –proxylibs Path to the proxy generator resources. (optional, alternatively use environment variable <PROXYLIBS=/my/sapcpsdk-resources>; if not configured, the default search path is <location of sapcpsdk-proxygenerator>/sapcpsdk-resources>).
  • -s, –service The generated service’s name. (optional)
  • -cp, –prefix Swift target name prefix. (optional)
  • -d, –destination The destination where the proxy classes will be generated. (optional)
  • -dio, –disableOptionals Disable generation of optionals for non-nullable entity properties. By default, this is enabled. (optional)
  • –no-interaction Disable user interaction and use default values automatically. (optional)
  • -h, –help Help
  • -v, –verbose Verbose mode on. By default, this is turned off, however, it can be very useful for debugging.

Other Parameters

Additional parameters can be appended to the command line tool.

  • -package Output package name. Defaults to CSDL Schema Namespace. Use “none" to omit target package.
  • -internal Generate internal proxy classes. Defaults to public proxy classes.
  • -open:enumerations Enumeration types permit member addition. Defaults to non-extensible enumerations.
  • -prefix Name prefix for generated classes. Helps avoid conflict between models.
  • -reference Directory containing referenced schemas. Defaults to input schema directory.

Generated Proxy Class Usage

To use the SAPOData component, see the API documentation of the DataService and DataQuery classes as a starting point.

In the code samples below, generated proxy classes of the publicly accessible OData Test Service are used:


let serviceEndpoint = URL(string: "http://services.odata.org/V4/(S(readwrite))/OData/OData.svc/")!
let provider = OnlineODataProvider(serviceRoot: serviceEndpoint)

//initialize OData service from the generated proxy class with the provider.
var service = ODataV4ServiceProxy(provider: provider)

// Load the list of categories from the service
var categories = try service.categories()

// Create a new entity
var newCategory = ...
try service.createEntity(newCategory)

// Update an entity
newCategory.name = "updated name"
try service.updateEntity(newCategory)

// Remove an entity
try service.deleteEntity(newCategory)

// In order to have an access to the property of an entity which conforms a complex type,
// the list of entities should be loaded by data query which expands those properties
let query = DataQuery().expand(Product.productDetail, Product.supplier)
let products = try service.products(query: query)

// Load the value of a property of a previously loaded entity, or an entity whose key properties have been set.
// This can be applied to both structural and navigation properties.
// It is most useful for loading navigation properties, if the server cannot handle queries using DataQuery.expand
let customer = ...
try service.loadProperty(Customer.orders, into: customer)

Debugging

If you want to troubleshoot OData communication between the app and the server, you can enable trace logging. This shows the complete network traffic. Be aware that this might also print confidential data and might not be appropriate on productive systems.

Logger.shared(named: "SAP.OData").logLevel = .debug

service.traceRequests = true
service.prettyTracing = true
service.traceWithData = true


Refresh metadata from the backend server

If the metadata was previously loaded (or was obtained from generated proxy classes), then a compatibility check is performed. If the latest metadata is not compatible with the previous metadata, CsdlException will be thrown. If the latest metadata is compatible with the previous metadata, the latest metadata will be applied. It is generally recommended to use this function during application startup to check if the server’s metadata has been updated since the client application was constructed.

Compatible metadata changes include:

  • Adding structural/navigation properties to complex/entity types.
  • Adding new types (enumeration, simple, complex, entity).
  • Adding new entity sets or singletons.
  • Adding new actions or functions.

Other additions, changes, and removals are considered incompatible by default, including:

  • Adding members to an enumeration type.
  • Changing the base type for any type.
  • Changing the value of an enumeration member.
  • Changing the type (or nullability) of any structural/navigation property.
  • Changing the type (or nullability) of any action/function parameter or result.
  • Removing the definition of a model element.
  • Removing members from an enumeration type.
  • Removing structural/navigation properties from a complex/entity type.

Addition of enumeration members can be pre-approved by a caller using the dynamic API before calling refreshMetadata (see CsdlDocument.hasOpenEnumerations). If an application uses generated proxy classes, then generating them with the -open:enumerations option will automate the necessary pre-approval. The hasOpenEnumerations flag should only be explicitly set when using the dynamic API. Explicitly setting the hasOpenEnumerations flag when using generated proxy classes (generated without the -open:enumerations option) could result in runtime exceptions.

Changes to model elements can be pre-approved by a caller using the dynamic API before calling refreshMetadata (see CsdlDocument.canChangeAnything). Applications using generated proxy classes should not pre-approve such changes, as they are likely to result in application instability. For example, if a property’s data type is changed, it could result in runtime exceptions since proxy class properties have a pre-determined type that is embedded into the application’s compiled code.

Removal of model elements can be pre-approved by the caller before calling refreshMetadata (see CsdlDocument.canRemoveAnything), or preferably by setting the canBeRemoved flag on model elements that the application is prepared for the removal of. Application developers should take care not to pre-approve the removal of model elements unless the application is coded to check at runtime for the possible removal of those elements. The allowance for removals is intended to support newer versions of client applications communicating with older service implementations but in the general case may require the application to have some embedded knowledge of the changes that were made to the service metadata between the older and newer service implementations. If a newer client application makes unconditional use of a model element that did not exist in an older service implementation, then the non-existence of that model element after calling refreshMetadata could result in runtime exceptions.

If refreshMetadata succeeds, then any added model elements will have isExtension == true, and any removed model elements will have isRemoved == true. Changed model elements will not be distinguishable.

Comparision of the dynamic API and the proxy classes

The following code samples are based on Northwind OData service.

Getting an entity list using proxy classes


func entityListExample() throws -> Void {
let ds = self.service
let query = DataQuery().select(Customer.customerID, Customer.companyName, Customer.contactName).orderBy(Customer.companyName)
var customers = try ds.customers(query: query)
for customer in customers {
try self.showCustomer(customer)
}
}

Getting an entity list using dynamic API


func entityListExample() throws -> Void {
let ds = self.service
let s_customers = try ds.entitySet(withName: "Customers")
let t_customer = s_customers.entityType
let p_customerID = t_customer.property(withName: "CustomerID")
let p_companyName = t_customer.property(withName: "CompanyName")
let p_contactName = t_customer.property(withName: "ContactName")
let query = DataQuery().select(p_customerID, p_companyName, p_contactName).from(s_customers).orderBy(p_companyName)
let customers = try ds.executeQuery(query).entityList()
for customer in customers {
try self.showCustomer(customer)
}
}

Creating an entity using proxy classes


func createEntityExample() throws -> Void {
let ds = self.service
let customer = Customer()
try customer.customerID = GuidValue.random().toString()
customer.companyName = "Enterprise Inc."
customer.contactName = "Jean-Luc Picard"
try ds.createEntity(customer)
}

Creating an entity using dynamic API


func createEntityExample() throws -> Void {
let ds = self.service
let s_customers = try ds.entitySet(withName: "Customers")
let t_customer = s_customers.entityType
let p_customerID = t_customer.property(withName: "CustomerID")
let p_companyName = t_customer.property(withName: "CompanyName")
let p_contactName = t_customer.property(withName: "ContactName")
let customer = EntityValue.ofType(t_customer)
p_customerID.setStringValue(in: customer, to: GuidValue.random().toString())
p_companyName.setStringValue(in: customer, to: "Enterprise Inc.")
p_contactName.setStringValue(in: customer, to: "Jean-Luc Picard")
try ds.createEntity(customer)
}

Updating an entity using proxy classes


func updateEntityExample() throws -> Void {
let ds = self.service
let query = DataQuery().top(1).filter(Customer.contactName.equal("Jean-Luc Picard"))
let customers = try ds.customers(query: query)
if let customer = customers.first {
customer.contactName = "Beverly Crusher"
try ds.updateEntity(customer)
}
}

Updating an entity using dynamic API


func updateEntityExample() throws -> Void {
let ds = self.service
let s_customers = try ds.entitySet(withName: "Customers")
let t_customer = s_customers.entityType
let p_contactName = t_customer.property(withName: "ContactName")
let query = DataQuery().top(1).from(s_customers).filter(p_contactName.equal("Jean-Luc Picard"))
let customer = try ds.executeQuery(query).requiredEntity()
p_contactName.setStringValue(in: customer, to: "Beverly Crusher")
try ds.updateEntity(customer)
}

Deleting an entity using proxy classes


func deleteEntityExample() throws -> Void {
let ds = self.service
let query = DataQuery().top(1).selectKey().filter(Customer.contactName.equal("Beverly Crusher"))
let customers = try ds.customers(query: query)
if let customer = customers.first {
try ds.deleteEntity(customer)
}
}

Deleting an entity using dynamic API


func deleteEntityExample() throws -> Void {
let ds = self.service
let s_customers = try ds.entitySet(withName: "Customers")
let t_customer = s_customers.entityType
let p_contactName = t_customer.property(withName: "ContactName")
let query = DataQuery().top(1).selectKey().from(s_customers).filter(p_contactName.equal("Beverly Crusher"))
let customer = try ds.executeQuery(query).requiredEntity()
try ds.deleteEntity(customer)
}