Author: ramyav
Date: Wed Oct 25 06:11:37 2017
New Revision: 1813249

URL: http://svn.apache.org/viewvc?rev=1813249&view=rev
Log:
Update Bound Action Tutorial

Modified:
    
olingo/site/trunk/content/doc/odata4/tutorials/action/tutorial_bound_action.mdtext

Modified: 
olingo/site/trunk/content/doc/odata4/tutorials/action/tutorial_bound_action.mdtext
URL: 
http://svn.apache.org/viewvc/olingo/site/trunk/content/doc/odata4/tutorials/action/tutorial_bound_action.mdtext?rev=1813249&r1=1813248&r2=1813249&view=diff
==============================================================================
--- 
olingo/site/trunk/content/doc/odata4/tutorials/action/tutorial_bound_action.mdtext
 (original)
+++ 
olingo/site/trunk/content/doc/odata4/tutorials/action/tutorial_bound_action.mdtext
 Wed Oct 25 06:11:37 2017
@@ -18,7 +18,7 @@ Notice:    Licensed to the Apache Softwa
 
 # How to build an OData Service with Olingo V4
 
-# Part 6: Action Imports and Function Imports
+# Part 6: Bound Actions
 
 
 **Table of Contents**
@@ -27,7 +27,7 @@ Notice:    Licensed to the Apache Softwa
 
 ## Introduction
 
