<repo_name>billyjf/CRUDUsers<file_sep>/settings.gradle
rootProject.name = 'CRUDUsers'

<file_sep>/src/main/java/com/billyjf/resources/UserResource.java
package com.billyjf.resources;

import com.billyjf.api.User;
import com.codahale.metrics.annotation.Timed;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

import static javax.ws.rs.core.Response.Status;
import static javax.ws.rs.core.Response.status;

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
    private final List<User> users;
    private final AtomicInteger idCounter = new AtomicInteger(2);

    public UserResource(List<User> users) {
        this.users = users;
    }

    @GET
    @Path("/users")
    @Timed
    public List<User> listUsers() {
        return users;
    }

    @POST
    @Path("/users")
    @Consumes({MediaType.APPLICATION_JSON})
    @Timed
    public Response createUser(User user) {
        final User prospectiveUser = new User(idCounter.incrementAndGet(), user.getFirst(), user.getLast(), user.getZip(), user.getEmail());
        Response response = status(Status.OK).build();

        if(users.stream().noneMatch(u -> u.equals(prospectiveUser)))
            users.add(prospectiveUser);
        else
            response = status(Status.CONFLICT).build();

        return response;
    }

    @GET
    @Path("/users/{id}")
    @Timed
    public Object getUser(@PathParam("id") long id) {
        final Optional<User> first = users.stream().filter(u -> u.getId() == id).findFirst();

        if(first.isPresent())
            return first.get();
        else
            return status(Status.NOT_FOUND).build();
    }

    @PUT
    @Path("/users/{id}")
    @Consumes({MediaType.APPLICATION_JSON})
    @Timed
    public Response updateUser(@PathParam("id") long id, User user) {
        final Optional<User> firstMatch = users.stream().filter(u -> u.getId() == id).findFirst();
        Response response;

        if(firstMatch.isPresent()) {
            final User oldUser = firstMatch.get();
            final User prospectiveUser = new User(oldUser.getId(), user.getFirst(), user.getLast(), user.getZip(), user.getEmail());

            if(users.stream().noneMatch(u -> u.equals(prospectiveUser))) {
                users.remove(oldUser);
                users.add(prospectiveUser);

                response = status(Status.NO_CONTENT).build();
            }
            else
                response = status(Status.CONFLICT).build();
        }
        else
            response = status(Status.NOT_FOUND).build();

        return response;
    }

    @DELETE
    @Path("/users/{id}")
    @Timed
    public Response deleteUser(@PathParam("id") long id) {
        Response response = status(Status.NO_CONTENT).build();
        Optional<User> userToDelete = users.stream().filter(u -> u.getId() == id).findFirst();

        if(userToDelete.isPresent())
            users.remove(userToDelete.get());
        else
            response = status(Status.NOT_FOUND).build();

        return response;
    }
}
<file_sep>/src/test/java/com/billyjf/UserResourceTest.java
package com.billyjf;

import com.billyjf.api.User;
import com.billyjf.resources.UserResource;
import org.junit.Test;

import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;

import static javax.ws.rs.core.Response.Status;
import static org.junit.Assert.assertEquals;

public class UserResourceTest {
    private User billy = new User(1, "Billy", "Fisher", "97078", "<EMAIL>");
    private User janTheMan = new User(2, "Jan", "Spooner", "12345", "<EMAIL>");
    private UserResource userResource;

    public UserResourceTest() {
        List<User> users = new ArrayList<User>();
        users.add(billy);
        userResource = new UserResource(users);
    }

    @Test
    public void listUsers() {
        assertEquals(List.of(billy), userResource.listUsers());
    }

    @Test
    public void createUser() {
        userResource.createUser(janTheMan);

        assertEquals(janTheMan, userResource.listUsers().get(1));
    }

    @Test
    public void createUserReturnsConflictOnDuplicate() {
        userResource.createUser(janTheMan);

        assertEquals(Status.CONFLICT.getStatusCode(), userResource.createUser(janTheMan).getStatus());
    }

    @Test
    public void getUser() {
        User user = (User)userResource.getUser(1);

        assertEquals(billy, user);
    }

    @Test
    public void getUserThatDoesNotExist() {
        Response response = (Response)userResource.getUser(2);

        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
    }

    @Test
    public void updateUserThatExists() {
        assertEquals(billy, userResource.getUser(1));
        Response response = userResource.updateUser(1, janTheMan);
        assertEquals(janTheMan, userResource.getUser(1));
        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
    }

    @Test
    public void updateUserThatDoesNotExist() {
        final Response unknownUserResponse = (Response)userResource.getUser(2);
        assertEquals(Status.NOT_FOUND.getStatusCode(), unknownUserResponse.getStatus());
        Response updateResponse = userResource.updateUser(2, billy);
        assertEquals(Status.NOT_FOUND.getStatusCode(), updateResponse.getStatus());
    }

    @Test
    public void updateUserThatWouldCauseDuplicate() {
        Response updateResponse = userResource.updateUser(1, billy);
        assertEquals(Status.CONFLICT.getStatusCode(), updateResponse.getStatus());
    }

    @Test
    public void deleteUser() {
        Response response = userResource.deleteUser(1);
        assertEquals(0, userResource.listUsers().stream().filter(u -> u.getId() == 1).count());
        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
    }

