Skip to content

nikiforov-alexander/pt12-recipe-site

Repository files navigation

Techdegree project 10 : Recipes

1. Eclipse Installation Instructions

Program can be run with Eclipse very easily, because I’ve added apply plugin: 'eclipse' in build.gradle file.

2. Tasks

2.1. Task 1 - Overview: Model/Dao/Service, Unit Tests

Create model classes, DAO interfaces, services, and add unit tests to components. Determine test coverage using a code coverage tool and ensure your tests cover of at least 60% of your code.


2.1.1. Model

Model classes created are:

Some of them inherit BaseEntity, which is the @MappedSuperClass that has @Id id and @Version version fields, that are repeated in child classes.

RecipeCategory enum is used in Recipe class as @Enumerated field.

2.1.2. DAO

DAO interfaces are created as Spring-Data CrudRepository-ies:

User related DAOs:
Other DAOs

All User related DAOs are not exposed to REST API.

FavoriteRecipesDao is interface that allows us to add custom methods to Spring-Data DAOs. And RecipeDaoImpl is the class that implements FavoriteRecipesDao and defines manually defined methods, all of them are relevant to deal with @ManyToMany relationship between User.favoriteRecipes, and User.favoriteUsers

2.1.3. Service

Service layer implementations do not have anything in particular, except repeating commands at DAO layer:

Most of them repeat commands at DAO layer with exceptions, that are relevant for RecipeServiceImpl, because we actually can access Ingredient, Item only through changing recipe.

There are many methods that were introduced to facilitate request processing in RecipeController.

CustomUserDetailsService implements UserDetailsService and is very common service that has loadUserByUsername method. It is used through the UserService interface.

2.1.4. Coverage

Coverage report is in gzipped file called coverage-report.tar.gz

It was generated after I created configuration in Intellijidea with all tests and after running it I saved coverage and gzipped it, because it has too many files. Later I will try to create task in Gradle, but for now that is best I can do.

2.2. Task 2 - Recipes Index Page

Using the supplied files, create the template for the recipe list page. Use the following requirements list to ensure all functionality is included in the recipe list page.

Recipes:

  1. Favorites

    Displays a list of recipes by name and indicates with a heart icon whether a user has favorited the recipe

  2. Filter By Category

    Allows the user to filter the list by the selected category

  3. Add New Recipe

    Allows the user to add a new recipe

  4. User Account Page

    A user must have an account

  5. Edit and Delete Recipe

    Allows the user to edit or delete a recipe

  6. Recipe Owner

    A user must own the recipe


2.2.1. Favorites

Displays a list of recipes by name and indicates with a heart icon whether a user has favorited the recipe


Recipes are displayed at the main page with the address

http://localhost:8080/
http://localhost:8080/recipes
http://localhost:8080/recipes/

IndexRedirectController takes care of redirecting from both "/" and "/recipes" pages to "/recipes/" that is mapped in RecipeController. There is probably a better way. But I’ll leave it for now as is. The functionality is tested in IndexRedirectControllerTest

In order to show whether recipe is favorite for user we generate List<Recipe> favoriteRecipesWithNullsForNonFavorites that has exactly the same size as all recipes printed, but on the places where recipe is favorite for user, it contains recipe, and elsewhere is null. This list is passed along with all recipes to Model.

The list to be passed is generated in a RecipeController.generateFavoritesWithNullsForNonFavoritesList method. And because of that is tested separately in @Test favoritesWithNonNullsListIsGeneratedCorrectly in RecipeControllerTest

Simple list with favorite recipes for users is generated at DAO level at customized RecipeDaoImpl using simple SQL query. It was rather problematic for me to write Spring Query in annotation to some method in RecipeDao, that is why I decided to make custom implementation for now.

The function returning favorite recipes at DAO level is tested in RecipeDaoTest in findAllFavoritesReturnsOneFavoriteRecipeWithDataLoader test.

In the REST API for now it is impossible to get favorite recipes

This can be done in many different ways, but I decided to leave it so.

2.2.2. Filter By Category

Allows the user to filter the list by the selected category


Usage

In the DataLoader for now 5 recipes are added, one for each category. They are all have "tags" that can be clicked and user will be redirected to the index page sorted by selected category tag:

