blob: 44c9c67f86eca9d213e2ebe46f286d31cf597dbf [file] [log] [blame]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Apache Olingo provides libraries which enable developers to implement OData producers and OData consumers. The available OData Java library implements OData version 2.0. In future on goal is to provide an OData 4.0 compliant library once the OData standard is published at OASIS. The focus within the community is currently on the Java technology but it is up to the community to discuss if other environments find interest.">
<meta name="author" content="">
<link rel="icon" href="/favicon.ico">
<title>Apache Olingo Library</title>
<!-- Bootstrap core CSS -->
<link href="/css/bootstrap.css" rel="stylesheet" type="text/css"><!-- Custom styles for this template -->
<link href="/css/navbar.css" rel="stylesheet" type="text/css"><!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
<link href="/css/offcanvas.css" rel="stylesheet" type="text/css"><!-- Custom styles for this template -->
<link rel="stylesheet" href="/css/main.css">
<!--[if lt IE 9]><script src="/js/ie8-responsive-file-warning.js"></script><![endif]-->
<style>
.headerlink {
visibility: hidden;
}
dt:hover > .headerlink, p:hover > .headerlink, td:hover > .headerlink, h1:hover > .headerlink, h2:hover > .headerlink, h3:hover > .headerlink, h4:hover > .headerlink, h5:hover > .headerlink, h6:hover > .headerlink {
visibility: visible
} </style>
<script src="/js/ie-emulation-modes-warning.js" type="text/javascript">
</script><!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="/js/ie10-viewport-bug-workaround.js" type="text/javascript">
</script><!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="/js/html5shiv.min.js"></script>
<script src="/js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<!-- Static navbar -->
<div class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<img class="navbar-brand" src="/img/OlingoOrangeTM.png" style="width:62px;" >
<a class="navbar-brand" href="/">Apache Olingo™</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">ASF <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="http://www.apache.org/foundation/">ASF Home</a></li>
<li><a href="http://projects.apache.org/">Projects</a></li>
<li><a href="http://people.apache.org/">People</a></li>
<li><a href="http://www.apache.org/foundation/getinvolved.html">Get Involved</a></li>
<li><a href="http://www.apache.org/dyn/closer.cgi">Download</a></li>
<li><a href="http://www.apache.org/security/">Security</a></li>
<li><a href="http://www.apache.org/foundation/sponsorship.html">Support Apache</a></li>
</ul>
</li>
<li><a href="http://www.apache.org/licenses/">License</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Download <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/doc/odata2/download.html">Download OData 2.0 Java</a></li>
<li><a href="/doc/odata4/download.html">Download OData 4.0 Java</a></li>
<li><a href="/doc/javascript/download.html">Download OData 4.0 JavaScript</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Documentation <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/doc/odata2/index.html">Documentation OData 2.0 Java</a></li>
<li><a href="/doc/odata4/index.html">Documentation OData 4.0 Java</a></li>
<li><a href="/doc/javascript/index.html">Documentation OData 4.0 JavaScript</a></li>
</ul>
</li>
<li><a href="/support.html">Support</a></li>
<li><a href="/contribute.html">Contribute</a></li>
</ul>
<a class="navbar-right" href="http://www.apache.org/foundation/" target="_blank">
<img class="navbar-right" height="50px" src="/img/asf_logo_url.svg" alt="Apache Software Foundation">
</a>
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</div><!-- Main component for a primary marketing message or call to action -->
<h1 id="how-to-build-an-odata-service-with-olingo-v4">How to build an OData Service with Olingo V4<a class="headerlink" href="#how-to-build-an-odata-service-with-olingo-v4" title="Permalink">&para;</a></h1>
<h1 id="part-10-bound-actions-and-functions">Part 10: Bound Actions and Functions<a class="headerlink" href="#part-10-bound-actions-and-functions" title="Permalink">&para;</a></h1>
<p><strong>Table of Contents</strong></p>
<div id="toc"><ul><li><a class="toc-href" href="#introduction" title="Introduction">Introduction</a></li><li><a class="toc-href" href="#preparation" title="Preparation">Preparation</a></li><li><a class="toc-href" href="#implementation" title="Implementation">Implementation</a><ul><li><a class="toc-href" href="#extend-the-metadata-model" title="Extend the Metadata model">Extend the Metadata model</a></li><li><a class="toc-href" href="#extend-the-data-store" title="Extend the data store">Extend the data store</a></li><li><a class="toc-href" href="#extend-the-entity-collection-and-the-entity-processor-to-handle-functions" title="Extend the entity collection and the entity processor to handle functions">Extend the entity collection and the entity processor to handle functions</a></li><li><a class="toc-href" href="#implement-an-action-processor" title="Implement an action processor">Implement an action processor</a></li></ul></li><li><a class="toc-href" href="#run-the-implemented-service" title="Run the implemented service">Run the implemented service</a></li><li><a class="toc-href" href="#links" title="Links">Links</a><ul><li><a class="toc-href" href="#tutorials" title="Tutorials">Tutorials</a></li><li><a class="toc-href" href="#code-and-repository" title="Code and Repository">Code and Repository</a></li><li><a class="toc-href" href="#further-reading" title="Further reading">Further reading</a></li></ul></li></ul></div>
<h2 id="introduction">Introduction<a class="headerlink" href="#introduction" title="Permalink">&para;</a></h2>
<p>In the present tutorial, we&rsquo;ll implement a bound action and function.</p>
<p><strong>Note:</strong>
The final source code can be found in the project <a href="https://gitbox.apache.org/repos/asf/olingo-odata4">git repository</a>.
A detailed description how to checkout the tutorials can be found <a href="/doc/odata4/tutorials/prerequisites/prerequisites.html">here</a>.<br/>
This tutorial can be found in subdirectory <code>/samples/tutorials/p9_action</code></p>
<p>The <a href="http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html#_Toc406398201">OData V4 specification</a> gives us a definition what <em>Functions</em>, <em>Actions</em> are:</p>
<blockquote>
<p>Operations allow the execution of custom logic on parts of a data
model. Functions are operations that do not have side effects and may
support further composition, for example, with additional filter
operations, functions or an action. Actions are operations that allow
side effects, such as data modification, and cannot be further
composed in order to avoid non-deterministic behavior. Actions and
functions are either bound to a type, enabling them to be called as
members of an instance of that type, or unbound, in which case they
are called as static operations. Action imports and function imports
enable unbound actions and functions to be called from the service
root.</p>
</blockquote>
<p>In this short definition are several terms which are to be explained first. As you might expect operation is the superordinate of functions and actions. The result of operations can be:</p>
<ul>
<li>An <em>entity</em> or a <em>collection of entities</em></li>
<li>A <em>primitive property</em> or a <em>collection of primitive properties</em></li>
<li>A <em>complex property</em> or a <em>collection of complex properties</em></li>
</ul>
<p>In addition an <em>Action</em> can return void that means there is no return value. A <em>Function</em> must return a value.</p>
<p>First an <em>Operation</em> can be bound or unbound.In this tutorial we will focus on bound operation.</p>
<p>Bound actions and functions support overloading (multiple actions having the same name within the same namespace) by binding parameter type. The combination of action name and the binding parameter type MUST be unique within a namespace.</p>
<p>An action or a function element MAY specify a Boolean value for the IsBound attribute.
Actions/Functions whose IsBound attribute is false or not specified are considered unbound. Unbound actions/functions are invoked through an action import/function import.
Actions/Functions whose IsBound attribute is true are considered bound. Bound actions/functions are invoked by appending a segment containing the qualified action name to a segment of the appropriate binding parameter type within the resource path. Bound actions/functions MUST contain at least one edm:Parameter element, and the first parameter is the binding parameter. The binding parameter can be of any type, and it MAY be nullable.</p>
<p>Bound actions/functions that return an entity or a collection of entities MAY specify a value for the EntitySetPath attribute if determination of the entity set for the return type is contingent on the binding parameter. The value for the EntitySetPath attribute consists of a series of segments joined together with forward slashes. The first segment of the entity set path MUST be the name of the binding parameter. The remaining segments of the entity set path MUST represent navigation segments or type casts.</p>
<p>A navigation segment names the SimpleIdentifier of the navigation property to be traversed. A type cast segment names the QualifiedName of the entity type that should be returned from the type cast.</p>
<p><strong>Example</strong></p>
<p>For example there can be a bound action createOrders which is bound to the Customer entity having 2 parameters</p>
<p>Such an action can be expressed in the metadata document as follows</p>
<pre><code class="language-xml"> &lt;Action Name="CreateOrder" isBound=&rdquo;true&rdquo;&gt;
&lt;Parameter Name="Customers" Type="SampleEntities.Customer" Nullable="false"/&gt;
&lt;Parameter Name="quantity" Type="Edm.Int16" Nullable="false"/&gt;
&lt;Parameter Name="discountCode" Type="Edm.String" Nullable="false"/&gt;
&lt;ReturnType Type="Collection(SampleEntities.Orders)"/&gt;
&lt;/Action&gt;
</code></pre>
<p>To call such a bound action the client issues a POST request to a URL identifying the action. In this simple case such a call could look like this:</p>
<pre><code> POST http://host/service/Customers('ALFKI')/SampleEntities.CreateOrder
{
"quantity": 2,
"discountCode": "BLACKFRIDAY"
}
</code></pre>
<p>Similarly there can be a bound function GetOrders which is bound to the Customer entity having 1 parameter</p>
<p>Such a function can be expressed in the metadata document as follows</p>
<pre><code class="language-xml"> &lt;Function Name="GetOrders" isBound=&rdquo;true&rdquo;&gt;
&lt;Parameter Name="Customers" Type="SampleEntities.Customer" Nullable="false"/&gt;
&lt;Parameter Name="discountCode" Type="Edm.String" Nullable="false"/&gt;
&lt;ReturnType Type="Collection(SampleEntities.Orders)"/&gt;
&lt;/Function&gt;
</code></pre>
<p>To call such a bound function the client issues a GET request to a URL identifying the function. In this simple case such a call could look like this:</p>
<pre><code> GET http://host/service/Customers('ALFKI')/SampleEntities.GetOrders(discountCode='BLACKFRIDAY')
</code></pre>
<h2 id="preparation">Preparation<a class="headerlink" href="#preparation" title="Permalink">&para;</a></h2>
<p>You should read the previous tutorials first to have an idea how to read entities and entity collections.</p>
<p>As a shortcut you should checkout the prepared tutorial project in the <a href="https://gitbox.apache.org/repos/asf/olingo-odata4">git repository</a> in folder /samples/tutorials/p9_action_preparation.</p>
<p>Afterwards do a Deploy and run: it should be working. At this state you can perform CRUD operations and do navigations between products and categories.</p>
<h2 id="implementation">Implementation<a class="headerlink" href="#implementation" title="Permalink">&para;</a></h2>
<p>We use the given data model you are familiar with. To keep things simple we implement one bound action and one bound function.</p>
<p><strong>Bound Action that returns a collection of entities: DiscountProducts</strong><br/>
This action takes bound parameter &ldquo;<em>ParamCategory</em>&rdquo; and an additional parameter &ldquo;<em>Amount</em>&rdquo;. The action updates the price of all products related to categories by applying the discount amount.</p>
<p>After finishing the implementation the definition of the action should be like this:</p>
<pre><code class="language-xml"> &lt;Action Name="DiscountProducts" IsBound="true"&gt;
&lt;Parameter Name="ParamCategory" Type="Collection(OData.Demo.Category)"/&gt;
&lt;Parameter Name="Amount" Type="Edm.Int32"/&gt;
&lt;ReturnType Type="Collection(OData.Demo.Product)"/&gt;
&lt;/Action&gt;
</code></pre>
<p><strong>Bound Action that returns an entity: DiscountProduct</strong></p>
<p>This action takes bound parameter &ldquo;<em>ParamCategory</em>&rdquo; and an additional parameter &ldquo;<em>Amount</em>&rdquo;. The actions updates the price of a specific products related to a category by applying the discount amount.
After finishing the implementation the definition of the action should be like this:</p>
<pre><code class="language-xml"> &lt;Action Name="DiscountProduct" IsBound="true"&gt;
&lt;Parameter Name="ParamCategory" Type=&rdquo;OData.Demo.Category"/&gt;
&lt;Parameter Name="Amount" Type="Edm.Int32"/&gt;
&lt;ReturnType Type=" OData.Demo.Product"/&gt;
&lt;/Action&gt;
</code></pre>
<p>While actions are called by using HTTP Method POST is nessesary to introduce new processor interfaces for actions. So there exists a bunch of interfaces, for each return type strictly one.</p>
<p><strong>Bound Function that returns a collection of entities: GetDiscountedProducts</strong><br/>
This function takes bound parameter &ldquo;<em>ParamCategory</em>&rdquo; and an additional parameter &ldquo;<em>Amount</em>&rdquo;. The function lists all the products related to categories which are eligible for the discount amount.</p>
<p>After finishing the implementation the definition of the action should be like this:</p>
<pre><code class="language-xml"> &lt;Function Name="GetDiscountedProducts" IsBound="true"&gt;
&lt;Parameter Name="ParamCategory" Type="Collection(OData.Demo.Category)"/&gt;
&lt;Parameter Name="Amount" Type="Edm.Int32"/&gt;
&lt;ReturnType Type="Collection(OData.Demo.Product)"/&gt;
&lt;/Function&gt;
</code></pre>
<p><strong>Bound Function that returns an entity: GetDiscountedProduct</strong></p>
<p>This function takes bound parameter &ldquo;<em>ParamCategory</em>&rdquo; and an additional parameter &ldquo;<em>Amount</em>&rdquo;. The function lists one specific product related to a category which is eligible for the discount amount.
After finishing the implementation the definition of the action should be like this:</p>
<pre><code class="language-xml"> &lt;Function Name="GetDiscountedProduct" IsBound="true"&gt;
&lt;Parameter Name="ParamCategory" Type=&rdquo;OData.Demo.Category"/&gt;
&lt;Parameter Name="Amount" Type="Edm.Int32"/&gt;
&lt;ReturnType Type=" OData.Demo.Product"/&gt;
&lt;/Function&gt;
</code></pre>
<p><strong>Steps</strong></p>
<ul>
<li>Extend the Metadata model</li>
<li>Extend the data store</li>
<li>Implement an action processor</li>
</ul>
<h3 id="extend-the-metadata-model">Extend the Metadata model<a class="headerlink" href="#extend-the-metadata-model" title="Permalink">&para;</a></h3>
<p>Create the following constants in the DemoEdmProvider. These constants are used to address the actions.</p>
<pre><code class="language-java"> //Bound Action
public static final String ACTION_PROVIDE_DISCOUNT = "DiscountProducts";
public static final FullQualifiedName ACTION_PROVIDE_DISCOUNT_FQN = new FullQualifiedName(NAMESPACE, ACTION_PROVIDE_DISCOUNT);
public static final String ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT = "DiscountProduct";
public static final FullQualifiedName ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN = new FullQualifiedName(NAMESPACE, ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT);
//Bound Function
public static final String FUNCTION_PROVIDE_DISCOUNT = "GetDiscountedProducts";
public static final FullQualifiedName FUNCTION_PROVIDE_DISCOUNT_FQN = new FullQualifiedName(NAMESPACE, FUNCTION_PROVIDE_DISCOUNT);
public static final String FUNCTION_PROVIDE_DISCOUNT_FOR_PRODUCT = "GetDiscountedProduct";
public static final FullQualifiedName FUNCTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN = new FullQualifiedName(NAMESPACE, FUNCTION_PROVIDE_DISCOUNT_FOR_PRODUCT);
//Parameters
public static final String PARAMETER_AMOUNT = "Amount";
//Binding Parameter
public static final String PARAMETER_CATEGORY = "ParamCategory";
</code></pre>
<p>The way to announce the operations is very similar to announcing EntityTypes. We have to override some methods. Those methods provide the definition of the Edm elements. We need methods for:</p>
<ul>
<li>Actions</li>
<li>Functions</li>
</ul>
<p>The code is simple and straight forward. We need to create a list of parameters of which the first parameter should be the binding parameter, then create a return type. At the end all parts are fit together and get returned as new CsdlAction Object.</p>
<pre><code class="language-java"> @Override
public List&lt;CsdlAction&gt; getActions(final FullQualifiedName actionName) {
// It is allowed to overload actions, so we have to provide a list of Actions for each action name
final List&lt;CsdlAction&gt; actions = new ArrayList&lt;CsdlAction&gt;();
if (actionName.equals(ACTION_PROVIDE_DISCOUNT_FQN)) {
// Create parameters
final List&lt;CsdlParameter&gt; parameters = new ArrayList&lt;CsdlParameter&gt;();
CsdlParameter parameter = new CsdlParameter();
parameter.setName(PARAMETER_CATEGORY);
parameter.setType(ET_CATEGORY_FQN);
parameter.setCollection(true);
parameters.add(parameter);
parameter = new CsdlParameter();
parameter.setName(PARAMETER_AMOUNT);
parameter.setType(EdmPrimitiveTypeKind.Int32.getFullQualifiedName());
parameters.add(parameter);
// Create the Csdl Action
final CsdlAction action = new CsdlAction();
action.setName(ACTION_PROVIDE_DISCOUNT_FQN.getName());
action.setBound(true);
action.setParameters(parameters);
action.setReturnType(new CsdlReturnType().setType(ET_PRODUCT_FQN).setCollection(true));
actions.add(action);
return actions;
} else if (actionName.equals(ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN)) {
// Create parameters
final List&lt;CsdlParameter&gt; parameters = new ArrayList&lt;CsdlParameter&gt;();
CsdlParameter parameter = new CsdlParameter();
parameter.setName(PARAMETER_CATEGORY);
parameter.setType(ET_CATEGORY_FQN);
parameter.setCollection(false);
parameters.add(parameter);
parameter = new CsdlParameter();
parameter.setName(PARAMETER_AMOUNT);
parameter.setType(EdmPrimitiveTypeKind.Int32.getFullQualifiedName());
parameters.add(parameter);
// Create the Csdl Action
final CsdlAction action = new CsdlAction();
action.setName(ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN.getName());
action.setBound(true);
action.setParameters(parameters);
action.setReturnType(new CsdlReturnType().setType(ET_PRODUCT_FQN).setCollection(false));
actions.add(action);
return actions;
}
return null;
}
</code></pre>
<p>Similarly, for functions we need to create a list of parameters of which the first parameter should be the binding parameter, then create a return type. At the end all parts are fit together and get returned as new CsdlFunction Object.</p>
<pre><code class="language-java"> @Override
public List&lt;CsdlFunction&gt; getFunctions(final FullQualifiedName functionName) {
// It is allowed to overload functions, so we have to provide a list of Functions for each function name
final List&lt;CsdlFunction&gt; functions = new ArrayList&lt;CsdlFunction&gt;();
if (functionName.equals(FUNCTION_PROVIDE_DISCOUNT_FQN)) {
// Create parameters
final List&lt;CsdlParameter&gt; parameters = new ArrayList&lt;CsdlParameter&gt;();
CsdlParameter parameter = new CsdlParameter();
parameter.setName(PARAMETER_CATEGORY);
parameter.setType(ET_CATEGORY_FQN);
parameter.setCollection(true);
parameters.add(parameter);
parameter = new CsdlParameter();
parameter.setName(PARAMETER_AMOUNT);
parameter.setType(EdmPrimitiveTypeKind.Int32.getFullQualifiedName());
parameters.add(parameter);
// Create the Csdl Function
final CsdlFunction function = new CsdlFunction();
function.setName(FUNCTION_PROVIDE_DISCOUNT_FQN.getName());
function.setBound(true);
function.setParameters(parameters);
function.setReturnType(new CsdlReturnType().setType(ET_PRODUCT_FQN).setCollection(true));
functions.add(function);
return functions;
} else if (functionName.equals(FUNCTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN)) {
// Create parameters
final List&lt;CsdlParameter&gt; parameters = new ArrayList&lt;CsdlParameter&gt;();
CsdlParameter parameter = new CsdlParameter();
parameter.setName(PARAMETER_CATEGORY);
parameter.setType(ET_CATEGORY_FQN);
parameter.setCollection(false);
parameters.add(parameter);
parameter = new CsdlParameter();
parameter.setName(PARAMETER_AMOUNT);
parameter.setType(EdmPrimitiveTypeKind.Int32.getFullQualifiedName());
parameters.add(parameter);
// Create the Csdl Function
final CsdlFunction function= new CsdlFunction();
function.setName(ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN.getName());
function.setBound(true);
function.setParameters(parameters);
function.setReturnType(new CsdlReturnType().setType(ET_PRODUCT_FQN).setCollection(false));
functions.add(function);
return functions;
}
return null;
}
</code></pre>
<p>Finally we have to announce these operations to the schema. Add the following lines to the method getSchemas():</p>
<pre><code class="language-java"> // add actions
List&lt;CsdlAction&gt; actions = new ArrayList&lt;CsdlAction&gt;();
actions.addAll(getActions(ACTION_PROVIDE_DISCOUNT_FQN));
actions.addAll(getActions(ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN));
schema.setActions(actions);
// add functions
List&lt;CsdlFunction&gt; functions = new ArrayList&lt;CsdlFunction&gt;();
functions.addAll(getFunctions(FUNCTION_PROVIDE_DISCOUNT_FQN));
functions.addAll(getFunctions(FUNCTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN));
schema.setFunctions(functions);
</code></pre>
<h3 id="extend-the-data-store">Extend the data store<a class="headerlink" href="#extend-the-data-store" title="Permalink">&para;</a></h3>
<p>We need two methods in the data store to read the action DiscountProducts and DiscountProduct. The first method returns a collection of entites and second method returns a single entity.</p>
<pre><code class="language-java"> public EntityCollection processBoundActionEntityCollection(EdmAction action, Map&lt;String, Parameter&gt; parameters) {
EntityCollection collection = new EntityCollection();
if ("DiscountProducts".equals(action.getName())) {
for (Entity entity : categoryList) {
Entity en = getRelatedEntity(entity, (EdmEntityType) action.getReturnType().getType());
Integer currentValue = (Integer)en.getProperty("Price").asPrimitive();
Integer newValue = currentValue - (Integer)parameters.get("Amount").asPrimitive();
en.getProperty("Price").setValue(ValueType.PRIMITIVE, newValue);
collection.getEntities().add(en);
}
}
return collection;
}
public DemoEntityActionResult processBoundActionEntity(EdmAction action, Map&lt;String, Parameter&gt; parameters,
List&lt;UriParameter&gt; keyParams) throws ODataApplicationException {
DemoEntityActionResult result = new DemoEntityActionResult();
if ("DiscountProduct".equals(action.getName())) {
for (Entity entity : categoryList) {
Entity en = getRelatedEntity(entity, (EdmEntityType) action.getReturnType().getType(), keyParams);
Integer currentValue = (Integer)en.getProperty("Price").asPrimitive();
Integer newValue = currentValue - (Integer)parameters.get("Amount").asPrimitive();
en.getProperty("Price").setValue(ValueType.PRIMITIVE, newValue);
result.setEntity(en);
result.setCreated(true);
return result;
}
}
return null;
}
</code></pre>
<p>In the second method, we are returning a custom object DemoEntityActionResult. This holds the entity and the status as to whether the entity is created or just returned. This information is used to set the response status.</p>
<pre><code class="language-java"> public class DemoEntityActionResult {
private Entity entity;
private boolean created = false;
public Entity getEntity() {
return entity;
}
public DemoEntityActionResult setEntity(final Entity entity) {
this.entity = entity;
return this;
}
public boolean isCreated() {
return created;
}
public DemoEntityActionResult setCreated(final boolean created) {
this.created = created;
return this;
}
}
</code></pre>
<p>We also create methods for GetDiscountedProducts and GetDiscountedProduct functions.</p>
<pre><code class="language-java"> public EntityCollection getBoundFunctionEntityCollection(EdmFunction function, Integer amount) {
EntityCollection collection = new EntityCollection();
if ("GetDiscountedProducts".equals(function.getName())) {
for (Entity entity : categoryList) {
if(amount &gt;= entity.getProperty("amount")){
Entity en = getRelatedEntity(entity, (EdmEntityType) function.getReturnType().getType());
collection.getEntities().add(en);
}
}
}
return collection;
}
public Entity getBoundFunctionEntity(EdmAction function, Integer amount) throws ODataApplicationException {
if ("GetDiscountedProduct".equals(function.getName())) {
for (Entity entity : categoryList) {
if(amount== entity.getProperty("amount")){
return getRelatedEntity(entity, (EdmEntityType) function.getReturnType().getType(), keyParams);
}
}
}
return null;
}
</code></pre>
<h3 id="extend-the-entity-collection-and-the-entity-processor-to-handle-functions">Extend the entity collection and the entity processor to handle functions<a class="headerlink" href="#extend-the-entity-collection-and-the-entity-processor-to-handle-functions" title="Permalink">&para;</a></h3>
<p>We start with the entity collection processor DemoEntityCollectionProcessor. To keep things simple, the first steps is to distinguish between entity collections and function imports. A cleverer implementation can handle both cases in one method to avoid duplicated code.</p>
<p>The recent implementation of the readEntityCollection() has been moved to readEntityCollectionInternal()</p>
<pre><code class="language-java">public void readEntityCollection(ODataRequest request, ODataResponse response, UriInfo uriInfo, ContentType responseFormat) throws ODataApplicationException, SerializerException {
final UriResource firstResourceSegment = uriInfo.getUriResourceParts().get(0);
if(firstResourceSegment instanceof UriResourceEntitySet) {
readEntityCollectionInternal(request, response, uriInfo, responseFormat);
} else if(firstResourceSegment instanceof UriResourceFunction) {
readFunctionImportCollection(request, response, uriInfo, responseFormat);
} else {
throw new ODataApplicationException("Not implemented",
HttpStatusCode.NOT&amp;#95;IMPLEMENTED.getStatusCode(),
Locale.ENGLISH);
}
}
</code></pre>
<p>Like by reading entity collections, the first step is to analyze the URI and then fetch the data (of the function).</p>
<pre><code class="language-java"> private void readEntityCollectionInternal(final ODataRequest request, final ODataResponse response,
final UriInfo uriInfo, final ContentType responseFormat) throws ODataApplicationException, SerializerException {
EdmEntitySet responseEdmEntitySet = null; // we'll need this to build the ContextURL
EntityCollection responseEntityCollection = null; // we'll need this to set the response body
// 1st retrieve the requested EntitySet from the uriInfo (representation of the parsed URI)
List&lt;UriResource&gt; resourceParts = uriInfo.getUriResourceParts();
int segmentCount = resourceParts.size();
UriResource uriResource = resourceParts.get(0); // in our example, the first segment is the EntitySet
if (!(uriResource instanceof UriResourceEntitySet)) {
throw new ODataApplicationException("Only EntitySet is supported",
HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ROOT);
}
UriResourceEntitySet uriResourceEntitySet = (UriResourceEntitySet) uriResource;
EdmEntitySet startEdmEntitySet = uriResourceEntitySet.getEntitySet();
if (segmentCount == 1) {
// This is a normal query fetch the entity from backend and return entityset
}
else if (segmentCount == 2) { // in case of function or navigation
UriResource lastSegment = resourceParts.get(1); // in our example we don't support more complex URIs
if (lastSegment instanceof UriResourceFunction) {// For bound function
UriResourceFunction uriResourceFunction = (UriResourceFunction) lastSegment;
// 2nd: fetch the data from backend
// first fetch the target entity type
String targetEntityType = uriResourceFunction.getFunction().getReturnType().getType().getName();
// contextURL displays the last segment
for(EdmEntitySet entitySet : serviceMetadata.getEdm().getEntityContainer().getEntitySets()){
if(targetEntityType.equals(entitySet.getEntityType().getName())){
responseEdmEntitySet = entitySet;
break;
}
}
// error handling for null entities
if (targetEntityType == null || responseEdmEntitySet == null) {
throw new ODataApplicationException("Entity not found.",
HttpStatusCode.NOT_FOUND.getStatusCode(), Locale.ROOT);
}
Integer amount = Integer.parseInt(uriResourceFunction.getParameters().get(0).getText())
// then fetch the entity collection for the target type
responseEntityCollection = storage.getBoundFunctionEntityCollection(function, amount);
}
}
// 3rd: create and configure a serializer
ContextURL contextUrl = ContextURL.with().entitySet(responseEdmEntitySet).build();
final String id = request.getRawBaseUri() + "/" + responseEdmEntitySet.getName();
EntityCollectionSerializerOptions opts = EntityCollectionSerializerOptions.with()
.contextURL(contextUrl).id(id).build();
EdmEntityType edmEntityType = responseEdmEntitySet.getEntityType();
ODataSerializer serializer = odata.createSerializer(responseFormat);
SerializerResult serializerResult = serializer.entityCollection(serviceMetadata, edmEntityType,
responseEntityCollection, opts);
// 4th: configure the response object: set the body, headers and status code
response.setContent(serializerResult.getContent());
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
}
</code></pre>
<p>Next we will implement the processor to read a single entity. The implementation is quite similar to the implementation of the collection processor.</p>
<pre><code class="language-java"> public void readEntity(ODataRequest request, ODataResponse response, UriInfo uriInfo, ContentType responseFormat)
throws ODataApplicationException, SerializerException {
UriResource uriResource = uriInfo.getUriResourceParts().get(0);
if(uriResource instanceof UriResourceEntitySet) {
readEntityInternal(request, response, uriInfo, responseFormat);
} else if(uriResource instanceof UriResourceFunction) {
readFunctionImportInternal(request, response, uriInfo, responseFormat);
} else {
throw new ODataApplicationException("Only EntitySet is supported",
HttpStatusCode.NOT&amp;#95;IMPLEMENTED.getStatusCode(), Locale.ENGLISH);
}
}
private void readEntityInternal(final ODataRequest request, final ODataResponse response,
final UriInfo uriInfo, final ContentType responseFormat) throws ODataApplicationException, SerializerException {
EdmEntityType responseEdmEntityType = null; // we'll need this to build the ContextURL
Entity responseEntity = null; // required for serialization of the response body
EdmEntitySet responseEdmEntitySet = null; // we need this for building the contextUrl
// 1st step: retrieve the requested Entity: can be "normal" read operation, or navigation (to-one)
List&lt;UriResource&gt; resourceParts = uriInfo.getUriResourceParts();
int segmentCount = resourceParts.size();
UriResource uriResource = resourceParts.get(0); // in our example, the first segment is the EntitySet
UriResourceEntitySet uriResourceEntitySet = (UriResourceEntitySet) uriResource;
EdmEntitySet startEdmEntitySet = uriResourceEntitySet.getEntitySet();
if (segmentCount == 1) {
// This is a normal read call fetch the entity from backend and return entity
} else if (segmentCount == 2) { // Bound Function or navigation
UriResource segment = resourceParts.get(1);
if (segment instanceof UriResourceFunction) {
UriResourceFunction uriResourceFunction = (UriResourceFunction) segment;
// 2nd: fetch the data from backend.
// first fetch the target entity type
String targetEntityType = uriResourceFunction.getFunction().getReturnType().getType().getName();
// contextURL displays the last segment
for(EdmEntitySet entitySet : serviceMetadata.getEdm().getEntityContainer().getEntitySets()){
if(targetEntityType.equals(entitySet.getEntityType().getName())){
responseEdmEntityType = entitySet.getEntityType();
responseEdmEntitySet = entitySet;
break;
}
}
// error handling for null entities
if (targetEntityType == null || responseEdmEntitySet == null) {
throw new ODataApplicationException("Entity not found.",
HttpStatusCode.NOT_FOUND.getStatusCode(), Locale.ROOT);
}
Integer amount = Integer.parseInt(uriResourceFunction.getParameters().get(0).getText())
// then fetch the entity collection for the target type
responseEntity = storage.getBoundFunctionEntity(function, amount);
}
}
if (responseEntity == null) {
// this is the case for e.g. DemoService.svc/Categories(4) or DemoService.svc/Categories(3)/Products(999)
throw new ODataApplicationException("Nothing found.", HttpStatusCode.NOT_FOUND.getStatusCode(), Locale.ROOT);
}
// 3. serialize
ContextURL contextUrl = ContextURL.with().entitySet(responseEdmEntitySet).suffix(Suffix.ENTITY).build();
EntitySerializerOptions opts = EntitySerializerOptions.with().contextURL(contextUrl).build();
ODataSerializer serializer = odata.createSerializer(responseFormat);
SerializerResult serializerResult = serializer.entity(serviceMetadata,
responseEdmEntityType, responseEntity, opts);
// 4. configure the response object
response.setContent(serializerResult.getContent());
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
}
</code></pre>
<h3 id="implement-an-action-processor">Implement an action processor<a class="headerlink" href="#implement-an-action-processor" title="Permalink">&para;</a></h3>
<p>Create a new class <code>DemoActionProcessor</code> make them implement the interface interface 'ActionEntityCollectionProcessor' and 'ActionEntityProcessor'.</p>
<pre><code class="language-java"> public class DemoActionProcessor implements ActionEntityCollectionProcessor, ActionEntityProcessor {
private OData odata;
private Storage storage;
private ServiceMetadata serviceMetadata;
public DemoActionProcessor(final Storage storage) {
this.storage = storage;
}
@Override
public void init(final OData odata, final ServiceMetadata serviceMetadata) {
this.odata = odata;
this.serviceMetadata = serviceMetadata;
}
</code></pre>
<p>The first overriden method returns a collection of entities.</p>
<p>First analyze the uri. Bound Actions will have the first segment in the resource path to be an entity set. It can then be followed by a navigation segment or a type cast. The last segment will be the fully qualified action name.</p>
<p>Then deserialize the action parameters.</p>
<p>Execute the action and set the response code.</p>
<pre><code class="language-java">
@Override
public void processActionEntityCollection(ODataRequest request, ODataResponse response, UriInfo uriInfo,
ContentType requestFormat, ContentType responseFormat) throws ODataApplicationException, ODataLibraryException {
Map&lt;String, Parameter&gt; parameters = new HashMap&lt;String, Parameter&gt;();
EdmAction action = null;
EntityCollection collection = null;
if (requestFormat == null) {
throw new ODataApplicationException("The content type has not been set in the request.",
HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT);
}
List&lt;UriResource&gt; resourcePaths = uriInfo.asUriInfoResource().getUriResourceParts();
final ODataDeserializer deserializer = odata.createDeserializer(requestFormat);
UriResourceEntitySet boundEntitySet = (UriResourceEntitySet) resourcePaths.get(0);
if (resourcePaths.size() &gt; 1) {
// Check if there is a navigation segment added after the bound parameter
if (resourcePaths.get(1) instanceof UriResourceAction) {
action = ((UriResourceAction) resourcePaths.get(2))
.getAction();
throw new ODataApplicationException("Action " + action.getName() + " is not yet implemented.",
HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); } else {
action = ((UriResourceAction) resourcePaths.get(1))
.getAction();
parameters = deserializer.actionParameters(request.getBody(), action)
.getActionParameters();
collection =
storage.processBoundActionEntityCollection(action, parameters);
}
}
// Collections must never be null.
// Not nullable return types must not contain a null value.
if (collection == null
|| collection.getEntities().contains(null) &amp;&amp; !action.getReturnType().isNullable()) {
throw new ODataApplicationException("The action could not be executed.",
HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ROOT);
}
final Return returnPreference = odata.createPreferences(request.getHeaders(HttpHeader.PREFER)).getReturn();
if (returnPreference == null || returnPreference == Return.REPRESENTATION) {
final EdmEntitySet edmEntitySet = boundEntitySet.getEntitySet();
final EdmEntityType type = (EdmEntityType) action.getReturnType().getType();
final EntityCollectionSerializerOptions options = EntityCollectionSerializerOptions.with()
.contextURL(isODataMetadataNone(responseFormat) ? null : getContextUrl(action.getReturnedEntitySet(edmEntitySet), type, false))
.build();
response.setContent(odata.createSerializer(responseFormat)
.entityCollection(serviceMetadata, type, collection, options).getContent());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
} else {
response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
}
if (returnPreference != null) {
response.setHeader(HttpHeader.PREFERENCE_APPLIED,
PreferencesApplied.with().returnRepresentation(returnPreference).build().toValueString());
}
}
//This method fetches the context URL
private ContextURL getContextUrl(final EdmEntitySet entitySet, final EdmEntityType entityType,
final boolean isSingleEntity) throws ODataLibraryException {
Builder builder = ContextURL.with();
builder = entitySet == null ?
isSingleEntity ? builder.type(entityType) : builder.asCollection().type(entityType) :
builder.entitySet(entitySet);
builder = builder.suffix(isSingleEntity &amp;&amp; entitySet != null ? Suffix.ENTITY : null);
return builder.build();
}
protected boolean isODataMetadataNone(final ContentType contentType) {
return contentType.isCompatible(ContentType.APPLICATION_JSON)
&amp;&amp; ContentType.VALUE_ODATA_METADATA_NONE.equalsIgnoreCase(
contentType.getParameter(ContentType.PARAMETER_ODATA_METADATA));
}
</code></pre>
<p>The second method to be overriden returns a single entity.</p>
<p>Again first analyze the uri. Bound Actions will have the first segment in the resource path to be an entity set with a key predicate. It can then be followed by a navigation segment or a type cast. The last segment will be the fully qualified action name.</p>
<p>Then deserialize the action parameters.</p>
<p>Execute the action and set the response code.</p>
<pre><code class="language-java">
@Override
public void processActionEntity(ODataRequest request, ODataResponse response, UriInfo uriInfo,
ContentType requestFormat, ContentType responseFormat) throws ODataApplicationException, ODataLibraryException {
EdmAction action = null;
Map&lt;String, Parameter&gt; parameters = new HashMap&lt;String, Parameter&gt;();
// DemoEntityActionResult is a custom object that holds the entity and the status as to whether the entity is created or just returned. This information is used to set the response status
DemoEntityActionResult entityResult = null;
if (requestFormat == null) {
throw new ODataApplicationException("The content type has not been set in the request.",
HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT);
}
final ODataDeserializer deserializer = odata.createDeserializer(requestFormat);
final List&lt;UriResource&gt; resourcePaths = uriInfo.asUriInfoResource().getUriResourceParts();
UriResourceEntitySet boundEntity = (UriResourceEntitySet) resourcePaths.get(0);
if (resourcePaths.size() &gt; 1) {
// Checks if there is a navigation segment added after the binding parameter
if (resourcePaths.get(1) instanceof UriResourceAction) {
action = ((UriResourceAction) resourcePaths.get(1))
.getAction();
throw new ODataApplicationException("Action " + action.getName() + " is not yet implemented.",
HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH);
} else if (resourcePaths.get(0) instanceof UriResourceEntitySet) {
action = ((UriResourceAction) resourcePaths.get(1))
.getAction();
parameters = deserializer.actionParameters(request.getBody(), action)
.getActionParameters();
entityResult =
storage.processBoundActionEntity(action, parameters, boundEntity.getKeyPredicates());
}
}
final EdmEntitySet edmEntitySet = boundEntity.getEntitySet();
final EdmEntityType type = (EdmEntityType) action.getReturnType().getType();
if (entityResult == null || entityResult.getEntity() == null) {
if (action.getReturnType().isNullable()) {
response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
} else {
// Not nullable return type so we have to give back a 500
throw new ODataApplicationException("The action could not be executed.",
HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ROOT);
}
} else {
final Return returnPreference = odata.createPreferences(request.getHeaders(HttpHeader.PREFER)).getReturn();
if (returnPreference == null || returnPreference == Return.REPRESENTATION) {
response.setContent(odata.createSerializer(responseFormat).entity(
serviceMetadata,
type,
entityResult.getEntity(),
EntitySerializerOptions.with()
.contextURL(isODataMetadataNone(responseFormat) ? null : getContextUrl(action.getReturnedEntitySet(edmEntitySet), type, true))
.build())
.getContent());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
response.setStatusCode((entityResult.isCreated() ? HttpStatusCode.CREATED : HttpStatusCode.OK)
.getStatusCode());
} else {
response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
}
if (returnPreference != null) {
response.setHeader(HttpHeader.PREFERENCE_APPLIED,
PreferencesApplied.with().returnRepresentation(returnPreference).build().toValueString());
}
if (entityResult.isCreated()) {
final String location = request.getRawBaseUri() + '/'
+ odata.createUriHelper().buildCanonicalURL(edmEntitySet, entityResult.getEntity());
response.setHeader(HttpHeader.LOCATION, location);
if (returnPreference == Return.MINIMAL) {
response.setHeader(HttpHeader.ODATA_ENTITY_ID, location);
}
}
if (entityResult.getEntity().getETag() != null) {
response.setHeader(HttpHeader.ETAG, entityResult.getEntity().getETag());
}
}
}
</code></pre>
<h2 id="run-the-implemented-service">Run the implemented service<a class="headerlink" href="#run-the-implemented-service" title="Permalink">&para;</a></h2>
<p>After building and deploying your service to your server, you can try the following requests:</p>
<p><strong>Functions (Called via GET)</strong></p>
<ul>
<li>
<p><a href="http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.GetDiscountedProducts(Amount=50)">http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.GetDiscountedProducts(Amount=50)</a></p>
</li>
<li>
<p><a href="http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.GetDiscountedProduct(Amount=50)">http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.GetDiscountedProduct(Amount=50)</a></p>
</li>
</ul>
<p><strong>Actions (Called via POST)</strong><br/>
<em>Note:</em> Set the Content-Type header to: <code>Content-Type: application/json</code></p>
<ul>
<li>
<p><a href="http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.DiscountProducts">http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.DiscountProducts</a></p>
<p>Content:</p>
<p>{"Amount":50 }</p>
</li>
<li>
<p><a href="http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.DiscountProduct">http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.DiscountProduct</a></p>
<p>Content:</p>
<p>{ "Amount": 50 }</p>
</li>
</ul>
<h1 id="links">Links<a class="headerlink" href="#links" title="Permalink">&para;</a></h1>
<h3 id="tutorials">Tutorials<a class="headerlink" href="#tutorials" title="Permalink">&para;</a></h3>
<p>Further topics to be covered by follow-up tutorials:</p>
<ul>
<li>Tutorial OData V4 service part 1: <a href="/doc/odata4/tutorials/read/tutorial_read.html">Read Entity Collection</a></li>
<li>Tutorial OData V4 service part 2: <a href="/doc/odata4/tutorials/readep/tutorial_readep.html">Read Entity, Read Property</a></li>
<li>Tutorial OData V4 service part 3: <a href="/doc/odata4/tutorials/write/tutorial_write.html">Write (Create, Update, Delete Entity)</a></li>
<li>Tutorial OData V4 service, part 4: <a href="/doc/odata4/tutorials/navigation/tutorial_navigation.html">Navigation</a></li>
<li>Tutorial OData V4 service, part 5.1: <a href="/doc/odata4/tutorials/sqo_tcs/tutorial_sqo_tcs.html">System Query Options $top, $skip, $count (this page)</a></li>
<li>Tutorial OData V4 service, part 5.2: <a href="/doc/odata4/tutorials/sqo_es/tutorial_sqo_es.html">System Query Options $select, $expand</a></li>
<li>Tutorial OData V4 service, part 5.3: <a href="/doc/odata4/tutorials/sqo_o/tutorial_sqo_o.html">System Query Options $orderby</a></li>
<li>Tutorial OData V4 service, part 5.4: <a href="/doc/odata4/tutorials/sqo_f/tutorial_sqo_f.html">System Query Options $filter</a></li>
<li>Tutorial ODATA V4 service, part 6: Action and Function Imports</li>
<li>Tutorial ODATA V4 service, part 7: <a href="/doc/odata4/tutorials/media/tutorial_media.html">Media Entities</a></li>
<li>Tutorial OData V4 service, part 8: <a href="/doc/odata4/tutorials/batch/tutorial_batch.html">Batch Request support</a></li>
<li>Tutorial OData V4 service, part 9: <a href="/doc/odata4/tutorials/deep_insert/tutorial_deep_insert.html">Handling "Deep Insert" requests</a></li>
</ul>
<h3 id="code-and-repository">Code and Repository<a class="headerlink" href="#code-and-repository" title="Permalink">&para;</a></h3>
<ul>
<li><a href="https://gitbox.apache.org/repos/asf/olingo-odata4">Git Repository</a></li>
<li><a href="/doc/odata4/tutorials/prerequisites/prerequisites.html">Guide - To fetch the tutorial sources</a></li>
<li><a href="http://www.apache.org/dyn/closer.lua/olingo/odata4/4.0.0/DemoService_Tutorial.zip">Demo Service source code as zip file (contains all tutorials)</a></li>
</ul>
<h3 id="further-reading">Further reading<a class="headerlink" href="#further-reading" title="Permalink">&para;</a></h3>
<ul>
<li><a href="http://odata.org/">Official OData Homepage</a></li>
<li><a href="http://www.odata.org/documentation/">OData documentation</a></li>
<li><a href="/javadoc/odata4/index.html">Olingo Javadoc</a></li>
</ul>
<div align="center">
<p>Copyright © 2013-2022, The Apache Software Foundation<br>
Apache Olingo, Olingo, Apache, the Apache feather, and
the Apache Olingo project logo are trademarks of the Apache Software
Foundation.</p>
<small><a href="/doc/odata2/privacy.html">Privacy</a></small>
</div>
</div><!-- /container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/jquery.js" type="text/javascript">
</script>
<script src="/js/bootstrap.js" type="text/javascript">
</script>
<script src="/js/offcanvas.js" type="text/javascript">
</script>
<link rel="stylesheet" href="/css/docco.css">
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.0.1/build/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</body>
</html>