Spring In Action 02 - Developing web app

This chapter focues on Spring web framework without database.

2.1 Displaying infomation

2.1.1 Establishing the domain

package tacos; 
import lombok.Data;
import lombok.RequiredArgsConstructor; 
@Data
@RequiredArgsConstructor 
public class Ingredient {
    private final String id; 
    private final String name; 
    private final Type type;

    public static enum Type { WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE } 
}

2.1.2 Creating a controller class

Controllers are the major players in Spring’s MVC framework. Their primary job is to handle HTTP requests and either hand a request off to a view to render HTML (browser-displayed) or write data directly to the body of a response (RESTful). In this chapter, we’re focusing on the kinds of controllers that use views to produce content for web browsers. When we get to chapter 6, we’ll look at writing controllers that handle requests in a REST API.

  • Handle HTTP GET requests where the request path is /design
  • Build a list of ingredients
  • Hand the request and the ingredient data off to a view template to be rendered as HTML and sent to the requesting web browser

the @Data annotation at the class level is provided by Lombok and tells Lombok to generate all of those missing methods as well as a constructor that accepts all final properties as arguments. By using Lombok, you can keep the code for Ingredient slim and trim.

@GetMapping is just one member of a family of request-mapping annotations. Table 2.1 lists all of the request-mapping annotations available in Spring MVC.

	Type[] types = Ingredient.Type.values();
	for (Type type : types) {
	  model.addAttribute(type.toString().toLowerCase(),
	      filterByType(ingredients, type));
	}

@GetMapping
  public String showDesignForm(Model model) {
    model.addAttribute("design", new Taco());
    return "design";
  }

Data placed in model attributes is copied into the servlet response attributes, where view can find them.

2.1.3 Designing the view

Spring offers several great options for defining views, including JavaServer Pages (JSP), Thymeleaf, FreeMarker, Mustache, and Groovy-based templates. For now, we’ll use Thymeleaf.

<p th:text="${message}">placeholder message</p>

When the template is rendered into HTML, the body of the element will be replaced with the value of the servlet request attribute whose key is “message”. The th:text attribute is a Thymeleaf-namespaced attribute that performs the replacement. The ${} operator tells it to use the value of a request attribute (“message”, in this case).

Model has attribute “wrap” etc. already, we saved them before manually and here we show them. Then we save our checkbox result into “ingredients”, they are bound to ingredients properties of Taco class, so it can be created automatically later.

    <h3>Designate your wrap:</h3>
    <div th:each="ingredient : ${wrap}">
    <input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
    <span th:text="${ingredient.name}">INGREDIENT</span><br/>
    </div>

Here you use th:each attribute on the tag to repeat rendering of the once for each item in the collection found in the wrap request attribute. On each iteration, the ingredient item is bound to a Thymeleaf variable named ingredient. Inside , there’s a checkbox and a .

2.2 Processing form submission

Just like @GetMapping handles GET requests, you can use @PostMapping to handle POST requests. For handling taco design submissions, add the processDesign() method in the following listing to DesignTacoController.

@PostMapping
public String processDesign(Taco design) { // Save the taco design... 
// We'll do this in chapter 3 
    log.info("Processing design: " + design);
    return "redirect:/orders/current"; 
}

When the form is submitted, the fields in the form are bound to properties of a Taco object (name and ingredients) that’s passed as a parameter into processDesign(). From there, the processDesign() method can do whatever it wants with the Taco object.

   <input type="text" th:field="*{name}"/>

If you look back at the form in listing 2.3, you’ll see several checkbox elements, all with the name ingredients, and a text input element named name. Those fields in the form correspond directly to the ingredients and name properties of the Taco class. The Name field on the form only needs to capture a simple textual value. Thus the name property of Taco is of type String. The ingredients check boxes also have textual values, but because zero or many of them may be selected, the ingredients property that they’re bound to is a List that will capture each of the chosen ingredients.

2.3 Validating form input

