diff --git a/README.md b/README.md index 05e5cc8..866aee7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # Spring Boot Web Application -This repository has the project files for a tutorial series on Spring Boot available from by website at [Spring Framework Guru](https://springframework.guru) \ No newline at end of file +##Part 4 +This repository has the project files for a tutorial series on Spring Boot available from by website at [Spring Framework Guru](https://springframework.guru/spring-boot-web-application-part-4-spring-mvc/) + +In this part of the tutorial series, I show how to setup a Spring MVC controller to suport CRUD operations, a Spring service facad over a Spring Data JPA repository, and Thymeleaf templates for the web application. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 575dff6..58d9bce 100644 --- a/pom.xml +++ b/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 1.2.4.RELEASE + 2.2.2.RELEASE UTF-8 - 1.8 + 11 @@ -40,11 +40,22 @@ org.springframework.boot spring-boot-starter-web - + + + + org.webjars + bootstrap + 3.3.4 + + + org.webjars + jquery + 2.1.4 + + com.h2database h2 - runtime org.springframework.boot diff --git a/src/main/java/guru/springframework/bootstrap/ProductLoader.java b/src/main/java/guru/springframework/bootstrap/ProductLoader.java new file mode 100644 index 0000000..5555705 --- /dev/null +++ b/src/main/java/guru/springframework/bootstrap/ProductLoader.java @@ -0,0 +1,47 @@ +package guru.springframework.bootstrap; + +import guru.springframework.domain.Product; +import guru.springframework.repositories.ProductRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +public class ProductLoader implements ApplicationListener { + + private ProductRepository productRepository; + + private Logger log = LoggerFactory.getLogger(ProductLoader.class); + + @Autowired + public void setProductRepository(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + + Product shirt = new Product(); + shirt.setDescription("Spring Framework Guru Shirt"); + shirt.setPrice(new BigDecimal("18.95")); + shirt.setImageUrl("https://springframework.guru/wp-content/uploads/2015/04/spring_framework_guru_shirt-rf412049699c14ba5b68bb1c09182bfa2_8nax2_512.jpg"); + shirt.setProductId("235268845711068308"); + productRepository.save(shirt); + + log.info("Saved Shirt - id: " + shirt.getId()); + + Product mug = new Product(); + mug.setDescription("Spring Framework Guru Mug"); + mug.setImageUrl("https://springframework.guru/wp-content/uploads/2015/04/spring_framework_guru_coffee_mug-r11e7694903c348e1a667dfd2f1474d95_x7j54_8byvr_512.jpg"); + mug.setProductId("168639393495335947"); + mug.setPrice(new BigDecimal("11.95")); + productRepository.save(mug); + + log.info("Saved Mug - id:" + mug.getId()); + } +} diff --git a/src/main/java/guru/springframework/configuration/SecurityConfiguration.java b/src/main/java/guru/springframework/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..87c00b7 --- /dev/null +++ b/src/main/java/guru/springframework/configuration/SecurityConfiguration.java @@ -0,0 +1,19 @@ +package guru.springframework.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity.authorizeRequests().antMatchers("/").permitAll().and() + .authorizeRequests().antMatchers("/console/**").permitAll(); + + httpSecurity.csrf().disable(); + httpSecurity.headers().frameOptions().disable(); + } + +} \ No newline at end of file diff --git a/src/main/java/guru/springframework/configuration/WebConfiguration.java b/src/main/java/guru/springframework/configuration/WebConfiguration.java new file mode 100644 index 0000000..1872322 --- /dev/null +++ b/src/main/java/guru/springframework/configuration/WebConfiguration.java @@ -0,0 +1,16 @@ +package guru.springframework.configuration; + +import org.h2.server.web.WebServlet; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class WebConfiguration { + @Bean + ServletRegistrationBean h2servletRegistration(){ + ServletRegistrationBean registrationBean = new ServletRegistrationBean( new WebServlet()); + registrationBean.addUrlMappings("/console/*"); + return registrationBean; + } +} diff --git a/src/main/java/guru/springframework/controllers/IndexController.java b/src/main/java/guru/springframework/controllers/IndexController.java new file mode 100644 index 0000000..95280b6 --- /dev/null +++ b/src/main/java/guru/springframework/controllers/IndexController.java @@ -0,0 +1,12 @@ +package guru.springframework.controllers; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class IndexController { + @RequestMapping("/") + String index(){ + return "index"; + } +} diff --git a/src/main/java/guru/springframework/controllers/ProductController.java b/src/main/java/guru/springframework/controllers/ProductController.java new file mode 100644 index 0000000..3e8f09b --- /dev/null +++ b/src/main/java/guru/springframework/controllers/ProductController.java @@ -0,0 +1,58 @@ +package guru.springframework.controllers; + +import guru.springframework.domain.Product; +import guru.springframework.services.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +public class ProductController { + + private ProductService productService; + + @Autowired + public void setProductService(ProductService productService) { + this.productService = productService; + } + + @RequestMapping(value = "/products", method = RequestMethod.GET) + public String list(Model model){ + model.addAttribute("products", productService.listAllProducts()); + return "products"; + } + + @RequestMapping("product/{id}") + public String showProduct(@PathVariable Integer id, Model model){ + model.addAttribute("product", productService.getProductById(id)); + return "productshow"; + } + + @RequestMapping("product/edit/{id}") + public String edit(@PathVariable Integer id, Model model){ + model.addAttribute("product", productService.getProductById(id)); + return "productform"; + } + + @RequestMapping("product/new") + public String newProduct(Model model){ + model.addAttribute("product", new Product()); + return "productform"; + } + + @RequestMapping(value = "product", method = RequestMethod.POST) + public String saveProduct(Product product){ + productService.saveProduct(product); + return "redirect:/product/" + product.getId(); + } + + @RequestMapping("product/delete/{id}") + public String delete(@PathVariable Integer id){ + productService.deleteProduct(id); + return "redirect:/products"; + } + +} diff --git a/src/main/java/guru/springframework/domain/Product.java b/src/main/java/guru/springframework/domain/Product.java new file mode 100644 index 0000000..a40c704 --- /dev/null +++ b/src/main/java/guru/springframework/domain/Product.java @@ -0,0 +1,67 @@ +package guru.springframework.domain; + +import javax.persistence.*; +import java.math.BigDecimal; + +@Entity +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + @Version + private Integer version; + + private String productId; + private String description; + private String imageUrl; + private BigDecimal price; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getProductId() { + return productId; + } + + public void setProductId(String productId) { + this.productId = productId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } +} diff --git a/src/main/java/guru/springframework/repositories/ProductRepository.java b/src/main/java/guru/springframework/repositories/ProductRepository.java new file mode 100644 index 0000000..820f086 --- /dev/null +++ b/src/main/java/guru/springframework/repositories/ProductRepository.java @@ -0,0 +1,7 @@ +package guru.springframework.repositories; + +import guru.springframework.domain.Product; +import org.springframework.data.repository.CrudRepository; + +public interface ProductRepository extends CrudRepository{ +} diff --git a/src/main/java/guru/springframework/services/ProductService.java b/src/main/java/guru/springframework/services/ProductService.java new file mode 100644 index 0000000..9c68033 --- /dev/null +++ b/src/main/java/guru/springframework/services/ProductService.java @@ -0,0 +1,14 @@ +package guru.springframework.services; + + +import guru.springframework.domain.Product; + +public interface ProductService { + Iterable listAllProducts(); + + Product getProductById(Integer id); + + Product saveProduct(Product product); + + void deleteProduct(Integer id); +} diff --git a/src/main/java/guru/springframework/services/ProductServiceImpl.java b/src/main/java/guru/springframework/services/ProductServiceImpl.java new file mode 100644 index 0000000..1aa2861 --- /dev/null +++ b/src/main/java/guru/springframework/services/ProductServiceImpl.java @@ -0,0 +1,36 @@ +package guru.springframework.services; + +import guru.springframework.domain.Product; +import guru.springframework.repositories.ProductRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ProductServiceImpl implements ProductService { + private ProductRepository productRepository; + + @Autowired + public void setProductRepository(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + @Override + public Iterable listAllProducts() { + return productRepository.findAll(); + } + + @Override + public Product getProductById(Integer id) { + return productRepository.findById(id).get(); + } + + @Override + public Product saveProduct(Product product) { + return productRepository.save(product); + } + + @Override + public void deleteProduct(Integer id) { + productRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..8681379 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +#logging.level.org.h2.server: DEBUG diff --git a/src/main/resources/static/css/guru.css b/src/main/resources/static/css/guru.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/static/images/FBcover1200x628.png b/src/main/resources/static/images/FBcover1200x628.png new file mode 100644 index 0000000..d28620a Binary files /dev/null and b/src/main/resources/static/images/FBcover1200x628.png differ diff --git a/src/main/resources/static/images/NewBannerBOOTS_2.png b/src/main/resources/static/images/NewBannerBOOTS_2.png new file mode 100644 index 0000000..e6b7ea3 Binary files /dev/null and b/src/main/resources/static/images/NewBannerBOOTS_2.png differ diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html new file mode 100644 index 0000000..61de20b --- /dev/null +++ b/src/main/resources/templates/fragments/header.html @@ -0,0 +1,39 @@ + + + + + + + +
+
+ + +
+
+
+

