Lesson Wednesday

Alright, we left off our project with some functionality in place to CREATE and READ objects, and display them on the page. But a quick glance at our App.java shows us we are still missing core functionality: We don’t have any routes (or any backend logic) to UPDATE and DELETE. Let’s add that now.

Adding Backend Code to UPDATE

Alright, let’s begin with a test. We’ll want to make sure that we can update our blog posts correctly in case we need to change something. Jump to PostTest.java and add:

my-epicodus-blog/src/test/java/models/PostTest.java
...
@Test
public void updateChangesPostContent() throws Exception {
   Post post = setupNewPost();
   String formerContent = post.getContent();
   LocalDateTime formerDate = post.getCreatedAt();
   int formerId = post.getId();

   post.update("Android: Day 40");

   assertEquals(formerId, post.getId());
   assertEquals(formerDate, post.getCreatedAt());
   assertNotEquals(formerContent, post.getContent());
}
...

As we can see, we're being thorough and checking that our ID and creation date haven’t changed, but that our content has. We don't want other parts of our Post to change when we want to update the content.

my-epicodus-blog/src/main/java/models/Post.java
public class Post {

   private String content; //check i’m not set to final!
   private static ArrayList<Post> instances = new ArrayList<>();
   private boolean published;
   private LocalDateTime createdAt;
   private int id;

   public Post (String content){
       this.content = content;
       this.published = false;
       this.createdAt = LocalDateTime.now();
       instances.add(this);
       this.id = instances.size();
   }

   public String getContent() {
       return content;
   }

   public static ArrayList<Post> getAll(){
       return instances;
   }

   public static void clearAllPosts(){
       instances.clear();
   }

   public boolean getPublished(){
       return this.published;
   }

   public LocalDateTime getCreatedAt() {
       return createdAt;
   }

   public int getId() {
       return id;
   }

   public static Post findById(int id){
       return instances.get(id-1); //why minus 1? See if you can figure it out.
   }