2.3.1 Declaring validation rules

  • Declare validation rules on the class that is to be validated: specifically, the Taco class.
  • Specify that validation should be performed in the controller methods that require validation: specifically, the DesignTacoController’s processDesign() method and OrderController’s processOrder() method.
  • Modify the form views to display validation errors

2.3.2 Performing validation at form binding

The @Valid annotation tells Spring MVC to perform validation on the submitted Taco object after it’s bound to the submitted form data and before the processDesign() method is called.

2.3.3 Displaying validation errors

Thymeleaf offers convenient access to the Errors object via the fields property and with its th:errors attribute

2.4 Working with view controllers

  • They’re all annotated with @Controller to indicate that they’re controller classes that should be automatically discovered by Spring component scanning and instantiated as beans in the Spring application context.
  • All but HomeController are annotated with @RequestMapping at the class level to define a baseline request pattern that the controller will handle.
  • They all have one or more methods that are annotated with @GetMapping or @PostMapping to provide specifics on which methods should handle which kinds of requests.
@Configuration
public class WebConfig implements WebMvcConfigurer { 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) { 
        registry.addViewController("/").setViewName("home");
    } 
}

2.5 Choosing a view template library

2.5.1 Caching templates

For example, to disable Thymeleaf caching, add the following line in application.properties: spring.thymeleaf.cache=false

Summary

  • Spring offers a powerful web framework called Spring MVC that can be used to develop the web frontend for a Spring application.
  • Spring MVC is annotation-based, enabling the declaration of request-handling methods with annotations such as @RequestMapping, @GetMapping, and @PostMapping.
  • Most request-handling methods conclude by returning the logical name of a view, such as a Thymeleaf template, to which the request (along with any model data) is forwarded.
  • Spring MVC supports validation through the Java Bean Validation API and implementations of the Validation API such as Hibernate Validator.
  • View controllers can be used to handle HTTP GET requests for which no model data or processing is required.
  • In addition to Thymeleaf, Spring supports a variety of view options, including FreeMarker, Groovy Templates, and Mustache.

Error Correction

Listing 2.4, processDesign(Taco design), rather than (Design design).

  • DesignTacoController.java
// tag::head[]
package tacos.web;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;

@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {

//end::head[]

@ModelAttribute
public void addIngredientsToModel(Model model) {
	List<Ingredient> ingredients = Arrays.asList(
	  new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
	  new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
	  new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
	  new Ingredient("CARN", "Carnitas", Type.PROTEIN),
	  new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
	  new Ingredient("LETC", "Lettuce", Type.VEGGIES),
	  new Ingredient("CHED", "Cheddar", Type.CHEESE),
	  new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
	  new Ingredient("SLSA", "Salsa", Type.SAUCE),
	  new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
	);
	
	Type[] types = Ingredient.Type.values();
	for (Type type : types) {
	  model.addAttribute(type.toString().toLowerCase(),
	      filterByType(ingredients, type));
	}
}
	
//tag::showDesignForm[]
  @GetMapping
  public String showDesignForm(Model model) {
    model.addAttribute("design", new Taco());
    return "design";
  }

//end::showDesignForm[]

/*
//tag::processDesign[]
  @PostMapping
  public String processDesign(Design design) {
    // Save the taco design...
    // We'll do this in chapter 3
    log.info("Processing design: " + design);

    return "redirect:/orders/current";
  }

//end::processDesign[]
 */

//tag::processDesignValidated[]
  @PostMapping
  public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, Model model) {
    if (errors.hasErrors()) {
      return "design";
    }

    // Save the taco design...
    // We'll do this in chapter 3
    log.info("Processing design: " + design);

    return "redirect:/orders/current";
  }

//end::processDesignValidated[]

//tag::filterByType[]
  private List<Ingredient> filterByType(
      List<Ingredient> ingredients, Type type) {
    return ingredients
              .stream()
              .filter(x -> x.getType().equals(type))
              .collect(Collectors.toList());
  }

//end::filterByType[]
// tag::foot[]
}
// end::foot[]