Spring Framework Guru

+ +

Spring Boot Web App

+
+
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/headerinc.html b/src/main/resources/templates/fragments/headerinc.html new file mode 100644 index 0000000..57b1803 --- /dev/null +++ b/src/main/resources/templates/fragments/headerinc.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..99830d5 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,15 @@ + + + + + Spring Framework Guru + + + + + +
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/productform.html b/src/main/resources/templates/productform.html new file mode 100644 index 0000000..cea3803 --- /dev/null +++ b/src/main/resources/templates/productform.html @@ -0,0 +1,44 @@ + + + + + Spring Framework Guru + + + + +
+ + +

Product Details

+
+
+ + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/products.html b/src/main/resources/templates/products.html new file mode 100644 index 0000000..4e6124d --- /dev/null +++ b/src/main/resources/templates/products.html @@ -0,0 +1,39 @@ + + + + + Spring Framework Guru + + + + +
+ +
+

Product List

+ + + + + + + + + + + + + + + + + + + +
IdProduct IdDescriptionPriceViewEditDelete
IdProduct IddescirptionpriceViewEditDelete
+ +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/productshow.html b/src/main/resources/templates/productshow.html new file mode 100644 index 0000000..234b985 --- /dev/null +++ b/src/main/resources/templates/productshow.html @@ -0,0 +1,44 @@ + + + + + Spring Framework Guru + + + + +
+ + +

