├── .gitignore ├── README.md ├── SpringMVC.pdf ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── kousenit │ │ └── shopping │ │ ├── ShoppingApplication.java │ │ ├── config │ │ ├── AppInit.java │ │ └── FunctionalBeans.java │ │ ├── controllers │ │ ├── ProductController.java │ │ ├── ProductHandler.java │ │ └── ProductRestController.java │ │ ├── dao │ │ └── ProductRepository.java │ │ ├── entities │ │ ├── ControllerAdvice.java │ │ ├── Product.java │ │ └── ProductNotFoundException.java │ │ └── services │ │ └── ProductService.java └── resources │ ├── application.yml │ ├── public │ └── error │ │ └── 404.html │ ├── static │ ├── sarah-kilian-52jRtc2S_VE-unsplash.jpg │ └── styles.css │ └── templates │ └── products.html └── test ├── java └── com │ └── kousenit │ └── shopping │ ├── ShoppingApplicationTests.java │ ├── controllers │ ├── ProductControllerTest.java │ ├── ProductHandlerTest.java │ └── ProductRestControllerTest.java │ ├── dao │ └── ProductRepositoryTest.java │ └── entities │ └── ProductTest.java └── resources ├── data.sql └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shopping_rest 2 | Spring REST API for products 3 | -------------------------------------------------------------------------------- /SpringMVC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kousen/shopping_rest/2ff874913a99f0739c95693da1da0b1235d36ae1/SpringMVC.pdf -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.7.5' 3 | id 'io.spring.dependency-management' version '1.0.15.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'com.kousenit' 8 | version = '1.0' 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 16 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 17 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 18 | implementation 'org.springframework.boot:spring-boot-starter-validation' 19 | implementation 'org.springframework.boot:spring-boot-starter-web' 20 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 21 | // Note: rest api docs at localhost:8080/v3/api-docs 22 | implementation 'org.springdoc:springdoc-openapi-ui:1.6.12' 23 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 24 | runtimeOnly 'com.h2database:h2' 25 | runtimeOnly 'mysql:mysql-connector-java' 26 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 27 | testImplementation 'io.projectreactor:reactor-test' 28 | } 29 | 30 | tasks.named('test', Test) { 31 | useJUnitPlatform() 32 | } 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kousen/shopping_rest/2ff874913a99f0739c95693da1da0b1235d36ae1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'shopping' 2 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/ShoppingApplication.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping; 2 | 3 | import com.kousenit.shopping.services.ProductService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class ShoppingApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(ShoppingApplication.class, args); 15 | } 16 | 17 | @Bean 18 | public CommandLineRunner initialize(@Autowired ProductService service) { 19 | return (String... args) -> service.initializeDatabase(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/config/AppInit.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.config; 2 | 3 | import com.kousenit.shopping.services.ProductService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @Profile("!test") 11 | public class AppInit implements CommandLineRunner { 12 | private final ProductService service; 13 | 14 | @Autowired 15 | public AppInit(ProductService service) { 16 | this.service = service; 17 | } 18 | 19 | @Override 20 | public void run(String... args) { 21 | service.initializeDatabase(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/config/FunctionalBeans.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.config; 2 | 3 | import com.kousenit.shopping.controllers.ProductHandler; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.function.RouterFunction; 7 | import org.springframework.web.servlet.function.ServerResponse; 8 | 9 | import static org.springframework.http.MediaType.APPLICATION_JSON; 10 | import static org.springframework.web.servlet.function.RequestPredicates.accept; 11 | import static org.springframework.web.servlet.function.RouterFunctions.route; 12 | 13 | @Configuration // JavaConfig approach to adding beans to the app ctx 14 | public class FunctionalBeans { 15 | 16 | // Alternative CommandLineRunner (replaces the AppInit class) 17 | // @Bean 18 | // public CommandLineRunner initialize(@Autowired ProductService service) { 19 | // return args -> service.initializeDatabase(); 20 | // } 21 | 22 | @Bean 23 | public RouterFunction routerFunction(ProductHandler handler) { 24 | return route().path("/function", 25 | builder -> builder 26 | .GET("", accept(APPLICATION_JSON), handler::getAllProducts) 27 | .GET("/{id}", accept(APPLICATION_JSON), handler::getProductById) 28 | .POST("", handler::createProduct)) 29 | .build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/controllers/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import com.kousenit.shopping.entities.ProductNotFoundException; 5 | import com.kousenit.shopping.services.ProductService; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.validation.Errors; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.validation.Valid; 15 | import java.util.Optional; 16 | 17 | @Controller 18 | @RequestMapping("/products") 19 | public class ProductController { 20 | private final Logger log = LoggerFactory.getLogger(ProductController.class.getName()); 21 | 22 | private final ProductService service; 23 | 24 | public ProductController(ProductService service) { 25 | this.service = service; 26 | } 27 | 28 | @GetMapping 29 | public String showProducts(Model model) { 30 | model.addAttribute("product", new Product()); 31 | model.addAttribute("products", service.findAll()); 32 | return "products"; 33 | } 34 | 35 | @GetMapping("{id}") 36 | public String showProduct(@PathVariable Integer id, Model model) { 37 | Optional optional = service.findById(id); 38 | if (optional.isPresent()) { 39 | model.addAttribute("product", optional.get()); 40 | } else { 41 | throw new ProductNotFoundException(id); 42 | } 43 | return "products"; 44 | } 45 | 46 | @PostMapping 47 | @ResponseStatus(HttpStatus.CREATED) 48 | public String addProduct(@Valid Product product, Errors errors) { 49 | if (errors.hasErrors()) { 50 | log.info("Errors: " + errors); 51 | return "products"; 52 | } 53 | 54 | log.info("Saving product: " + product); 55 | service.saveProduct(product); 56 | return "redirect:/products"; 57 | } 58 | 59 | @DeleteMapping("{id}") 60 | @ResponseStatus(HttpStatus.NO_CONTENT) 61 | public String deleteProduct(@PathVariable Integer id) { 62 | service.deleteProduct(id); 63 | return "redirect:/products"; 64 | } 65 | 66 | @DeleteMapping 67 | @ResponseStatus(HttpStatus.NO_CONTENT) 68 | public String deleteAll() { 69 | service.deleteAllInBatch(); 70 | return "redirect:/products"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/controllers/ProductHandler.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import com.kousenit.shopping.services.ProductService; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.function.ServerRequest; 8 | import org.springframework.web.servlet.function.ServerResponse; 9 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 10 | 11 | import javax.servlet.ServletException; 12 | import java.io.IOException; 13 | import java.net.URI; 14 | import java.util.Optional; 15 | 16 | import static org.springframework.web.servlet.function.ServerResponse.ok; 17 | 18 | @Component 19 | public class ProductHandler { 20 | private final ProductService service; 21 | 22 | public ProductHandler(ProductService service) { 23 | this.service = service; 24 | } 25 | 26 | public ServerResponse getAllProducts(ServerRequest request) { 27 | return ok().body(service.findAll()); 28 | } 29 | 30 | public ServerResponse getProductById(ServerRequest request) { 31 | int id = Integer.parseInt(request.pathVariable("id")); 32 | Optional optional = service.findById(id); 33 | return optional.map(product -> 34 | ok().contentType(MediaType.APPLICATION_JSON).body(product)) 35 | .orElseGet(() -> ServerResponse.notFound().build()); 36 | } 37 | 38 | public ServerResponse createProduct(ServerRequest request) throws ServletException, IOException { 39 | Product product = service.saveProduct(request.body(Product.class)); 40 | URI uri = ServletUriComponentsBuilder.fromServletMapping(request.servletRequest()) 41 | .path(product.getId().toString()).build() 42 | .toUri(); 43 | return ServerResponse.created(uri).body(product); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/controllers/ProductRestController.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import com.kousenit.shopping.services.ProductService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 10 | 11 | import java.net.URI; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | @RestController 16 | @RequestMapping("/rest") 17 | public class ProductRestController { 18 | private final ProductService service; 19 | 20 | @Autowired 21 | public ProductRestController(ProductService service) { 22 | this.service = service; 23 | } 24 | 25 | @GetMapping // localhost:8080/rest?minimumPrice=5.0 26 | public List getAllProducts( 27 | @RequestParam(required = false) Double minimumPrice) { 28 | if (minimumPrice != null) { 29 | return service.findAllByMinimumPrice(minimumPrice); 30 | } 31 | return service.findAll(); 32 | } 33 | 34 | @GetMapping("{id}") 35 | public ResponseEntity findById(@PathVariable Integer id) { 36 | // Simplest option: 37 | return ResponseEntity.of(service.findById(id)); 38 | 39 | // Best option if you need to customize the return value (see the ControllerAdvice class): 40 | // return service.findById(id).orElseThrow( 41 | // () -> new ProductNotFoundException(id + "")); 42 | 43 | // Works, but overly verbose 44 | // Optional optionalProduct = service.findById(id); 45 | // if (optionalProduct.isPresent()) { 46 | // return ResponseEntity.ok(optionalProduct.get()); 47 | // } else { 48 | // return ResponseEntity.notFound().build(); 49 | // } 50 | // 51 | // Functional version that replaces "if" with "map" and "orElseGet" 52 | // return optionalProduct.map(ResponseEntity::ok) 53 | // .orElseGet(() -> ResponseEntity.notFound().build()); 54 | } 55 | 56 | @PostMapping 57 | // @ResponseStatus(HttpStatus.CREATED) 58 | public ResponseEntity insertProduct(@RequestBody Product product) { 59 | Product p = service.saveProduct(product); 60 | URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() 61 | .path("/{id}") 62 | .buildAndExpand(p.getId()) 63 | .toUri(); 64 | return ResponseEntity.created(location).body(p); 65 | } 66 | 67 | @PutMapping("{id}") 68 | public Product updateOrInsertProduct(@PathVariable Integer id, 69 | @RequestBody Product newProduct) { 70 | return service.findById(id).map(product -> { 71 | product.setName(newProduct.getName()); 72 | product.setPrice(newProduct.getPrice()); 73 | return service.saveProduct(product); 74 | }).orElseGet(() -> { 75 | newProduct.setId(id); 76 | return service.saveProduct(newProduct); 77 | }); 78 | } 79 | 80 | @DeleteMapping("{id}") 81 | public ResponseEntity deleteProduct(@PathVariable Integer id) { 82 | Optional existingProduct = service.findById(id); 83 | if (existingProduct.isPresent()) { 84 | service.deleteProduct(id); 85 | return ResponseEntity.noContent().build(); 86 | } else { 87 | return ResponseEntity.notFound().build(); 88 | } 89 | } 90 | 91 | @DeleteMapping 92 | @ResponseStatus(HttpStatus.NO_CONTENT) 93 | public void deleteAllProducts() { 94 | service.deleteAllInBatch(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/dao/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.dao; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import java.util.List; 8 | 9 | @Transactional 10 | public interface ProductRepository extends JpaRepository { 11 | List findAllByPriceGreaterThanEqual(double amount); 12 | List findTop10ByNameContainsOrderByPrice(String regex); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/entities/ControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.entities; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | 8 | import javax.validation.ConstraintViolationException; 9 | 10 | @RestControllerAdvice 11 | public class ControllerAdvice { 12 | 13 | @ExceptionHandler(ProductNotFoundException.class) 14 | @ResponseStatus(HttpStatus.NOT_FOUND) 15 | public String productNotFoundHandler(ProductNotFoundException ex) { 16 | return ex.getMessage(); 17 | } 18 | 19 | @ExceptionHandler(ConstraintViolationException.class) 20 | @ResponseStatus(HttpStatus.BAD_REQUEST) 21 | public String constraintViolation(ConstraintViolationException e) { 22 | return e.getConstraintViolations().toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/entities/Product.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.entities; 2 | 3 | import javax.persistence.*; 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.PositiveOrZero; 6 | import java.util.Objects; 7 | 8 | @SuppressWarnings("JpaDataSourceORMInspection") 9 | @Entity 10 | @Table(name = "products") 11 | public class Product { 12 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Integer id; 14 | 15 | @NotBlank(message = "Products must have a name") 16 | private String name; 17 | 18 | @PositiveOrZero(message = "Price must be greater than zero") 19 | private double price; 20 | 21 | public Product() {} 22 | 23 | public Product(String name, double price) { 24 | this(null, name, price); 25 | } 26 | 27 | public Product(Integer id, String name, double price) { 28 | this.id = id; 29 | this.name = name; 30 | this.price = price; 31 | } 32 | 33 | public Integer getId() { 34 | return id; 35 | } 36 | 37 | public void setId(Integer id) { 38 | this.id = id; 39 | } 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public double getPrice() { 50 | return price; 51 | } 52 | 53 | public void setPrice(double price) { 54 | this.price = price; 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if (this == o) return true; 60 | if (o == null || getClass() != o.getClass()) return false; 61 | Product product = (Product) o; 62 | return Objects.equals(id, product.id); 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | return Objects.hash(id); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "Product{" + 73 | "id=" + id + 74 | ", name='" + name + '\'' + 75 | ", price=" + price + 76 | '}'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/entities/ProductNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.entities; 2 | 3 | public class ProductNotFoundException extends RuntimeException { 4 | public ProductNotFoundException(Integer id) { 5 | super("Product not found with id " + id); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/kousenit/shopping/services/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.services; 2 | 3 | import com.kousenit.shopping.dao.ProductRepository; 4 | import com.kousenit.shopping.entities.Product; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.transaction.Transactional; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @Service // transaction boundaries and business logic 14 | @Transactional 15 | public class ProductService { 16 | private final ProductRepository repository; 17 | 18 | @Autowired 19 | public ProductService(ProductRepository repository) { 20 | this.repository = repository; 21 | } 22 | 23 | public void initializeDatabase() { 24 | if (repository.count() == 0) { 25 | repository.saveAll(Arrays.asList( 26 | new Product("baseball", 5.0), 27 | new Product("football", 12.0), 28 | new Product("basketball", 10.0) 29 | )); 30 | } 31 | } 32 | 33 | public List findAll() { 34 | return repository.findAll(); 35 | } 36 | 37 | public List findAllByMinimumPrice(Double minPrice) { 38 | return repository.findAllByPriceGreaterThanEqual(minPrice); 39 | } 40 | 41 | public Optional findById(Integer id) { 42 | return repository.findById(id); 43 | } 44 | 45 | public Product saveProduct(Product product) { 46 | return repository.save(product); 47 | } 48 | 49 | public void deleteProduct(Integer id) { 50 | repository.deleteById(id); 51 | } 52 | 53 | public void deleteAll() { 54 | repository.deleteAll(); 55 | } 56 | 57 | public void deleteAllInBatch() { 58 | repository.deleteAllInBatch(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: test 4 | jpa: 5 | hibernate: 6 | ddl-auto: update 7 | properties: 8 | hibernate.format_sql: true 9 | logging: 10 | level: 11 | web: debug 12 | sql: debug 13 | org: 14 | springframework: 15 | transaction: 16 | interceptor: trace 17 | --- 18 | spring: 19 | datasource: 20 | url: jdbc:h2:mem:testdb 21 | username: sa 22 | password: sa 23 | driver-class-name: org.h2.Driver 24 | # schema: classpath*:schema.sql 25 | # data: classpath*:data.sql 26 | config: 27 | activate: 28 | on-profile: test 29 | --- 30 | spring: 31 | datasource: 32 | url: jdbc:mysql://localhost:3306/shopping 33 | username: root 34 | password: 35 | config: 36 | activate: 37 | on-profile: prod 38 | --- 39 | spring: 40 | datasource: 41 | url: jdbc:h2:mem:devdb 42 | username: sa 43 | password: sa 44 | driver-class-name: org.h2.Driver 45 | jpa: 46 | hibernate: 47 | ddl-auto: create-drop 48 | config: 49 | activate: 50 | on-profile: dev 51 | -------------------------------------------------------------------------------- /src/main/resources/public/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oops! 5 | 6 | 7 | Oops! 8 |