/recipes/?category=name

There is also JavaScript function in app.js file that is executed when <select> element is changed. It redirects to index page filtered by category.

Implementation Details

Implementation starts on model layer in RecipeCategory enum. I introduced new methods getRecipeCategoryWithHtmlName that returns RecipeCategory.NONE or RecipeCategory found by member htmlName.

This method is tested in RecipeCategoryTest allRecipeCategoriesCanBeFoundByName.

At DAO level I introduced Spring Query method called findByRecipeCategory that comes from Spring and returns List<Recipe> with RecipeCategory passed in argument.

This is tested in RecipeDaoTest listOfRecipesReturnedWhenFindByRecipeCategoryIsCalled.

At Service layer I introduced in RecipeService and implemented in RecipeServiceImpl findByRecipeCategoryName

I didn’t test that because it directly returns result of RecipeDao.findByRecipeCategory with argument as a result of RecipeCategory.getRecipeCategoryWithHtmlName. It may be not a good idea to put this functionality on Service layer, but I decided to leave it here.

At Controller layer in RecipeController filterByCategory method was introduced, that is passing to Model.attribute "recipes" not all recipes, but the ones recipeService.findByCategoryName returns. Argument is query parameter. It comes from JavaScript or from tag anchor, see Usage.

Also here is "selectedCategory" attribute is added to Model because we want to display selected option on the redirected page.

This functionality is tested in RecipeControllerTest recipesCanBeListedByCategoryOnIndexPage test.

2.2.3. Add New Recipe

Allows the user to add a new recipe


New recipe can be added from the home page by pressing "Add Recipe" button.

After pressing that button page /recipes/add-new is generated. Template edit.html is used for both /recipes/add-new and /recipes/edit/id, with the difference that new Recipe object is passed to Model when /recipes/add-new is generated, and Recipe from database is passed to Model when /recipes/edit/id page is generated.

I made a try to re-use some code by creating addAttributesToModelForBothEditAndAddNewPages. Don’t know whether that was a good idea or not, but I tried.

I also tried to use the same method saveRecipe for both adding new recipe POST request and updating already existing recipe.

2.2.4. User Account Page

A user must have an account


User has an account page at the address

/profile

In order to get to this page, being logged on, user has to click on his name in the top left corner on <nav> element

This is implemented in UserController class, and is tested in UserControllerTest class.

The template rendering this page is called profile.html.

On this page all user’s favorite recipes are displayed. May be later I will include also owned recipes. For now I’ll leave that as TODO.

2.2.5. Edit and Delete Recipe

Allows the user to edit or delete a recipe


User can delete Recipe only if he is owner, or admin.

This is enforced by @PreAuthorize in RecipeDao and works both for REST API part and non-REST part.

Unfortunately in order to enforce security upon editing recipe, I had to use RecipeEventHandler for REST API and checkIfUserCanEditRecipe method in RecipeServiceImpl

2.2.6. Recipe Owner

A user must own the recipe


When we save recipe we set recipe’s owner. The responsible field for this relationship is @ManyToOne Recipe.owner on the Recipe side, and @OneToMany User.ownedRecipes on the User side. The relationship is determined by foreign_key owner_id in the recipes table.

When Recipe is updated, we get the owner from database.

All functionality was moved by me to service layer in RecipeService.save method, that takes care not only about the owner of recipe, but also sets favorite recipes from database, and set Recipe.ingredients[i].items because with the POST request we set only item id-s.

Related tests can be found in Mock-test in RecipeServiceTest class:

  • savingNewRecipeSetsOwner

  • updatingRecipeDoesNotChangeOwner

Integration tests in RecipeControllerItTest are checking recipe owner consistency as well. See tests:

  • updatingRecipeWithAllValidFieldsWorks

  • savingNewRecipeWithAllValidFieldsWorks

  • deletingRecipeShouldBePossible

2.3. Task 3 : Recipe Details Page

Using the supplied files, create the template for the recipe detail page. Use the following requirements list to ensure all functionality is included in the recipe detail page.