Product Details

+
+
+
+ +
+

Product Id

+
+
+ +
+

description

+
+
+
+ +
+

Priceaddd

+
+
+
+ +
+

url....

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/test/java/guru/springframework/SpringBootWebApplicationTests.java b/src/test/java/guru/springframework/SpringBootWebApplicationTests.java index a1db764..fab8c48 100644 --- a/src/test/java/guru/springframework/SpringBootWebApplicationTests.java +++ b/src/test/java/guru/springframework/SpringBootWebApplicationTests.java @@ -2,12 +2,13 @@ import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = SpringBootWebApplication.class) + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SpringBootWebApplication.class) @WebAppConfiguration public class SpringBootWebApplicationTests { diff --git a/src/test/java/guru/springframework/configuration/RepositoryConfiguration.java b/src/test/java/guru/springframework/configuration/RepositoryConfiguration.java new file mode 100644 index 0000000..4f8aad2 --- /dev/null +++ b/src/test/java/guru/springframework/configuration/RepositoryConfiguration.java @@ -0,0 +1,15 @@ +package guru.springframework.configuration; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableAutoConfiguration +@EntityScan(basePackages = {"guru.springframework.domain"}) +@EnableJpaRepositories(basePackages = {"guru.springframework.repositories"}) +@EnableTransactionManagement +public class RepositoryConfiguration { +} diff --git a/src/test/java/guru/springframework/repositories/ProductRepositoryTest.java b/src/test/java/guru/springframework/repositories/ProductRepositoryTest.java new file mode 100644 index 0000000..17c3f49 --- /dev/null +++ b/src/test/java/guru/springframework/repositories/ProductRepositoryTest.java @@ -0,0 +1,72 @@ +package guru.springframework.repositories; + +import guru.springframework.configuration.RepositoryConfiguration; +import guru.springframework.domain.Product; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +@ExtendWith(SpringExtension.class) +@DataJpaTest +public class ProductRepositoryTest { + + @Autowired + private ProductRepository productRepository; + + @Test + public void testSaveProduct(){ + //setup product + Product product = new Product(); + product.setDescription("Spring Framework Guru Shirt"); + product.setPrice(new BigDecimal("18.95")); + product.setProductId("1234"); + + //save product, verify has ID value after save + assertNull(product.getId()); //null before save + productRepository.save(product); + assertNotNull(product.getId()); //not null after save + + //fetch from DB + Product fetchedProduct = productRepository.findById(product.getId()).orElse(null); + + //should not be null + assertNotNull(fetchedProduct); + + //should equal + assertEquals(product.getId(), fetchedProduct.getId()); + assertEquals(product.getDescription(), fetchedProduct.getDescription()); + + //update description and save + fetchedProduct.setDescription("New Description"); + productRepository.save(fetchedProduct); + + //get from DB, should be updated + Product fetchedUpdatedProduct = productRepository.findById(fetchedProduct.getId()).orElse(null); + assertEquals(fetchedProduct.getDescription(), fetchedUpdatedProduct.getDescription()); + + //verify count of products in DB + long productCount = productRepository.count(); + assertEquals(productCount, 1); + + //get all products, list should only have one + Iterable products = productRepository.findAll(); + + int count = 0; + + for(Product p : products){ + count++; + } + + assertEquals(count, 1); + } +}