Kitchen Duty Planning REST Resource

Basically we will build the Kitchen Duty Planning REST Resource and the Kitchen Duty Planning REST Model which will persist our selected weeks and users to the database.

You also might want to look into the Story Workshop again to recapitulate what we are going to build now.

This chapter heavily depends on the configuration done in Step 2.

What do we need exactly?

After some serious brain usage on how to store the actual data of «who has Kitchen Duty in which week?» I came up with the Data-Model seen on the right.

The Week will be the actual week number in the year, and we don't care about the year in this tutorial. So if you had kitchen duty in week 24 this year you will have it the years after as well. With a little thinking ahead of how I want to use it in the JS Controller I basically will select a week via a date picker and calculate the week number in the frontend using moment.js

The User part is obvious. We will need to store a list of usernames in the database in relation with Weeks (@ManyToMany).

Concerning the REST Endpoint I will want to have an URL which I can fire GET requests on something like /planning/week/15/users and receive a list of users.

Obviously the other way round would be a PUT request on /planning/week/15/users with a list of users as request body to store the user list for the week 15.

And for our stretch-goal a GET request on /planning/user/bob/weeks should return a list of week numbers in which Bob has Kitchen Duty.

Week n    m User
week: Integer username: String


How exactly do we persist our data?

Alright we know how we want to use our REST Endpoint from the perspective of the JS Controller. And we have some kind of clue on how the data should be stored theoretically.

There are two main possibilities to persist data in JIRA.

  • Using the PluginSettingsFactory you have an easy way to persist data. But you don't have queries and cannot build relationships like you can with a real Database. So this is good if you just want to store simple stuff when you always know how to retrieve data from the PluginSettingsFactory (for example by a fixed key).
     
  • Then there are Active Objects which let you use a real database with an ORM Interface. And that is definitely the choice we want to make here. So you might want to stroll around the Active Objects documents a little to get a grasp of it or just continue reading since I will also introduce you to it step by step.

Enabling Active Objects on our Project

All I will be writing about Active Objects is taken from the Developing your plugin with Active Objects Guide and the Getting Started with Active Objects Guide.

First of all add Active Objects dependency to your pom.xml.

pom.xml
<dependency>
   <groupId>com.atlassian.activeobjects</groupId>
   <artifactId>activeobjects-plugin</artifactId>
   <version>1.1.5</version>
   <scope>provided</scope>
</dependency>

We need a Component Import for ActiveObjects which will be the object we use to access the database.

src/main/java/com/codeclou/kitchen/duty/impl/.../MyPluginComponentImpl.java
package io.codeclou.kitchen.duty.rest;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
...

@Named ("myPluginComponent")
public class MyPluginComponentImpl implements MyPluginComponent {
   ...
   @ComponentImport
   private ActiveObjects activeObjects;
   ...
}

Now we create our first Entity Week and define the week as integer via getter/setter.

src/main/java/com/codeclou/kitchen/duty/ao/.../Week.java
package io.codeclou.kitchen.duty.ao;

import net.java.ao.Entity;
import net.java.ao.Preload;

@Preload 
public interface Week extends Entity { 
    Integer getWeek();

    void setWeek(Integer week);
}
We annotate our class with @Preload which I think makes it not lazy loaded ... but you have to read the docs if you want to know in detail.
The important thing is that we extend Entity that makes our Class automatically have getID() so we just need to define our fields which is currently only the week number (week of year).