Recipe Detail
  1. Only Owner And Admin Can Edit And/Or Update Recipe

    Allows a user to add a recipe, or edit the recipe if they are the owner

  2. User Can Add Recipe details

    Allows a user to provide a recipe name, description, category (from a list of values), prep time, and cook time

  3. User Can Add Ingredients

    Allows a user to provide a list of ingredients. Each ingredient includes an item, condition, and quantity

  4. User Can Add Steps

    Allows a user to provide a list of steps. Each step includes a description

  5. User Can Add/Remove Recipe to/from Favorites

    Any user can add the recipe to their favorites


2.3.1. Only Owner And Admin Can Edit And/Or Update recipe

Allows a user to add a recipe, or edit the recipe if they are the owner


In order to introduce security here, additional method that throws AccessDeniedException was introduced in Service layer, in RecipeServiceImpl:

checkIfUserCanEditRecipe

This method is used in saveRecipe processing POST request to add/update Recipe and in editRecipePage that is responsible to GET request that is sent when user wants to see "edit" recipe page.

The sole purpose of this method as it follows from name is to check is user is admin or owner. If he is not, than exception is thrown.

The functionality tested on all levels:

In RecipeControllerItTest only positive test is done, i.e. for now all the updates are made with logged owner user.

2.3.2. User Can Add Recipe Details

Allows a user to provide a recipe name, description, category (from a list of values), prep time, and cook time Under construction.


All fields that were provided in template files can be filled by user. I used @NotNull and @NotEmpty annotation for all of them for simplicity.

When one will press 'Add Recipe' button at Home Page and will be redirect to page with adding new recipe.

In order to see that fields are all required, one can press submit button, and see how all of them turn red.

I tested the validation errors only in "Integration" RecipeControllerItTest. The problem emerged, because Thymeleaf did not set Recipe for each recipe.ingredients and recipe.steps. That is why we had to remove @Valid as annotation from arguments of saveRecipe controller method, and before validation, set missing relationships (see RecipeController saveRecipe for more).

It is probably possible to test the functionality in "mock" RecipeControllerTest, but I need to somehow to @Mock Validator manually, and I yet don’t know how to do it.

The Validation errors are tested in RecipeControllerItTest in following test:

  • updatingRecipeWithAllNullInvalidFieldsShouldGiveThatNumberOfErrors

  • addingRecipeWithAllEmptyInvalidFieldsShouldGiveThatNumberOfErrors

2.3.3. User Can Add Ingredients

Allows a user to provide a list of ingredients. Each ingredient includes an item, condition, and quantity


In order to add Ingredients I used JavaScript. All functionality that is bound to 'Add Ingredient' button can be found in app.js file, in function that is bound to #add-another-ingredient-button and executed upon click.

Simple Jquery is used all over the place and in sudo code can be described as following

  • get last <div class="ingredient-row">

  • get id from that divWithLastIngredient

  • increment id

  • clone divWithLastIngredient

  • add clone after divWithLastIngredient

  • change id and name attributes for <select> element that selects ingredient.item.id

  • change id and name attributes for <input> elements for ingredient.condition and ingredient.quantity

In the end the purpose of JavaScript is from this <div>:

<div class="ingredient-row">
    <input hidden=""
        type="text"
        id="ingredients0.id"
        name="ingredients[0].id"
        value="">
    <input
        hidden=""
        type="text"
        id="ingredients0.version"
        name="ingredients[0].version"
        value="">
    <div class="prefix-20 grid-30">
        <p>
            <select id="ingredients0.item.id" name="ingredients[0].item.id">
                <option value="0">Select Item</option>
                <option value="1">item 1</option>
                <option value="2">item 2</option>
            </select>
        </p>
    </div>
    <div class="grid-30">
        <p>
            <input
                type="text"
                id="ingredients0.condition"
                name="ingredients[0].condition"
                value="">
        </p>
    </div>
    <div class="grid-10 suffix-10">
        <p>
            <input
                type="text"
                id="ingredients0.quantity"
                name="ingredients[0].quantity"
                value="">
        </p>
    </div>
    <div class="clear"></div>
</div>

Create new <div> where:

ingredient.quantity
  • id="ingredients0.quantity"

  • name="ingredients[0].quantity"

ingredient.condition
  • id="ingredients0.condition"

  • name="ingredients[0].condition"

