Quantcast
Viewing latest article 2
Browse Latest Browse All 11

Writing a betting pool application for the Soccer World Cup 2006 (Part II)

After making the decision on the technology used, collected the requirements and made the basic design of the application, it is now time to show you how to build the Business Layer of our soccer betting pool application using CAF Core.
In the following sections we'll use a lot of wizards. I'll not describe every field or every wizard step. Therefore if nothing is mentioned leave the default values.

Go and Create

CAF Core provides Entity Services which represent Business Objects and Application Services which represent the business logic. If you are familiar with EJB's you could compare Entity Services with Entity Beans and Application Services with Session Beans. Under the hood CAF actually uses Session Beans in combination with JDO to represent Entity and Application Services.

Create a CAF DC (Development Component)

1) In NW Dev. Studio click File > New > Project and choose Development Component (DC)

2) Select your Software Component on the next screen you want the DC be added to (for example "MyComponents" if you work locally).

3) Enter wc as the name of the development component and select the DC type Composite Application Services

4) Clicking next you see that in the background 6 IDE projects will be generated. For example the "Java Dictionary Project" will be used to define the DB tables where the Entites are stored or the "EJB Module Project" contains the automatically generated Session Beans.

5) Now click Finish and wait until all projects are generated

The good thing about this wizard is that he also generates a Web Dynpro project which we can use later to build our UI. The WD project is ready to use (e.g. the wizard already adds the CAF DCs as "used DCs" in our WD project and other configurations we need).

Creating the Player Entity Service

1) Switch to the Composite Application Services perspective in NW Dev Studio and select the Service Explorer View

2) Create a new Entity Service named Player (right mouse click)
Image may be NSFW.
Clik here to view.
image


3) Select the Attributes tab of the Player Entity. As you can see CAF already added some default attributes like a unique key or the createdBy attribute. Right click on Player and select Create Attribute
Image may be NSFW.
Clik here to view.
image


4) Name the attribute uniqueID, mark the checkbox key and select the datatype com.sap.caf.core.longText. Defining the attribute as a key, the CAF framework automatically adds a new operation (method) to read a player with the uniqueID

5) Select the attribute uniqueID and switch to the Properties tab of this attribute. Here you can change the properties of the attribute
Image may be NSFW.
Clik here to view.
image


6) Create the following additional attributes for the Player and maintain the properties. The property mandatory=true means, that in order to create a Player you need to set this attribute.

  • Name: firstname; Type: com.sap.caf.core.longText; Properties: Mandatory=true
  • Name: lastname; Type: com.sap.caf.core.longText; Properties: Mandatory=true
  • Name: points; Type: com.sap.caf.base.integer; Properties: leave default

7) Once a user has registered himself, a new UME (User Management Engine) user is created. Therefore we need a method (operation) to be able to read a user by his unique UME ID. Switch to the Operations tab of the Player Entity. You can see, that CAF has already created the typical lifecycle methods (CRUD operations). Because we marked the uniqueID attribute as key, CAF has generated the operation "readByCustomKeys". Select this method and press the Edit button. Change the name of the method to "readByUniqueID"

8) To be able to create a rank list for all Players, we need an additional operation, which returns a list of all Players. Therefore we add an operation called "getAllPlayers". It should not have any input parameter, so do not check any attributes

9) Because we do not have any role or permission concept, switch to the Permissions tab of the Player and deselect "Permission checks enabled".

10) Save all metadata (there is a button in the toolbar ot the CAF perspective)

Creating the Match Entity Service

1) Create a new Entity Service with the name Match as described above

2) Create the following attributes for the Match and maintain the properties

  • Name: team1; Type: com.sap.caf.core.longText; Properties: Mandatory=true
  • Name: team2; Type: com.sap.caf.core.longText; Properties: Mandatory=true
  • Name: goal1; Type: com.sap.caf.base.integer; Properties: leave default
  • Name: goal2; Type: com.sap.caf.base.integer; Properties: leave default
  • Name: matchTime; Type: com.sap.caf.base.timestamp; Properties: Mandatory=true

3) To be able to make predictions for all matches, we need an additional operation. Therefore switch to the Operations tab and add a new operation called "getAllMatches". It should have no input parameters, so do not check any attributes in the wizard.

4) Switch to the Permissions Tab and deselect the checkbox "Permission checks enabled"

5) Save all Metadata

Creating the Prediction Entity Service

1) Create a new Entity Service with the name Prediction as described above

