/ SpringFramework

<dev> Resume's are BORING Part 2

I've determined a stack. Actually, I've determined several stacks and, given time, I may even build this simple project more than once!

Officially though, I'm going to go with:
Database: PostgreSQL
Language: Java with the SpringFramework

Why?

  1. I like Postgres.
  2. Any job that I'm looking at wants the 200 years of .NET / Java Experience, so I may as well code to expectations just to prove that I can!

Issues?

  1. Java / Spring used to take FOREVER to get up and running. The nada -> App used to take ages. Apparently SpringBoot has fixed this. We shall see indeed!
  2. The security section might get a little convoluted. That is, the public access tokens. It might take a bit more work than originally anticipated.
  3. CrudRepositories and Swagger.io --> I don't think they really play all that nicely. It might be either a sacrifice of CrudRepository's built in HATEOAS or Swagger's documentation. I have yet to decide on this. I've decided to axe HATEOAS. I'll run through that process with you though.

Anyways, Let's get started.

1. Set up the app.

I use eclipse, so I'm going to go ahead and make a maven project.
I plan on using Spring Boot v. 2.0.0 so I'm going to prepare my pom file accordingly with:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.0.RELEASE</version>
</parent>

Reading some documentation and quick-start guides, I've determined that I really only minimally require the following dependencies:

<dependencies>
    <!-- A SpringBoot Rest Starter -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-rest</artifactId>
	</dependency>
    <!-- A SpringBoot JPA Starter -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
    <!-- An in memory database for dev purposes-->
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
	</dependency>
    <!-- The Test Framework -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
    <!-- DevTools for things such as live reloads -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<optional>true</optional>
	</dependency>
</dependencies>

In the default application package (I've chosen com.jimdhughes.raas), create an App.java file to run our new app.


@SpringBootApplication
public class App {
	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}
}

Open a browser window and navigate to http://localhost:8080 and you'll get a response like this:

{
  "_links": {
    "profile": {
      "href": "http://localhost:8080/profile"
    }
  }
}

Up and running!