ingredient.item.id
  • id="ingredients0.item.id"

  • name="ingredients[0].item.id"

Will be changed respectively to:

ingredient.quantity
  • id="ingredients1.quantity"

  • name="ingredients[1].quantity"

ingredient.condition
  • id="ingredients1.condition"

  • name="ingredients[1].condition"

ingredient.item.id
  • id="ingredients1.item.id"

  • name="ingredients[1].item.id"

Of course id of last ingredient is taken from divWithLastIngredient. When we add new Recipe, we generated "add new" page with one Ingredient and one Step, so that cloning works.

The new div will be without hidden ingredient.version and ingredient.id, because this fields should be left null when we add new Ingredient.

The resulting <div> with new Ingredient made from example above should look like this:

<div class="ingredient-row">
    <div class="prefix-20 grid-30">
        <p>
            <select id="ingredients1.item.id" name="ingredients[1].item.id">
                <option value="0">Select Item</option>
                <option value="1">item 1</option>
                <option value="2">item 2</option>
            </select>
        </p>
    </div>
    <div class="grid-30">
        <p>
            <input
                type="text"
                id="ingredients1.condition"
                name="ingredients[1].condition"
                value="">
        </p>
    </div>
    <div class="grid-10 suffix-10">
        <p>
            <input
                type="text"
                id="ingredients1.quantity"
                name="ingredients[1].quantity"
                value="">
        </p>
    </div>
    <div class="clear"></div>
</div>

One should definitely test this somehow, hopefully I’ll get to JavaScript Unit Testing someday.

2.3.4. User Can Add Steps

Allows a user to provide a list of steps. Each step includes a description


The way "Add New Step" button works on "edit" recipe page, is the same as ingredient. It is also defined in app.js file using Jquery that is tracking the click on button with id="add-another-step-button".

Upon click in the same manner new <div> with new Step is added in a bit different and easier manner, because Step has only one field: itself description:

  • get last <div class="step-row">

  • get id or index of last step at page

  • increment id

  • create new div with new id and name attributes

  • insert new div after divWithLastStep

So if initial div in blank "add new" recipe page looks like

<div class="step-row">
    <div class="prefix-20 grid-80">
        <p>
            <input
                id="steps0"
                name="steps[0]"
                value="">
        </p>
    </div>
</div>

Then according new div with new Step should look like:

<div class="step-row">
    <div class="prefix-20 grid-80">
        <p>
            <input
                id="steps1"
                name="steps[1]"
                value="">
        </p>
    </div>
</div>

2.3.5. User Can Add/Remove Recipe to/from Favorites

DAO level

At DAO level in RecipeDaoImpl class following to methods were introduced:

  • addFavoriteRecipeForUser

  • removeFavoriteRecipeForUser

Because relationship between User.favoriteRecipes and Recipe.favoriteUsers is of type @ManyToMany, all we need to do is to remove respective links in @JoinTable with name 'users_favorite_recipes'

That is exactly what is done in both methods with following simple SQL queries.

-- add to favorites query
INSERT INTO users_favorite_recipes
 (recipe_id, user_id)
 VALUES (?, ?)

-- remove from favorites query
DELETE FROM users_favorite_recipes
 WHERE recipe_id = ? AND user_id = ?

It could’ve been done with easier @NativeQuery, but well, I found that pretty late, so this later can be refactored.

The functionality tested at DAO level with the following tests in RecipeDaoTest:

  • recipeCanBeAddedToFavorites

  • recipeCanBeRemovedFromFavorites

Service Layer

At Service Layer we decided to take into account the check if recipe is favorite for user or not, and based on that update favorite status of recipe.

That is why in RecipeServiceImpl two new methods were introduced:

  • updateFavoriteRecipesForUser

  • checkIfRecipeIsFavoriteForUser

The last one simply checks and returns true if recipe is favorite and false otherwise.

The first one uses the last one, and depending on that calls respective method in DAO layer, that adds or removes recipe from favorites