2) Create the following attributes for the Prediction and maintain the properties

  • Name: prediction1; Type: com.sap.caf.base.integer; Properties: Mandatory=true
  • Name: prediction2; Type: com.sap.caf.base.integer; Properties: Mandatory=true

3) Since each Prediction belongs exactly to one Match and one Player, we need a Match and a Player reference attribute for our Prediction Entity. Therefore drag the Player from the left side (from the Entity Catalog) to the right side (Attributes section), and set the property of the newly created attribute "playerRef" to Mandatory=true (see next picture). Do the same with the Match Entity. At the end you must have 2 attributes (prediction1, prediction2) and 2 references (playerRef, matchRef) on the Prediction Entity, all having Mandatory=true.
Image may be NSFW.
Clik here to view.
image


4) To be able to read a single Prediction for a given Match and a given Player, we add the operation "getPredictionByPlayerAndMatch". Switch to the Operations tab and add the operation "getPredictionByPlayerAndMatch". Check the input parameters "matchRef" and "playerRef" as shown in the following picture
Image may be NSFW.
Clik here to view.
image


5) Create another operation called "getPredictionsByPlayer" and check the input parameter "playerRef". This method is used to build the prediciton UI view, where the player can see the predictions he has already done.

6) Switch to the Permissions Tab and deselect the checkbox "Permission checks enabled".

7) Save all metadata (there is a button in the toolbar ot the CAF perspective).

What have we reached so far?

Without writing any code, we have defined our Business Objects (Entities). CAF has generated the fundamental lifecycle methods to create, read, update and delete the BO's in the database. We also have added additional methods (operations) to retrieve the BO's. In the next steps we are going to implement the business functions using a CAF Application Service.

Creating the Application Service

Application Services are used to implement the business logic. In our betting pool application we have for example the requirement that nobody can make a prediction on a match, which already has started. Such a requirement can be implemented in an Application Service.

1) In the Service Explorer right click on the Application Services, select "New" and name the Service "BettingPool"
Image may be NSFW.
Clik here to view.
image


2) Go to the Dependencies tab of the BettingPool Application Service and add the Entity Services "Match", "Player" and "Prediction" to the available Services on the left side (use the button with the arrow)
Image may be NSFW.
Clik here to view.
image




In the next steps we define the interface, which will be visible to the UI layer. The UI layer does not directly access the Entity Services. It only has access to methods (called operations) which are defined in the Application Service.

Create the operation createPlayer

If a user has registered himself in the UME, a new Player Entity must be created. Since the attributes "uniqueID", "firstname" and "lastname" are all mandatory the createPlayer method must provide them in the constructor.

1) Switch to the Operations tab and click the Add... button to add a new operation.

2) Select the "Access" radio button and only mark the checkbox "Create".

3) Insert a description and select the Player Entity Service as shown in the next picture
Image may be NSFW.
Clik here to view.
image


4) In the next screen the mandatory attributes "firstname", "lastname" and "uniqueID" are already flaged. Just press Finish

5) Add the ServiceException to the createPlayer operation by clicking the "Fault" button
Image may be NSFW.
Clik here to view.
image


6) Switch to the Implementation tab. The operation "createPlayer" should have the following signature:

public com.sap.wc.besrv.player.Player createPlayer( java.lang.String firstname, java.lang.String lastname, java.lang.String uniqueID) throws com.sap.caf.rt.exception.CAFCreateException, com.sap.caf.rt.exception.ServiceException

7) Go to the just created method createPlayer and insert the following code between the two comments //@@ custom code start and //@@ custom code end

retValue = this.getPlayerService().create(uniqueID, firstname, lastname);

The code just gets a reference to the Player Entity Service and calls the create method defined on the Player Entity Service. The result is, that a Player is generated in the DB.

Create the operation getPlayerByUniqueID

If a user has logged on to the betting pool application, the unique ID is retrieved from UME. To check if this is a new or already existing user we need a method which reads a user from the DB by his unique ID. If the user does not exist, null should be returned (we do not want an exception to be thrown here). If the user exists a Player Entity should be returned

1) Switch to the Operations tab of the Application Service and click the Add... button to add a new operation.

2) Select the "Custom" radio button and click next

3) Insert the name "getPlayerByUniqueID" and add a description. Set the Transaction Type to "Optional". Click Finish

4) So far the operation does not have any input or output parameters defined. Select the operation "getPlayerByUniqueID" and add the type "com.sap.caf.core.longText" as input parameter by pressing the Input button (see picture below)
Image may be NSFW.
Clik here to view.
image


5) Define an output parameter "Player" by selecting the Player Entity on the left side and pressing the "Output" button (the Player Entity can be found in the attributes catalog under Data Structures > wc)