-In the present tutorial, we’ll implement a function import and an action 
import as well.
+In the present tutorial, we’ll implement a bound action.
 
 **Note:**
 The final source code can be found in the project [git 
repository](https://git-wip-us.apache.org/repos/asf/olingo-odata4).
@@ -56,66 +56,44 @@ In this short definition are several ter
 
 In addition an *Action* can return void that means there is no return value. A 
*Function* must return a value.
 
-The definition gives us some parts where function and actions can be used. 
First an *Operation* can be bound or unbound. In this tutorial we will focus on 
unbound operations. Unbound operations are something like static methods in 
Java, so if one of these operations have parameters we have to pass all of them 
explicit to the operation.
+First an *Operation* can be bound or unbound.In this tutorial we will focus on 
bound operation. 
 
+Bound actions 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.
 
-**Example**
-
-For example there could be a function that calculates the VAT. The result 
depends on the one hand from the net price and on the other hand from the 
country in which the customer lives.
-
+An action element MAY specify a Boolean value for the IsBound attribute.
+Actions whose IsBound attribute is false or not specified are considered 
unbound. Unbound actions are invoked through an action import.
+Actions whose IsBound attribute is true are considered bound. Bound actions 
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 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.
 
-Such a function can be expressed in the metadata document as follows
-
-    ::::xml
-    <Function Name="CalculateVAT">
-         <Parameter Name="NetPrice" Type="Edm.Decimal" Nullable="false"/>
-         <Parameter Name="Country" Type="Edm.String" Nullable="false"/>
-
-        <ReturnType Type="Edm.Decimal"/>
-    </Function>
-
-To make this function statically callable we have to define a Function Import.
-
-    ::::xml
-    <EntityContainer Name="Container">
-         <FunctionImport Name="StaticCalculateVAT"
-                         Function="CalculateVAT"
-                         IncludeInServiceDocument="true"/>
-    </EntityContainer>
+Bound actions 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.
 
-To call such a Function Import the client issues a GET requests to a URL 
identifying the function import. The parameters are passed by using the so 
called inline parameter syntax. In this simple case such a call could look like 
this:
+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.
 
-    http://host/myService/StaticCalculateVAT(NetPrice=123.00,Country=’US’)
+**Example**
 
-The definition talks about composing of functions. By default every function 
is Composable=false, that means there must be no further resource parts after 
the function call and there must also be no system query options. If a function 
is composable you are be able to use system query options. Which options are 
allowed in particular is based on the return type of the function. Further you 
can navigate to properties and use Navigation Properties to navigate to related 
entities as well.
+For example there can be a bound action createOrders which is bound to the 
Customer entity having 2 parameters
 
-The definition of Actions / Action Imports in metadata document is similar to 
functions.
+Such an action can be expressed in the metadata document as follows
 
     ::::xml
-    <Action Name="Reset">
-        <Parameter Name="Amount" Type="Edm.Int32"/>
+    <Action Name="CreateOrder" isBound=”true”>
+     <Parameter Name="Customers" Type="SampleEntities.Customer" 
Nullable="false"/>
+     <Parameter Name="quantity" Type="Edm.Int16" Nullable="false"/>
+     <Parameter Name="discountCode" Type="Edm.String" Nullable="false"/>
+     <ReturnType Type="Collection(SampleEntities.Orders)"/>
     </Action>
 
-    <EntityContainer Name="Container">
-        <ActionImport Name="StaticReset" Action="Reset"/>
-    </EntityContainer>
-
-As you can see, this function does not return any value and takes one 
parameter “*Amount*” To call this action import, you have to issue an POST 
request to
-
-    http://host/myService/StaticReset
-
-The parameters are passed within the body of the request. In this case such a 
body could look like the following (JSON - Syntax):
+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:
 
-    ::::json
-    {
-      "Amount": 2
-    }
+      POST http://host/service/Customers('ALFKI')/SampleEntities.CreateOrder
+      {
+        "quantity": 2,
+        "discountCode": "BLACKFRIDAY"
+      }
 
-As you read in the definition, actions can have side effects (modifying the 
data) but cannot be composed like functions.
 
 ## Preparation
 
-You should read the previous tutorials first to have an idea how to read 
entities and entity collections. In addition the following code is based on the 
write tutorial merged with the navigation tutorial.
+You should read the previous tutorials first to have an idea how to read 
entities and entity collections. 
 
 As a shortcut you should checkout the prepared tutorial project in the [git 
repository](https://git-wip-us.apache.org/repos/asf/olingo-odata4) in folder 
/samples/tutorials/p9_action_preparation.
 
@@ -123,459 +101,412 @@ Afterwards do a Deploy and run: it shoul
 
 ## Implementation
 
-We use the given data model you are familiar with. To keep things simple we 
implement one function import and one action import.
+We use the given data model you are familiar with. To keep things simple we 
implement one bound action.
 
-**Function Import: CountCategories**    
-This function takes a mandatory parameter “*Amount*”. The function returns 
a collection of categories with the very same number of related products.
+**Bound Action that returns a collection of entities: DiscountProducts**    
+This action takes bound parameter “*ParamCategory*” and an additional 
parameter “*Amount*”. The action updates the price of all products related 
to categories by applying the discount amount.
 
-After finishing the implementation the definition of the function should look 
like this:
+After finishing the implementation the definition of the action should be like 
this:
 
     ::::xml
-    <Function Name="CountCategories">
-          <Parameter Name="Amount" Type="Edm.Int32" Nullable="false"/>
-          <ReturnType Type="Collection(OData.Demo.Category)"/>
-    </Function>
-
-As described in the previous tutorials, the type of the response determines 
which processor interface will be called by the Olingo library. It is 
**important to know, that functions are dispatched to the traditional processor 
interfaces**.
-That means there are no special "FunctionProcessors". In our case, the 
function returns a collection of Categories. So we have to extend the 
`DemoEntityCollectionProcessor`. As you will see it is possible to address a 
single entity by its  key. So we have to extend the `DemoEntityProcessor` as 
well to handle requests which responds a single entity.
+    <Action Name="DiscountProducts" IsBound="true">
+      <Parameter Name="ParamCategory" Type="Collection(OData.Demo.Category)"/>
+      <Parameter Name="Amount" Type="Edm.Int32"/>
+      <ReturnType Type="Collection(OData.Demo.Product)"/>
+    </Action>
 
-**Action Import: Reset**    
-This action takes an optional parameter “*Amount*”. The actions resets the 
whole data of the service and creates  *# of Amount* products with the related 
categories.
+**Bound Action that returns an entity: DiscountProduct**
 
+This action takes bound parameter “*ParamCategory*” and an additional 
parameter “*Amount*”. 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:
 
     :::xml
-    <Action Name="Reset" IsBound="false">
-      <Parameter Name="Amount" Type="Edm.Int32"/>
+    <Action Name="DiscountProduct" IsBound="true">
+       <Parameter Name="ParamCategory" Type=”OData.Demo.Category"/>
+       <Parameter Name="Amount" Type="Edm.Int32"/>
+       <ReturnType Type=" OData.Demo.Product"/>
     </Action>
 
+
 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.
 
 **Steps**    
 
   * Extend the Metadata model
   * Extend the data store
-  * Extend the entity collection and the entity processor to handle function 
imports
   * Implement an action processor
 
 ### Extend the Metadata model
 
-Create the following constants in the DemoEdmProvider. These constants are 
used to address the operations.
+Create the following constants in the DemoEdmProvider. These constants are 
used to address the actions.
 
     ::::java
-    // Action
-    public static final String ACTION_RESET = "Reset";
-    public static final FullQualifiedName ACTION_RESET_FQN = new 
FullQualifiedName(NAMESPACE, ACTION_RESET);
-
-    // Function
-    public static final String FUNCTION_COUNT_CATEGORIES = "CountCategories";
-    public static final FullQualifiedName FUNCTION_COUNT_CATEGORIES_FQN = new 
FullQualifiedName(NAMESPACE, FUNCTION_COUNT_CATEGORIES);
-
-    // Function/Action Parameters
+    //Bound Action
+    public static final String ACTION_PROVIDE_DISCOUNT = "DiscountProducts";
+    public static final FullQualifiedName ACTION_PROVIDE_DISCOUNT_FQN = new 
FullQualifiedName(NAMESPACE, ACTION_PROVIDE_DISCOUNT);
+      
+    //Bound Action
+    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);
+    
+    //Action Parameters
     public static final String PARAMETER_AMOUNT = "Amount";
+    
+    //Bound Action Binding Parameter
+    public static final String PARAMETER_CATEGORY = "ParamCategory";
+
 
 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:
 
   - Actions
-  - Functions
-  - Action Imports
-  - Function Imports
-
-The code is simple and straight forward. First, we check which function we 
have to return. Then, a list of parameters and the return type are created. At 
the end all parts are fit together and get returned as new `CsdlFunction` 
Object.
-
-    ::::java
-    @Override
-    public List<CsdlFunction> getFunctions(final FullQualifiedName 
functionName) {
-      if (functionName.equals(FUNCTION_COUNT_CATEGORIES_FQN)) {
-        // It is allowed to overload functions, so we have to provide a list 
of functions for each function name
-        final List<CsdlFunction> functions = new ArrayList<CsdlFunction>();
-
-        // Create the parameter for the function
-        final CsdlParameter parameterAmount = new CsdlParameter();
-        parameterAmount.setName(PARAMETER_AMOUNT);
-        parameterAmount.setNullable(false);
-        
parameterAmount.setType(EdmPrimitiveTypeKind.Int32.getFullQualifiedName());
-
-        // Create the return type of the function
-        final CsdlReturnType returnType = new CsdlReturnType();
-        returnType.setCollection(true);
-        returnType.setType(ET_CATEGORY_FQN);
-
-        // Create the function
-        final CsdlFunction function = new CsdlFunction();
-        function.setName(FUNCTION_COUNT_CATEGORIES_FQN.getName())
-            .setParameters(Arrays.asList(parameterAmount))
-            .setReturnType(returnType);
-        functions.add(function);
-
-        return functions;
-      }
-
-      return null;
-    }
 
-We have created the function itself. To express that function can be called 
statically we have to override the method `getFunctionImport()`.
+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.
 
     ::::java
-    @Override
-    public CsdlFunctionImport getFunctionImport(FullQualifiedName 
entityContainer, String functionImportName) {
-      if(entityContainer.equals(CONTAINER)) {
-        if(functionImportName.equals(FUNCTION_COUNT_CATEGORIES_FQN.getName())) 
{
-          return new CsdlFunctionImport()
-                    .setName(functionImportName)
-                    .setFunction(FUNCTION_COUNT_CATEGORIES_FQN)
-                    .setEntitySet(ES_CATEGORIES_NAME)
-                    .setIncludeInServiceDocument(true);
-        }
-      }
-
-      return null;
-    }
-
-To define the actions and the action imports the `getActions()` and 
`getActionImport()` methods have to be overriden and the necessary code is 
quite similar to the functions sample above:
-
-    ::::java
-    @Override
-    public List<CsdlAction> getActions(final FullQualifiedName actionName) {
-      if(actionName.equals(ACTION_RESET_FQN)) {
-        // It is allowed to overload actions, so we have to provide a list of 
Actions for each action name
+        @Override
+        public List<CsdlAction> 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<CsdlAction> actions = new ArrayList<CsdlAction>();
-
-        // Create parameters
-        final List<CsdlParameter> parameters = new ArrayList<CsdlParameter>();
-        final CsdlParameter 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_RESET_FQN.getName());
-        action.setParameters(parameters);
-        actions.add(action);
-
-        return actions;
-      }
-
-      return null;
-    }
-
-    @Override
-    public CsdlActionImport getActionImport(final FullQualifiedName 
entityContainer, final String actionImportName) {
-      if(entityContainer.equals(CONTAINER)) {
-        if(actionImportName.equals(ACTION_RESET_FQN.getName())) {
-          return new CsdlActionImport()
-                  .setName(actionImportName)
-                  .setAction(ACTION_RESET_FQN);
+        
+        if (actionName.equals(ACTION_PROVIDE_DISCOUNT_FQN)) {
+          // Create parameters
+          final List<CsdlParameter> parameters = new 
ArrayList<CsdlParameter>();
+          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<CsdlParameter> parameters = new 
ArrayList<CsdlParameter>();
+          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;
       }
 
-      return null;
-    }
-
-Finally we have to announce these operations to the schema and the entity 
container.
-Add the following lines to the method `getSchemas()`:
+Finally we have to announce these operations to the schema. Add the following 
lines to the method getSchemas():
 
     ::::java
     // add actions
     List<CsdlAction> actions = new ArrayList<CsdlAction>();
-    actions.addAll(getActions(ACTION_RESET_FQN));
+    actions.addAll(getActions(ACTION_PROVIDE_DISCOUNT_FQN));
+    actions.addAll(getActions(ACTION_PROVIDE_DISCOUNT_FOR_PRODUCT_FQN));
     schema.setActions(actions);
 
-    // add functions
-    List<CsdlFunction> functions = new ArrayList<CsdlFunction>();
-    functions.addAll(getFunctions(FUNCTION_COUNT_CATEGORIES_FQN));
-    schema.setFunctions(functions);
-
-Also add the following lines to the method `getEntityContainer()`
-
-    ::::java
-    // Create function imports
-    List<CsdlFunctionImport> functionImports = new 
ArrayList<CsdlFunctionImport>();
-    functionImports.add(getFunctionImport(CONTAINER, 
FUNCTION_COUNT_CATEGORIES));
-
-    // Create action imports
-    List<CsdlActionImport> actionImports = new ArrayList<CsdlActionImport>();
-    actionImports.add(getActionImport(CONTAINER, ACTION_RESET));
-
-    entityContainer.setFunctionImports(functionImports);
-    entityContainer.setActionImports(actionImports);
-
-
 ### Extend the data store
 
-We need two methods in the data store to read the function import 
`CountCategories`.
+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.
 
-The first method returns a collection of entites and the second returns a 
single entity of this collection.
 
     ::::java
-    public EntityCollection readFunctionImportCollection(final 
UriResourceFunction uriResourceFunction, final ServiceMetadata serviceMetadata) 
throws ODataApplicationException {
-
-      
if(DemoEdmProvider.FUNCTION_COUNT_CATEGORIES.equals(uriResourceFunction.getFunctionImport().getName()))
 {
-        // Get the parameter of the function
-        final UriParameter parameterAmount = 
uriResourceFunction.getParameters().get(0);
-
-        // Try to convert the parameter to an Integer.
-        // We have to take care, that the type of parameter fits to its EDM 
declaration
-        int amount;
-        try {
-          amount = Integer.parseInt(parameterAmount.getText());
-        } catch(NumberFormatException e) {
-          throw new ODataApplicationException("Type of parameter Amount must 
be Edm.Int32",
-            HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ENGLISH);
-        }
-
-        final EdmEntityType productEntityType = 
serviceMetadata.getEdm().getEntityType(DemoEdmProvider.ET_PRODUCT_FQN);
-        final List<Entity> resultEntityList = new ArrayList<Entity>();
-
-        // Loop over all categories and check how many products are linked
-        for(final Entity category : categoryList) {
-          final EntityCollection products = 
getRelatedEntityCollection(category, productEntityType);
-          if(products.getEntities().size() == amount) {
-            resultEntityList.add(category);
+        public EntityCollection processBoundActionEntityCollection(EdmAction 
action, Map<String, Parameter> 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);
           }
         }
-
-        final EntityCollection resultCollection = new EntityCollection();
-        resultCollection.getEntities().addAll(resultEntityList);
-        return resultCollection;
-      } else {
-          throw new ODataApplicationException("Function not implemented", 
HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(),
-        Locale.ROOT);
-      }
-    }
-
-    public Entity readFunctionImportEntity(final UriResourceFunction 
uriResourceFunction,
-      final ServiceMetadata serviceMetadata) throws ODataApplicationException {
-
-      final EntityCollection entityCollection = 
readFunctionImportCollection(uriResourceFunction, serviceMetadata);
-      final EdmEntityType edmEntityType = (EdmEntityType) 
uriResourceFunction.getFunction().getReturnType().getType();
-
-      return Util.findEntity(edmEntityType, entityCollection, 
uriResourceFunction.getKeyPredicates());
-    }
-
-We also create two methods to reset the data of our service.
-
-    ::::java
-    public void resetDataSet(final int amount) {
-      // Replace the old lists with empty ones
-      productList = new ArrayList<Entity>();
-      categoryList = new ArrayList<Entity>();
-
-      // Create new sample data
-      initProductSampleData();
-      initCategorySampleData();
-
-      // Truncate the lists
-      if(amount < productList.size()) {
-        productList = productList.subList(0, amount);
-        // Products 0, 1 are linked to category 0
-        // Products 2, 3 are linked to category 1
-        // Products 4, 5 are linked to category 2
-        categoryList = categoryList.subList(0, (amount / 2) + 1);
+        return collection;
       }
-    }
-
-    public void resetDataSet() {
-      resetDataSet(Integer.MAX_VALUE);
-    }
-
-
-### Extend the entity collection and the entity processor to handle function 
imports
-
-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.
-
-The recent implementation of the `readEntityCollection()` has been moved to 
`readEntityCollectionInternal()`
-
-    ::::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_IMPLEMENTED.getStatusCode(),
-        Locale.ENGLISH);
+    
+      public DemoEntityActionResult processBoundActionEntity(EdmAction action, 
Map<String, Parameter> parameters,
+          List<UriParameter> 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;
       }
-    }
 