    @Test
    public void deleteUserThatDoesNotExist() {
        Response response = userResource.deleteUser(2);
        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
    }
}
<file_sep>/src/main/java/com/billyjf/CRUDUsersConfiguration.java
package com.billyjf;

import com.billyjf.api.User;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration;

import javax.validation.constraints.NotEmpty;
import java.util.List;

public class CRUDUsersConfiguration extends Configuration {
    @NotEmpty
    private List<User> users;

    @JsonProperty
    public List<User> getUsers() { return users; }

    @JsonProperty
    public void setUsers(List<User> users) { this.users = users; }
}
<file_sep>/build.gradle
plugins {
    id 'java'
    id 'jacoco'
    id "com.github.johnrengelman.shadow" version "5.2.0"
}

version '1.0'

sourceCompatibility = 1.9

repositories {
    mavenCentral()
    maven {
        name 'Shadow'
        url 'http://dl.bintray.com/content/johnrengelman/gradle-plugins'
    }
}

dependencies {
    final String dropWizardVersion = '2.0.0'

    compile "io.dropwizard:dropwizard-core:${dropWizardVersion}"
    compile "io.dropwizard:dropwizard-json-logging:${dropWizardVersion}"

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

jar {
    manifest {
        attributes(
                'Main-Class': 'com.billyjf.CRUDUsersApplication'
        )
    }
}

shadowJar {
    mergeServiceFiles()
    exclude 'META-INF/*.DSA'
    exclude 'META-INF/*.RSA'
}

build.dependsOn shadowJar<file_sep>/README.md
[![Build Status](https://www.travis-ci.com/billyjf/CRUDUsers.svg?branch=master)](https://www.travis-ci.com/billyjf/CRUDUsers)

# CRUDUsers
Sample built on https://www.dropwizard.io/ and tested on JDK 12 and Gradle 5.2.1. Users deserialized from `server.yml` and maintained in memory.

  * ✅ UserResource allows clients to ✅ create, ✅ read, ✅ update, ✅ delete or ✅ list users.
  * ✅ A list of maps is used to track users by their ids.
  * ✅ Structured logging has been configured for the `console`.
  * ✅ Routes are `@Timed` using Dropwizard codahale metric annotations.
  * ✅ Write unit tests for the service.
  * ✅ A code coverage report is generated by Jacoco at `build/reports/jacoco/test/html/index.html`.
  * ✅ The user JSON includes id, first name, last name, zip code, and email address and is immutable.

## Building
`./gradlew build`

## Starting
`java -jar build/libs/CRUDUsers-1.0-all.jar server server.yml`

## Testing

### Unit
`./gradlew test jacocoTestReport`

### API

#### List
```
$ curl localhost:8080/users 2>/dev/null | jq .
[
  {
    "id": 1,
    "first": "Billy",
    "last": "Fisher",
    "zip": "97078",
    "email": "<EMAIL>"
  },
  {
    "id": 2,
    "first": "Stacey",
    "last": "Wright",
    "zip": "11029",
    "email": "<EMAIL>"
  }
]
```

#### Create
```
$ curl --request POST --header 'Content-Type: application/json' --data "$(cat unique_user.json)" -i 2>/dev/null localhost:8080/users | head -n 1
HTTP/1.1 200 OK
$ curl --request POST --header 'Content-Type: application/json' --data "$(cat unique_user.json)" -i 2>/dev/null localhost:8080/users | head -n 1
HTTP/1.1 409 Conflict
```

#### Read
```
$ curl localhost:8080/users/1 2>/dev/null | jq .
{
  "id": 1,
  "first": "Billy",
  "last": "Fisher",
  "zip": "97078",
  "email": "<EMAIL>"
}

$ curl localhost:8080/users/3 -i 2>/dev/null | head -n 1
HTTP/1.1 404 Not Found
```

#### Update
```
$ curl localhost:8080/users/2 2>/dev/null | jq .
{
  "id": 2,
  "first": "Stacey",
  "last": "Wright",
  "zip": "11029",
  "email": "<EMAIL>"
}

$ curl --request PUT --header 'Content-Type: application/json' --data "$(cat new_user.json)" localhost:8080/users/2 -i 2>/dev/null | head -n 1
HTTP/1.1 409 Conflict

$ curl --request PUT --header 'Content-Type: application/json' --data "$(cat unique_user.json)" localhost:8080/users/2 -i 2>/dev/null | head -n 1
HTTP/1.1 204 No Content

$ curl localhost:8080/users/2 2>/dev/null | jq .
{
  "id": 2,
  "first": "Stefani",
  "last": "Germanotta",
  "zip": "97221",
  "email": "<EMAIL>"
}
```

#### Delete
```
$ curl --request DELETE localhost:8080/users/2 -i 2>/dev/null | head -n 1
HTTP/1.1 204 No Content

$ curl --request DELETE localhost:8080/users/2 -i 2>/dev/null | head -n 1
HTTP/1.1 404 Not Found
```

### Health
```
$ curl http://localhost:8081/healthcheck 2>/dev/null | jq .
{
  "deadlocks": {
    "healthy": true,
    "duration": 0,
    "timestamp": "2019-12-26T16:05:22.852-08:00"
  },
  "dependency1": {
    "healthy": true,
    "duration": 0,
    "timestamp": "2019-12-26T16:05:22.852-08:00"
  }
}
```