6) We now change the name of the input parameter. Right now it is named arg0. This does not look very nice in the method signature. Therefore select the input parameter arg0 and switch to the Properties tab below. Here you can change the name of the input parameter to "uniqueID"
Image may be NSFW.
Clik here to view.
image


7) Switch to the Implemtation tab. The operation "getPlayerByUniqueID" should have the following signature:

public com.sap.wc.besrv.player.Player getPlayerByUniqueID( java.lang.String uniqueID)

8) Go to the just created method "getPlayerByUniqueID" and insert the following code between the two comments //@@ custom code start and //@@ custom code end

retValue = this.getPlayerService().readByUniqueID(uniqueID); return retValue; } catch(ServiceException e){ // player did not exist -> return null retValue = null;

Again we call the Player Entity Service and execute the readByUniqueID method. We catch the exception in case there is no Player there and return null.

Create the operation getRankList

This operation is needed to display the rank list of all players and their points. If an administrator maintains the result of a match, all Predictions for that Match should be evaluated, the points for each Player should be calculated and the Player attribute "points" must be updated. This is the recommended way, but this requires you to build your own admin UI where you can trigger the update of the points after the admin has saved the match result. If you look at the length of this blog and there are still 2 blogs comming up, you'll probably know why I didn't want to extend the application.

 

The good thing is that we actually don't need an admin UI, because CAF already provides UI's where we can create, read, update and delete entities. These UI's are generated from CAF for test purposes. After deployment, you can right click on an Entity or Application Service and select "Test". Doing this, a WD application will start which allows you to call all operations (methods) on the Services. So this will be our Administrator interface for creating Matches or maintaining match results

So how do we calculate the points for each Player instead?
We will calculate them "on the fly" without storing the points in the DB. Each time the "getRankList" operation is called, all points for all Players are calculated dynamically based on the given Predictions. Yes I know this is not very fast, but it seems to be the only solution without building an admin UI or scheduling background jobs which updates the Player points every night.

1) Switch to the Operations tab of the Application Service and click the Add... button to add a new operation.

2) Select the "Query" radio button and click next

3) Insert the name "getRankList", a description and choose the Player Entity Service as the data structure. Press Finish

4) Add the "ServiceException" to the "getRankList" operation by clicking the "Fault" button

5) Switch to the Implemtation tab. Navigate to the just created method "getRankList" and insert the following code between the two comments //@@ custom code start and //@@ custom code end

/** * This solution calculates the points for each player dynamicaly. This is not the best solution * regarding preformance, since the points are recalculated every time the method is called */ List resultList = new ArrayList(); List playerList = this.getPlayerService().getAllPlayers(); PredictionServiceLocal predictionService = this.getPredictionService(); MatchServiceLocal matchService = this.getMatchService(); // loop over all players for(Iterator it = playerList.iterator(); it.hasNext(); ){ Player aPlayer = (Player)it.next(); // get all Predictions for this Player List predictionList = predictionService.getPredictionsByPlayer(new QueryFilter(aPlayer.getKey())); // loop over all predictions and calculate the rank points for(Iterator it2 = predictionList.iterator(); it2.hasNext();){ Prediction aPrediction = (Prediction)it2.next(); // read the corresponding match Match aMatch = matchService.read(aPrediction.getMatchRef()); int points = 0; long prediction1 = aPrediction.getPrediction1(); long prediction2 = aPrediction.getPrediction2(); long deltaPrediction = prediction1-prediction2; long goal1 = aMatch.getGoal1(); long goal2 = aMatch.getGoal2(); long deltaGoal = goal1-goal2; // -1 for the goal is the default value, if a match is not finished if(goal1 != -1 && goal2 != -1){ // match is over -> calculate the rank points if(goal1 == prediction1 && goal2 == prediction2) // exact match -> 3 points points = 3; else if((deltaPrediction > 0 && deltaGoal > 0) || (deltaPrediction < 0 && deltaGoal < 0) || (deltaPrediction == 0 && deltaGoal == 0)) // right tendency -> 1 point points = 1; // set the calculated points aPlayer.setPoints(aPlayer.getPoints()+points); } } // inner for loop // add the player to the return list resultList.add(aPlayer); } // outer for loop retValue = resultList;

The code is well documented. What's worth mentioning is that the default value for all integer based attributes must be -1. Because 0:0 is a valid result of a match, we need to distinguish this from the default value. If a Prediction is an exact match, the Player gets 3 Points. If they only predict the right winning team, they will receive 1 point.
The QueryFilter class is used to define a value range. We use it here only for an exact value (this concept of value range is well known in the ABAP area ;-)