The functionality is tested in RecipeServiceTest:

  • updatingRecipeWhenRecipeIsAlreadyFavoriteShouldRemoveRecipeFromFavorites

  • updatingRecipeWhenRecipeIsNotFavoriteShouldAddRecipeToFavorites

  • trueIsReturnedWhenRecipeIsFavoriteForCheckIfFavoriteMethod

  • falseIsReturnedWhenRecipeIsNotFavoriteForCheckIfFavoriteMethod

Controller

Finally in RecipeController we introduced updateFavoriteStatusOfRecipe method that is responsible for inline form POST request, that updates favorite recipe for user, using method introduced in RecipeServiceImpl and sets flash message depending whether the recipe was removed or added from favorites.

In the 'detail' recipe page, detailRecipePage method, we use introduced in Service Layer method checkIfRecipeIsFavoriteForUser to pass proper heart icon, filled or empty and proper button name changing favorite status of recipe.

The following tests were added in "mock" RecipeControllerTest:

  • detailRecipePageWithFavoriteRecipeShouldRenderSuccessfully

  • detailRecipePageWithNonFavoriteRecipeShouldRenderSuccessfully

  • userCanAddRecipeToFavoritesFromDetailPage

  • userCanRemoveRecipeFromFavoritesFromDetailPage

2.4. Task 4 : Search Features

The recipe list page should have a search feature. A user can enter a search term and the recipe list will display results that have the search phrase in the description.


At RecipeDao interface Spring "query" method findByDescriptionContaining was added, that does all the hard work for us.

The functionality is tested just in case in RecipeDaoTest

  • recipeCanBeSearchedByDescription

At RecipeServiceImpl class defined DAO method without no changes is used. And I decided not to test this.

At RecipeController class filterByDescription method is introduced, with one @RequestParam "description". It looks very similar to filterByCategory method. I’m glad I’ve decided to re-use that "circular" fillModelWithRecipesFavoritesAndCategories method.

The functionality is tested in RecipeControllerTest:

  • recipesCanBeSearchedByDescriptionOnIndexPage

In actual index.html template inline <form> was introduced with GET method and simple one parameter "description". So that when user presses button "Search" recipes are searched by "description" provided.

As a future TODO we definitely will be doing combined search, but only after submission of the project. It’s been two months now. And it is time to finish this.

2.5. Task 5 : Security

Enable user authentication with Spring Security. Use the supplied files to create templates for login page, registration page, and profile page. You must build the registration component, as it does not come with Spring Security. Create necessary controllers, services, and DAO to add a new user. Make sure to include validation so that a user may not use a username that already exists. Check out links in external resources if you get stuck.


2.5.1. Security Configuration class

User authentication is enabled in SecurityConfig class.

It enables @PreAuthorize and other annotations, as well as password encoder.

In configure(WebSecurity web) method we setup ignoring everything that is in src/main/resources/static/assets, i.e. all images, CSS and JavaScript.

In configure(HttpSecurity http) we permit user before logging in, access only SIGN_UP_PAGE and LOGIN_PAGE.

loginSuccessHandler redirects user to RECIPES_HOME_PAGE, whereas loginFailureHandler creates flash message that attaches FlashMessage with FAILURE status.

2.5.2. LoginController

LoginController class is created in order to set up

  • LOGIN_PAGE in loginForm method

  • SIGN_UP_PAGE in signUpPage method

  • SIGN_UP_PAGE POST request in registerNewUser method.

I don’t exactly know how loginForm works. But I know that it sets "flash" from session if user types wrong password. If however, user register valid user on SIGN_UP_PAGE, then he’ll be redirected to this page with successful flash to log in.

SIGN_UP_PAGE simply loads new UserDto object into Model, and if user made mistakes when creating new user, then he is redirected back with all user parameters except password and matchingPassword

registerNewUser processes POST request of creating new user.

2.5.3. UserDto And Password

Idea of UserDto came from here. It is first of all needed because we set password indirectly and cannot use validation annotation on User.password field directly. Also nice thing about this approach that we created our own PasswordMatches annotation that uses PasswordMatchesValidator and checks if passwords match. Whereas we cannot simply introduce new matchingPassword field in User class.

One has to note here on userDto.password field regex matcher is used taken from here.

The regex is tested separately in the UserDtoTest class.

2.5.4. JavaScript In Registration