   public void update(String content) {
       this.content = content;
   }

Run your test - it should now pass. Cool. Now let’s implement our frontend for this in our App.java and templates:

my-epicodus-blog/src/main/java/App.java
...
//get: show a form to update a post
get("/posts/:id/update", (req, res) -> {
   Map<String, Object> model = new HashMap<>();
   int idOfPostToEdit = Integer.parseInt(req.params("id"));
   Post editPost = Post.findById(idOfPostToEdit);
   model.put("editPost", editPost);
   return new ModelAndView(model, "newpost-form.hbs");
}, new HandlebarsTemplateEngine());
...

But wait. Why are we serving newpost-form.hbs here for this route when we're trying to edit a Post? Here’s why this is actually really cool. See if you can understand what we're changing in newpost-form.hbs to make it work for BOTH new Posts and editing existing Posts:

my-epicodus-blog/src/main/resources/templates/newpost-form.hbs
{{#partial "content"}}

   {{#if editPost}}
       <h1>Edit this Post!</h1>
       <form action="/posts/{{editPost.id}}/update" method="post">
           <label for="content">Edit this post's content</label>
           <input id="content" name="content" type="text" value="{{editPost.content}}">
      </form>
   {{else}}
   <h1>Add a new Post!</h1>

   <form action="/posts/new" method="post">
       <label for="content">Post Content</label>
       <input id="content" name="content" type="text">
   {{/if}}
       <button type="submit" class="btn btn-default">Go!</button>
   </form>
{{/partial}}

{{> layout.hbs}}

Did you figure it out? Basically, if editPost exists in the model, we render a certain part of the template, and preload the input field with information from the object. If it doesn’t, we show the other part of the form. Neat!

Let’s rename our newpost-form.hbs to post-form.hbs to make it less confusing. To do this, right click on the filename at the top of the editor window, and choose “Rename file”. Click “Refactor”. Then, important, click “Do Refactor” at the bottom of your window, and the name will be changed in all files. Awesome.

Let’s add a link to to post-detail.hbs so we can reach our multipurpose form, otherwise how are we going to access it?

my-epicodus-blog/src/main/resources/templates/post-detail.hbs
{{#partial "content"}}
   <p>Content:</p>
   <p>{{post.content}}</p>
   <h5>Created At:</h5>
   <p> {{post.createdAt}}</p>
   <h5>Post status:</h5>
   <p>
       {{#if post.published}}
           <span class="glyphicon glyphicon-star" aria-hidden="true"></span>  Published
       {{else}}
           <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Draft
       {{/if}}

       <a href="/posts/{{post.id}}/update">Edit this post</a>
   </p>
{{/partial}}
{{> layout.hbs}}

As you can see, again, we're using information from our object to create the route that’ll be picked up by our route handler. Oh yes! And we also still need to write this route handler. It needs to handle something being fired at posts/{{editPost.id}}/update

Let’s add that to App.java, and then we can test it in a browser.

my-epicodus-blog/src/main/java/App.java
...
post("/posts/:id/update", (req, res) -> {
   Map<String, Object> model = new HashMap<>();
   String newContent = req.queryParams("content");
   int idOfPostToEdit = Integer.parseInt(req.params("id"));
   Post editPost = Post.findById(idOfPostToEdit);
   editPost.update(newContent); //don’t forget me
   return new ModelAndView(model, "success.hbs");
}, new HandlebarsTemplateEngine());
...

Boot it up, make a Post, go to its page and edit - you should see your changes persist. Woohoo!

Adding Backend Code to DELETE

Next is DELETE. We are nearly done with our frontend - one more piece of CRUD remains. We need to have a way to delete a post in case we post something we’re embarrassed by later.

Let’s begin - with a TEST!

my-epicodus-blog/src/test/java/models/PostTest.java
...
@Test
public void deleteDeletesASpecificPost() throws Exception {
   Post post = setupNewPost();
   Post otherPost = new Post("How to pair successfully");
   post.deletePost();
   assertEquals(1, Post.getAll().size()); //one is left
   assertEquals(Post.getAll().get(0).getId(), 2); //the one that was deleted has the id of 2. Why do we care?
}
...

Here comes the method:

my-epicodus-blog/src/main/java/Post.java
...
public void deletePost(){
   instances.remove(id-1); //same reason
}
...

And the route:

my-epicodus-blog/src/main/java/App.java
...
get("/posts/:id/delete", (req, res) -> {
   Map<String, Object> model = new HashMap<>();
   int idOfPostToDelete = Integer.parseInt(req.params("id")); //pull id - must match route segment
   Post deletePost = Post.findById(idOfPostToDelete); //use it to find post
   deletePost.deletePost();
   return new ModelAndView(model, "success.hbs");
}, new HandlebarsTemplateEngine());
...

Now we need to add a link to execute this command in our post-detail.hbs template:

my-epicodus-blog/src/main/resources/templates/post-detail.hbs
{{#partial "content"}}

   <p>Content:</p>

   <p>{{ post.content }}</p>
   <h5>Created At:</h5>
   <p> {{ post.createdAt }}</p>
   <h5>Post status:</h5>
   <p>
       {{#if post.published }}
           <span class="glyphicon glyphicon-star" aria-hidden="true"></span>  Published
       {{else}}
           <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Draft
       {{/if}}

       <a href="/posts/{{post.id}}/update">Edit this post</a><br>
       <a href="/posts/{{post.id}}/delete">DELETE this post (no undo)</a>
   </p>

{{/partial}}

{{> layout.hbs}}

Let’s run it and try! You should be able to delete a post successfully from the details page.

One more chunk and we are DONE! It’s even shorter than any other piece we’ve had to code, as we actually already wrote a static method that deletes all Posts when we began testing! Sweet.

Let’s open layout.hbs and add a link to our footer to delete all Posts.

my-epicodus-blog/src/main/resources/templates/layout.hbs
...
<!--end main template-->
{{#block "footer"}}
   <footer class="footer">
       <div class="container">
           <p class="text-muted"><a href="/posts/delete">DELETE ALL POSTS</a></p>
       </div>
   </footer>
   </body>
   </html>
{{/block}}

Now let’s add a test:

my-epicodus-blog/src/test/java/models/PostTest.java
...
@Test
public void deleteAllPostsDeletesAllPosts() throws Exception {
   Post post = setupNewPost();
   Post otherPost = setupNewPost();

   Post.clearAllPosts();
   assertEquals(0, Post.getAll().size());
}
...

Run your tests and make sure they pass.

Then, let’s add a route handler for this:

my-epicodus-blog/src/main/java/App.java
...
get("/posts/delete", (req, res) -> {
   Map<String, Object> model = new HashMap<>();
   Post.clearAllPosts();
   return new ModelAndView(model, "success.hbs");
}, new HandlebarsTemplateEngine());
...

Please read on before you try this in the browser, as you may receive an error.Our App.java in its entirety should now look like this:

my-epicodus-blog/src/main/java/App.java
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import models.Post;
import spark.ModelAndView;
import spark.template.handlebars.HandlebarsTemplateEngine;
import static spark.Spark.*;

public class App {
   public static void main(String[] args) { //type “psvm + tab” to autocreate this
       staticFileLocation("/public");

       //get: show new post form
       get("/posts/new", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           return new ModelAndView(model, "post-form.hbs");
       }, new HandlebarsTemplateEngine());

       //post: process new post form
       post("/posts/new", (req, res) -> { //URL to make new post on POST route
           Map<String, Object> model = new HashMap<>();

           String content = request.queryParams("content");
           Post newPost = new Post(content);
           model.put("post", newPost);
           return new ModelAndView(model, "success.hbs");
       }, new HandlebarsTemplateEngine());

       //get: show all posts
       get("/", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           ArrayList<Post> posts = Post.getAll();
           model.put("posts", posts);

           return new ModelAndView(model, "index.hbs");
       }, new HandlebarsTemplateEngine());

       //get: show an individual post
       get("/posts/:id", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           int idOfPostToFind = Integer.parseInt(req.params("id")); //pull id - must match route segment
           Post foundPost = Post.findById(idOfPostToFind); //use it to find post
           model.put("post", foundPost); //add it to model for template to display
           return new ModelAndView(model, "post-detail.hbs"); //individual post page.
       }, new HandlebarsTemplateEngine());

       //get: show a form to update a post
       get("/posts/:id/update", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           int idOfPostToEdit = Integer.parseInt(req.params("id"));
           Post editPost = Post.findById(idOfPostToEdit);
           model.put("editPost", editPost);
           return new ModelAndView(model, "post-form.hbs");
       }, new HandlebarsTemplateEngine());

       //post: process a form to update a post
       post("/posts/:id/update", (req, res) -> { //URL to make new post on POST route
           Map<String, Object> model = new HashMap<>();
           String newContent = req.queryParams("content");
           int idOfPostToEdit = Integer.parseInt(req.params("id"));
           Post editPost = Post.findById(idOfPostToEdit);
           editPost.update(newContent);
           return new ModelAndView(model, "success.hbs");
       }, new HandlebarsTemplateEngine());

       //get: delete an individual post
       get("/posts/:id/delete", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           int idOfPostToDelete = Integer.parseInt(req.params("id")); //pull id - must match route segment
           Post deletePost = Post.findById(idOfPostToDelete); //use it to find post
           deletePost.deletePost();
           return new ModelAndView(model, "success.hbs");
       }, new HandlebarsTemplateEngine());

       //get: delete all posts
       get("/posts/delete", (req, res) -> {
           Map<String, Object> model = new HashMap<>();
           Post.clearAllPosts();
           return new ModelAndView(model, "success.hbs");
       }, new HandlebarsTemplateEngine());
   }
}

This code will compile perfectly - but if we run it in the browser and then try and delete all of our Posts, we’ll get a nasty

500 server error

and a crash. WHY??

Let’s check our output:

[qtp1861268528-18] ERROR spark.http.matching.GeneralError -
java.lang.NumberFormatException: For input string: "delete"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:580)
    at java.lang.Integer.parseInt(Integer.java:615)
    at App.lambda$main$3(App.java:43)

What the blazes? We’re not even trying to execute the code on line 43:

...
get("/posts/:id", (req, res) -> {
   Map<String, Object> model = new HashMap<>();
   int idOfPostToFind = Integer.parseInt(req.params("id")); // I am line 43 and I am innocent
...

Here’s why this happens:

  • Our App.java file gets read from top to bottom. Spark reads through it, and checks each route handler against a request that it just received.
  • When it finds a match, it executes the code that belongs to that route. If it doesn’t find a match, it keeps going until it does, kind of like branching that we are already familiar with.

Because our route: get("/posts/:id", (req, res) is ABOVE the route get("/posts/delete", (req, res) in our App.java file, and Spark senses a match, it tries to parse the url segment “delete” into an id property of an object! That fails for obvious reasons, and the app crashes.

We can implement some logic to handle this, but we can also just move static routes ABOVE any dynamic routes that are shaped similarly, and then this error won’t occur. Try it, and you’ll see that deleting works perfectly.

Well done. You now have a blogging app with basic CRUD functionality you can build out to your heart’s content. You could change the styling, extend your model, and flesh out the navigation too. And, next week, we’ll learn how to add database storage to our apps, so we'll be able to persist Blog posts past a server reboot. For now, congratulations on what you have made and learned so far!

Note: If you are thinking that some of the methods we wrote here (such as findById() and deletePost()) can cause problems if they are not called correctly, (for example if they were run on non-existing Posts or incorrect id’s) you’re correct. Good thinking! Next week, we will address handling issues such as these with try/catch blocks and Exceptions that make our apps more stable, and allow them to fail gracefully when errors do occur.