6) As a result the operation should have the following signature:

public java.util.List getRankList() throws com.sap.caf.rt.exception.CAFFindException, com.sap.caf.rt.exception.ServiceException


Save all metadata

Create the operation getAllMatchesAndPredictions

This method is needed to provide the UI with a table of all Matches and all Predictions. The user should be able to enter or update a prediction, if the match has not already started. This means the table must show information from two Entity Services (Match and Prediction). Therefore, we create a new datastructure, which transports the data from both Entity Services to the client. Such a datastructure is called DTO (Data Transfer Object). So let's start to create this structure.

1) In your Application Service, switch to the Operations tab. Go to the Attributes/Type Repository and on the node "wc" click right and select New
Image may be NSFW.
Clik here to view.
image


2) Insert the name "MatchPredictionDTO". To add new attributes to your DTO, you must first select a type from the left side, then click the Add button and then change the name of this attribute (see the steps 1,2 and 3 in the picture above)
Image may be NSFW.
Clik here to view.
image


Attribute Name: team1; Type: com.sap.caf.core.longText
Attribute Name: team2; Type: com.sap.caf.core.longText
Attribute Name: matchTime; Type: com.sap.caf.base.timestamp
Attribute Name: goal1; Type: com.sap.caf.base.integer
Attribute Name: goal2; Type: com.sap.caf.base.integer
Attribute Name: prediction1; Type: com.sap.caf.base.integer
Attribute Name: prediction2; Type: com.sap.caf.base.integer
Attribute Name: locked; Type: com.sap.caf.base.boolean
Attribute Name: matchRef; Type: com.sap.caf.core.longText
Attribute Name: playerRef; Type: com.sap.caf.core.longText
Attribute Name: predictionRef; Type: com.sap.caf.core.longText

3) Press Finish

4) Switch to the Operations tab of the Application Service and click the Add... button to add a new operation.

5) Select the "Custom" radio button and click next

6) Insert the name "getAllMatchesAndPredictions" and add a description. Set the Transaction Type to "Optional". Click Finish

7) Since this is a Custom operation, we need to add the input and output parameters manually as described in "Create the operation getPlayerByUniqueID". Select the operation "getAllMatchesAndPredictions". Select the MatchPredictionDTO from the Type Repository on the left side and click the button "Output".

8) Select the output parameter named "Response" and change the property Collection to "list"
Image may be NSFW.
Clik here to view.
image


9) Select the type "com.sap.caf.core.longText" from the Simple Types Repository and click the button "Input"

10) Change the name of the input parameter "arg0" to "playerKey"

11) Add the ServiceException to the "getAllMatchesAndPredictions" operation by clicking the "Fault" button

12) Navigate to the implementation tab and insert the following code to the just created method

retValue = new ArrayList(); // get the prediction and match service PredictionServiceLocal predictionService = this.getPredictionService(); MatchServiceLocal matchService = this.getMatchService(); // get the player Player thisPlayer = this.getPlayerService().read(playerKey); List allMatches = null; // get all matches allMatches = matchService.getAllMatches(); Date now = Calendar.getInstance().getTime(); // loop over all matches for(Iterator it = allMatches.iterator(); it.hasNext();){ // create a DTO instance MatchPredictionDTO dto = new MatchPredictionDTO(); Match aMatch = (Match)it.next(); // set attributes of DTO dto.setTeam1(aMatch.getTeam1()); dto.setTeam2(aMatch.getTeam2()); dto.setMatchTime(aMatch.getMatchTime()); dto.setGoal1(aMatch.getGoal1()); dto.setGoal2(aMatch.getGoal2()); dto.setMatchRef(aMatch.getKey()); dto.setPlayerRef(thisPlayer.getKey()); // get the user's prediction for the current match QueryFilter matchFilter = new QueryFilter(aMatch.getKey()); matchFilter.setEntityRef(true); QueryFilter playerFilter = new QueryFilter(thisPlayer.getKey()); playerFilter.setEntityRef(true); List prediction = predictionService.getPredictionByPlayerAndMatch(playerFilter, matchFilter); if(prediction.size() > 0){ // there is a prediction for the match, fill the DTO Prediction thePrediction = (Prediction)prediction.get(0); dto.setPrediction1(thePrediction.getPrediction1()); dto.setPrediction2(thePrediction.getPrediction2()); dto.setPredictionRef(thePrediction.getKey()); } else{ // there is no prediction for this match, fill default values dto.setPrediction1(-1); dto.setPrediction2(-1); dto.setPredictionRef(null); } // check if the match has already started -> set the locked flag if(now.before(aMatch.getMatchTime())) dto.setLocked(false); else dto.setLocked(true); retValue.add(dto); }