Photo by Sarah Kilian on Unsplash

9 |

10 | Aw, nutbunnies. This is our error page. Now I haz a sad. 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/static/sarah-kilian-52jRtc2S_VE-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kousen/shopping_rest/2ff874913a99f0739c95693da1da0b1235d36ae1/src/main/resources/static/sarah-kilian-52jRtc2S_VE-unsplash.jpg -------------------------------------------------------------------------------- /src/main/resources/static/styles.css: -------------------------------------------------------------------------------- 1 | #products { 2 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 3 | border-collapse: collapse; 4 | width: 75%; 5 | } 6 | 7 | #products td, #products th { 8 | border: 1px solid #ddd; 9 | padding: 8px; 10 | } 11 | 12 | #products tr:nth-child(even) { 13 | background-color: #f2f2f2; 14 | } 15 | 16 | #products tr:hover { 17 | background-color: #ddd; 18 | } 19 | 20 | #products th { 21 | padding-top: 12px; 22 | padding-bottom: 12px; 23 | text-align: left; 24 | background-color: #4CAF50; 25 | color: white; 26 | } 27 | 28 | input[type=text] { 29 | width: 100%; 30 | padding: 12px 20px; 31 | margin: 8px 0; 32 | display: inline-block; 33 | border: 1px solid #ccc; 34 | border-radius: 4px; 35 | box-sizing: border-box; 36 | } 37 | 38 | input[type=submit] { 39 | width: 100%; 40 | background-color: #4CAF50; 41 | color: white; 42 | padding: 14px 20px; 43 | margin: 8px 0; 44 | border: none; 45 | border-radius: 4px; 46 | cursor: pointer; 47 | } 48 | 49 | input[type=submit]:hover { 50 | background-color: #45A049; 51 | } 52 | 53 | div { 54 | border-radius: 5px; 55 | background-color: #f2f2f2; 56 | padding: 20px; 57 | } 58 | 59 | span.validationError { 60 | color: red; 61 | } -------------------------------------------------------------------------------- /src/main/resources/templates/products.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Products 8 | 9 | 10 | 11 |

