Java Intro to Java Web Development with Spark Bells and Whistles Build Your Own Flash Messages

Jonny Patterson
Jonny Patterson
7,099 Points

Avoiding duplication using global 'before' filter

I'm interested to know how others avoided duplicating code for the flash messaging. I see there is another thread with some suggestions, but I'm not sure how closely they follow Craig's 'Teacher's Notes'. I thought I would share my interpretation.

In the global 'before' filter we create a model and add any flash messages to it (I'm also adding the username to the model - more on that later!):

before((req, res) -> {
            Map<String, String> model = new HashMap<>();
            if (req.cookie("username") != null) {
                model.put("username", req.cookie("username"));
            }
            model.put("flashMessage", captureFlashMessage(req));
            req.attribute("model", model);
});

We can now access this global model off 'req.attribute("model")' rather than declaring a new model in each controller method:

get("/", (req, res) -> {
            return new ModelAndView(req.attribute("model"), "index.hbs");
}, new HandlebarsTemplateEngine());

If we want to add something to the model before returning a template, for example adding a CourseIdea for the detail page, we can:

get("/ideas/:slug", (req, res) -> {
            Map<String, Object> model = req.attribute("model");
            model.put("idea", dao.findBySlug(req.params("slug")));
            return new ModelAndView(model, "idea.hbs");
}, new HandlebarsTemplateEngine());

You may notice I also added the username to the global model rather than setting it in each controller method. This means anywhere we use the username, for example setting the creator on a new CourseIdea, we need to get it from the model:

post("/ideas", (req, res) -> {
            String title = req.queryParams("title");
            Map<String, Object> model = req.attribute("model");
            CourseIdea courseIdea = new CourseIdea(title, (String) model.get("username"));
            dao.add(courseIdea);
            res.redirect("/ideas");
            return null;
});

I'm actually starting to wonder if there's a reason we wouldn't set the username in the session instead?

I'd love to hear your thoughts on this approach, or any better suggestions you might have. Thanks!

1 Answer

Boban Talevski
Boban Talevski
24,771 Points

I think your approach is more along the lines of what Craig had in mind in the teacher's notes, and it helped me with my solution, so thanks for that :). Thought I'd share my solution here as well.

The whole Main.java (without imports)

public class Main {
    private static final String FLASH_MESSAGE_KEY = "flash_message";
    private static final String MODEL_KEY = "model_key";

    public static void main(String[] args) {
        staticFileLocation("/public");
        CourseIdeaDAO dao = new SimpleCourseIdeaDAO();

        before((req, res) -> {
            Map<String, Object> model = new HashMap<>();
            if (req.cookie("username") != null) {
                model.put("username", req.cookie("username"));
            }
            model.put("flashMessage", captureFlashMessage(req));
            req.attribute(MODEL_KEY, model);
        });

        before("/ideas", (req, res) -> {
            if (!((Map) req.attribute(MODEL_KEY)).containsKey("username")) {
                setFlashMessage(req, "Whoops, please sign in first!");
                res.redirect("/");
                halt();
            }
        });

        get("/", (req, res) -> {
            return new ModelAndView(req.attribute(MODEL_KEY), "index.hbs");
        }, new HandlebarsTemplateEngine());

        post("/sign-in", (req, res) -> {
            String username = req.queryParams("username");
            res.cookie("username", username);
            res.redirect("/");
            return null;
        });

        get("/ideas", (req, res) -> {
            Map<String, Object> model = req.attribute(MODEL_KEY);
            model.put("ideas", dao.findAll());
            return new ModelAndView(model, "ideas.hbs");
        }, new HandlebarsTemplateEngine());

        post("/ideas", (req, res) -> {
            CourseIdea idea = new CourseIdea(
                    req.queryParams("title"),
                    req.attribute("username"));
            dao.add(idea);
            res.redirect("/ideas");
            return null;
        });

        get("/ideas/:slug", (req, res) -> {
            Map<String, Object> model = req.attribute(MODEL_KEY);
            model.put("idea", dao.findBySlug(req.params("slug")));
            return new ModelAndView(model, "idea-details.hbs");
        }, new HandlebarsTemplateEngine());

        post("/ideas/:slug/vote", (req, res) -> {
            CourseIdea idea = dao.findBySlug(req.params("slug"));
            boolean added = idea.addVoter(req.attribute("username"));
            if (added) {
                setFlashMessage(req, "Thanks for your vote!");
            } else {
                setFlashMessage(req, "You already voted!");
            }
            res.redirect("/ideas");
            return null;
        });

        exception(NotFoundExcepetion.class, (exc, req, res) -> {
            res.status(404);
            HandlebarsTemplateEngine engine = new HandlebarsTemplateEngine();
            String html = engine.render(
                    new ModelAndView(null, "not-found.hbs"));
            res.body(html);
        });
    }

    private static void setFlashMessage(Request req, String message) {
        req.session().attribute(FLASH_MESSAGE_KEY, message);
    }

    private static String getFlashMessage(Request req) {
        if (req.session(false) == null) {
            return null;
        }
        if (!req.session().attributes().contains(FLASH_MESSAGE_KEY)) {
            return null;
        }
        return (String) req.session().attribute(FLASH_MESSAGE_KEY);
    }

    private static String captureFlashMessage(Request req) {
        String message = getFlashMessage(req);
        if (message != null) {
            req.session().removeAttribute(FLASH_MESSAGE_KEY);
        }
        return message;
    }
}

I personally wasn't sure that we could just put a model object in the request attributes, but since Craig hinted at it, it looked possible. And I assume it all works because of the fact that most collections (HashMap included) implement the Serializable interface. And all that serializing isn't using some non HTTP allowed characters. I just initially thought it would break because of something :P.

Still, we couldn't create a HashMap model in the before filter and pass it along with objects which are not serializable, like CourseIdea, though we could just add to the class to implement the Serializable interface. But I guess we can't without it.

Now I'm not sure my implementation of this before filter is ok, and I'm curious what's your implementation

before("/ideas", (req, res) -> {
            if (!((Map) req.attribute(MODEL_KEY)).containsKey("username")) {
                setFlashMessage(req, "Whoops, please sign in first!");
                res.redirect("/");
                halt();
            }
        });

I am pulling the model from the request attributes, casting it as a Map and checking if it contains the key "username", because we would've put it already in the previous global before filter if it was present. Not sure if I should cast to a HashMap instead of Map, I mean, not sure if the model inside req.attributes keeps the info that it's a HashMap after being (de)serialized ? :) Looks like it does though, since it seems to work either way.