If you just copy and paste the sourcecode make shure, that the call-sequence of the parameters for the method call "getPredictionByPlayerAndMatch(playerFilter, matchFilter)" is correct.
The locked attribute of the DTO is used at the UI layer to disable the prediction input field, if the match has already started. Again, if no prediction is made, the prediction values are set to their default values, -1. This will later be handled in the UI layer.
The method loops over all matches, reads the corresponding Predictions and fills the DTO object. The method returns a list of DTO objects.

13) Save all metadata

14) As a result the operation should have the following signature:

public java.util.List getAllMatchesAndPredictions( java.lang.String playerKey) throws com.sap.caf.rt.exception.ServiceException


Create the operation saveAllPredictions

This is the last operation - believe me ;-). Ok this operation is needed for saving all Predictions a Player has made

1) Switch to the Operations tab of the Application Service and click the Add... button to add a new operation.

2) Select the "Custom" radio button and click next

3) Insert the name "saveAllPredictions" and add a description. Set the Transaction Type to "Optional". Click Finish

4) Add the "MatchPredictionDTO" from the Type Repository on the left as input parameter by clicking the "Input" button

5) Change the property "Collection" of the input parameter to "list"
Image may be NSFW.
Clik here to view.
image


6) Add the ServiceException to the "saveAllPrediction" operation by clicking the "Fault" button

7) Goto the implemetation tab and add the following code

PredictionServiceLocal predictionService = this.getPredictionService(); // loop over the List of DTO's for(Iterator it = matchPredictionDTO0.iterator(); it.hasNext(); ){ MatchPredictionDTO dto = (MatchPredictionDTO)it.next(); if(!dto.getLocked() && dto.getPrediction1() != -1 && dto.getPrediction2() != -1){ // match is not locked and has valid prediction values if(dto.getPredictionRef() == null){ // Prediction does not exist before, so create it predictionService.create(dto.getPrediction1(), dto.getPrediction2(), dto.getPlayerRef(), dto.getMatchRef()); } else if(dto.getPredictionRef() != null){ // Prediction did exist before -> update it // we need to re-read the Prediction, because for optimistic locking the last modified // date is checked for equality. Problem: we do not have the last modified date in dto Prediction prediction = predictionService.read(dto.getPredictionRef()); prediction.setPrediction1(dto.getPrediction1()); prediction.setPrediction2(dto.getPrediction2()); predictionService.update(prediction); } } }



8) Save all metadata


If you just copy and paste the sourcecode make shure, that the call-sequence of the parameters for the method call "predictionService.create(dto.getPrediction1(), dto.getPrediction2(), dto.getPlayerRef(), dto.getMatchRef())" is correct.
Only if a match is not locked, and a prediction was made, a new Prediction is created or the Prediction is updated. A Prediction exists, if the predictionRef attribute is set in the DTO.

Deploy and test the Services

Now it's time to test and deploy the services.

1) Right click on the CAF project "wc", select "Generate All Project Code"

2) Right click on the CAF project "wc", select "Development Component > Build...". This will build all projects, so be patient ;-)

3) Deploy all projects by right click on the "wc" project and select "Deploy to J2EE Engine". Please wait till you see all 5 deploy success messages in the Output view. Before deployment you have to configure the J2EE engine under Window > Preferences > SAP J2EE Engine.

4) In the next step you should test your services. Right click on the Application Service "Betting Pool" and select "Test". Login with User = 'Administrator' and your admin password (abcd1234 is the default for the Sneak Preview Edition). Launching the WD application the first time takes a while, so be patient

5) If you open the tree-nodes "sap.com > wc" on the left side you see your Application Service (BettingPool) and your three Entity Services (Match, Player and Prediction Service). Here you can also select your operations which you want to test. On the right side you can create, delete or update instances of your Entity Services. This is the mentioned UI, which can be used by the Administrator to create and update Matches

6) For example if you want to add a new match, open the following nodes on the left side: "sap.com > wc > MatchService" and click on Match. On the right side click the button "New". In the above table you can now insert the team names (team1, team2) and the match time. Make shure the default values for the fields goal1 and goal2 are -1. After that press the "Save" button.


In the next blog I will show you how the connection between the Business Layer and the UI layer is done.

Viewing latest article 2
Browse Latest Browse All 11

Trending Articles