Products:

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 |
IDNamePrice
idnameprice
28 | 29 |

New Product

30 |
31 |
32 | 33 | 35 | Name Error 38 | 39 | 40 | 42 | Price Error 45 | 46 | 47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/ShoppingApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ShoppingApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/controllers/ProductControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import com.kousenit.shopping.services.ProductService; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.Mockito; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static org.hamcrest.Matchers.is; 19 | import static org.mockito.ArgumentMatchers.anyInt; 20 | import static org.mockito.Mockito.verify; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 23 | 24 | @WebMvcTest(ProductController.class) 25 | class ProductControllerTest { 26 | @Autowired 27 | private MockMvc mvc; 28 | 29 | @MockBean 30 | private ProductService service; 31 | 32 | private final List products = Arrays.asList( 33 | new Product(1,"baseball", 9.99), 34 | new Product(2, "football", 14.95), 35 | new Product(3, "basketball", 11.99) 36 | ); 37 | 38 | @BeforeEach 39 | void setUp() { 40 | Mockito.when(service.findAll()) 41 | .thenReturn(products); 42 | Mockito.when(service.saveProduct(Mockito.any(Product.class))) 43 | .thenReturn(products.get(0), 44 | products.get(1), 45 | products.get(2)); 46 | Mockito.when(service.findById(1)) 47 | .thenReturn(Optional.of(products.get(0))); 48 | } 49 | 50 | @Test 51 | void getAllProducts() throws Exception { 52 | mvc.perform(get("/products")) 53 | .andExpect(status().isOk()) 54 | .andExpect(view().name("products")) 55 | .andExpect(model().attribute("products", products)); 56 | verify(service).findAll(); 57 | } 58 | 59 | @Test 60 | void getProductById() throws Exception { 61 | mvc.perform(get("/products/1")) 62 | .andExpect(status().isOk()) 63 | .andExpect(view().name("products")) 64 | .andExpect(model().attribute("product", is(products.get(0)))); 65 | verify(service).findById(1); 66 | } 67 | 68 | @Test 69 | void getProductByIdDoesNotExist() throws Exception { 70 | mvc.perform(get("/products/999")) 71 | .andExpect(status().isNotFound()) 72 | .andExpect(content() 73 | .string("Product not found with id 999")); 74 | } 75 | 76 | @Test 77 | void saveProduct() throws Exception { 78 | mvc.perform(post("/products") 79 | .content("name=golfball&price=5.0") 80 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)) 81 | .andExpect(status().isCreated()) 82 | .andExpect(view().name("redirect:/products")); 83 | } 84 | 85 | @Test 86 | void deleteAllProducts() throws Exception { 87 | mvc.perform(delete("/products")) 88 | .andExpect(status().isNoContent()) 89 | .andExpect(view().name("redirect:/products")); 90 | verify(service).deleteAllInBatch(); 91 | } 92 | 93 | @Test 94 | void deleteSingleProduct() throws Exception { 95 | mvc.perform(delete("/products/1")) 96 | .andExpect(status().isNoContent()) 97 | .andExpect(view().name("redirect:/products")); 98 | verify(service).deleteProduct(anyInt()); 99 | } 100 | } -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/controllers/ProductHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.web.client.TestRestTemplate; 8 | import org.springframework.boot.test.web.server.LocalServerPort; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.core.ParameterizedTypeReference; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.jdbc.core.JdbcTemplate; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import java.util.List; 18 | 19 | import static org.junit.jupiter.api.Assertions.*; 20 | 21 | @SuppressWarnings({"GrazieInspection", "SqlResolve", "SqlNoDataSourceInspection"}) 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | @Transactional 24 | @Profile("test") 25 | class ProductHandlerTest { 26 | @LocalServerPort 27 | private int randomServerPort; 28 | 29 | @Autowired 30 | private TestRestTemplate template; 31 | 32 | @Autowired 33 | private JdbcTemplate jdbcTemplate; 34 | 35 | private List getIds() { 36 | return jdbcTemplate.query("select id from products", 37 | (rs, rowNum) -> rs.getInt("id")); 38 | } 39 | 40 | // Odd contortions you need to go through to get a List 41 | private List getProducts() { 42 | String url = "http://localhost:" + randomServerPort + "/function"; 43 | ResponseEntity> productsEntity = 44 | template.exchange(url, HttpMethod.GET, null, 45 | new ParameterizedTypeReference<>() {}); 46 | return productsEntity.getBody(); 47 | } 48 | 49 | @Test 50 | void getAll() { 51 | List products = getProducts(); 52 | assertNotNull(products); 53 | assertEquals(4, products.size()); 54 | } 55 | 56 | @Test 57 | void getSingleProductThatExists() { 58 | List ids = getIds(); 59 | ids.forEach(id -> { 60 | Product product = template.getForObject("/function/{id}", Product.class, id); 61 | assertAll( 62 | () -> assertNotNull(product.getId()), 63 | () -> assertTrue(product.getName().length() > 0), 64 | () -> assertTrue(product.getPrice() >= 0.0) 65 | ); 66 | } 67 | ); 68 | } 69 | 70 | @Test 71 | void getSingleProductThatDoesNotExist() { 72 | List ids = getIds(); 73 | assertFalse(ids.contains(999)); 74 | 75 | ResponseEntity entity = template.getForEntity( 76 | "/function/{id}", Product.class, 999); 77 | assertEquals(HttpStatus.NOT_FOUND, entity.getStatusCode()); 78 | } 79 | 80 | @Test 81 | void insertProduct() { 82 | Product product = new Product(); 83 | product.setName("baseball bat"); 84 | product.setPrice(20.97); 85 | 86 | ResponseEntity response = template.postForEntity("/function", product, Product.class); 87 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 88 | Product savedProduct = response.getBody(); 89 | assert savedProduct != null; 90 | assertAll( 91 | () -> assertNotNull(savedProduct), 92 | () -> assertEquals(product.getName(), savedProduct.getName()), 93 | () -> assertEquals(product.getPrice(), savedProduct.getPrice(), 0.01), 94 | () -> assertNotNull(savedProduct.getId())); 95 | } 96 | } -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/controllers/ProductRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.controllers; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.client.TestRestTemplate; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.jdbc.core.JdbcTemplate; 13 | import org.springframework.test.web.reactive.server.WebTestClient; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.util.List; 18 | 19 | import static org.junit.jupiter.api.Assertions.*; 20 | 21 | @SuppressWarnings({"SqlResolve", "SqlNoDataSourceInspection"}) 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | @AutoConfigureWebTestClient 24 | @Transactional 25 | @Profile("test") 26 | class ProductRestControllerTest { 27 | @Autowired 28 | private WebTestClient client; 29 | 30 | @Autowired 31 | private JdbcTemplate jdbcTemplate; 32 | 33 | private List getIds() { 34 | return jdbcTemplate.query("select id from products", 35 | (rs, rowNum) -> rs.getInt("id")); 36 | } 37 | 38 | @Test 39 | void getAll() { 40 | client.get() 41 | .uri("/rest") 42 | .accept(MediaType.APPLICATION_JSON) 43 | .exchange() 44 | .expectStatus().isOk() 45 | .expectHeader().contentType(MediaType.APPLICATION_JSON) 46 | .expectBodyList(Product.class) 47 | .hasSize(4) 48 | .consumeWith(System.out::println); 49 | } 50 | 51 | @Test 52 | void getSingleProductThatExists() { 53 | List ids = getIds(); 54 | client.get() 55 | .uri("/rest/{id}", ids.get(0)) 56 | .exchange() 57 | .expectStatus().isOk() 58 | .expectHeader().contentType(MediaType.APPLICATION_JSON) 59 | .expectBody(Product.class) 60 | .consumeWith(System.out::println); 61 | } 62 | 63 | @Test 64 | void getSingleProductThatDoesNotExist() { 65 | List ids = getIds(); 66 | assertFalse(ids.contains(999)); 67 | 68 | client.get() 69 | .uri("/rest/{id}", 999) 70 | .exchange() 71 | .expectStatus().isNotFound(); 72 | } 73 | 74 | @Test 75 | void getWithTRT(@Autowired TestRestTemplate template) { 76 | Product product = template.getForObject("/rest/1", Product.class); 77 | Product correct = new Product(1, "baseball", 9.99); 78 | assertEquals(correct, product); 79 | } 80 | 81 | @Test 82 | void insertProduct() { 83 | Product product = new Product("baseball bat", 20.97); 84 | 85 | client.post() 86 | .uri("/rest") 87 | .contentType(MediaType.APPLICATION_JSON) 88 | .accept(MediaType.APPLICATION_JSON) 89 | .body(Mono.just(product), Product.class) 90 | .exchange() 91 | .expectStatus().isCreated() 92 | .expectBody() 93 | .jsonPath("$.id").isNotEmpty() 94 | .jsonPath("$.name").isEqualTo("baseball bat") 95 | .jsonPath("$.price").isEqualTo(20.97) 96 | .consumeWith(System.out::println); 97 | } 98 | 99 | @Test 100 | void postWithTRT(@Autowired TestRestTemplate template) { 101 | Product product = new Product(); 102 | product.setName("baseball bat"); 103 | product.setPrice(20.97); 104 | 105 | ResponseEntity response = template.postForEntity("/rest", product, Product.class); 106 | Product savedProduct = response.getBody(); 107 | assert savedProduct != null; 108 | assertAll( 109 | () -> assertEquals(product.getName(), savedProduct.getName()), 110 | () -> assertEquals(product.getPrice(), savedProduct.getPrice(), 0.01), 111 | () -> assertNotNull(savedProduct.getId())); 112 | } 113 | 114 | @Test 115 | void updateProduct(@Autowired TestRestTemplate template) { 116 | List ids = getIds(); 117 | Product product = template.getForObject("/rest/{id}", Product.class, ids.get(0)); 118 | product.setPrice(product.getPrice() * 1.10); 119 | 120 | client.put() 121 | .uri("/products/{id}", product.getId()) 122 | .body(Mono.just(product), Product.class) 123 | .exchange() 124 | .expectBody(Product.class) 125 | .consumeWith(System.out::println); 126 | } 127 | 128 | 129 | @Test 130 | void getProductsWithMinimumPrice() { 131 | client.get() 132 | .uri(uriBuilder -> uriBuilder.path("/rest") 133 | .queryParam("minimumPrice", 12.0) 134 | .build()) 135 | .accept(MediaType.APPLICATION_JSON) 136 | .exchange() 137 | .expectStatus().isOk() 138 | .expectBodyList(Product.class) 139 | .hasSize(3); 140 | } 141 | 142 | @Test 143 | void deleteSingleProduct() { 144 | List ids = getIds(); 145 | if (ids.size() == 0) { 146 | System.out.println("No ids found"); 147 | return; 148 | } 149 | 150 | client.get() 151 | .uri("/rest/{id}", ids.get(0)) 152 | .exchange() 153 | .expectStatus().isOk(); 154 | 155 | client.delete() 156 | .uri("/rest/{id}", ids.get(0)) 157 | .exchange() 158 | .expectStatus().isNoContent(); 159 | 160 | client.get() 161 | .uri("/rest/{id}", ids.get(0)) 162 | .exchange() 163 | .expectStatus().isNotFound(); 164 | } 165 | 166 | @Test 167 | void deleteAllProducts() { 168 | client.delete() 169 | .uri("/rest") 170 | .exchange() 171 | .expectStatus().isNoContent(); 172 | 173 | client.get() 174 | .uri("/rest") 175 | .exchange() 176 | .expectBodyList(Product.class) 177 | .hasSize(0); 178 | } 179 | } -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/dao/ProductRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.dao; 2 | 3 | import com.kousenit.shopping.entities.Product; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | @SpringBootTest 15 | @Transactional // In a test, @Transactional causes each tx to rollback at end of test 16 | class ProductRepositoryTest { 17 | @Autowired 18 | private ProductRepository dao; 19 | 20 | @Test 21 | void autowiringWorked() { 22 | assertNotNull(dao); 23 | } 24 | 25 | @Test 26 | void findById() { 27 | Optional optionalProduct = dao.findById(1); 28 | assertTrue(optionalProduct.isPresent()); 29 | } 30 | 31 | @Test 32 | void shouldBeFourProductsInSampleDB() { 33 | assertEquals(4, dao.count()); 34 | } 35 | 36 | @Test 37 | void deleteAllProducts() { 38 | dao.deleteAll(); 39 | assertEquals(0, dao.count()); 40 | } 41 | 42 | @Test 43 | void insertProduct() { 44 | Product bat = new Product("cricket bat", 35.00); 45 | dao.save(bat); 46 | assertAll( 47 | () -> assertNotNull(bat.getId()), 48 | () -> assertEquals(5, dao.count()) 49 | ); 50 | } 51 | 52 | @Test 53 | void priceGE12() { 54 | List products = dao.findAllByPriceGreaterThanEqual(12.0); 55 | assertEquals(3, products.size()); 56 | System.out.println(products); 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/java/com/kousenit/shopping/entities/ProductTest.java: -------------------------------------------------------------------------------- 1 | package com.kousenit.shopping.entities; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; 7 | 8 | import javax.validation.ConstraintViolation; 9 | import javax.validation.Validator; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | @SpringBootTest 16 | class ProductTest { 17 | 18 | @Autowired 19 | private Validator validator; 20 | 21 | @Test 22 | void autowiringWorked() { 23 | assertNotNull(validator); 24 | assertEquals(LocalValidatorFactoryBean.class, validator.getClass()); 25 | } 26 | 27 | @Test 28 | void nameCanNotBeBlank() { 29 | Product product = new Product("", 10.0); 30 | Set> violations = validator.validate(product); 31 | assertEquals(1, violations.size()); 32 | 33 | // Extract violation using set.iterator(); alternative using streams in price test 34 | ConstraintViolation violation = violations.iterator().next(); 35 | assertEquals("Products must have a name", violation.getMessage()); 36 | } 37 | 38 | @Test 39 | void priceMustBeGEZero() { 40 | Product product = new Product("name", -1); 41 | Set> violations = validator.validate(product); 42 | assertEquals(1, violations.size()); 43 | 44 | Optional> optionalViolation = violations.stream().findFirst(); 45 | assertTrue(optionalViolation.isPresent()); 46 | assertEquals("Price must be greater than zero", optionalViolation.get().getMessage()); 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/resources/data.sql: -------------------------------------------------------------------------------- 1 | -- noinspection SqlResolveForFile 2 | 3 | -- noinspection SqlResolveForFile 4 | 5 | -- noinspection SqlResolveForFile 6 | 7 | -- noinspection SqlResolveForFile 8 | 9 | -- noinspection SqlResolveForFile 10 | 11 | -- noinspection SqlResolveForFile 12 | 13 | -- noinspection SqlResolveForFile 14 | 15 | -- noinspection SqlResolveForFile 16 | 17 | -- noinspection SqlResolveForFile 18 | 19 | -- noinspection SqlResolveForFile 20 | 21 | -- noinspection SqlResolveForFile 22 | 23 | -- noinspection SqlResolveForFile 24 | 25 | insert into products(name, price) values ('baseball', 9.99); 26 | insert into products(name, price) values ('football', 14.95); 27 | insert into products(name, price) values ('basketball', 11.99); 28 | insert into products(name, price) values ('soccer ball', 12.50); -------------------------------------------------------------------------------- /src/test/resources/schema.sql: -------------------------------------------------------------------------------- 1 | drop table if exists products; 2 | create table products 3 | ( 4 | id int not null auto_increment, 5 | name varchar(50) not null, 6 | price decimal not null, 7 | primary key (id) 8 | ) --------------------------------------------------------------------------------