One has also note here, that in order to ensure more security and not sending password back and forth, i.e. to increase user experience I’ve added in app.js file JavaScript Jquery function checking whether passwords in SIGN_UP_PAGE match, and whether they are strong or not. The most of the code was taken from here.

The button "sign-up" sending POST request will appear only if both password match and are "strong".

So we won’t see backend check unless we actually disable JavaScript. But I decided to leave them both hanging.

2.5.5. User Registration On Service Layer

"Check whether the user exists with such username" is done on service layer in userService.registerNewUser method.

It throws UserAlreadyExistsException that unfortunately does not work properly with @ExceptionHandler that is why it is explicitly caught in LoginController registerNewUser method

2.5.6. Tests

The tests checking most of the user registration can be found in following classes:

2.6. Task 6 : REST API

Create REST endpoints for CRUD operations.


2.6.1. Configuration

REST API is build with HATEOAS and HAL browser, from the following dependencies in build.gradle:

    compile 'org.springframework.boot:spring-boot-starter-data-rest'
    compile 'org.springframework.data:spring-data-rest-hal-browser'

In application.properties file spring.data.rest.base-path is defined, so that we know upon which address to find out REST API.

In the @Configuration RestConfig class we add @Validator for all entities as ValidatingRepositoryEventListener. Apparently that is because REST is built from DAO, and without having Service layer helping out with the stuff, we are using Events and Listeners.

Validator itself is defined from LocalValidatorFactoryBean in AppConfig class.

One more important class that interacts with REST is RecipeEventHandler. For now @HandleBeforeCreate event is defined there, with which we set-up owner of the Recipe before saving, and @HandleBeforeSave as well, by checking that only owner of Recipe or admin can update Recipe.

2.6.2. End Points

Below is the list of links REST generates for us to use:

/api/v1/recipes : GET, PUT, POST, DELETE
/api/v1/ingredients : GET, PUT, POST, DELETE
/api/v1/steps : GET, PUT, POST, DELETE
/api/v1/items : GET, PUT, POST, DELETE

URI-s are defined automatically, but I’ve also specified them in WebConstants class, so that we can use them type-safe in tests.

In order to see specific object one has to add id at the end.

2.6.3. Life Cycle of REST requests

It is impossible to create Recipe with Ingredient in one request unfortunately.

However one can create recipe with recipe.steps, because it is @ElementCollection, which means List<String> that is controlled solely by Recipe without DAO.

In order to create new Recipe one has to do the following:

  • make POST request to create new Recipe without Ingredient:

    {
        "id" : null,
        "version" : null,
        "name" : "test name",
        "description" : "test description",
        "recipeCategory" : "BREAKFAST",
        "photoUrl" : "test photo url",
        "preparationTime" : "test prep time",
        "cookTime" : "test cook time",
        "ingredients" : [],
        "steps" : [
            "step 1",
            "step 2"
        ]
    }
  • make POST request to create new Ingredient with Recipe and Item link:

    {
       "id" : "null",
       "version" : "null",
       "item" : "http://localhost:8080/api/v1/items/1",
       "condition" : "condition",
       "quantity" : "quantity",
       "recipe" : "http://localhost:8080/api/v1/recipes/1"
    }
    Note
    Item can be created without links easily via POST with: { "name" : "item" }

PUT requests can be done in the same manner. So I omit this section description here

DELETE requests are done with empty body but with id of the entity to be deleted.

2.6.4. Security

Recipes

Recipe can be updated only by owner of Recipe or admin user. Whether user is admin or owner is checked in RecipeEventHandler class in checkIfOwnerOrAdminIsEditing method. Upon authentication error, CustomAccessDeniedException is thrown that is just like usual AccessDeniedException provided by Spring, but this custom can be tested. When we throw AccessDeniedException then in tests NestedServletException is thrown, that complicates test checks.

Recipe deletion is managed by @PreAuthorize Spring Security Expression, that is enabled in SecurityConfig class. The idea here is naturally same: only owner of Recipe or admin user can delete Recipe.

Ingredients

For both of these entities save and delete methods are introduced with the same Spring Security @PreAuthorize expression that permits actions only to ingredient.recipe.owner or admin.

2.6.5. Tests

Mostly REST API is tested with following integration tests:

JavaScript