We just created the Week Entity now and we need to tell the Active Objects module of our Plugin where he can find our Entity. Therefore we add a section to our atlassian-plugin.xml file listing the Entities (Maybe there is some kind of nice Annotation to skip that step, but since the docs are very thin on that I don't know. So if you find out please tell me).

src/main/resources/.../atlassian-plugin.xml
<ao key="ao-module">
    <description>The module configuring the Active Objects service used by this plugin</description>
    <entity>io.codeclou.kitchen.duty.ao.Week</entity>
</ao>

Creating a basic Week Entity and a persist-Endpoint

We are agile coders and wanna see basic results now of what we did. Therefore we will create a very basic persistTest-Endpoint to try out the ORM stuff provided by Active Objects. We will get rid of the endpoint later we just use it to get a feel of how Active Objects works.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResource.java
package io.codeclou.kitchen.duty.rest;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.jira.bc.user.search.UserSearchService;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.sal.api.transaction.TransactionCallback;
import io.codeclou.kitchen.duty.ao.Week;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;

@Named
@Path("/planning")
public class KitchenDutyPlanningResource {

    private ActiveObjects activeObjects; 

    @Inject 
    public KitchenDutyPlanningResource(ActiveObjects activeObjects) {
        this.activeObjects = activeObjects;
    }

    public KitchenDutyPlanningResource() {
    }


    @GET
    @Path("/persistTest") 
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response persistTest() {
        activeObjects.executeInTransaction(new TransactionCallback<Week>() 
        {
            @Override
            public Week doInTransaction() 
            {
                final Week testWeek = activeObjects.create(Week.class); 
                testWeek.setWeek(42);
                testWeek.save();
                return testWeek;
            }
        });
        return Response.ok("ok").build();
    }


    @GET
    @Path("/health")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response health() {
        return Response.ok("ok").build();
    }

}
Define ActiveObjects as property so that we can use it.
Add an @Inject to the constructor so that we get it injected via DI.
Define a Endpoint with path persistTest and Method @Get which we will use to play with the ORM.
We start a Transaction (... this should be done with Annotations ... but we have to deal with it ...) And here we would need to define our return type which in our case is Week. You could set it to Void here too.
We need to override doInTransaction and inside here we can do actual Active Objects ORM stuff. Obviously here we could override more methods to handle transaction rollbacks and usually we should wrap all these shenanigans inside a try-catch block and do proper exception handling BUT we are currently just playing around so it is fine.
Now that we are deep inside the rabbit hole we can start to persist our first Week object. We call activeObjects.create(..) and pass our Class-Type. After that we set our week value and call .save(). Now our week is persisted to the Database. Banzai!

Alright now startup JIRA with atlas-run but capture the output a file called jira-stdout.log so that we can grep it later. You need to have tee installed.

atlas-run | tee jira-stdout.log
...
[INFO] [talledLocalContainer] INFORMATION: Server startup in 38377 ms
[INFO] [talledLocalContainer] Tomcat 8.x started on port [2990]
[INFO] jira started successfully in 58s at http://localhost:2990/jira
[INFO] Type Ctrl-D to shutdown gracefully
[INFO] Type Ctrl-C to exit

Open another terminal to execute some CURL commands and once JIRA has started up execute a GET Request to our persistTest Endpoint.

curl --user admin:admin -H 'Content-Type: application/json' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/persistTest
ok

Now our Week object with week 42 should have been persisted to the table Week in the database

Let's have a look into the Database

Alright now you need to know that JIRA runs a H2 Database when started with atlas-run. And it does NOT use HSQLDB anymore even though it has used HSQLDB up to some version of JIRA - but those days seem over.

You might have wondered why you needed to capture the STDOUT of JIRA previously to jira-stdout.log. Because on line 12131415 of one trillion overall lines that atlas-run echos you will find a line starting containing Database URL which tells you the location of the H2 Database. And since we want to have a look we need to know where the Database resides in order to connect.

Now stop the atlas-run by pressing CTRL+C (we can only look into the H2 DB if no other process uses it). And grep for the Database URL in the captured STDOUT.

grep "Database URL" jira-stdout.log
[INFO] [talledLocalContainer]     Database URL   : jdbc:h2:file:/Users/...../target/jira/home/database/h2db

Copy the whole jdbc:h2:file:/Users/...../target/jira/home/database/h2db JDBC URL (I have shortened the path here)

In the same terminal we now start the H2 Database Console by executing the h2.jar file. (Depending on your SDK version it should be somehwere around that location and might have an alternate version number.)

java -jar target/container/tomcat8x/cargo-jira-home/webapps/jira/WEB-INF/lib/h2-1.4.185.jar

Now a Browser Tab opens with the console and you just paste the JDBC URL and click connect.

Please remember to always just look into the database. You should never alter the Database with SQL Statements since JIRA manages autoincrement IDs and some other Caches and you will surely run into strange errors if you still do so. When using Object Relational Mappers it is best not to fiddle with the Database. Number One Rule: Only alter the Database through Active Objects.

You should see our Week table and if you execute a SELECT Statement on it you can see contents.

You can see the whole process of calling the testPersist Endpoint and connecting to the H2 Database in the following gif.

Adding User Class and Many-to-Many Relationship

Since we got Active Objects running successfully we now want to add a @ManyToMany relationship between User an Week.

And when you have read the docs you will note that we need a Mapping Entity for that case. We will call it UserToWeek.

But first we want to define the week and username as unique to avoid duplicate entries which are developer's nightmares.

src/main/java/com/codeclou/kitchen/duty/ao/.../Week.java
package io.codeclou.kitchen.duty.ao;

import net.java.ao.Entity;
import net.java.ao.ManyToMany;
import net.java.ao.Preload;
import net.java.ao.schema.NotNull;
import net.java.ao.schema.Unique;

@Preload
public interface Week extends Entity {

    @NotNull  
    @Unique  
    Integer getWeek();
    void setWeek(Integer week);

    @ManyToMany(value = UserToWeek.class)  
    User[] getUsers();
}
First we define week @NotNull which does not allow NULL values anymore.
Secondly we define week @Unique which does not allow duplicate values anymore.
Thirdly we define the Many-To-Many relationship on getUsers() with @ManyToMany. The value = UserToWeek.class defines which entity is used to map this relationship. You might know that you always need a mapping-table in the database when mapping to objects many-to-many since the relational paradigma can only map one-to-many. We will create UserToWeek a little further down below.
src/main/java/com/codeclou/kitchen/duty/ao/.../User.java
package io.codeclou.kitchen.duty.ao;

import net.java.ao.Entity;
import net.java.ao.ManyToMany;
import net.java.ao.Preload;
import net.java.ao.schema.NotNull;
import net.java.ao.schema.Unique;

@Preload
public interface User extends Entity {

    @NotNull 
    @Unique 
    String getName();
    void setName(String week);

    @ManyToMany(value = UserToWeek.class) 
    Week[] getWeeks();
}
First we define the username @NotNull which does not allow NULL values anymore.
Secondly we define the username @Unique which does not allow duplicate values anymore.
Thirdly we define the Many-To-Many relationship on getWeeks() with @ManyToMany. It works the same way as in Week just the other way round.

We just defined our real entities and the relation they have through UserToWeek so all that is left is to define the mapping entity.

src/main/java/com/codeclou/kitchen/duty/ao/.../UserToWeek.java
package io.codeclou.kitchen.duty.ao;

import net.java.ao.Entity;

public interface UserToWeek extends Entity { 

    void setUser(User user); 
    User getUser();

    void setWeek(Week week); 
    Week getWeek();
}
The UserToWeek also extends Entity.
We define getter and setter for User.
We define getter and setter for Week.

That was all we needed to do to map the Many-To-Many relationship with Active Objects. And now don't forget to list the entities in the ao section of your atlassian-plugin.xml.

src/main/resources/.../atlassian-plugin.xml
<ao key="ao-module">
    <description>The module configuring the Active Objects service used by this plugin</description>
    <entity>io.codeclou.kitchen.duty.ao.Week</entity>
    <entity>io.codeclou.kitchen.duty.ao.User</entity>
    <entity>io.codeclou.kitchen.duty.ao.UserToWeek</entity>
</ao>

 

We change the testPersist Endpoint to now persist a full relationship.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResource.java
package io.codeclou.kitchen.duty.rest;
...

@Named
@Path("/planning")
public class KitchenDutyPlanningResource {
...

    @GET
    @Path("/persistTest")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response persistTest() {
        activeObjects.executeInTransaction(new TransactionCallback<Week>()
        {
            @Override
            public Week doInTransaction()
            {
                final Week testWeek = activeObjects.create(
                    Week.class, 
                    new DBParam("WEEK", 42)); 
                testWeek.save();

                final User user = activeObjects.create(
                    User.class, 
                    new DBParam("NAME", "ichiban")); 
                user.save();

                final UserToWeek relationship =
                           activeObjects.create(UserToWeek.class); 
                relationship.setUser(user); 
                relationship.setWeek(testWeek);
                relationship.save();

                return testWeek;
            }
        });
        return Response.ok("ok").build();
    }
...
}
This is not new to you, we use activeObjects#create(...) to create a Week object with reference to the Database.
But this is new. DBParam is used to immediately set the @NotNull values when creating the object. Why do we need this? You can read 'Creating an entity with "not null"-constraints' OR let me explain. What does actually happen when activeObjects#create(...) is called? Usually the Active Objects does an INSERT into the database with null values and the autoincrement ID of the table row is then the only thing set (except for default values). So right after create(Week.class) the week object already has an ID when calling getID(). And now comes the tricky part. If you have defined a property as @NotNull Active Objects also just tries to do a INSERT into the table to write the ID back to the object BUT WAIT an ActiveObjectsSqlException is thrown since our field is null and it is not allowed to be null. SOLUTION: We use DBParams to immediately set the NotNull-Values so that the initial INSERT that is triggered by create(...) contains actual values for our NotNull-Fields.
We create a User object mapped to a Database Table row as we just did with Week.
We also need to set the NAME value initially for our NotNull-Fields.
We create a UserToWeek object mapped to a Database Table row as we just did with User.
Now we need to set the User and Week objects as reference and save. By that the Many-To-Many relation between User and Week is now established.

 

We are all set now to start JIRA again and to test if we can really save our relationship. But before we do that do a quick atlas-clean before the next atlas-run so that the database gets purged. Otherwise you might run into Unique index or primary key violation errors since we have 4 week entries already all with week=42 values.

atlas-clean

We startup JIRA again and call the persistTest Endpoint again.

atlas-run

curl --user admin:admin -H 'Content-Type: application/json' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/persistTest
ok

And by calling the testPersist endpoint a second time we will get a unique constraint exception as expected. That is fine since that only shows that the unique constraint works and we will be throwing away the persistTest Endpoint anyways.

We shutdown JIRA again and fire up the H2 Console again to see what tables have been created.

Creating the real Kitchen Duty Planning REST Resource with GET Endpoints for User and Week

Ok we have played around a little with active objects so far and now have our data model and entities in place. That means we can now create the real Endpoint Methods and throw away the testPersist Endpoint.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResource.java
package io.codeclou.kitchen.duty.rest;
...

@Named
@Path("/planning")
public class KitchenDutyPlanningResource {
...

    /**
     * Get the Users assigned to the weekNumber.
     *
     * @param weekNumber
     * @return
     */
    @GET
    @Path("/week/{weekNumber}/users")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response getUsersForWeek(@PathParam("weekNumber") final Integer weekNumber) {
        Week[] weeks = activeObjects.executeInTransaction(new TransactionCallback<Week[]>() { 
            @Override
            public Week[] doInTransaction() {
                return activeObjects.find( 
                           Week.class,
                           Query.select().where("WEEK = ?", weekNumber)); 
            }
        });
        List<KitchenDutyPlanningResourceUserModel> users = new ArrayList<>(); 
        if (weeks != null && weeks.length > 0) { 
            for (User user : weeks[0].getUsers()) { 
                users.add(
                   new KitchenDutyPlanningResourceUserModel( 
                            user.getID(),
                            user.getName()
                   )
                );
            }
        }
        return Response.ok(users).build(); 
    }

    /**
     * Get the Weeks assigned to the User.
     *
     * @param weekNumber
     * @return
     */
    @GET
    @Path("/user/{username}/weeks")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response getWeeksForUser(@PathParam("username") final String username) { 
        User[] users = activeObjects.executeInTransaction(new TransactionCallback<User[]>() {
            @Override
            public User[] doInTransaction() {
                return activeObjects.find(User.class, Query.select().where("NAME = ?", username));
            }
        });
        List<KitchenDutyPlanningResourceWeekModel> weeks = new ArrayList<>();
        if (users != null && users.length > 0) {
            for (Week week : users[0].getWeeks()) {
                weeks.add(new KitchenDutyPlanningResourceWeekModel(week.getID(), week.getWeek()));
            }
        }
        return Response.ok(weeks).build();
    }
...
}
In our new /week/{weekNumber}/users Endpoint we start a Transaction and specify Week[] as return type since there is no way (at least I did not find one) to get only one entity from the Database. We always get a List. Since we are having our unique constraints setup our lists will most likely only contain one value.
Now we use a new method find(...) of the Active Objects class which will return a List of Weeks found in the database.
Via the Query.select().where(...) we define that we only want to get records that have the week number set we passed as the Endpoints @PathParam. And please be aware that you HAVE TO add spaces in "WEEK = ?" otherwise it is Exception-Day for you.
What are we doing now? We just received a Week-Array from the activeObjects object most likely being cglib-proxy objects that might be lazy (even though we specified @Preload). That being said and me being very paranoid when it comes to my REST APIs I always want to seperate the Data Model that is populate to the User via the API from my internal Data Model used by the Database. Therefore I always setup simple POJOs that get a copy of the relevant database values. For example your REST-API User object might not contain a password property even though your Database User object does. (And yes we could hide it with some kind of Hide-Json-Annotation, but a clear separation is what worked best for me so far.) If you like you can try to skip out this step and directly pass your Database Entitiy objects out via the REST Endpoint. You might need to setup the JAX-WS Annotations on your Entities then.
We simply check if we got an empty result from out database lookup and if not we continue. And if we got an empty result we return the empty List as Endpoint response. This is always better as passing NULL and having trouble in your JS Controller handling these shenanigans.
We iterate over our Results with a for-loop. And if you want to be fancy you can do this in a more fashioned way ... I already see the scala nerds roll their eyes .... anyways we are fine with it.
Now we create such a REST-API-Pojo via a convenience constructor to copy the needed values from our database entity.
Lastly we return our list of users as Endpoint response.
The exact same thing is done for the /user/{username}/weeks Endpoint. The same explanations apply.

We have just used the REST-API-Pojos which we will define right now. No magic just getters and setters and some JAX-WS Annotations you have seen in previous steps of the tutorial already. The only fancy thing is the convenience constructor but yeah ... just copy paste those two since it's not a big deal.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResourceWeekModel.java
package io.codeclou.kitchen.duty.rest;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "weeks")
@XmlAccessorType(XmlAccessType.FIELD)
public class KitchenDutyPlanningResourceWeekModel {