-Like by reading *entity collections*, the first step is to analyze the URI and 
then fetch the data (of the function import).
-
-    ::::java
-    private void readFunctionImportCollection(final ODataRequest request, 
final ODataResponse response,
-      final UriInfo uriInfo, final ContentType responseFormat) throws 
ODataApplicationException, SerializerException {
+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
 
-      // 1st step: Analyze the URI and fetch the entity collection returned by 
the function import
-      // Function Imports are always the first segment of the resource path
-      final UriResource firstSegment = uriInfo.getUriResourceParts().get(0);
+        ::::java
 
-      if(!(firstSegment instanceof UriResourceFunction)) {
-        throw new ODataApplicationException("Not implemented",
-          HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH);
+    public class DemoEntityActionResult {
+      private Entity entity;
+      private boolean created = false;
+    
+      public Entity getEntity() {
+        return entity;
       }
-
-      final UriResourceFunction uriResourceFunction = (UriResourceFunction) 
firstSegment;
-      final EntityCollection entityCol = 
storage.readFunctionImportCollection(uriResourceFunction, serviceMetadata);
-
-Then the result has to be serialized. The only difference to entity sets is 
the way how the `EdmEntityType` is determined.
-
-    ::::java
-      // 2nd step: Serialize the response entity
-      final EdmEntityType edmEntityType = (EdmEntityType) 
uriResourceFunction.getFunction().getReturnType().getType();
-      final ContextURL contextURL = 
ContextURL.with().asCollection().type(edmEntityType).build();
-      EntityCollectionSerializerOptions opts = 
EntityCollectionSerializerOptions.with().contextURL(contextURL).build();
-      final ODataSerializer serializer = 
odata.createSerializer(responseFormat);
-      final SerializerResult serializerResult = 
serializer.entityCollection(serviceMetadata, edmEntityType, entityCol, opts);
-
-      // 3rd configure the response object
-      response.setContent(serializerResult.getContent());
-      response.setStatusCode(HttpStatusCode.OK.getStatusCode());
-      response.setHeader(HttpHeader.CONTENT_TYPE, 
responseFormat.toContentTypeString());
-    }
-
-Next we will implement the processor to read a *single entity*. The 
implementation is quite similar to the implementation of the collection 
processor.
-
-    ::::java
-    public void readEntity(ODataRequest request, ODataResponse response, 
UriInfo uriInfo, ContentType responseFormat)
-      throws ODataApplicationException, SerializerException {
-
-      // The sample service supports only functions imports and entity sets.
-      // We do not care about bound functions and composable functions.
-
-      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_IMPLEMENTED.getStatusCode(), Locale.ENGLISH);
+    
+      public DemoEntityActionResult setEntity(final Entity entity) {
+        this.entity = entity;
+        return this;
       }
-    }
-
-    private void readFunctionImportInternal(final ODataRequest request, final 
ODataResponse response,
-      final UriInfo uriInfo, final ContentType responseFormat) throws 
ODataApplicationException, SerializerException {
-
-      // 1st step: Analyze the URI and fetch the entity returned by the 
function import
-      // Function Imports are always the first segment of the resource path
-      final UriResource firstSegment = uriInfo.getUriResourceParts().get(0);
-
-      if(!(firstSegment instanceof UriResourceFunction)) {
-        throw new ODataApplicationException("Not implemented",
-          HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH);
+    
+      public boolean isCreated() {
+        return created;
       }
-
-      final UriResourceFunction uriResourceFunction = (UriResourceFunction) 
firstSegment;
-      final Entity entity = 
storage.readFunctionImportEntity(uriResourceFunction, serviceMetadata);
-
-      if(entity == null) {
-        throw new ODataApplicationException("Nothing found.",
-          HttpStatusCode.NOT_FOUND.getStatusCode(), Locale.ROOT);
+    
+      public DemoEntityActionResult setCreated(final boolean created) {
+        this.created = created;
+        return this;
       }
-
-      // 2nd step: Serialize the response entity
-      final EdmEntityType edmEntityType = (EdmEntityType) 
uriResourceFunction.getFunction().getReturnType().getType();
-      final ContextURL contextURL = 
ContextURL.with().type(edmEntityType).build();
-      final EntitySerializerOptions opts = 
EntitySerializerOptions.with().contextURL(contextURL).build();
-      final ODataSerializer serializer = 
odata.createSerializer(responseFormat);
-      final SerializerResult serializerResult = 
serializer.entity(serviceMetadata, edmEntityType, entity, opts);
-
-      // 3rd configure the response object
-      response.setContent(serializerResult.getContent());
-      response.setStatusCode(HttpStatusCode.OK.getStatusCode());
-      response.setHeader(HttpHeader.CONTENT_TYPE, 
responseFormat.toContentTypeString());
+    
     }
 
-
 ### Implement an action processor
 
-Create a new class `DemoActionProcessor` make them implement the interface 
`ActionVoidProcessor`.
+Create a new class `DemoActionProcessor` make them implement the interface 
interface 'ActionEntityCollectionProcessor' and 'ActionEntityProcessor'.
 
     ::::java
-    public class DemoActionProcessor implements ActionVoidProcessor {
-
+      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;
       }
 
-First analyze the uri.
 
-    ::::java
-    public void processActionVoid(ODataRequest request, ODataResponse 
response, UriInfo uriInfo,
-        ContentType requestFormat) throws ODataApplicationException, 
ODataLibraryException {
+The first overriden method returns a collection of entities.
 
-      // 1st Get the action from the resource path
-      final EdmAction edmAction = ((UriResourceAction) 
uriInfo.asUriInfoResource().getUriResourceParts()
-                                            .get(0)).getAction();
+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. 
 
-Then deserialize the *action parameters*.
+Then deserialize the action parameters.
+
+Execute the action and set the response code.
 
     ::::java
-      // 2nd Deserialize the parameter
-      // In our case there is only one action. So we can be sure that 
parameter "Amount" has been provided by the client
-      if (requestFormat == null) {
-        throw new ODataApplicationException("The content type has not been set 
in the request.",
-          HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT);
+
+        @Override
+      public void processActionEntityCollection(ODataRequest request, 
ODataResponse response, UriInfo uriInfo,
+          ContentType requestFormat, ContentType responseFormat) throws 
ODataApplicationException, ODataLibraryException {
+    
+        Map<String, Parameter> parameters = new HashMap<String, Parameter>();
+        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<UriResource> resourcePaths = 
uriInfo.asUriInfoResource().getUriResourceParts();
+        final ODataDeserializer deserializer = 
odata.createDeserializer(requestFormat);
+        UriResourceEntitySet boundEntitySet = (UriResourceEntitySet) 
resourcePaths.get(0);
+        if (resourcePaths.size() > 1) {
+       // Check if there is a navigation segment added after the bound 
parameter
+          if (resourcePaths.get(1) instanceof UriResourceNavigation) {
+           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) && 
!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 && entitySet != null ? 
Suffix.ENTITY : null);
+        return builder.build();
+      }
+      
+      protected boolean isODataMetadataNone(final ContentType contentType) {
+        return contentType.isCompatible(ContentType.APPLICATION_JSON)
+            && ContentType.VALUE_ODATA_METADATA_NONE.equalsIgnoreCase(
+                
contentType.getParameter(ContentType.PARAMETER_ODATA_METADATA));
       }
 
-      final ODataDeserializer deserializer = 
odata.createDeserializer(requestFormat);
-      final Map<String, Parameter> actionParameter = 
deserializer.actionParameters(request.getBody(), edmAction)
-                                     .getActionParameters();
-      final Parameter parameterAmount = 
actionParameter.get(DemoEdmProvider.PARAMETER_AMOUNT);
+The second method to be overriden returns a single entity.
+
+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. 
+
+Then deserialize the action parameters.
 
 Execute the action and set the response code.
 
     ::::java
-      // The parameter amount is nullable
-      if(parameterAmount.isNull()) {
-        storage.resetDataSet();
-      } else {
-        final Integer amount = (Integer) parameterAmount.asPrimitive();
-        storage.resetDataSet(amount);
+
+        @Override
+      public void processActionEntity(ODataRequest request, ODataResponse 
response, UriInfo uriInfo,
+          ContentType requestFormat, ContentType responseFormat) throws 
ODataApplicationException, ODataLibraryException {
+    
+        EdmAction action = null;
+        Map<String, Parameter> parameters = new HashMap<String, Parameter>(); 
+       // 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<UriResource> resourcePaths = 
uriInfo.asUriInfoResource().getUriResourceParts();
+        UriResourceEntitySet boundEntity = (UriResourceEntitySet) 
resourcePaths.get(0);
+        if (resourcePaths.size() > 1) {
+       // Checks if there is a navigation segment added after the binding 
parameter
+          if (resourcePaths.get(1) instanceof UriResourceNavigation) {
+            action = ((UriResourceAction) resourcePaths.get(2))
+                .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());
+          }
+        }    
       }
 
-      response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
-    }
 
 ## Run the implemented service
 
 After building and deploying your service to your server, you can try the 
following requests:
 
-**Functions (Called via GET)**    
-
-  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/CountCategories(Amount=2)](http://localhost:8080/DemoService-Action/DemoService.svc/CountCategories(Amount=2))
-  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/CountCategories(Amount=2)(0)](http://localhost:8080/DemoService-Action/DemoService.svc/CountCategories(Amount=2)(0))
-
 **Actions (Called via POST)**     
 *Note:* Set the Content-Type header to: `Content-Type: application/json`
 
-  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/Reset](http://localhost:8080/DemoService-Action/DemoService.svc/Reset)
+  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.DiscountProducts](http://localhost:8080/DemoService-Action/DemoService.svc/Categories/OData.Demo.DiscountProducts)
 
     Content:
 
-    { }
+    {"Amount":50 }
 
 
-  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/Reset](http://localhost:8080/DemoService-Action/DemoService.svc/Reset)
+  * 
[http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.DiscountProduct](http://localhost:8080/DemoService-Action/DemoService.svc/Categories(0)/OData.Demo.DiscountProduct)
 
     Content:
 
-    { "Amount": 1 }
+    { "Amount": 50 }
 
 
 To verify that the service has been reseted, you can request the collection of 
products


Reply via email to