Next we're going to add some domain models. Obviously we are going to have Users.
These Users will have resumes.
Each resume will have specific sections that are important to a complete resume including:

  • Contact Information
  • Objectives
  • Work Experience
  • Skills (especially important for technical resumes such as programmers.
  • Formal Education
  • Certifications
  • Training

So let's make a couple of these.
I like to put my models in a separate package such as: com.jimdhughes.raas.models
I'm going to make a User.java file and annotate it as such:

package com.jimdhughes.raas.models;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {
	@Id
	@GeneratedValue
	private Long id;
	
	@Column(unique=true)
	private String email;
	
	private String fullName;
    // I Excluded Constructors, Getters and Setters for brevity.
}

Our next logical domain object would be the Resumes.

package com.jimdhughes.raas.models;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Resume {

	@Id
	@GeneratedValue
	private Long id;
	
	@ManyToOne
	private User user;
	
	private String identifier;

	private String objective;
	
}

Notice we added in an @ManyToOne relation. This is pretty straight forward. Users can have many resumes. Let's go back to our User and create a corresponding relationship.

...
  @OneToMany(mappedBy="user")
  private List<Resume> resumes;
...

This annotation shows that we have a One To Many relationship which is mapped by the corresponding entities 'user' variable.

This is all fine and dandy, and we will continue to build up our model, however we want to see some real feedback! Let's go ahead and create some simple repositories for these models.

Again, I like to organize my code in such a way that my models are separate from my repositories and controllers. I'm going to make another package called com.jimdhughes.raas.repositories and create my first repo for Users.

package com.jimdhughes.raas.repositories;

import org.springframework.data.repository.CrudRepository;

import com.jimdhughes.raas.models.User;

public interface UserRepository extends CrudRepository<User, Long> {

}

Spring does a LOT of autowiring here. By default, it creates endpoints for each repository to handle specific REST functions (GET, PATCH, PUT, POST, DELETE). If you look at the console, you will see a lot of statements that say something like:

Mapped "{[/{repository}/{id}],methods=[DELETE],produces=[application/hal+json || application/json]}" onto public org.springframework.http.ResponseEntity<?> org.springframework.data.rest.webmvc.RepositoryEntityController.deleteItemResource(org.springframework.data.rest.webmvc.RootResourceInformation,java.io.Serializable,org.springframework.data.rest.webmvc.support.ETag) throws org.springframework.data.rest.webmvc.ResourceNotFoundException,org.springframework.web.HttpRequestMethodNotSupportedException

This essentially is showing that spring is mapping its opinionated endpoints because we haven't told it not to. For this portion of the tutorial / discussion / walkthrough / whatever this is, I'm going to allow spring to just do its thing for now.

If you go ahead and check out localhost:8080 in your browser, you will see that the response has changed! We now have a /users/ endpoint mapped out in Springs HATEOAS.

Using either CURL or POSTMAN, let's add a user to our repo for fun. I'm going to use Postman because it's easier IMO. I'm going to send POST a resource to localhost:8080/users as follows:

{
  "email":"jim.d.hughes@gmail.com",
  "fullName":"James Hughes"
}

You will see a responsethat looks like so with a response status of 201 - created

{
    "email": "jhughes@jvdriver.com",
    "fullName": "James Hughes",
    "_links": {
        "self": {
            "href": "http://localhost:8080/users/1"
        },
        "user": {
            "href": "http://localhost:8080/users/1"
        }
    }
}

HATEOAS is nice because it provides links to related resources. It's sort of like next-level REST. I don't like it because it feels like I abandon developer control.

For instance:
HATEOAS requires that we perform 2 requests to separate endpoints to create a user and then to create a resume. We must then link those two resources together with a third request.

Check out this post for an example of what I’m talking about: http://www.baeldung.com/spring-data-rest-relationships

I don't really like that.
I like to build my REST applications in a logical format
My endpoint planning looks a little like this in my head:

  1. /api/v1/users
  • GET: Get a list of users
  • POST: Create a user
  1. /api/v1/users/{id}
  • GET: Get a user by id
  • PUT: Update a user
  • DELETE: Delete a user
  1. /api/v1/users/{id}/resumes
  • GET: Get a list of resumes for user {id}
  • POST: Create a new resume for user {id
  1. /api/v1/users/{id}/resumes/{rid}
  • GET: Get a resume for user {id} and resume {rid}
  • PUT: Update the resume {rid}
  • DELETE: Delete reusme {rid}

So if you're still on board with my thinking, then feel free to follow along. Else that other resource is pretty boss and you could extend it to mimic my project.

I'm going to create an application.properties file in my resources folder in my project. This will allow me to disable the automatic CrudRepository mappings to HTTP Endpoints.

spring.data.rest.detection-strategy=annotated

This will make it so that I have to specifically annotate endpoints that I want exposed. Which is none. So that's that.

Now, to make our controllers!
I organize a bit more by making another new package in my project called com.jimdhughes.raas.controllers
Here I'll create a new UserController.java class with the following contents:

@RestController
@RequestMapping(value="/users")
public class UserController  {
  @Autowired
  private UserRepository repository;
  
  @RequestMapping(value="", method = RequestMethod.GET)
  @ResponseStatus(HttpStatus.OK)
  @ResponseBody
  public Iterable<User> getUsers() {
    return repository.findAll();
  }
}

This is pretty verbose which is a pitfall to java, but it makes everything really clear. Most of this is self-explanatory in code and all it does is make an endpoint at /users that supports a GET verb which returns a list of all users.

Next I'll make a POST verb for the same endpoint to create a new user in the same UserController.java file.

...
    @RequestMapping(value="", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public User createUser(@RequestBody User user) {
        return this.repository.save(user);
    }
...

Now if you head back up to the Postman instructions and create a user, you will see that we can POST to localhost:8080/users and we will receive a Response Status of 201 and we will get back the User we just created! It doesn't have any of the fancy links that HATEOAS gave us, but it is functional and gives us steps for control that I so crave.

Once I implement the rest of my Users endpoint, I have a class that looks like so:

package com.jimdhughes.raas.controllers;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.jimdhughes.raas.models.User;
import com.jimdhughes.raas.repositories.UserRepository;

@RestController
@RequestMapping(value = "/users")
public class UserController {

	@Autowired
	private UserRepository repository;

	@RequestMapping(value = "", method = RequestMethod.GET)
	@ResponseStatus(HttpStatus.OK)
	@ResponseBody
	public Iterable<User> getUsers() {
		return this.repository.findAll();
	}

	@RequestMapping(value = "", method = RequestMethod.POST)
	@ResponseStatus(HttpStatus.CREATED)
	@ResponseBody
	public User createUser(@RequestBody User user) {
		return this.repository.save(user);
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	@ResponseStatus(HttpStatus.OK)
	@ResponseBody
	public Optional<User> getUserById(@PathVariable("id") Long id) {
		return this.repository.findById(id);
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
	@ResponseStatus(HttpStatus.OK)
	@ResponseBody
	public User updateUser(@PathVariable("id") Long id, @RequestBody User user) {
		if (this.repository.findById(id) != null) {
			throw new ResourceNotFoundException();
		} else {
			return this.repository.save(user);
		}
	}
	@RequestMapping(value="/{id}", method = RequestMethod.DELETE)
	@ResponseStatus(HttpStatus.ACCEPTED)
	@ResponseBody
	public Boolean deleteUser(@PathVariable("id") Long id) {
		if(this.repository.findById(id) != null) {
			throw new ResourceNotFoundException();
		} else {
			this.repository.deleteById(id);
			return true;
		}
	}
}

And there you have it. A perfectly working @RestController for all my User i/o functionality.

I'll go through creating the Resume portion of this, and then I'm going to stop since it's going to get REALLY repetitive if I go through the entire app. However here is why I like to go down this path!
Create a new java class in your controller package called ResumeController.java

@RestController
@RequestMapping(value="/users/{user_id}/resumes")
public class ResumeController {
    @Autowired
    private ResumeRepository repository;
    
    @RequestMapping(value="", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Iterable<Resume> getResumesForUserId(@PathVariable("user_id") Long userId) {
    return this.repository.findByUserId(userId);
}

WOAH!
findByUserId(Long) is not implemented! You're going to get this error, and that's okay.
We need to go to our ResumeRepository file and add it to the interface. This is some awesome Spring magic that makes getting these things up and running super easy.
In ResumeRepository.java ...

...
public interface ResumeRepository extends CrudRepository<Resume, Long> {
	public Iterable<Resume> findByUserId(Long userId);
}
...

Voila! We have functionality!

I'm going to just dump out the rest of the CRUD operations for the ResumeController below.

// ...
	@RequestMapping(value = "", method = RequestMethod.POST)
	@ResponseStatus(HttpStatus.CREATED)
	@ResponseBody
	public Resume createResumeForUser(@PathVariable("user_id") Long userId,
        @RequestBody Resume resume) {
		// Set the user for the resume
		resume.setUser(new User(userId));
		return repository.save(resume);
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	@ResponseStatus(HttpStatus.OK)
	@ResponseBody
	public Resume getResume(@PathVariable("user_id") Long userId,
            @PathVariable("id") Long id) {
		Optional<Resume> resume = repository.findById(id);
		if (!resume.isPresent() || resume.get().getUser().getId() != userId) {
			// If no resume is found or if there is a resume but it is not owned by the user
			throw new ResourceNotFoundException();
		} else {
			return resume.get();
		}
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
	@ResponseStatus(HttpStatus.OK)
	@ResponseBody
	public Resume updateResume(@PathVariable("user_id") Long userId,
            @PathVariable("id") Long id,
			@RequestBody Resume resume) {
		Optional<Resume> r = repository.findById(id);
		if (!r.isPresent() || r.get().getUser().getId() != userId) {
			throw new ResourceNotFoundException();
		} else {
			return this.repository.save(resume);
		}
	} 
	
	@RequestMapping(value="/{id}", method = RequestMethod.DELETE)
	@ResponseStatus(HttpStatus.ACCEPTED)
	@ResponseBody
	public Boolean deleteResume(@PathVariable("user_id") Long userId,
            @PathVariable("id") Long id) {
		Optional<Resume> r = repository.findById(id);
		if (!r.isPresent() || r.get().getUser().getId() != userId) {
			throw new ResourceNotFoundException();
		} else {
			this.repository.deleteById(id);
		}
		return true;
	}

Now if you repeat these steps for all the endpoints that you wish to serve, you can set up your own service pretty easily!

I'm not going to go through all of that. It's monotonous and trivial and I'm sure you've got this mostly figured out by now :)

Next, I'm going to add swagger and some security for my endpoints. This is coming out later than my previous 'project completion date' but I promise you - I've finished :). I'm just having troubles finding time to write.

Until the next installment!

Code away!