    @XmlElement
    private Integer id;

    @XmlElement
    private Integer week;

    KitchenDutyPlanningResourceWeekModel() { }

    KitchenDutyPlanningResourceWeekModel(Integer id, Integer week) {
        this.id = id;
        this.week = week;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getWeek() {
        return week;
    }

    public void setWeek(Integer week) {
        this.week = week;
    }
}

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResourceUserModel.java
package io.codeclou.kitchen.duty.rest;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
public class KitchenDutyPlanningResourceUserModel {

    @XmlElement
    private Integer id;

    @XmlElement
    private String username;

    KitchenDutyPlanningResourceUserModel() { }

    KitchenDutyPlanningResourceUserModel(Integer id, String username) {
        this.id = id;
        this.username = username;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

Alright we again are at a point where we want to start JIRA and fire some request against our API to check it out.

atlas-run

Now we can query these two endpoints with the following example requests.

List assigned users for week 42.

curl --user admin:admin -H 'Content-Type: application/json' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/week/42/users
[{"id":2,"username":"ichiban"}]

List weeks for user ichiban.

curl --user admin:admin -H 'Content-Type: application/json' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/user/ichiban/weeks
[{"id":2,"week":42}]

That is working fine. In the next section we will deal with assigning Users to Weeks and removing assignments.

Add the PUT and DELETE Endpoints to add a User to a Week or remove him

Now we add the functionality to add users to weeks and remove them from weeks. Therefore we will need a DELETE and PUT Endpoint which we add to the KitchenDutyPlanningResource Class.

We start with the PUT Endpoint to assign Users to Weeks.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResource.java
...
    /**
     * Add the User to the Week
     *
     * @param weekNumber
     * @return
     */
    @PUT 
    @Path("/week/{weekNumber}/users")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response addUserToWeek(@PathParam("weekNumber") final Integer weekNumber, 
                                  final KitchenDutyPlanningResourceUserModel userParam) { 
        activeObjects.executeInTransaction(new TransactionCallback<Void>() { 
            @Override
            public Void doInTransaction() {
                //
                // WEEK
                //
                Week week = KitchenDutyActiveObjectHelper.findUniqueWeek( 
                                 activeObjects,
                                 weekNumber
                );
                if (week == null) {
                    week = activeObjects.create(
                                 Week.class,
                                 new DBParam("WEEK", weekNumber) 
                    );
                    week.save();
                }

                //
                // USER
                //
                User user = KitchenDutyActiveObjectHelper.findUniqueUser( 
                                 activeObjects,
                                 userParam.getUsername()
                );
                if (user == null) {
                    user = activeObjects.create(
                                 User.class,
                                 new DBParam("NAME", userParam.getUsername())
                    );
                    user.save();
                }

                //
                // Establish ManyToMany Relationship
                //
                UserToWeek relationship = KitchenDutyActiveObjectHelper
                                             .findRelationship(activeObjects, user, week); 
                if (relationship != null) {
                    // relation already exists
                    return null; 
                }
                relationship = activeObjects.create(UserToWeek.class); 
                relationship.setUser(user);
                relationship.setWeek(week);
                relationship.save();

                return null;
            }
        });
        return Response.ok().build(); 
    }
...
We define our new PUT /week/{weekNumber}/users Endpoint with method @PUT (you can also use @Patch which would be more appropriate since we are sending increments to the Endpoints but it is up to you). There is nothing more time consuming as to argue with colleagues about REST API design ... at best you look at the GitHub REST API or the Spring Data REST Docs to get a feel for good API design. And if you are ever in the position to nag about someones API just ask him "Does it come with HATEOAS?" and you will immediately sound like an expert. Then take a deep sip of your coffee, lean back and nod your head constantly to everything beeing said as if everything being said is boring you ....
iswoWeekNumber is a @PathParam again.
Since it is a PUT Endpoint we will send some JSON-Request-Body from our JS-Controller or our CURL commands. This JSON-Body is being automatically parsed by JAX-WS into the KitchenDutyPlanningResourceUserModel and you can access it via userParam.
We start a Transaction again but this time we choose Void as return type since we will do all our shenanigans inside the transaction.
Get the week by the weekNumber using our findUniqueWeek helper method which we will define later - just trust me that it will return one for the weekNumber passed or null.
If the week is null we just create it on the fly and continue.
Same goes for the User. First try to find the existing user and if the user does not exist create it.
Now we lookup if there is an existing many-to-many relationship between User and Week mapped by UserToWeek. Therefore I create a helper method findRelationship which returns an UserToWeek object representing an existing relationship or null.
If the relationship is not null we skip out here since the relationship already exists.
If the relationship is null we simply create it.
We always return HTTP 200 because in case an Exception is thrown JAX-WS will return HTTP 500 anyway. If you are designing a real API add a try/catch block and return some wrapped human readable errors and define HTTP Response codes for different error types. We are fine if we can check for HTTP 200 in our JS-Controller, and if the response code is not 200 we know something went wrong anyway.

To break this up a little concerning readability now follows the DELETE Endpoint to remove Week assignments of Users in the same file.

src/main/java/com/codeclou/kitchen/duty/rest/.../KitchenDutyPlanningResource.java
...

    /**
     * Remove the User from Week
     *
     * @param weekNumber
     * @return
     */
    @DELETE 
    @Path("/week/{weekNumber}/users")
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed
    public Response deleteUserFomWeek(@PathParam("weekNumber") final Integer weekNumber,
                                  final KitchenDutyPlanningResourceUserModel userParam) { 
        activeObjects.executeInTransaction(new TransactionCallback<Void>() {
            @Override
            public Void doInTransaction() {
                //
                // WEEK
                //
                Week week = KitchenDutyActiveObjectHelper.findUniqueWeek( 
                                 activeObjects,
                                 weekNumber
                );
                if (week == null) {
                    // week does not exist => no relation to delete
                    return null;
                }

                //
                // USER
                //
                User user = KitchenDutyActiveObjectHelper.findUniqueUser( 
                                 activeObjects,
                                 userParam.getUsername()
                );
                if (user == null) {
                    // user does not exist => no relation to delete
                    return null;
                }

                //
                // Delete ManyToMany Relationship
                //
                UserToWeek relationship = KitchenDutyActiveObjectHelper
                                             .findRelationship(activeObjects, user, week); 
                if (relationship != null) {
                    activeObjects.delete(relationship); 
                }

                return null;
            }
        });
        return Response.ok().build();
    }
...
We define our Endpoint with method @DELETE.
The JSON-Request-Body will be parsed into our POJO the same way as in the Endpoint above.
In the same way as in the PUT Endpoint we try to find an existing Week by our findUniqueWeek helper method. And if it is null it indicates that there cannot be any existing relation between user and week and that we can skip out here.
Same goes for the User. Find existing user by findUniqueUser helper method. And if it is null it indicates that there cannot be any existing relation between user and week and that we can skip out here.
If both user and week already exist try to find an existing relation by findRelationship helper method. And if it is not null delete the relationship.

To prevent code duplication and improve readability of Endpoint-code I moved some common data-access-spaghetti-code into some static Methods of the KitchenDutyActiveObjectHelper Class. It should be pretty self-explanatory.

src/main/java/com/codeclou/kitchen/duty/ao/.../KitchenDutyActiveObjectHelper.java
package io.codeclou.kitchen.duty.ao;

import com.atlassian.activeobjects.external.ActiveObjects;
import net.java.ao.Query;

public class KitchenDutyActiveObjectHelper {

    public static Week findUniqueWeek(ActiveObjects activeObjects, Integer weekNumber) {
        Week[] weekRes = activeObjects.find(Week.class, Query.select().where("WEEK = ?", weekNumber));
        if ((weekRes != null && weekRes.length > 0)) {
            return weekRes[0];
        }
        return null;
    }

    public static User findUniqueUser(ActiveObjects activeObjects, String username) {
        User[] userRes = activeObjects.find(User.class, Query.select().where("NAME = ?", username));
        if ((userRes != null && userRes.length > 0)) {
            return userRes[0];
        }
        return null;
    }

    public static UserToWeek findRelationship(ActiveObjects activeObjects, User user, Week week) {
        UserToWeek[] relationships = activeObjects.find(UserToWeek.class, Query.select().where("USER_ID = ? AND WEEK_ID = ?", user.getID(), week.getID()));
        if ((relationships  != null && relationships .length > 0)) {
            return relationships[0];
        }
        return null;
    }
}

Now we can fire up atlas-run once more and wait for JIRA startup.

Let's add a user called jimmy to a week 42. Fire up this curl command and it should return a HTTP 200 telling you it worked (we use the CURL -i parameter to echo out the response headers and return code). We have written our Endpoints in a way so that the CURLs are idemponent. So if you fire it a second time you will get a HTTP 200 as well but since jimmy already is assigned to week 42 nothing happens internally.

And by the way, we don't need to pass an ID for user since we don't know it and since the username has a unique constraint which makes it our technical primary key.

curl -i --user admin:admin -H 'Content-Type: application/json' -X PUT -d '{ "username": "jimmy" }' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/week/42/users
HTTP/1.1 200 OK
...
Content-Type: application/json;charset=UTF-8
Content-Length: 0
Date: Fri, 09 Sep 2016 16:52:06 GMT

Let's remove a user called ichiban from week 42.

curl -i --user admin:admin -H 'Content-Type: application/json' -X DELETE -d '{ "username": "ichiban" }' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/week/42/users
HTTP/1.1 200 OK
...
Content-Type: application/json;charset=UTF-8
Content-Length: 0
Date: Fri, 09 Sep 2016 16:52:06 GMT

List assigned users for week 42.

curl --user admin:admin -H 'Content-Type: application/json' http://localhost:2990/jira/rest/kitchenduty/1.0/planning/week/42/users
[{"id":3,"username":"jimmy"}]

 

Now we have all our Endpoints in place and can continue to the next step. There we will actually use our Endpoints from the JS-Controller.

You have completed Step 4. Good Job! You can check out this step's solution on GitHub

The graphic shows marked green which components we implemented in this section.