├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── hantsylabs │ │ └── restexample │ │ └── springmvc │ │ ├── ApiErrors.java │ │ ├── Constants.java │ │ ├── DTOUtils.java │ │ ├── api │ │ ├── PingController.java │ │ ├── RestExceptionHandler.java │ │ ├── UserController.java │ │ └── post │ │ │ ├── CommentController.java │ │ │ └── PostController.java │ │ ├── config │ │ ├── AppConfig.java │ │ ├── AppInitializer.java │ │ ├── DataJpaConfig.java │ │ ├── DataSourceConfig.java │ │ ├── Jackson2ObjectMapperConfig.java │ │ ├── JpaConfig.java │ │ ├── MessageSourceConfig.java │ │ ├── SecurityConfig.java │ │ ├── SecurityInitializer.java │ │ ├── SwaggerConfig.java │ │ └── WebConfig.java │ │ ├── domain │ │ ├── Comment.java │ │ └── Post.java │ │ ├── exception │ │ ├── InvalidRequestException.java │ │ └── ResourceNotFoundException.java │ │ ├── model │ │ ├── CommentDetails.java │ │ ├── CommentForm.java │ │ ├── PostDetails.java │ │ ├── PostForm.java │ │ └── ResponseMessage.java │ │ ├── repository │ │ ├── CommentRepository.java │ │ ├── PostRepository.java │ │ └── PostSpecifications.java │ │ └── service │ │ └── BlogService.java ├── resources-dev │ └── database.properties ├── resources-prod │ └── database.properties ├── resources-staging │ └── database.properties ├── resources │ ├── META-INF │ │ ├── orm.xml │ │ └── persistence.xml │ ├── app.properties │ ├── i18n │ │ ├── errors.properties │ │ └── messsages.properties │ ├── import.sql │ └── log4j.properties └── webapp │ ├── META-INF │ └── context.xml │ ├── css │ └── main.css │ ├── i18n │ ├── messages_en.properties │ └── messages_zh_CN.properties │ ├── ico │ ├── apple-touch-icon-144-precomposed.png │ └── favicon.ico │ ├── img │ └── na.jpg │ ├── index.html │ ├── js │ ├── base64.js │ ├── controller.js │ ├── filter.js │ ├── i18n.js │ ├── init.js │ └── jquery.i18n.properties-min-1.0.9.js │ ├── main.html │ └── partials │ ├── home.html │ ├── login.html │ └── posts │ ├── details.html │ ├── home.html │ └── new.html └── test └── java └── com └── hantsylabs └── restexample └── springmvc ├── domain └── PostTest.java └── test ├── BlogServiceTest.java ├── Fixtures.java ├── MockBlogServiceTest.java ├── MockDataConfig.java ├── integration ├── BasicAuthRestTemplate.java └── IntegrationTest.java └── web ├── MockPostControllerTest.java └── PostControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .settings/ 3 | .classpath 4 | .project 5 | .idea/ 6 | *.log* 7 | *.iml 8 | licenseheader.txt 9 | nb-configuration.xml 10 | *.bat 11 | /nbproject/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angularjs-springmvc-sample 2 | ========================== 3 | 4 | An example application using AngularJS/Bootstrap as frontend and Spring MVC as REST API producer. 5 | 6 | **More details about the codes, please read the online GitBook: [Building REST APIs with Spring MVC](https://www.gitbook.com/book/hantsy/build-a-restful-app-with-spring-mvc-and-angularjs/details/).** 7 | 8 | > NOTE: This project is under maintainance, no more new features added in future. If you are looking for the new Spring Boot 2 and Angular 5, check [angular-spring-reactive-sample](https://github.com/hantsy/angular-spring-reactive-sample). 9 | 10 | [![Build Status](https://drone.io/github.com/hantsy/angularjs-springmvc-sample/status.png)](https://drone.io/github.com/hantsy/angularjs-springmvc-sample/latest) 11 | 12 | ## Contribution 13 | 14 | _I appreciate any contribution for this project, including suggestions, documentation improvements, reporting issues, forks and bugfixs, etc. I have found there are some unrelated issues added, before you file an issue, please **READ THE STEPS IN THIS README.md** carefully_. 15 | 16 | **在你提交 ISSUE 前,请务必确认已经严格完成了本文中描述的操作步骤**。 17 | 18 | Thank the DevFactory team member [@misgersameer](https://github.com/misgersameer) for sending several PRs to improve the code quaulity according to the sonar rules. 19 | 20 | 21 | ## Requirements 22 | 23 | * JDK 8 24 | 25 | Oracle Java 8 is required, go to [Oracle Java website](http://java.oracle.com) to download it and install into your system. 26 | 27 | Optionally, you can set **JAVA\_HOME** environment variable and add *<JDK installation dir>/bin* in your **PATH** environment variable. 28 | 29 | * Apache Maven 30 | 31 | Download the latest Apache Maven from [http://maven.apache.org](http://maven.apache.org), and uncompress it into your local system. 32 | 33 | Optionally, you can set **M2\_HOME** environment varible, and also do not forget to append *<Maven Installation dir>/bin* your **PATH** environment variable. 34 | 35 | ## Run this project 36 | 37 | 1. Clone the codes. 38 | 39 | ``` 40 | git clone https://github.com/hantsy/angularjs-springmvc-sample 41 | ``` 42 | 43 | 2. And enter the root folder, run `mvn tomcat7:run` to start up an embedded tomcat7 to serve this application. 44 | 45 | ``` 46 | mvn tomcat7:run 47 | ``` 48 | 49 | 3. Go to [http://localhost:8080/angularjs-springmvc-sample/](http://localhost:8080/angularjs-springmvc-sample/) to test it. If you want to explore the REST API docs online, there is a *Swagger UI* configured for visualizing the REST APIs, just go to [http://localhost:8080/angularjs-springmvc-sample/swagger-ui.html](http://localhost:8080/angularjs-springmvc-sample/swagger-ui.html). 50 | 51 | ## Spring Boot 52 | 53 | If you are interested in Spring Boot, I have moved the `boot` branch into a new project [angularjs-springmvc-sample-boot](https://github.com/hantsy/angularjs-springmvc-sample-boot). 54 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.hantsylabs.restexample.springmvc 7 | angularjs-springmvc-sample 8 | 1.0-SNAPSHOT 9 | 10 | war 11 | ${project.artifactId} 12 | 13 | 14 | 1.8.10 15 | 1.8 16 | UTF-8 17 | 1.7.21 18 | 1.2.17 19 | 22 | 2.8.10 23 | 0.7.5 24 | 2.5.0 25 | 26 | 27 | 28 | 29 | jboss-public-repository-group 30 | JBoss Public Maven Repository Group 31 | https://repository.jboss.org/nexus/content/groups/public/ 32 | 33 | 34 | io.spring.repo.maven.release 35 | https://repo.spring.io/release/ 36 | 37 | 38 | 39 | io.spring.repo.maven.milestone 40 | https://repo.spring.io/milestone/ 41 | 42 | 43 | 44 | oss-jfrog-artifactory 45 | oss-jfrog-artifactory-releases 46 | https://oss.jfrog.org/artifactory/oss-release-local 47 | 48 | 49 | 50 | jvnet-nexus-releases 51 | jvnet-nexus-releases 52 | https://maven.java.net/content/repositories/releases/ 53 | 54 | 55 | 56 | 57 | io.spring.repo.maven.release 58 | https://repo.spring.io/release/ 59 | 60 | 61 | io.spring.repo.maven.milestone 62 | https://repo.spring.io/milestone/ 63 | 64 | 65 | 66 | 67 | 68 | 69 | io.spring.platform 70 | platform-bom 71 | Brussels-SR5 72 | pom 73 | import 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.aspectj 82 | aspectjrt 83 | 84 | 85 | org.aspectj 86 | aspectjweaver 87 | 88 | 89 | 90 | 91 | org.springframework 92 | spring-core 93 | 94 | 95 | 96 | org.springframework 97 | spring-orm 98 | 99 | 100 | org.springframework 101 | spring-context 102 | 103 | 104 | org.springframework 105 | spring-beans 106 | 107 | 108 | org.springframework 109 | spring-context-support 110 | 111 | 112 | org.springframework 113 | spring-aop 114 | 115 | 116 | org.springframework 117 | spring-aspects 118 | 119 | 120 | 121 | org.springframework 122 | spring-tx 123 | 124 | 125 | 126 | org.springframework 127 | spring-web 128 | 129 | 130 | 131 | org.springframework 132 | spring-webmvc 133 | 134 | 135 | 136 | org.springframework 137 | spring-websocket 138 | 139 | 140 | 141 | org.springframework.security 142 | spring-security-core 143 | 144 | 145 | org.springframework.security 146 | spring-security-config 147 | 148 | 149 | org.springframework.security 150 | spring-security-web 151 | 152 | 153 | 154 | 155 | org.springframework.data 156 | spring-data-commons 157 | 158 | 159 | 160 | org.springframework.data 161 | spring-data-jpa 162 | 163 | 164 | 165 | 166 | javax.servlet 167 | javax.servlet-api 168 | 3.1.0 169 | provided 170 | 171 | 172 | javax.transaction 173 | javax.transaction-api 174 | 1.2 175 | 176 | 177 | 178 | javax.el 179 | javax.el-api 180 | 3.0.1-b04 181 | 182 | 183 | org.glassfish 184 | javax.el 185 | 3.0.1-b08 186 | 187 | 188 | 189 | 190 | 191 | org.hibernate.javax.persistence 192 | hibernate-jpa-2.1-api 193 | 1.0.0.Final 194 | 195 | 196 | org.hibernate 197 | hibernate-core 198 | 199 | 200 | org.hibernate 201 | hibernate-entitymanager 202 | 203 | 204 | cglib 205 | cglib 206 | 207 | 208 | dom4j 209 | dom4j 210 | 211 | 212 | 213 | 214 | 215 | 216 | org.hibernate 217 | hibernate-validator 218 | 219 | 220 | 221 | javax.validation 222 | validation-api 223 | 1.1.0.Final 224 | 225 | 226 | 227 | 228 | javax.inject 229 | javax.inject 230 | 1 231 | 232 | 233 | javax.annotation 234 | jsr250-api 235 | 1.0 236 | 237 | 238 | 239 | 240 | javax.mail 241 | javax.mail-api 242 | 1.5.6 243 | 244 | 245 | javax.activation 246 | activation 247 | 1.1.1 248 | 249 | 250 | 251 | 252 | com.fasterxml.jackson.core 253 | jackson-core 254 | ${jackson.version} 255 | 256 | 257 | 258 | com.fasterxml.jackson.core 259 | jackson-databind 260 | ${jackson.version} 261 | 262 | 263 | 264 | com.fasterxml.jackson.core 265 | jackson-annotations 266 | ${jackson.version} 267 | 268 | 269 | 270 | 271 | io.springfox 272 | springfox-swagger2 273 | ${springfox.version} 274 | 275 | 276 | 277 | io.springfox 278 | springfox-swagger-ui 279 | ${springfox.version} 280 | 281 | 282 | 283 | io.springfox 284 | springfox-staticdocs 285 | ${springfox.version} 286 | test 287 | 288 | 289 | 290 | 291 | org.modelmapper 292 | modelmapper 293 | ${modelmapper.version} 294 | 295 | 296 | 297 | 298 | log4j 299 | log4j 300 | ${log4j.version} 301 | runtime 302 | 303 | 304 | org.slf4j 305 | slf4j-api 306 | ${slf4j.version} 307 | 308 | 309 | org.slf4j 310 | jcl-over-slf4j 311 | ${slf4j.version} 312 | 313 | 314 | org.slf4j 315 | slf4j-log4j12 316 | ${slf4j.version} 317 | 318 | 319 | org.apache.commons 320 | commons-lang3 321 | 322 | 323 | commons-codec 324 | commons-codec 325 | 326 | 327 | commons-io 328 | commons-io 329 | 330 | 331 | commons-dbcp 332 | commons-dbcp 333 | 1.4 334 | 335 | 336 | commons-logging 337 | commons-logging 338 | 339 | 340 | xml-apis 341 | xml-apis 342 | 343 | 344 | 345 | 346 | 347 | org.apache.httpcomponents 348 | httpclient 349 | 4.5.2 350 | 351 | 352 | 353 | 354 | org.webjars 355 | bootstrap 356 | 3.3.6 357 | 358 | 359 | org.webjars 360 | angularjs 361 | 1.3.14 362 | 363 | 364 | 365 | org.webjars 366 | angular-ui-bootstrap 367 | 0.12.1-1 368 | 369 | 370 | 371 | org.webjars 372 | jquery 373 | 2.1.3 374 | 375 | 376 | 377 | org.webjars 378 | bootstrap-material-design 379 | 0.3.0 380 | 381 | 382 | 383 | 384 | org.springframework 385 | spring-test 386 | test 387 | 388 | 389 | 390 | junit 391 | junit 392 | test 393 | 394 | 395 | 396 | org.hamcrest 397 | hamcrest-all 398 | test 399 | 400 | 401 | 402 | org.mockito 403 | mockito-core 404 | test 405 | 406 | 407 | 408 | com.jayway.jsonpath 409 | json-path 410 | test 411 | 412 | 413 | com.h2database 414 | h2 415 | 416 | 417 | 418 | ${project.artifactId} 419 | 420 | 421 | src/main/resources 422 | 423 | **/*.xls 424 | 425 | true 426 | 427 | 428 | src/main/resources 429 | 430 | **/*.xls 431 | 432 | false 433 | 434 | 435 | 436 | ${project.build.directory}/generated-resources 437 | 438 | 439 | 440 | 441 | 442 | org.codehaus.mojo 443 | build-helper-maven-plugin 444 | 445 | 446 | add-source 447 | generate-sources 448 | 449 | add-source 450 | 451 | 452 | 453 | ${project.build.directory}/generated-sources/java/ 454 | 455 | 456 | 457 | 458 | 459 | 460 | org.apache.maven.plugins 461 | maven-resources-plugin 462 | 2.6 463 | 464 | ${project.build.sourceEncoding} 465 | 466 | 467 | 468 | org.apache.maven.plugins 469 | maven-war-plugin 470 | 2.2 471 | 472 | false 473 | 474 | 475 | 476 | org.apache.maven.plugins 477 | maven-compiler-plugin 478 | 2.5.1 479 | 480 | ${java.version} 481 | ${java.version} 482 | -proc:none 483 | ${project.build.sourceEncoding} 484 | 485 | 486 | 487 | org.bsc.maven 488 | maven-processor-plugin 489 | 2.0.5 490 | 491 | 492 | process 493 | 494 | process 495 | 496 | generate-sources 497 | 498 | 499 | org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor 500 | 501 | ${project.build.directory}/generated-sources/java/ 502 | 503 | 504 | 505 | 506 | 507 | org.hibernate 508 | hibernate-jpamodelgen 509 | 1.2.0.Final 510 | 511 | 512 | 513 | 514 | org.codehaus.mojo 515 | aspectj-maven-plugin 516 | 1.8 517 | 519 | 520 | 522 | 523 | org.aspectj 524 | aspectjrt 525 | ${aspectj.version} 526 | 527 | 528 | org.aspectj 529 | aspectjtools 530 | ${aspectj.version} 531 | 532 | 533 | 534 | 535 | process-classes 536 | 537 | compile 538 | test-compile 539 | 540 | 541 | 542 | 543 | true 544 | 545 | 546 | org.springframework 547 | spring-aspects 548 | 549 | 551 | 552 | ${java.version} 553 | ${java.version} 554 | ${java.version} 555 | 556 | 557 | 558 | 559 | org.apache.maven.plugins 560 | maven-surefire-plugin 561 | 2.19 562 | 563 | true 564 | true 565 | 566 | **/*IntegrationTest* 567 | 568 | 569 | 570 | 571 | 572 | org.apache.maven.plugins 573 | maven-failsafe-plugin 574 | 2.12.4 575 | 576 | 577 | **/*IntegrationTest* 578 | 579 | 580 | 581 | 582 | integration-test 583 | 584 | integration-test 585 | 586 | 587 | 588 | verify 589 | 590 | verify 591 | 592 | 593 | 594 | 595 | 596 | 597 | org.eclipse.jetty 598 | jetty-maven-plugin 599 | 9.3.7.v20160115 600 | 601 | 10 602 | 8005 603 | STOP 604 | 605 | /angularjs-springmvc-sample 606 | 607 | 608 | 609 | 610 | start-jetty 611 | pre-integration-test 612 | 613 | stop 614 | start 615 | 616 | 617 | 0 618 | true 619 | 620 | 621 | 622 | stop-jetty 623 | post-integration-test 624 | 625 | stop 626 | 627 | 628 | 629 | 630 | 631 | 632 | org.codehaus.cargo 633 | cargo-maven2-plugin 634 | 635 | 636 | tomcat8x 637 | embedded 638 | 639 | 640 | 641 | 642 | 9000 643 | high 644 | 645 | 646 | 647 | 648 | 649 | 650 | org.apache.tomcat.maven 651 | tomcat7-maven-plugin 652 | 2.2 653 | 654 | /angularjs-springmvc-sample 655 | 656 | 657 | 658 | 659 | org.apache.maven.plugins 660 | maven-assembly-plugin 661 | 2.3 662 | 663 | 664 | jar-with-dependencies 665 | 666 | 667 | 668 | 669 | org.apache.maven.plugins 670 | maven-deploy-plugin 671 | 2.7 672 | 673 | 674 | 675 | org.apache.maven.plugins 676 | maven-eclipse-plugin 677 | 2.7 678 | 679 | 680 | true 681 | false 682 | 2.0 683 | 684 | 685 | org.eclipse.ajdt.core.ajbuilder 686 | 687 | org.springframework.aspects 688 | 689 | 690 | 691 | 692 | org.springframework.ide.eclipse.core.springbuilder 693 | 694 | 695 | 696 | org.eclipse.ajdt.ui.ajnature 697 | org.springframework.ide.eclipse.core.springnature 698 | 699 | 700 | 701 | 702 | org.apache.maven.plugins 703 | maven-idea-plugin 704 | 2.2 705 | 706 | true 707 | true 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | dev 717 | 718 | true 719 | 720 | 721 | 722 | DEBUG 723 | 724 | dev 725 | 726 | create 727 | true 728 | true 729 | 730 | 731 | 732 | 733 | 734 | 735 | src/main/resources-dev 736 | true 737 | 738 | 739 | 740 | 741 | 742 | staging 743 | 744 | 745 | 746 | root 747 | 748 | 749 | INFO 750 | 751 | staging 752 | 753 | update 754 | false 755 | false 756 | org.hibernate.dialect.MySQL5Dialect 757 | 758 | 759 | 760 | 761 | mysql 762 | mysql-connector-java 763 | 764 | 765 | 766 | 767 | 768 | src/main/resources-staging 769 | true 770 | 771 | 772 | 773 | 774 | 775 | prod 776 | 777 | 778 | 779 | root 780 | 781 | 782 | INFO 783 | 784 | prod 785 | 786 | none 787 | false 788 | false 789 | org.hibernate.dialect.MySQL5Dialect 790 | 791 | 792 | 793 | mysql 794 | mysql-connector-java 795 | 796 | 797 | 798 | 799 | 800 | src/main/resources-prod 801 | true 802 | 803 | 804 | 805 | 806 | 807 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/ApiErrors.java: -------------------------------------------------------------------------------- 1 | 2 | package com.hantsylabs.restexample.springmvc; 3 | 4 | /** 5 | * 6 | * @author Hantsy Bai 7 | */ 8 | public final class ApiErrors { 9 | 10 | private static final String PREFIX = "errors."; 11 | 12 | public static final String INVALID_REQUEST = PREFIX + "INVALID_REQUEST"; 13 | 14 | private ApiErrors() { 15 | throw new InstantiationError( "Must not instantiate this class" ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/Constants.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc; 2 | 3 | public final class Constants { 4 | 5 | /** 6 | * prefix of REST API 7 | */ 8 | public static final String URI_API = "/api"; 9 | 10 | public static final String URI_POSTS = "/posts"; 11 | 12 | public static final String URI_COMMENTS = "/comments"; 13 | 14 | private Constants() { 15 | throw new InstantiationError( "Must not instantiate this class" ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/DTOUtils.java: -------------------------------------------------------------------------------- 1 | 2 | package com.hantsylabs.restexample.springmvc; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import org.modelmapper.ModelMapper; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.PageImpl; 9 | import org.springframework.data.domain.PageRequest; 10 | 11 | /** 12 | * 13 | * @author Hantsy Bai 14 | */ 15 | public final class DTOUtils { 16 | 17 | private static final ModelMapper INSTANCE = new ModelMapper(); 18 | 19 | private DTOUtils() { 20 | throw new InstantiationError( "Must not instantiate this class" ); 21 | } 22 | 23 | public static T map(S source, Class targetClass) { 24 | return INSTANCE.map(source, targetClass); 25 | } 26 | 27 | public static void mapTo(S source, T dist) { 28 | INSTANCE.map(source, dist); 29 | } 30 | 31 | public static List mapList(List source, Class targetClass) { 32 | List list = new ArrayList<>(); 33 | for (int i = 0; i < source.size(); i++) { 34 | T target = INSTANCE.map(source.get(i), targetClass); 35 | list.add(target); 36 | } 37 | 38 | return list; 39 | } 40 | 41 | public static Page mapPage(Page source, Class targetClass) { 42 | List sourceList = source.getContent(); 43 | 44 | List list = new ArrayList<>(); 45 | for (int i = 0; i < sourceList.size(); i++) { 46 | T target = INSTANCE.map(sourceList.get(i), targetClass); 47 | list.add(target); 48 | } 49 | 50 | return new PageImpl<>(list, new PageRequest(source.getNumber(), source.getSize(), source.getSort()), 51 | source.getTotalElements()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/api/PingController.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.api; 2 | 3 | import com.hantsylabs.restexample.springmvc.Constants; 4 | import com.hantsylabs.restexample.springmvc.model.ResponseMessage; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping(value = Constants.URI_API) 12 | public class PingController{ 13 | 14 | /** 15 | * check if the network connecting is ok. 16 | * @return 17 | */ 18 | @RequestMapping("/ping") 19 | public ResponseEntity ping() { 20 | return new ResponseEntity<>(ResponseMessage.info("connected"), HttpStatus.OK); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/api/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.api; 2 | 3 | import com.hantsylabs.restexample.springmvc.ApiErrors; 4 | import com.hantsylabs.restexample.springmvc.exception.InvalidRequestException; 5 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 6 | import com.hantsylabs.restexample.springmvc.model.ResponseMessage; 7 | import java.util.List; 8 | import javax.inject.Inject; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.context.MessageSource; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.validation.BindingResult; 15 | import org.springframework.validation.FieldError; 16 | import org.springframework.web.bind.annotation.ControllerAdvice; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | import org.springframework.web.bind.annotation.ResponseBody; 19 | import org.springframework.web.bind.annotation.RestController; 20 | import org.springframework.web.context.request.WebRequest; 21 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 22 | 23 | /** 24 | * Called when an exception occurs during request processing. Transforms exception message into JSON format. 25 | */ 26 | @ControllerAdvice(annotations = RestController.class) 27 | public class RestExceptionHandler extends ResponseEntityExceptionHandler { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class); 30 | 31 | @Inject 32 | private MessageSource messageSource; 33 | 34 | @ExceptionHandler(value = {Exception.class, RuntimeException.class}) 35 | @ResponseBody 36 | public ResponseEntity handleGenericException(Exception ex, WebRequest request) { 37 | if (log.isDebugEnabled()) { 38 | log.debug("handling exception..."); 39 | } 40 | return new ResponseEntity<>(new ResponseMessage(ResponseMessage.Type.danger, ex.getMessage()), HttpStatus.BAD_REQUEST); 41 | } 42 | 43 | @ExceptionHandler(value = {ResourceNotFoundException.class}) 44 | @ResponseBody 45 | public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) { 46 | if (log.isDebugEnabled()) { 47 | log.debug("handling ResourceNotFoundException..."); 48 | } 49 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 50 | } 51 | 52 | 53 | @ExceptionHandler(value = {InvalidRequestException.class}) 54 | public ResponseEntity handleInvalidRequestException(InvalidRequestException ex, WebRequest req) { 55 | if (log.isDebugEnabled()) { 56 | log.debug("handling InvalidRequestException..."); 57 | } 58 | 59 | ResponseMessage alert = new ResponseMessage( 60 | ResponseMessage.Type.danger, 61 | ApiErrors.INVALID_REQUEST, 62 | messageSource.getMessage(ApiErrors.INVALID_REQUEST, new String[]{}, null)); 63 | 64 | BindingResult result = ex.getErrors(); 65 | 66 | List fieldErrors = result.getFieldErrors(); 67 | 68 | if (!fieldErrors.isEmpty()) { 69 | fieldErrors.stream().forEach(e -> { 70 | alert.addError(e.getField(), e.getCode(), e.getDefaultMessage()); 71 | }); 72 | } 73 | 74 | return new ResponseEntity<>(alert, HttpStatus.UNPROCESSABLE_ENTITY); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/api/UserController.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.api; 2 | 3 | import com.hantsylabs.restexample.springmvc.Constants; 4 | import java.security.Principal; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping(value = Constants.URI_API) 14 | public class UserController { 15 | 16 | /** 17 | * Get the authenticated user info. 18 | * 19 | * @param principal 20 | * @return 21 | */ 22 | @RequestMapping("/me") 23 | public ResponseEntity> user(Principal principal) { 24 | 25 | Map map = new HashMap<>(); 26 | map.put("username", principal.getName()); 27 | return new ResponseEntity<>(map, HttpStatus.OK); 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/api/post/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.api.post; 2 | 3 | import com.hantsylabs.restexample.springmvc.Constants; 4 | import com.hantsylabs.restexample.springmvc.service.BlogService; 5 | import javax.inject.Inject; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | import org.springframework.web.bind.annotation.ResponseBody; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | @RestController 17 | @RequestMapping(value = Constants.URI_API + Constants.URI_COMMENTS) 18 | public class CommentController { 19 | 20 | private static final Logger log = LoggerFactory 21 | .getLogger(CommentController.class); 22 | 23 | @Inject 24 | private BlogService blogService; 25 | 26 | @RequestMapping(value = "{id}", method = RequestMethod.DELETE) 27 | @ResponseBody 28 | public ResponseEntity deleteComment(@PathVariable("id") Long id) { 29 | if (log.isDebugEnabled()) { 30 | log.debug("get comments of post id @" + id); 31 | } 32 | 33 | blogService.deleteCommentById(id); 34 | 35 | return new ResponseEntity<>(HttpStatus.NO_CONTENT); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/api/post/PostController.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.api.post; 2 | 3 | import com.hantsylabs.restexample.springmvc.Constants; 4 | import com.hantsylabs.restexample.springmvc.domain.Post; 5 | import com.hantsylabs.restexample.springmvc.exception.InvalidRequestException; 6 | import com.hantsylabs.restexample.springmvc.model.CommentDetails; 7 | import com.hantsylabs.restexample.springmvc.model.CommentForm; 8 | import com.hantsylabs.restexample.springmvc.model.PostDetails; 9 | import com.hantsylabs.restexample.springmvc.model.PostForm; 10 | import com.hantsylabs.restexample.springmvc.model.ResponseMessage; 11 | import com.hantsylabs.restexample.springmvc.service.BlogService; 12 | import javax.inject.Inject; 13 | import javax.validation.Valid; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.data.domain.Page; 17 | import org.springframework.data.domain.Pageable; 18 | import org.springframework.data.domain.Sort.Direction; 19 | import org.springframework.data.web.PageableDefault; 20 | import org.springframework.http.HttpHeaders; 21 | import org.springframework.http.HttpStatus; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.validation.BindingResult; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.RequestBody; 26 | import org.springframework.web.bind.annotation.RequestMapping; 27 | import org.springframework.web.bind.annotation.RequestMethod; 28 | import org.springframework.web.bind.annotation.RequestParam; 29 | import org.springframework.web.bind.annotation.ResponseBody; 30 | import org.springframework.web.bind.annotation.RestController; 31 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 32 | 33 | @RestController 34 | @RequestMapping(value = Constants.URI_API + Constants.URI_POSTS) 35 | public class PostController { 36 | 37 | private static final Logger log = LoggerFactory 38 | .getLogger(PostController.class); 39 | 40 | private BlogService blogService; 41 | 42 | @Inject 43 | public PostController(BlogService blogService) { 44 | this.blogService = blogService; 45 | } 46 | 47 | @RequestMapping(value = "", method = RequestMethod.GET) 48 | @ResponseBody 49 | public ResponseEntity> getAllPosts( 50 | @RequestParam(value = "q", required = false) String keyword, // 51 | @RequestParam(value = "status", required = false) Post.Status status, // 52 | @PageableDefault(page = 0, size = 10, sort = "title", direction = Direction.DESC) Pageable page) { 53 | 54 | log.debug("get all posts of q@" + keyword + ", status @" + status + ", page@" + page); 55 | 56 | Page posts = blogService.searchPostsByCriteria(keyword, status, page); 57 | 58 | log.debug("get posts size @" + posts.getTotalElements()); 59 | 60 | return new ResponseEntity<>(posts, HttpStatus.OK); 61 | } 62 | 63 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 64 | @ResponseBody 65 | public ResponseEntity getPost(@PathVariable("id") Long id) { 66 | 67 | log.debug("get postsinfo by id @" + id); 68 | 69 | PostDetails post = blogService.findPostById(id); 70 | 71 | log.debug("get post @" + post); 72 | 73 | return new ResponseEntity<>(post, HttpStatus.OK); 74 | } 75 | 76 | @RequestMapping(value = "/{id}/comments", method = RequestMethod.GET) 77 | @ResponseBody 78 | public ResponseEntity> getCommentsOfPost( 79 | @PathVariable("id") Long id, 80 | @PageableDefault(page = 0, size = 10, sort = "createdDate", direction = Direction.DESC) Pageable page) { 81 | 82 | log.debug("get comments of post@" + id + ", page@" + page); 83 | 84 | Page commentsOfPost = blogService.findCommentsByPostId(id, page); 85 | 86 | log.debug("get post comment size @" + commentsOfPost.getTotalElements()); 87 | 88 | return new ResponseEntity<>(commentsOfPost, HttpStatus.OK); 89 | } 90 | 91 | @RequestMapping(value = "", method = RequestMethod.POST) 92 | @ResponseBody 93 | public ResponseEntity createPost(@RequestBody @Valid PostForm post, BindingResult errResult) { 94 | 95 | log.debug("create a new post"); 96 | if (errResult.hasErrors()) { 97 | throw new InvalidRequestException(errResult); 98 | } 99 | 100 | PostDetails saved = blogService.savePost(post); 101 | 102 | log.debug("saved post id is @" + saved.getId()); 103 | 104 | HttpHeaders headers = new HttpHeaders(); 105 | headers.setLocation(ServletUriComponentsBuilder.fromCurrentContextPath() 106 | .path(Constants.URI_API + Constants.URI_POSTS + "/{id}") 107 | .buildAndExpand(saved.getId()) 108 | .toUri() 109 | ); 110 | return new ResponseEntity<>(ResponseMessage.success("post.created"), headers, HttpStatus.CREATED); 111 | } 112 | 113 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 114 | @ResponseBody 115 | public ResponseEntity deletePostById(@PathVariable("id") Long id) { 116 | 117 | log.debug("delete post by id @" + id); 118 | 119 | blogService.deletePostById(id); 120 | 121 | return new ResponseEntity<>(ResponseMessage.success("post.updated"), HttpStatus.NO_CONTENT); 122 | } 123 | 124 | @RequestMapping(value = "/{id}/comments", method = RequestMethod.POST) 125 | @ResponseBody 126 | public ResponseEntity createCommentOfPost( 127 | @PathVariable("id") Long id, @RequestBody CommentForm comment) { 128 | 129 | log.debug("new comment of post@" + id + ", comment" + comment); 130 | 131 | CommentDetails saved = blogService.saveCommentOfPost(id, comment); 132 | 133 | log.debug("saved comment @" + saved.getId()); 134 | 135 | return new ResponseEntity<>(ResponseMessage.success("comment.created"), HttpStatus.CREATED); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import com.hantsylabs.restexample.springmvc.Constants; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.ComponentScan.Filter; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.FilterType; 8 | import org.springframework.context.annotation.PropertySource; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @Configuration 13 | @ComponentScan( 14 | basePackageClasses = {Constants.class}, 15 | excludeFilters = { 16 | @Filter( 17 | type = FilterType.ANNOTATION, 18 | value = { 19 | RestController.class, 20 | ControllerAdvice.class, 21 | Configuration.class 22 | } 23 | ) 24 | } 25 | ) 26 | @PropertySource("classpath:/app.properties") 27 | @PropertySource(value = "classpath:/database.properties", ignoreResourceNotFound = true) 28 | public class AppConfig { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/AppInitializer.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import javax.servlet.Filter; 4 | import org.springframework.core.annotation.Order; 5 | import org.springframework.web.filter.CharacterEncodingFilter; 6 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 7 | 8 | @Order(0) 9 | public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 10 | 11 | @Override 12 | protected Class[] getRootConfigClasses() { 13 | return new Class[] { 14 | AppConfig.class, // 15 | DataSourceConfig.class, // 16 | JpaConfig.class, // 17 | DataJpaConfig.class,// 18 | SecurityConfig.class,// 19 | Jackson2ObjectMapperConfig.class,// 20 | MessageSourceConfig.class 21 | }; 22 | } 23 | 24 | @Override 25 | protected Class[] getServletConfigClasses() { 26 | return new Class[] { 27 | WebConfig.class, // 28 | SwaggerConfig.class // 29 | }; 30 | } 31 | 32 | @Override 33 | protected String[] getServletMappings() { 34 | return new String[] { "/" }; 35 | } 36 | 37 | @Override 38 | protected Filter[] getServletFilters() { 39 | CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); 40 | encodingFilter.setEncoding("UTF-8"); 41 | encodingFilter.setForceEncoding(true); 42 | 43 | return new Filter[] { encodingFilter }; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/DataJpaConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 5 | 6 | @Configuration 7 | @EnableJpaRepositories(basePackages = {"com.hantsylabs.restexample.springmvc"}) 8 | public class DataJpaConfig { 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import javax.inject.Inject; 4 | import javax.sql.DataSource; 5 | import org.apache.commons.dbcp.BasicDataSource; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 11 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 12 | import org.springframework.jndi.JndiObjectFactoryBean; 13 | 14 | /** 15 | * Different configurations for different stages. 16 | * 17 | * In development stage using an embedded database to get better performance. 18 | * 19 | * In production, container managed DataSource is highly recommended. 20 | * 21 | * @author Hantsy Bai 22 | * 23 | */ 24 | @Configuration 25 | public class DataSourceConfig { 26 | 27 | private static final String ENV_JDBC_PASSWORD = "jdbc.password"; 28 | private static final String ENV_JDBC_USERNAME = "jdbc.username"; 29 | private static final String ENV_JDBC_URL = "jdbc.url"; 30 | 31 | @Inject 32 | private Environment env; 33 | 34 | @Bean 35 | @Profile("dev") 36 | public DataSource dataSource() { 37 | return new EmbeddedDatabaseBuilder() 38 | .setType(EmbeddedDatabaseType.H2) 39 | .build(); 40 | } 41 | 42 | @Bean 43 | @Profile("staging") 44 | public DataSource testDataSource() { 45 | BasicDataSource bds = new BasicDataSource(); 46 | bds.setDriverClassName("com.mysql.jdbc.Driver"); 47 | bds.setUrl(env.getProperty(ENV_JDBC_URL)); 48 | bds.setUsername(env.getProperty(ENV_JDBC_USERNAME)); 49 | bds.setPassword(env.getProperty(ENV_JDBC_PASSWORD)); 50 | return bds; 51 | } 52 | 53 | @Bean 54 | @Profile("prod") 55 | public DataSource prodDataSource() { 56 | JndiObjectFactoryBean ds = new JndiObjectFactoryBean(); 57 | ds.setLookupOnStartup(true); 58 | ds.setJndiName("jdbc/postDS"); 59 | ds.setCache(true); 60 | 61 | return (DataSource) ds.getObject(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/Jackson2ObjectMapperConfig.java: -------------------------------------------------------------------------------- 1 | 2 | package com.hantsylabs.restexample.springmvc.config; 3 | 4 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 10 | 11 | /** 12 | * 13 | * @author Hantsy Bai 14 | */ 15 | @Configuration 16 | public class Jackson2ObjectMapperConfig { 17 | 18 | @Bean 19 | public ObjectMapper objectMapper() { 20 | 21 | Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); 22 | builder.serializationInclusion(Include.NON_EMPTY); 23 | builder.featuresToDisable( 24 | // SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, 25 | DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, 26 | DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 27 | builder.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); 28 | 29 | return builder.build(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import java.util.Properties; 4 | import javax.inject.Inject; 5 | import javax.sql.DataSource; 6 | import org.hibernate.jpa.HibernatePersistenceProvider; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.context.annotation.AdviceMode; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.core.env.Environment; 13 | import org.springframework.orm.jpa.JpaTransactionManager; 14 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 15 | import org.springframework.transaction.PlatformTransactionManager; 16 | import org.springframework.transaction.annotation.EnableTransactionManagement; 17 | 18 | 19 | @Configuration 20 | @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) 21 | public class JpaConfig { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(JpaConfig.class); 24 | 25 | private static final String ENV_HIBERNATE_DIALECT = "hibernate.dialect"; 26 | private static final String ENV_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; 27 | private static final String ENV_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; 28 | private static final String ENV_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; 29 | 30 | @Inject 31 | private Environment env; 32 | 33 | @Inject 34 | private DataSource dataSource; 35 | 36 | @Bean 37 | public LocalContainerEntityManagerFactoryBean entityManagerFactory() { 38 | LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); 39 | emf.setDataSource(dataSource); 40 | emf.setPackagesToScan("com.hantsylabs.restexample.springmvc"); 41 | emf.setPersistenceProvider(new HibernatePersistenceProvider()); 42 | emf.setJpaProperties(jpaProperties()); 43 | return emf; 44 | } 45 | 46 | private Properties jpaProperties() { 47 | Properties extraProperties = new Properties(); 48 | extraProperties.put(ENV_HIBERNATE_FORMAT_SQL, env.getProperty(ENV_HIBERNATE_FORMAT_SQL)); 49 | extraProperties.put(ENV_HIBERNATE_SHOW_SQL, env.getProperty(ENV_HIBERNATE_SHOW_SQL)); 50 | extraProperties.put(ENV_HIBERNATE_HBM2DDL_AUTO, env.getProperty(ENV_HIBERNATE_HBM2DDL_AUTO)); 51 | if (log.isDebugEnabled()) { 52 | log.debug(" hibernate.dialect @" + env.getProperty(ENV_HIBERNATE_DIALECT)); 53 | } 54 | if (env.getProperty(ENV_HIBERNATE_DIALECT) != null) { 55 | extraProperties.put(ENV_HIBERNATE_DIALECT, env.getProperty(ENV_HIBERNATE_DIALECT)); 56 | } 57 | return extraProperties; 58 | } 59 | 60 | @Bean 61 | public PlatformTransactionManager transactionManager() { 62 | return new JpaTransactionManager(entityManagerFactory().getObject()); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/MessageSourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 6 | 7 | @Configuration 8 | public class MessageSourceConfig { 9 | 10 | @Bean 11 | public ReloadableResourceBundleMessageSource messageSource() { 12 | ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); 13 | messageSource.setDefaultEncoding("UTF-8"); 14 | messageSource.setBasenames("classpath:i18n/messages", "classpath:i18n/errors"); 15 | 16 | return messageSource; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.authentication.AuthenticationManager; 6 | import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.config.http.SessionCreationPolicy; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 18 | 19 | @Override 20 | public void configure(WebSecurity web) throws Exception { 21 | web 22 | .ignoring() 23 | .antMatchers("/**/*.html", // 24 | "/css/**", // 25 | "/js/**", // 26 | "/i18n/**",// 27 | "/libs/**",// 28 | "/img/**", // 29 | "/webjars/**",// 30 | "/ico/**"); 31 | } 32 | 33 | @Override 34 | protected void configure(HttpSecurity http) throws Exception { 35 | http 36 | .authorizeRequests() 37 | .antMatchers("/api/ping") 38 | .permitAll() 39 | .and() 40 | .authorizeRequests() 41 | .antMatchers("/api/**") 42 | .authenticated() 43 | .and() 44 | .authorizeRequests() 45 | .anyRequest() 46 | .permitAll() 47 | .and() 48 | .sessionManagement() 49 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 50 | .and() 51 | .httpBasic() 52 | .and() 53 | .csrf() 54 | .disable(); 55 | } 56 | 57 | @Override 58 | protected void configure(AuthenticationManagerBuilder auth) 59 | throws Exception { 60 | auth.inMemoryAuthentication() 61 | .passwordEncoder(passwordEncoder()) 62 | .withUser("admin").password("test123").authorities("ROLE_ADMIN") 63 | .and() 64 | .withUser("test").password("test123").authorities("ROLE_USER"); 65 | 66 | //auth.userDetailsService(new SimpleUserDetailsServiceImpl(userRepository)) 67 | //.passwordEncoder(passwordEncoder); 68 | } 69 | 70 | 71 | @Bean 72 | @Override 73 | public AuthenticationManager authenticationManagerBean() throws Exception { 74 | return super.authenticationManagerBean(); 75 | } 76 | 77 | @Bean 78 | @Override 79 | public UserDetailsService userDetailsServiceBean() throws Exception { 80 | return super.userDetailsServiceBean(); 81 | } 82 | 83 | @Bean 84 | public PlaintextPasswordEncoder passwordEncoder() { 85 | return new PlaintextPasswordEncoder(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/SecurityInitializer.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import org.springframework.core.annotation.Order; 4 | import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; 5 | 6 | 7 | @Order(1) 8 | public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import com.google.common.base.Predicate; 4 | import static com.google.common.base.Predicates.or; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import springfox.documentation.builders.ApiInfoBuilder; 9 | import static springfox.documentation.builders.PathSelectors.regex; 10 | import springfox.documentation.service.ApiInfo; 11 | import springfox.documentation.service.Contact; 12 | import springfox.documentation.spi.DocumentationType; 13 | import springfox.documentation.spring.web.plugins.Docket; 14 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 15 | 16 | /** 17 | * 18 | * @author Hantsy Bai 19 | */ 20 | @Configuration 21 | @EnableSwagger2 22 | // Loads the spring beans required by the framework 23 | @Profile(value = {"dev", "staging"}) 24 | public class SwaggerConfig { 25 | 26 | @Bean 27 | public Docket postsApi() { 28 | return new Docket(DocumentationType.SWAGGER_2) 29 | .groupName("public-api") 30 | .apiInfo(apiInfo()) 31 | .select() 32 | .paths(postPaths()) 33 | .build(); 34 | } 35 | 36 | private Predicate postPaths() { 37 | return or( 38 | regex("/api/posts.*"), 39 | regex("/api/comments.*") 40 | ); 41 | } 42 | 43 | private ApiInfo apiInfo() { 44 | return new ApiInfoBuilder() 45 | .title("SpringMVC Example API") 46 | .description("SpringMVC Example API reference for developers") 47 | .termsOfServiceUrl("http://hantsy.blogspot.com") 48 | .contact(new Contact("Hantsy Bai", "http://hantsy.blogspot.com", "hantsy@gmail.com")) 49 | .license("Apache License Version 2.0") 50 | .licenseUrl("https://github.com/springfox/springfox/blob/master/LICENSE") 51 | .version("2.0") 52 | .build(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hantsylabs.restexample.springmvc.Constants; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import javax.inject.Inject; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.ComponentScan; 13 | import org.springframework.context.annotation.ComponentScan.Filter; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.context.annotation.FilterType; 16 | import org.springframework.data.web.config.SpringDataWebConfiguration; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.converter.HttpMessageConverter; 19 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 20 | import org.springframework.stereotype.Controller; 21 | import org.springframework.web.bind.annotation.ControllerAdvice; 22 | import org.springframework.web.bind.annotation.RestController; 23 | import org.springframework.web.servlet.HandlerExceptionResolver; 24 | import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 25 | import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; 26 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 27 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 28 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 29 | import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; 30 | 31 | @Configuration 32 | @EnableWebMvc 33 | @ComponentScan( 34 | basePackageClasses = {Constants.class}, 35 | useDefaultFilters = false, 36 | includeFilters = { 37 | @Filter( 38 | type = FilterType.ANNOTATION, 39 | value = { 40 | Controller.class, 41 | RestController.class, 42 | ControllerAdvice.class 43 | } 44 | ) 45 | } 46 | ) 47 | public class WebConfig extends SpringDataWebConfiguration { 48 | 49 | private static final Logger logger = LoggerFactory.getLogger(WebConfig.class); 50 | 51 | @Inject 52 | private ObjectMapper objectMapper; 53 | 54 | @Override 55 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 56 | 57 | registry.addResourceHandler("/swagger-ui.html") 58 | .addResourceLocations("classpath:META-INF/resources/"); 59 | 60 | registry.addResourceHandler("/webjars/**") 61 | .addResourceLocations("classpath:META-INF/resources/webjars/"); 62 | } 63 | 64 | @Override 65 | public void addViewControllers(ViewControllerRegistry registry) { 66 | 67 | } 68 | 69 | @Override 70 | public void configureHandlerExceptionResolvers(List exceptionResolvers) { 71 | exceptionResolvers.add(exceptionHandlerExceptionResolver()); 72 | } 73 | 74 | @Override 75 | public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { 76 | configurer.enable(); 77 | } 78 | 79 | @Override 80 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 81 | configurer.favorParameter(false); 82 | configurer.favorPathExtension(false); 83 | } 84 | 85 | @Override 86 | public void configureMessageConverters(List> converters) { 87 | List> messageConverters = messageConverters(); 88 | converters.addAll(messageConverters); 89 | } 90 | 91 | @Bean 92 | public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { 93 | ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver(); 94 | exceptionHandlerExceptionResolver.setMessageConverters(messageConverters()); 95 | 96 | return exceptionHandlerExceptionResolver; 97 | } 98 | 99 | private List> messageConverters() { 100 | List> messageConverters = new ArrayList<>(); 101 | 102 | MappingJackson2HttpMessageConverter jackson2Converter = new MappingJackson2HttpMessageConverter(); 103 | jackson2Converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON)); 104 | jackson2Converter.setObjectMapper(objectMapper); 105 | 106 | messageConverters.add(jackson2Converter); 107 | return messageConverters; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/domain/Comment.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import java.io.Serializable; 5 | import java.util.Date; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.JoinColumn; 12 | import javax.persistence.ManyToOne; 13 | import javax.persistence.PrePersist; 14 | import javax.persistence.Table; 15 | import javax.persistence.Temporal; 16 | import javax.persistence.TemporalType; 17 | 18 | /** 19 | * 20 | * @author Hantsy Bai 21 | * 22 | */ 23 | @Entity 24 | @Table(name = "comments") 25 | @JsonIgnoreProperties("post") 26 | public class Comment implements Serializable { 27 | 28 | /** 29 | * 30 | */ 31 | private static final long serialVersionUID = 1L; 32 | 33 | @Id() 34 | @GeneratedValue(strategy = GenerationType.IDENTITY) 35 | @Column(name = "id") 36 | private Long id; 37 | 38 | @Column(name = "content") 39 | private String content; 40 | 41 | @JoinColumn(name = "post_id") 42 | @ManyToOne(optional = false) 43 | private Post post; 44 | 45 | @Column(name = "created_date") 46 | @Temporal(TemporalType.TIMESTAMP) 47 | private Date createdDate; 48 | 49 | public Long getId() { 50 | return id; 51 | } 52 | 53 | public void setId(Long id) { 54 | this.id = id; 55 | } 56 | 57 | public String getContent() { 58 | return content; 59 | } 60 | 61 | public void setContent(String content) { 62 | this.content = content; 63 | } 64 | 65 | public Post getPost() { 66 | return post; 67 | } 68 | 69 | public void setPost(Post post) { 70 | this.post = post; 71 | } 72 | 73 | public Date getCreatedDate() { 74 | return createdDate; 75 | } 76 | 77 | public void setCreatedDate(Date createdDate) { 78 | this.createdDate = createdDate; 79 | } 80 | 81 | @PrePersist 82 | public void prePersist() { 83 | this.createdDate = new Date(); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return "Comment{" + "id=" + id + ", content=" + content + ", post=" + post.getId() + ", createdDate=" + createdDate + '}'; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/domain/Post.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.domain; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.EnumType; 8 | import javax.persistence.Enumerated; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.PrePersist; 13 | import javax.persistence.Table; 14 | import javax.persistence.Temporal; 15 | import javax.persistence.TemporalType; 16 | import javax.validation.constraints.Size; 17 | import org.springframework.data.annotation.CreatedDate; 18 | 19 | /** 20 | * 21 | * @author Hantsy Bai 22 | * 23 | */ 24 | @Entity 25 | @Table(name = "posts") 26 | public class Post implements Serializable { 27 | 28 | /** 29 | * 30 | */ 31 | private static final long serialVersionUID = 1L; 32 | 33 | public enum Status { 34 | 35 | DRAFT, 36 | PUBLISHED 37 | } 38 | 39 | @Id() 40 | @GeneratedValue(strategy = GenerationType.IDENTITY) 41 | @Column(name = "id") 42 | private Long id; 43 | 44 | @Column(name = "title") 45 | private String title; 46 | 47 | @Column(name = "content") 48 | @Size(max = 2000) 49 | private String content; 50 | 51 | @Column(name = "status") 52 | @Enumerated(value = EnumType.STRING) 53 | private Status status = Status.DRAFT; 54 | 55 | @Column(name = "created_date") 56 | @Temporal(TemporalType.TIMESTAMP) 57 | private Date createdDate; 58 | 59 | public Long getId() { 60 | return id; 61 | } 62 | 63 | public void setId(Long id) { 64 | this.id = id; 65 | } 66 | 67 | public String getTitle() { 68 | return title; 69 | } 70 | 71 | public void setTitle(String title) { 72 | this.title = title; 73 | } 74 | 75 | public String getContent() { 76 | return content; 77 | } 78 | 79 | public void setContent(String content) { 80 | this.content = content; 81 | } 82 | 83 | public Status getStatus() { 84 | return status; 85 | } 86 | 87 | public void setStatus(Status status) { 88 | this.status = status; 89 | } 90 | 91 | public Date getCreatedDate() { 92 | return createdDate; 93 | } 94 | 95 | public void setCreatedDate(Date createdDate) { 96 | this.createdDate = createdDate; 97 | } 98 | 99 | @PrePersist 100 | public void prePersist() { 101 | this.createdDate = new Date(); 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | return "Post{" + "id=" + id + ", title=" + title + ", content=" + content + ", status=" + status + '}'; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/exception/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.exception; 2 | 3 | import org.springframework.validation.BindingResult; 4 | 5 | /** 6 | * 7 | * @author Hantsy Bai 8 | */ 9 | public class InvalidRequestException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private final BindingResult errors; 14 | 15 | public InvalidRequestException(BindingResult errors) { 16 | this.errors = errors; 17 | } 18 | 19 | public BindingResult getErrors() { 20 | return this.errors; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | 2 | package com.hantsylabs.restexample.springmvc.exception; 3 | 4 | /** 5 | * 6 | * @author Hantsy Bai 7 | */ 8 | public class ResourceNotFoundException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private final Long id; 13 | 14 | public ResourceNotFoundException(Long id) { 15 | this.id = id; 16 | } 17 | 18 | public Long getId() { 19 | return id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/model/CommentDetails.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | /** 7 | * 8 | * @author Hantsy Bai 9 | * 10 | */ 11 | public class CommentDetails implements Serializable { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = 1L; 17 | 18 | private Long id; 19 | 20 | private String content; 21 | 22 | private Date createdDate; 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public void setId(Long id) { 29 | this.id = id; 30 | } 31 | 32 | public String getContent() { 33 | return content; 34 | } 35 | 36 | public void setContent(String content) { 37 | this.content = content; 38 | } 39 | 40 | public Date getCreatedDate() { 41 | return createdDate; 42 | } 43 | 44 | public void setCreatedDate(Date createdDate) { 45 | this.createdDate = createdDate; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "CommentDetails{" + "id=" + id + ", content=" + content + ", createdDate=" + createdDate + '}'; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/model/CommentForm.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.model; 2 | 3 | 4 | import java.io.Serializable; 5 | 6 | 7 | /** 8 | * 9 | * @author Hantsy Bai 10 | * 11 | */ 12 | public class CommentForm implements Serializable { 13 | 14 | /** 15 | * 16 | */ 17 | private static final long serialVersionUID = 1L; 18 | 19 | 20 | private String content; 21 | 22 | public String getContent() { 23 | return content; 24 | } 25 | 26 | public void setContent(String content) { 27 | this.content = content; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "CommentForm{ content=" + content + '}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/model/PostDetails.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | /** 7 | * 8 | * @author Hantsy Bai 9 | * 10 | */ 11 | public class PostDetails implements Serializable { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = 1L; 17 | 18 | private Long id; 19 | 20 | private String title; 21 | 22 | private String content; 23 | 24 | private String status; 25 | 26 | private Date createdDate; 27 | 28 | public Long getId() { 29 | return id; 30 | } 31 | 32 | public void setId(Long id) { 33 | this.id = id; 34 | } 35 | 36 | public String getTitle() { 37 | return title; 38 | } 39 | 40 | public void setTitle(String title) { 41 | this.title = title; 42 | } 43 | 44 | public String getContent() { 45 | return content; 46 | } 47 | 48 | public void setContent(String content) { 49 | this.content = content; 50 | } 51 | 52 | public String getStatus() { 53 | return status; 54 | } 55 | 56 | public void setStatus(String status) { 57 | this.status = status; 58 | } 59 | 60 | public Date getCreatedDate() { 61 | return createdDate; 62 | } 63 | 64 | public void setCreatedDate(Date createdDate) { 65 | this.createdDate = createdDate; 66 | } 67 | 68 | 69 | @Override 70 | public String toString() { 71 | return "PostDetails{" + "id=" + id + ", title=" + title + ", content=" + content + ", status=" + status + ", createdDate=" + createdDate + '}'; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/model/PostForm.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.model; 2 | 3 | import com.hantsylabs.restexample.springmvc.DTOUtils; 4 | import com.hantsylabs.restexample.springmvc.domain.Post; 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 9 | * @author Hantsy Bai 10 | * 11 | */ 12 | public class PostForm implements Serializable { 13 | 14 | /** 15 | * 16 | */ 17 | private static final long serialVersionUID = 1L; 18 | 19 | private String title; 20 | 21 | private String content; 22 | 23 | public String getTitle() { 24 | return title; 25 | } 26 | 27 | public void setTitle(String title) { 28 | this.title = title; 29 | } 30 | 31 | public String getContent() { 32 | return content; 33 | } 34 | 35 | public void setContent(String content) { 36 | this.content = content; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "PostForm{" + "title=" + title + ", content=" + content + '}'; 42 | } 43 | 44 | public Post toEntity() { 45 | return DTOUtils.map(this, Post.class); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/model/ResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Used to transport messages back to the client. 8 | * 9 | * @author Hantsy Bai 10 | */ 11 | public class ResponseMessage { 12 | 13 | public enum Type { 14 | success, warning, danger, info; 15 | } 16 | 17 | private Type type; 18 | private String text; 19 | private String code; 20 | 21 | private List errors = new ArrayList(); 22 | 23 | public ResponseMessage(Type type, String text) { 24 | this.type = type; 25 | this.text = text; 26 | } 27 | 28 | public ResponseMessage(Type type, String code, String message) { 29 | this.type = type; 30 | this.code = code; 31 | this.text = message; 32 | } 33 | 34 | public String getText() { 35 | return text; 36 | } 37 | 38 | public Type getType() { 39 | return type; 40 | } 41 | 42 | public String getCode() { 43 | return code; 44 | } 45 | 46 | public static ResponseMessage success(String text) { 47 | return new ResponseMessage(Type.success, text); 48 | } 49 | 50 | public static ResponseMessage warning(String text) { 51 | return new ResponseMessage(Type.warning, text); 52 | } 53 | 54 | public static ResponseMessage danger(String text) { 55 | return new ResponseMessage(Type.danger, text); 56 | } 57 | 58 | public static ResponseMessage info(String text) { 59 | return new ResponseMessage(Type.info, text); 60 | } 61 | 62 | 63 | public List getErrors() { 64 | return errors; 65 | } 66 | 67 | public void setErrors(List errors) { 68 | this.errors = errors; 69 | } 70 | 71 | public void addError(String field, String code, String message) { 72 | this.errors.add(new Error(field, code, message)); 73 | } 74 | 75 | class Error { 76 | 77 | private final String code; 78 | private final String message; 79 | private final String field; 80 | 81 | private Error(String field, String code, String message) { 82 | this.field = field; 83 | this.code = code; 84 | this.message = message; 85 | } 86 | 87 | public String getCode() { 88 | return code; 89 | } 90 | 91 | public String getMessage() { 92 | return message; 93 | } 94 | 95 | public String getField() { 96 | return field; 97 | } 98 | 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.repository; 2 | 3 | import com.hantsylabs.restexample.springmvc.domain.Comment; 4 | import com.hantsylabs.restexample.springmvc.domain.Post; 5 | import java.util.List; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | 10 | 11 | public interface CommentRepository extends JpaRepository { 12 | 13 | public List findByPost(Post post); 14 | 15 | public Page findByPostId(Long id, Pageable page); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.repository; 2 | 3 | import com.hantsylabs.restexample.springmvc.domain.Post; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | 7 | public interface PostRepository extends JpaRepository,// 8 | JpaSpecificationExecutor{ 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/repository/PostSpecifications.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.repository; 2 | 3 | import com.hantsylabs.restexample.springmvc.domain.Post; 4 | import com.hantsylabs.restexample.springmvc.domain.Post_; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import javax.persistence.criteria.CriteriaBuilder; 8 | import javax.persistence.criteria.CriteriaQuery; 9 | import javax.persistence.criteria.Predicate; 10 | import javax.persistence.criteria.Root; 11 | import org.springframework.data.jpa.domain.Specification; 12 | import org.springframework.util.StringUtils; 13 | 14 | /** 15 | * 16 | * @author Hantsy Bai 17 | * 18 | */ 19 | public final class PostSpecifications { 20 | 21 | private PostSpecifications() { 22 | throw new InstantiationError( "Must not instantiate this class" ); 23 | } 24 | 25 | public static Specification filterByKeywordAndStatus( 26 | final String keyword,// 27 | final Post.Status status) { 28 | return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { 29 | List predicates = new ArrayList<>(); 30 | if (StringUtils.hasText(keyword)) { 31 | predicates.add( 32 | cb.or( 33 | cb.like(root.get(Post_.title), "%" + keyword + "%"), 34 | cb.like(root.get(Post_.content), "%" + keyword + "%") 35 | ) 36 | ); 37 | } 38 | 39 | if (status != null) { 40 | predicates.add(cb.equal(root.get(Post_.status), status)); 41 | } 42 | 43 | return cb.and(predicates.toArray(new Predicate[predicates.size()])); 44 | }; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/hantsylabs/restexample/springmvc/service/BlogService.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.service; 2 | 3 | import com.hantsylabs.restexample.springmvc.DTOUtils; 4 | import com.hantsylabs.restexample.springmvc.domain.Comment; 5 | import com.hantsylabs.restexample.springmvc.domain.Post; 6 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 7 | import com.hantsylabs.restexample.springmvc.model.CommentDetails; 8 | import com.hantsylabs.restexample.springmvc.model.CommentForm; 9 | import com.hantsylabs.restexample.springmvc.model.PostDetails; 10 | import com.hantsylabs.restexample.springmvc.model.PostForm; 11 | import com.hantsylabs.restexample.springmvc.repository.CommentRepository; 12 | import com.hantsylabs.restexample.springmvc.repository.PostRepository; 13 | import com.hantsylabs.restexample.springmvc.repository.PostSpecifications; 14 | import javax.inject.Inject; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.data.domain.Page; 18 | import org.springframework.data.domain.Pageable; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | import org.springframework.util.Assert; 22 | 23 | /** 24 | * 25 | * @author Hantsy Bai 26 | */ 27 | @Service 28 | @Transactional 29 | public class BlogService { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(BlogService.class); 32 | 33 | private PostRepository postRepository; 34 | 35 | private CommentRepository commentRepository; 36 | 37 | @Inject 38 | public BlogService(PostRepository postRepository, CommentRepository commentRepository){ 39 | this.postRepository = postRepository; 40 | this.commentRepository = commentRepository; 41 | } 42 | 43 | public Page searchPostsByCriteria(String q, Post.Status status, Pageable page) { 44 | 45 | log.debug("search posts by keyword@" + q + ", page @" + page); 46 | 47 | Page posts = postRepository.findAll(PostSpecifications.filterByKeywordAndStatus(q, status), 48 | page); 49 | 50 | log.debug("get posts size @" + posts.getTotalElements()); 51 | 52 | return DTOUtils.mapPage(posts, PostDetails.class); 53 | } 54 | 55 | public PostDetails savePost(PostForm form) { 56 | 57 | log.debug("save post @" + form); 58 | 59 | Post post = DTOUtils.map(form, Post.class); 60 | 61 | Post saved = postRepository.save(post); 62 | 63 | log.debug("saved post is @" + saved); 64 | 65 | return DTOUtils.map(saved, PostDetails.class); 66 | 67 | } 68 | 69 | public PostDetails updatePost(Long id, PostForm form) { 70 | Assert.notNull(id, "post id can not be null"); 71 | 72 | log.debug("update post @" + form); 73 | 74 | Post post = postRepository.findOne(id); 75 | DTOUtils.mapTo(form, post); 76 | 77 | Post saved = postRepository.save(post); 78 | 79 | log.debug("updated post@" + saved); 80 | 81 | return DTOUtils.map(saved, PostDetails.class); 82 | } 83 | 84 | public PostDetails findPostById(Long id) { 85 | 86 | Assert.notNull(id, "post id can not be null"); 87 | 88 | log.debug("find post by id@" + id); 89 | 90 | Post post = postRepository.findOne(id); 91 | 92 | if (post == null) { 93 | throw new ResourceNotFoundException(id); 94 | } 95 | 96 | return DTOUtils.map(post, PostDetails.class); 97 | } 98 | 99 | public Page findCommentsByPostId(Long id, Pageable page) { 100 | 101 | log.debug("find comments by post id@" + id); 102 | 103 | Page comments = commentRepository.findByPostId(id, page); 104 | 105 | log.debug("found results@" + comments.getTotalElements()); 106 | 107 | return DTOUtils.mapPage(comments, CommentDetails.class); 108 | } 109 | 110 | public CommentDetails saveCommentOfPost(Long id, CommentForm fm) { 111 | Assert.notNull(id, "post id can not be null"); 112 | 113 | log.debug("find post by id@" + id); 114 | 115 | Post post = postRepository.findOne(id); 116 | 117 | if (post == null) { 118 | throw new ResourceNotFoundException(id); 119 | } 120 | 121 | Comment comment = DTOUtils.map(fm, Comment.class); 122 | 123 | comment.setPost(post); 124 | 125 | comment = commentRepository.save(comment); 126 | 127 | log.debug("comment saved@" + comment); 128 | 129 | return DTOUtils.map(comment, CommentDetails.class); 130 | } 131 | 132 | public boolean deletePostById(Long id) { 133 | Assert.notNull(id, "post id can not be null"); 134 | 135 | log.debug("find post by id@" + id); 136 | 137 | Post post = postRepository.findOne(id); 138 | 139 | if (post == null) { 140 | throw new ResourceNotFoundException(id); 141 | } 142 | 143 | postRepository.delete(post); 144 | 145 | return true; 146 | } 147 | 148 | public void deleteCommentById(Long id) { 149 | Assert.notNull(id, "comment id can not be null"); 150 | 151 | log.debug("delete comment by id@" + id); 152 | 153 | Comment comment = commentRepository.findOne(id); 154 | 155 | if (comment == null) { 156 | throw new ResourceNotFoundException(id); 157 | } 158 | 159 | commentRepository.delete(comment); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/resources-dev/database.properties: -------------------------------------------------------------------------------- 1 | hibernate.format_sql=@hibernate.format_sql@ 2 | hibernate.show_sql=@hibernate.show_sql@ 3 | hibernate.hbm2ddl.auto=@hibernate.hbm2ddl.auto@ 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources-prod/database.properties: -------------------------------------------------------------------------------- 1 | hibernate.format_sql=@hibernate.format_sql@ 2 | hibernate.show_sql=@hibernate.show_sql@ 3 | hibernate.hbm2ddl.auto=@hibernate.hbm2ddl.auto@ 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources-staging/database.properties: -------------------------------------------------------------------------------- 1 | hibernate.format_sql=@hibernate.format_sql@ 2 | hibernate.show_sql=@hibernate.show_sql@ 3 | hibernate.hbm2ddl.auto=@hibernate.hbm2ddl.auto@ 4 | 5 | jdbc.url=@jdbc.url@ 6 | jdbc.username=@jdbc.username@ 7 | jdbc.password=@jdbc.password@ 8 | hibernate.dialect=@hibernate.dialect@ 9 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/app.properties: -------------------------------------------------------------------------------- 1 | #app config properties 2 | spring.profiles.active=@spring.profiles.active@ 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/i18n/errors.properties: -------------------------------------------------------------------------------- 1 | 2 | errors.INVALID_REQUEST=request data is invalid. 3 | -------------------------------------------------------------------------------- /src/main/resources/i18n/messsages.properties: -------------------------------------------------------------------------------- 1 | # To change this license header, choose License Headers in Project Properties. 2 | # To change this template file, choose Tools | Templates 3 | # and open the template in the editor. 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | -- insert into Users (id, username, password, displayName, role) values (1, 'admin', '$2a$10$bycJzXyLhZe3NSKuobGZT.ITSfm4FzunftMhXZrSqG.VHGe8BIFjK', 'Administrator', 'ADMIN'),(2, 'ouser1', '$2a$10$jz7rjZpt1atuZGzBpW/j0eUNkmab7iu4lFVtYonZI0dBSFV1ClZDu', 'Operator 1', 'OPERATOR'), (3, 'euser1', '$2a$10$.qLhCp/GQuM7BwITP0Gyo.XtMEEU.u42Zs3lzcWUIM63MxfoqddSS', 'Engineer 1', 'ENGINEER') 2 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, R, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 5 | 6 | # Print the date in ISO 8601 format 7 | log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 8 | 9 | log4j.appender.R=org.apache.log4j.RollingFileAppender 10 | log4j.appender.R.File=application.log 11 | 12 | log4j.appender.R.MaxFileSize=100KB 13 | # Keep one backup file 14 | log4j.appender.R.MaxBackupIndex=1 15 | 16 | log4j.appender.R.layout=org.apache.log4j.PatternLayout 17 | log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n 18 | 19 | log4j.logger.org.springframework.security=debug 20 | log4j.logger.com.hantsylabs=@log4j.level@ 21 | 22 | -------------------------------------------------------------------------------- /src/main/webapp/META-INF/context.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/webapp/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | } 4 | 5 | .page{ 6 | min-height: 400px; 7 | } 8 | 9 | footer{ 10 | margin-top:50px; 11 | padding-top:50px; 12 | } 13 | 14 | .toolbar{ 15 | margin:10px; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/webapp/i18n/messages_en.properties: -------------------------------------------------------------------------------- 1 | appName=REST EXAMPLE APP 2 | search=Search 3 | personalHome=Personal Home Page 4 | username=Username 5 | password=Password 6 | displayName=Display Name 7 | role=Role 8 | save=Save 9 | cancel=Cancel 10 | changePassword=Change Password 11 | updateProfile=Update Profile 12 | oldPassword=Old Password 13 | newPassword=New Password 14 | currentPasswordIsWrong=Current password is wrong! 15 | passwordUpdated=Password is updated successfully! 16 | profileUpdated=Profile is updated successfully! 17 | logout=Logout 18 | login=Login 19 | availableActions=AVAILABLE ACTIONS 20 | userAdmin=User Administration 21 | actions=ACTIONS 22 | userSaved=User is saved! 23 | userDeleted=User is deleted! 24 | addUser=Add User 25 | signin=Sign in 26 | ok=OK 27 | posts=Posts 28 | newPost=New Post 29 | title=Title 30 | createdOn=Created Date 31 | createdBy=Created By 32 | administration=Administration 33 | content=Content 34 | post.created=Post was created successfully! 35 | post.deleted=Post was deleted successfully! 36 | comment.created=Comment was created successfully! 37 | comment.deleted=Comment was deleted successfully! 38 | comment=Comment 39 | addComment=Add Comment 40 | delete=Delete 41 | status=Status 42 | name=Name 43 | ALL=ALL 44 | DRAFT=DRAFT 45 | PUBLISHED=PUBLISHED 46 | connected=Connected! 47 | -------------------------------------------------------------------------------- /src/main/webapp/i18n/messages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | appName=REST \u5b9e\u4f8b 2 | search=\u67e5\u8be2 3 | 4 | personalHome=\u4e2a\u4eba\u8d44\u6599 5 | username=\u7528\u6237\u540d 6 | password=\u5bc6\u7801 7 | name=\u59d3\u540d 8 | role=\u89d2\u8272 9 | save=\u4fdd\u5b58 10 | cancel=\u53d6\u6d88 11 | changePassword=\u4fee\u6539\u5bc6\u7801 12 | updateProfile=\u66f4\u65b0\u4e2a\u4eba\u8d44\u6599 13 | oldPassword=\u5f53\u524d\u5bc6\u7801 14 | newPassword=\u65b0\u5bc6\u7801 15 | currentPasswordIsWrong=\u5f53\u524d\u5bc6\u7801\u9519\u8bef! 16 | passwordUpdated=\u5bc6\u7801\u66f4\u65b0\u6210\u529f! 17 | profileUpdated=\u4e2a\u4eba\u8d44\u6599\u5df2\u7ecf\u66f4\u65b0\u6210\u529f! 18 | logout=\u767b\u51fa 19 | login=\u767b\u5165 20 | availableActions=\u53ef\u7528\u64cd\u4f5c 21 | userAdmin=\u7528\u6237\u7ba1\u7406 22 | actions=\u64cd\u4f5c 23 | userSaved=\u7528\u6237\u5df2\u4fdd\u5b58! 24 | userDeleted=\u7528\u6237\u5df2\u7ecf\u5220\u9664! 25 | addUser=\u65b0\u589e\u7528\u6237 26 | signin=\u767b\u5f55 27 | ok=\u786e\u5b9a 28 | 29 | posts=\u6240\u6709\u6587\u7ae0 30 | newPost=\u65b0\u5efa\u535a\u6587 31 | title=\u6807\u9898 32 | createdOn=\u521b\u5efa\u65f6\u95f4 33 | createdBy=\u4f5c\u8005 34 | administration=\u7ba1\u7406 35 | content=\u5185\u5bb9 36 | post.created=\u6587\u7ae0\u5df2\u7ecf\u53d1\u5e03! 37 | post.deleted=\u6587\u7ae0\u5df2\u7ecf\u5220\u9664! 38 | comment.created=\u8bc4\u8bba\u5df2\u7ecf\u53d1\u5e03! 39 | comment.deleted=\u8bc4\u8bba\u5df2\u7ecf\u5220\u9664! 40 | comment=\u8bc4\u8bba 41 | addComment=\u6dfb\u52a0\u8bc4\u8bba 42 | delete=\u5220\u9664 43 | status=\u72b6\u6001 44 | ALL=\u5168\u90e8 45 | DRAFT=\u8349\u7a3f 46 | PUBLISHED=\u5df2\u53d1\u5e03 47 | connected=Connected! 48 | -------------------------------------------------------------------------------- /src/main/webapp/ico/apple-touch-icon-144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angularjs-springmvc-sample/198bc5f5eeedab8c47f828379097849e77003d6c/src/main/webapp/ico/apple-touch-icon-144-precomposed.png -------------------------------------------------------------------------------- /src/main/webapp/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angularjs-springmvc-sample/198bc5f5eeedab8c47f828379097849e77003d6c/src/main/webapp/ico/favicon.ico -------------------------------------------------------------------------------- /src/main/webapp/img/na.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angularjs-springmvc-sample/198bc5f5eeedab8c47f828379097849e77003d6c/src/main/webapp/img/na.jpg -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/webapp/js/base64.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var as = angular.module('exampleApp.services', []); 3 | as.service('base64', function () { 4 | var keyStr = "ABCDEFGHIJKLMNOP" + 5 | "QRSTUVWXYZabcdef" + 6 | "ghijklmnopqrstuv" + "wxyz0123456789+/" + 7 | "="; 8 | this.encode = function (input) { var output = "", 9 | chr1, chr2, chr3 = "", 10 | enc1, enc2, enc3, enc4 = "", 11 | i = 0; 12 | while (i < input.length) { 13 | chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); 14 | chr3 = input.charCodeAt(i++); 15 | enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 16 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 17 | enc4 = chr3 & 63; 18 | if (isNaN(chr2)) { 19 | enc3 = enc4 = 64; 20 | } else if (isNaN(chr3)) { 21 | enc4 = 64; 22 | } 23 | output = output + 24 | keyStr.charAt(enc1) + 25 | keyStr.charAt(enc2) + 26 | keyStr.charAt(enc3) + 27 | keyStr.charAt(enc4); 28 | chr1 = chr2 = chr3 = ""; 29 | enc1 = enc2 = enc3 = enc4 = ""; 30 | } 31 | 32 | return output; 33 | }; 34 | this.decode = function (input) { 35 | var output = "", 36 | chr1, chr2, chr3 = "", 37 | enc1, enc2, enc3, enc4 = "", 38 | i = 0; 39 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or = 40 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 41 | while (i < input.length) { 42 | enc1 = keyStr.indexOf(input.charAt(i++)); 43 | enc2 = keyStr.indexOf(input.charAt(i++)); 44 | enc3 = keyStr.indexOf(input.charAt(i++)); 45 | enc4 = keyStr.indexOf(input.charAt(i++)); 46 | chr1 = (enc1 << 2) | (enc2 >> 4); 47 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 48 | chr3 = ((enc3 & 3) << 6) | enc4; 49 | output = output + String.fromCharCode(chr1); 50 | if (enc3 != 64) { 51 | output = output + String.fromCharCode(chr2); 52 | } 53 | if (enc4 != 64) { 54 | output = output + String.fromCharCode(chr3); 55 | } 56 | 57 | chr1 = chr2 = chr3 = ""; 58 | enc1 = enc2 = enc3 = enc4 = ""; 59 | } 60 | }; 61 | }); 62 | })(); -------------------------------------------------------------------------------- /src/main/webapp/js/controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var as = angular.module('exampleApp.controllers', []); 3 | 4 | as.controller('MainController', function ($q, $scope, $rootScope, $http, i18n, $location) { 5 | var load = function () { 6 | }; 7 | 8 | load(); 9 | 10 | $scope.language = function () { 11 | return i18n.language; 12 | }; 13 | $scope.setLanguage = function (lang) { 14 | i18n.setLanguage(lang); 15 | }; 16 | $scope.activeWhen = function (value) { 17 | return value ? 'active' : ''; 18 | }; 19 | 20 | $scope.path = function () { 21 | return $location.url(); 22 | }; 23 | 24 | $scope.logout = function () { 25 | $rootScope.user = null; 26 | $scope.username = $scope.password = null; 27 | $scope.$emit('event:logoutRequest'); 28 | $location.url('/login'); 29 | }; 30 | 31 | }); 32 | 33 | as.controller('LoginController', function ($scope, $rootScope, $http, base64, $location) { 34 | 35 | $scope.login = function () { 36 | console.log('username:password @' + $scope.username + ',' + $scope.password); 37 | $scope.$emit('event:loginRequest', $scope.username, $scope.password); 38 | // $('#login').modal('hide'); 39 | }; 40 | }); 41 | 42 | as.controller('NewPostController', function ($scope, $http, i18n, $location) { 43 | var actionUrl = 'api/posts/'; 44 | 45 | $scope.save = function () { 46 | $http.post(actionUrl, $scope.newPost).success(function () { 47 | $location.path('/posts'); 48 | }); 49 | }; 50 | 51 | 52 | $scope.cancel = function () { 53 | $location.path('/posts'); 54 | }; 55 | 56 | }); 57 | 58 | as.controller('DetailsController', function ($scope, $http, $routeParams, $q) { 59 | $scope.p = 1; 60 | var actionUrl = 'api/posts/', 61 | loadComments = function () { 62 | $http.get(actionUrl + $routeParams.id + '/comments') 63 | .success(function (data) { 64 | $scope.comments = data.content; 65 | $scope.totalItems = data.totalElements; 66 | }); 67 | }, 68 | load = function () { 69 | $q.all([ 70 | $http.get(actionUrl + $routeParams.id), 71 | $http.get(actionUrl + $routeParams.id + '/comments') 72 | ]) 73 | .then(function (result) { 74 | $scope.post = result[0].data; 75 | $scope.comments = result[1].data.content; 76 | $scope.totalItems = result[1].data.totalElements; 77 | }); 78 | }; 79 | 80 | load(); 81 | 82 | $scope.newComment = {}; 83 | 84 | $scope.save = function () { 85 | $http.post(actionUrl + $routeParams.id + '/comments', $scope.newComment).success(function () { 86 | $('#commentDialog').modal('hide'); 87 | loadComments(); 88 | $scope.newComment = {}; 89 | }); 90 | }; 91 | 92 | $scope.delComment = function (idx) { 93 | $http.delete('api/comments/' + $scope.comments[idx].id).success(function () { 94 | $scope.comments.splice(idx, 1); 95 | }); 96 | }; 97 | 98 | $scope.addComment = function () { 99 | $('#commentDialog').modal('show'); 100 | }; 101 | 102 | $scope.search = function () { 103 | loadComments(); 104 | }; 105 | 106 | }); 107 | 108 | as.controller('PostsController', function ($scope, $http, $location, i18n) { 109 | $scope.p = 1; 110 | $scope.q = ''; 111 | $scope.statusOpt = {'label': $.i18n.prop('ALL'), 'value': 'ALL'}; 112 | $scope.statusOpts = [ 113 | {'label': $.i18n.prop('ALL'), 'value': 'ALL'}, 114 | {'label': $.i18n.prop('DRAFT'), 'value': 'DRAFT'}, 115 | {'label': $.i18n.prop('PUBLISHED'), 'value': 'PUBLISHED'} 116 | ]; 117 | 118 | 119 | 120 | var actionUrl = 'api/posts/', 121 | load = function () { 122 | $http.get(actionUrl + '?q=' + $scope.q 123 | + '&status=' + ($scope.statusOpt.value == 'ALL' ? '' : $scope.statusOpt.value) 124 | + '&page=' + ($scope.p - 1)) 125 | .success(function (data) { 126 | $scope.posts = data.content; 127 | $scope.totalItems = data.totalElements; 128 | }); 129 | }; 130 | 131 | load(); 132 | 133 | $scope.search = function () { 134 | load(); 135 | }; 136 | 137 | $scope.toggleStatus = function (r) { 138 | $scope.statusOpt = r; 139 | }; 140 | 141 | $scope.add = function () { 142 | $location.path('/posts/new'); 143 | }; 144 | 145 | $scope.delPost = function (idx) { 146 | console.log('delete index @' + idx + ', id is@' + $scope.users[idx].id); 147 | if (confirm($.i18n.prop('confirm.delete'))) { 148 | $http.delete(actionUrl + $scope.posts[idx].id) 149 | .success(function () { 150 | $scope.posts.splice(idx, 1); 151 | }); 152 | } 153 | }; 154 | 155 | }); 156 | 157 | }()); -------------------------------------------------------------------------------- /src/main/webapp/js/filter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var app = angular.module('exampleApp.filters', []); 3 | 4 | app.filter('range', function() { 5 | return function(input, total) { 6 | total = parseInt(total); 7 | for (var i = 0; i < total; i++) 8 | input.push(i); 9 | return input; 10 | }; 11 | }); 12 | 13 | }()); 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webapp/js/i18n.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var as = angular.module('exampleApp.i18n',[]); 3 | 4 | as.service('i18n', function () { 5 | var self = this; 6 | this.setLanguage = function (language) { 7 | $.i18n.properties({ 8 | name: 'messages', 9 | path: 'i18n/', 10 | mode: 'map', 11 | language: language, 12 | callback: function () { 13 | self.language = language; 14 | } 15 | }); 16 | }; 17 | this.setLanguage('zh_CN'); 18 | }); 19 | 20 | as.directive('msg', function () { 21 | return { 22 | restrict: 'EA', 23 | link: function (scope, element, attrs) { 24 | var key = attrs.key; 25 | if (attrs.keyExpr) { 26 | scope.$watch(attrs.keyExpr, function (value) { 27 | key = value; 28 | element.text($.i18n.prop(value)); 29 | }); 30 | } 31 | scope.$watch('language()', function (value) { 32 | element.text($.i18n.prop(key)); 33 | }); 34 | } 35 | }; 36 | }); 37 | }()); -------------------------------------------------------------------------------- /src/main/webapp/js/init.js: -------------------------------------------------------------------------------- 1 | //Define a function scope, variables used inside it will NOT be globally visible. 2 | (function () { 3 | 4 | var 5 | //the HTTP headers to be used by all requests 6 | httpHeaders, 7 | //the message to be shown to the user 8 | message, 9 | //Define the main module. 10 | //The module is accessible everywhere using "angular.module('angularspring')", therefore global variables can be avoided totally. 11 | as = angular.module('exampleApp', ['ngRoute', 'ngResource', 'ngCookies', 'ui.bootstrap', 'ngMessages', 'exampleApp.i18n', 'exampleApp.services', 'exampleApp.controllers', 'exampleApp.filters']); 12 | 13 | as.config(function ($routeProvider, $httpProvider) { 14 | //configure the rounting of ng-view 15 | $routeProvider 16 | .when('/', 17 | {templateUrl: 'partials/home.html', 18 | publicAccess: true}) 19 | .when('/home', 20 | {templateUrl: 'partials/home.html', 21 | publicAccess: true}) 22 | .when('/login', 23 | {templateUrl: 'partials/login.html', 24 | publicAccess: true}) 25 | .when('/posts', 26 | {controller: 'PostsController', 27 | templateUrl: 'partials/posts/home.html'}) 28 | .when('/posts/new', 29 | {controller: 'NewPostController', 30 | templateUrl: 'partials/posts/new.html'}) 31 | .when('/posts/:id', 32 | {controller: 'DetailsController', 33 | templateUrl: 'partials/posts/details.html'}); 34 | 35 | 36 | //configure $http to catch message responses and show them 37 | $httpProvider.interceptors.push(function ($q) { 38 | var setMessage = function (response) { 39 | //if the response has a text and a type property, it is a message to be shown 40 | if (response.data.text && response.data.type) { 41 | message = { 42 | text: response.data.text, 43 | type: response.data.type, 44 | show: true 45 | }; 46 | } 47 | }; 48 | 49 | return { 50 | //this is called after each successful server request 51 | 'response': function (response) { 52 | // console.log('request:' + response); 53 | setMessage(response); 54 | return response || $q.when(response); 55 | }, 56 | //this is called after each unsuccessful server request 57 | 'responseError': function (response) { 58 | //console.log('requestError:' + response); 59 | setMessage(response); 60 | return $q.reject(response); 61 | } 62 | 63 | }; 64 | }); 65 | 66 | $httpProvider.interceptors.push(function ($rootScope, $q) { 67 | 68 | return { 69 | 'request': function (config) { 70 | // console.log('request:' + config); 71 | return config || $q.when(config); 72 | }, 73 | 'requestError': function (rejection) { 74 | // console.log('requestError:' + rejection); 75 | return rejection; 76 | }, 77 | //success -> don't intercept 78 | 'response': function (response) { 79 | // console.log('response:' + response); 80 | return response || $q.when(response); 81 | }, 82 | //error -> if 401 save the request and broadcast an event 83 | 'responseError': function (response) { 84 | console.log('responseError:' + response); 85 | if (response.status === 401) { 86 | var deferred = $q.defer(), 87 | req = { 88 | config: response.config, 89 | deferred: deferred 90 | }; 91 | $rootScope.requests401.push(req); 92 | $rootScope.$broadcast('event:loginRequired'); 93 | return deferred.promise; 94 | } 95 | return $q.reject(response); 96 | } 97 | 98 | }; 99 | }); 100 | 101 | 102 | httpHeaders = $httpProvider.defaults.headers; 103 | }); 104 | 105 | 106 | as.run(function ($rootScope, $http, $route, $location, base64) { 107 | //make current message accessible to root scope and therefore all scopes 108 | $rootScope.message = function () { 109 | return message; 110 | }; 111 | 112 | /** 113 | * Holds all the requests which failed due to 401 response. 114 | */ 115 | $rootScope.requests401 = []; 116 | 117 | $rootScope.$on('event:loginRequired', function () { 118 | //$('#login').modal('show'); 119 | $location.path('/login'); 120 | }); 121 | 122 | /** 123 | * On 'event:loginConfirmed', resend all the 401 requests. 124 | */ 125 | $rootScope.$on('event:loginConfirmed', function () { 126 | var i, 127 | requests = $rootScope.requests401, 128 | retry = function (req) { 129 | $http(req.config).then(function (response) { 130 | req.deferred.resolve(response); 131 | }); 132 | }; 133 | 134 | for (i = 0; i < requests.length; i += 1) { 135 | retry(requests[i]); 136 | } 137 | 138 | $rootScope.requests401 = []; 139 | $location.path('/posts'); 140 | }); 141 | 142 | /** 143 | * On 'event:loginRequest' send credentials to the server. 144 | */ 145 | $rootScope.$on('event:loginRequest', function (event, username, password) { 146 | httpHeaders.common['Authorization'] = 'Basic ' + base64.encode(username + ':' + password); 147 | console.log('httpHeaders.common[\'Authorization\']@' + httpHeaders.common['Authorization'] + ':::' + username + ':' + password); 148 | $http.get('api/me') 149 | .success(function (data) { 150 | $rootScope.authenticated = true; 151 | $rootScope.name = data.username; 152 | $rootScope.$broadcast('event:loginConfirmed'); 153 | }) 154 | .error(function (data) { 155 | console.log('login failed...@' + data); 156 | }); 157 | }); 158 | 159 | /** 160 | * On 'logoutRequest' invoke logout on the server and broadcast 'event:loginRequired'. 161 | */ 162 | $rootScope.$on('event:logoutRequest', function () { 163 | $rootScope.authenticated = false; 164 | delete $rootScope.name; 165 | delete httpHeaders.common['Authorization']; 166 | }); 167 | 168 | var routesOpenToPublic = []; 169 | angular.forEach($route.routes, function (route, path) { 170 | // push route onto routesOpenToPublic if it has a truthy publicAccess value 171 | route.publicAccess && (routesOpenToPublic.push(path)); 172 | }); 173 | 174 | $rootScope.$on('$routeChangeStart', function (event, nextLoc, currentLoc) { 175 | //console.log('fire event@$routeChangeStart'); 176 | var closedToPublic = (-1 === routesOpenToPublic.indexOf($location.path())); 177 | if (closedToPublic && !$rootScope.authenticated) { 178 | //console.log('login required...'); 179 | $rootScope.$broadcast('event:loginRequired'); 180 | } else if (!!$rootScope.authenticated) { 181 | //console.log('already logged in...'); 182 | if (!!nextLoc && nextLoc.templateUrl == 'partials/login.html') { 183 | $location.path('/posts'); 184 | } else { 185 | //do nothing... 186 | } 187 | } 188 | }); 189 | 190 | //$rootScope.$on('$viewContentChange', funtion()); 191 | //check the networking connection. 192 | 193 | $http.get('api/ping') 194 | .success(function (data) { 195 | console.log("ping result@"+data); 196 | }) 197 | .error(function (data) { 198 | $rootScope.message={text:'Network connection eror!', type:'danger', show:true}; 199 | }); 200 | }); 201 | }()); -------------------------------------------------------------------------------- /src/main/webapp/js/jquery.i18n.properties-min-1.0.9.js: -------------------------------------------------------------------------------- 1 | (function(k){function n(c,a){k.ajax({url:c,async:!1,cache:a.cache,contentType:"text/plain;charset="+a.encoding,dataType:"text",success:function(b){r(b,a.mode)}})}function r(c,a){for(var b="",e=c.split(/\n/),d=/(\{\d+\})/g,q=/\{(\d+)\}/g,m=/(\\u.{4})/ig,f=0;f0&&e[f].match("^#")!="#"){var g=e[f].split("=");if(g.length>0){for(var o=unescape(g[0]).replace(/^\s\s*/,"").replace(/\s\s*$/,""),h=g.length==1?"":g[1];h.match(/\\$/)== 2 | "\\";)h=h.substring(0,h.length-1),h+=e[++f].replace(/\s\s*$/,"");for(var l=2;l0&&(a+="."),a+=c[b],eval("typeof "+a+' == "undefined"')&&eval(a+"={};")}function s(c){var a=[],c=parseInt(c.substr(2),16);c>=0&&c=2&&n(c.path+a[i]+"_"+c.language.substring(0,2)+".properties",c),c.language.length>=5&&n(c.path+a[i]+"_"+c.language.substring(0,5)+".properties",c);c.callback&&c.callback()};k.i18n.prop=function(c){var a=k.i18n.map[c];if(a==null)return"["+ 5 | c+"]";var b;if(typeof a=="string"){for(b=0;(b=a.indexOf("\\",b))!=-1;)a=a[b+1]=="t"?a.substring(0,b)+"\t"+a.substring(b++ +2):a[b+1]=="r"?a.substring(0,b)+"\r"+a.substring(b++ +2):a[b+1]=="n"?a.substring(0,b)+"\n"+a.substring(b++ +2):a[b+1]=="f"?a.substring(0,b)+"\u000c"+a.substring(b++ +2):a[b+1]=="\\"?a.substring(0,b)+"\\"+a.substring(b++ +2):a.substring(0,b)+a.substring(b+1);var e=[],d,j;for(b=0;b=0){var m=a.substring(0,b);m!=""&&e.push(m);e.push(j);b=0;a=a.substring(d+1)}else b=d+1;else b++;a!=""&&e.push(a);a=e;k.i18n.map[c]=e}if(a.length==0)return""; 7 | if(a.lengh==1&&typeof a[0]=="string")return a[0];m="";for(b=0;b3&&(c=c.substring(0,3)+c.substring(3).toUpperCase());return c};var j;if(!j)j=function(c,a,b){if(Object.prototype.toString.call(a)!=="[object RegExp]")return typeof j._nativeSplit=="undefined"?c.split(a,b):j._nativeSplit.call(c, 8 | a,b);var e=[],d=0,k=(a.ignoreCase?"i":"")+(a.multiline?"m":"")+(a.sticky?"y":""),a=RegExp(a.source,k+"g"),m,f,g;c+="";j._compliantExecNpcg||(m=RegExp("^"+a.source+"$(?!\\s)",k));if(b===void 0||+b<0)b=Infinity;else if(b=Math.floor(+b),!b)return[];for(;f=a.exec(c);){k=f.index+f[0].length;if(k>d&&(e.push(c.slice(d,f.index)),!j._compliantExecNpcg&&f.length>1&&f[0].replace(m,function(){for(var a=1;a1&&f.index=b))break;a.lastIndex===f.index&&a.lastIndex++}d===c.length?(g||!a.test(""))&&e.push(""):e.push(c.slice(d));return e.length>b?e.slice(0,b):e},j._compliantExecNpcg=/()??/.exec("")[1]===void 0,j._nativeSplit=String.prototype.split;String.prototype.split=function(c,a){return j(this,c,a)}})(jQuery); -------------------------------------------------------------------------------- /src/main/webapp/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | REST EXAMPLE APP 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 57 |
58 |
60 | 61 | 62 |
63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 | 简体中文 English 76 |
77 |
78 |
79 |
80 |
81 |
©2014 HantsyLabs.com, all 82 | rights reserved.
83 |
84 |
85 |
86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/main/webapp/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |

REST EXAMPLE APP

3 | 4 |

5 | This example application is designated to demonstrate how to build a RESTful application using the cutting-edge technologies, including 6 | :
7 | 1. AngularJS/Bootstrap as frontend
8 | 2. Spring MVC as backend REST API producers
9 | 3. JPA 2.1/Spring Data JPA/Hibnerate 4.3 is responsible for data persistence
10 |

11 | 12 |

START HERE

13 |
14 | 15 | -------------------------------------------------------------------------------- /src/main/webapp/partials/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
10 | 12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 | 22 |
23 |
24 |
25 |
26 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Login as:
37 | admin/test123(for ADMIN ROLE)
38 | test/test123. 39 |
40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /src/main/webapp/partials/posts/details.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{{post.content}}

6 | 7 |

Comments({{comments.length||'0'}})

8 |
9 |
10 | 12 |
13 |
14 |
    15 |
  • 16 |

    {{comment.content}}

    17 |
  • 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/webapp/partials/posts/home.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | 43 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
41 | 42 | 44 | 45 | 47 | 48 |
{{post.title}}{{post.status}}{{post.createdDate|date:'short'}}
56 |
57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/webapp/partials/posts/new.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 12 |

Title is required!

13 |
14 | 15 |
16 | 17 | 19 |

At least 15 chars

20 |
21 | 22 |
23 | 26 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/domain/PostTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.domain; 2 | 3 | import org.junit.After; 4 | import org.junit.AfterClass; 5 | import org.junit.Before; 6 | import org.junit.BeforeClass; 7 | import org.junit.Test; 8 | import static org.junit.Assert.*; 9 | 10 | /** 11 | * 12 | * @author hantsy 13 | */ 14 | public class PostTest { 15 | 16 | public PostTest() { 17 | } 18 | 19 | @BeforeClass 20 | public static void setUpClass() { 21 | } 22 | 23 | @AfterClass 24 | public static void tearDownClass() { 25 | } 26 | 27 | private Post post; 28 | 29 | @Before 30 | public void setUp() { 31 | post = new Post(); 32 | post.setTitle("test title"); 33 | post.setContent("test content"); 34 | } 35 | 36 | @After 37 | public void tearDown() { 38 | post = null; 39 | } 40 | 41 | /** 42 | * Test of getId method, of class Post. 43 | */ 44 | @Test 45 | public void testPojo() { 46 | assertEquals("test title", post.getTitle()); 47 | assertEquals("test content", post.getContent()); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/BlogServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test; 2 | 3 | import com.hantsylabs.restexample.springmvc.config.AppConfig; 4 | import com.hantsylabs.restexample.springmvc.config.DataJpaConfig; 5 | import com.hantsylabs.restexample.springmvc.config.DataSourceConfig; 6 | import com.hantsylabs.restexample.springmvc.config.JpaConfig; 7 | import com.hantsylabs.restexample.springmvc.domain.Post; 8 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 9 | import com.hantsylabs.restexample.springmvc.model.PostDetails; 10 | import com.hantsylabs.restexample.springmvc.model.PostForm; 11 | import com.hantsylabs.restexample.springmvc.repository.PostRepository; 12 | import com.hantsylabs.restexample.springmvc.service.BlogService; 13 | import java.util.Objects; 14 | import javax.inject.Inject; 15 | import org.junit.After; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.data.domain.Page; 22 | import org.springframework.data.domain.PageRequest; 23 | import org.springframework.test.context.ContextConfiguration; 24 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertTrue; 27 | 28 | @RunWith(SpringJUnit4ClassRunner.class) 29 | @ContextConfiguration(classes = {AppConfig.class, DataSourceConfig.class, DataJpaConfig.class, JpaConfig.class}) 30 | public class BlogServiceTest { 31 | 32 | private static final Logger LOG = LoggerFactory.getLogger(BlogServiceTest.class); 33 | 34 | @Inject 35 | private PostRepository postRepository; 36 | 37 | @Inject 38 | private BlogService blogService; 39 | 40 | private Post post; 41 | 42 | public BlogServiceTest() { 43 | } 44 | 45 | @Before 46 | public void setUp() { 47 | postRepository.deleteAll(); 48 | post = postRepository.save(Fixtures.createPost("My first post", "content of my first post")); 49 | 50 | assertNotNull(post.getId()); 51 | } 52 | 53 | @After 54 | public void tearDown() { 55 | } 56 | 57 | @Test 58 | public void testSavePost() { 59 | PostForm form = new PostForm(); 60 | form.setTitle("saving title"); 61 | form.setContent("saving content"); 62 | 63 | PostDetails details = blogService.savePost(form); 64 | 65 | LOG.debug("post details @" + details); 66 | assertNotNull("saved post id should not be null@", details.getId()); 67 | assertNotNull(details.getId()); 68 | 69 | Page allPosts = blogService.searchPostsByCriteria("", null, new PageRequest(0, 10)); 70 | assertTrue(allPosts.getTotalElements() == 2); 71 | 72 | Page posts = blogService.searchPostsByCriteria("first", Post.Status.DRAFT, new PageRequest(0, 10)); 73 | assertTrue(posts.getTotalPages() == 1); 74 | assertTrue(!posts.getContent().isEmpty()); 75 | assertTrue(Objects.equals(posts.getContent().get(0).getId(), post.getId())); 76 | 77 | PostForm updatingForm = new PostForm(); 78 | updatingForm.setTitle("updating title"); 79 | updatingForm.setContent("updating content"); 80 | PostDetails updatedDetails = blogService.updatePost(post.getId(), updatingForm); 81 | 82 | assertNotNull(updatedDetails.getId()); 83 | assertTrue("updating title".equals(updatedDetails.getTitle())); 84 | assertTrue("updating content".equals(updatedDetails.getContent())); 85 | 86 | } 87 | 88 | @Test(expected = ResourceNotFoundException.class) 89 | public void testGetNoneExistingPost() { 90 | blogService.findPostById(1000L); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/Fixtures.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test; 2 | 3 | import com.hantsylabs.restexample.springmvc.domain.Post; 4 | import com.hantsylabs.restexample.springmvc.model.PostForm; 5 | 6 | /** 7 | * 8 | * @author hantsy 9 | */ 10 | public final class Fixtures { 11 | 12 | private Fixtures() { 13 | throw new InstantiationError( "Must not instantiate this class" ); 14 | } 15 | 16 | public static Post createPost(String title, String content) { 17 | Post post = new Post(); 18 | post.setTitle(title); 19 | post.setContent(content); 20 | 21 | return post; 22 | } 23 | 24 | public static PostForm createPostForm(String title, String content) { 25 | PostForm post = new PostForm(); 26 | post.setTitle(title); 27 | post.setContent(content); 28 | 29 | return post; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/MockBlogServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test; 2 | 3 | import com.hantsylabs.restexample.springmvc.domain.Post; 4 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 5 | import com.hantsylabs.restexample.springmvc.model.PostDetails; 6 | import com.hantsylabs.restexample.springmvc.model.PostForm; 7 | import com.hantsylabs.restexample.springmvc.repository.CommentRepository; 8 | import com.hantsylabs.restexample.springmvc.repository.PostRepository; 9 | import com.hantsylabs.restexample.springmvc.service.BlogService; 10 | import javax.inject.Inject; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.data.domain.Page; 18 | import org.springframework.data.domain.PageRequest; 19 | import org.springframework.test.context.ContextConfiguration; 20 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 21 | import static org.junit.Assert.assertNotNull; 22 | import static org.junit.Assert.assertTrue; 23 | 24 | @RunWith(SpringJUnit4ClassRunner.class) 25 | @ContextConfiguration(classes = {MockDataConfig.class}) 26 | public class MockBlogServiceTest { 27 | 28 | private static final Logger LOG = LoggerFactory.getLogger(MockBlogServiceTest.class); 29 | 30 | @Inject 31 | private PostRepository postRepository; 32 | 33 | @Inject 34 | private CommentRepository commentRepository; 35 | 36 | private BlogService blogService; 37 | 38 | public MockBlogServiceTest() { 39 | } 40 | 41 | @Before 42 | public void setUp() { 43 | blogService = new BlogService(postRepository, commentRepository); 44 | } 45 | 46 | @After 47 | public void tearDown() { 48 | } 49 | 50 | @Test 51 | public void testSavePost() { 52 | PostForm form = new PostForm(); 53 | form.setTitle("saving title"); 54 | form.setContent("saving content"); 55 | 56 | PostDetails details = blogService.savePost(form); 57 | 58 | LOG.debug("post details @" + details); 59 | assertNotNull("saved post id should not be null@", details.getId()); 60 | assertTrue(details.getId() == 1L); 61 | 62 | Page posts = blogService.searchPostsByCriteria("any keyword", Post.Status.DRAFT, new PageRequest(0, 10)); 63 | 64 | assertTrue(posts.getTotalPages() == 1); 65 | assertTrue(!posts.getContent().isEmpty()); 66 | assertTrue(posts.getContent().get(0).getId() == 1L); 67 | 68 | PostForm updatingForm = new PostForm(); 69 | updatingForm.setTitle("updating title"); 70 | updatingForm.setContent("updating content"); 71 | PostDetails updatedDetails = blogService.updatePost(1L, updatingForm); 72 | 73 | assertNotNull(updatedDetails.getId()); 74 | assertTrue("updating title".equals(updatedDetails.getTitle())); 75 | assertTrue("updating content".equals(updatedDetails.getContent())); 76 | 77 | } 78 | 79 | @Test(expected = ResourceNotFoundException.class) 80 | public void testGetNoneExistingPost() { 81 | blogService.findPostById(1000L); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/MockDataConfig.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test; 2 | 3 | import static org.mockito.Matchers.any; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.util.Arrays; 8 | import java.util.Date; 9 | 10 | import org.mockito.invocation.InvocationOnMock; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import com.hantsylabs.restexample.springmvc.domain.Post; 14 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 15 | import com.hantsylabs.restexample.springmvc.repository.CommentRepository; 16 | import com.hantsylabs.restexample.springmvc.repository.PostRepository; 17 | import com.hantsylabs.restexample.springmvc.service.BlogService; 18 | import org.springframework.data.domain.PageImpl; 19 | import org.springframework.data.domain.PageRequest; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.data.jpa.domain.Specification; 22 | 23 | @Configuration 24 | public class MockDataConfig { 25 | 26 | @Bean 27 | public PostRepository postRepository() { 28 | final Post post = createPost(); 29 | PostRepository posts = mock(PostRepository.class); 30 | when(posts.save(any(Post.class))).thenAnswer((InvocationOnMock invocation) -> { 31 | Object[] args = invocation.getArguments(); 32 | Post result = (Post) args[0]; 33 | result.setId(post.getId()); 34 | result.setTitle(post.getTitle()); 35 | result.setContent(post.getContent()); 36 | result.setCreatedDate(post.getCreatedDate()); 37 | return result; 38 | }); 39 | 40 | when(posts.findOne(1000L)).thenThrow(new ResourceNotFoundException(1000L)); 41 | when(posts.findOne(1L)).thenReturn(post); 42 | when(posts.findAll(any(Specification.class), any(Pageable.class))).thenReturn(new PageImpl(Arrays.asList(post), new PageRequest(0, 10), 1L)); 43 | when(posts.findAll()).thenReturn(Arrays.asList(post)); 44 | return posts; 45 | } 46 | 47 | @Bean 48 | public CommentRepository commentRepository() { 49 | return mock(CommentRepository.class); 50 | } 51 | 52 | @Bean 53 | public BlogService blogService(PostRepository posts, CommentRepository comments){ 54 | return new BlogService(posts, comments); 55 | } 56 | 57 | @Bean 58 | public Post createPost() { 59 | Post post = new Post(); 60 | post.setCreatedDate(new Date()); 61 | post.setId(1L); 62 | post.setTitle("First post"); 63 | post.setContent("Content of my first post!"); 64 | post.setCreatedDate(new Date()); 65 | 66 | return post; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/integration/BasicAuthRestTemplate.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test.integration; 2 | 3 | import java.io.IOException; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Base64; 7 | import org.springframework.http.HttpRequest; 8 | import org.springframework.http.client.ClientHttpRequestExecution; 9 | import org.springframework.http.client.ClientHttpRequestInterceptor; 10 | import org.springframework.http.client.ClientHttpResponse; 11 | import org.springframework.http.client.InterceptingClientHttpRequestFactory; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | /** 15 | * copy from https://github.com/izeye/samples-spring-boot-branches/blob/rest-and-actuator-with-security/src/main/java/samples/springboot/util/BasicAuthRestTemplate.java 16 | * 17 | * Spring Boot itself provides a simpler TestRestTemplate for the test purpose. 18 | * 19 | * @author hantsy 20 | */ 21 | public class BasicAuthRestTemplate extends RestTemplate { 22 | 23 | public BasicAuthRestTemplate(String username, String password) { 24 | addAuthentication(username, password); 25 | } 26 | 27 | private void addAuthentication(String username, String password) { 28 | if (username == null) { 29 | return; 30 | } 31 | List interceptors = Collections 32 | .singletonList( 33 | new BasicAuthorizationInterceptor(username, password)); 34 | setRequestFactory(new InterceptingClientHttpRequestFactory(getRequestFactory(), 35 | interceptors)); 36 | } 37 | 38 | private static class BasicAuthorizationInterceptor implements 39 | ClientHttpRequestInterceptor { 40 | 41 | private final String username; 42 | 43 | private final String password; 44 | 45 | public BasicAuthorizationInterceptor(String username, String password) { 46 | this.username = username; 47 | this.password = (password == null ? "" : password); 48 | } 49 | 50 | @Override 51 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, 52 | ClientHttpRequestExecution execution) throws IOException { 53 | byte[] token = Base64.getEncoder().encode( 54 | (this.username + ":" + this.password).getBytes()); 55 | request.getHeaders().add("Authorization", "Basic " + new String(token)); 56 | return execution.execute(request, body); 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/integration/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test.integration; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.BeforeClass; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.junit.runners.BlockJUnit4ClassRunner; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.http.HttpMethod; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.client.HttpClientErrorException; 18 | import org.springframework.web.client.RestTemplate; 19 | 20 | import com.hantsylabs.restexample.springmvc.domain.Post; 21 | import com.hantsylabs.restexample.springmvc.model.PostForm; 22 | import com.hantsylabs.restexample.springmvc.test.Fixtures; 23 | 24 | @RunWith(BlockJUnit4ClassRunner.class) 25 | public class IntegrationTest { 26 | 27 | private static final Logger log = LoggerFactory.getLogger(IntegrationTest.class); 28 | 29 | private static final String BASE_URL = "http://localhost:8080/angularjs-springmvc-sample/"; 30 | 31 | private RestTemplate template; 32 | 33 | @BeforeClass 34 | public static void init() { 35 | log.debug("==================before class========================="); 36 | } 37 | 38 | @Before 39 | public void beforeTestCase() { 40 | log.debug("==================before test case========================="); 41 | template = new BasicAuthRestTemplate("admin", "test123"); 42 | } 43 | 44 | @After 45 | public void afterTestCase() { 46 | log.debug("==================after test case========================="); 47 | } 48 | 49 | @Test 50 | public void testPostCrudOperations() throws Exception { 51 | PostForm newPost = Fixtures.createPostForm("My first post", "content of my first post"); 52 | String postsUrl = BASE_URL + "api/posts"; 53 | 54 | ResponseEntity postResult = template.postForEntity(postsUrl, newPost, Void.class); 55 | assertTrue(HttpStatus.CREATED.equals(postResult.getStatusCode())); 56 | String createdPostUrl = postResult.getHeaders().getLocation().toString(); 57 | assertNotNull("created post url should be set", createdPostUrl); 58 | 59 | ResponseEntity getPostResult = template.getForEntity(createdPostUrl, Post.class); 60 | assertTrue(HttpStatus.OK.equals(getPostResult.getStatusCode())); 61 | log.debug("post @" + getPostResult.getBody()); 62 | assertTrue(getPostResult.getBody().getTitle().equals(newPost.getTitle())); 63 | 64 | ResponseEntity deleteResult = template.exchange(createdPostUrl, HttpMethod.DELETE, null, Void.class); 65 | assertTrue(HttpStatus.NO_CONTENT.equals(deleteResult.getStatusCode())); 66 | 67 | } 68 | 69 | @Test 70 | public void noneExistingPost() throws Exception { 71 | String noneExistingPostUrl = BASE_URL + "api/posts/1000"; 72 | try { 73 | template.getForEntity(noneExistingPostUrl, Post.class); 74 | } catch (HttpClientErrorException e) { 75 | assertTrue(HttpStatus.NOT_FOUND.equals(e.getStatusCode())); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/web/MockPostControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test.web; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hantsylabs.restexample.springmvc.api.post.PostController; 5 | import com.hantsylabs.restexample.springmvc.domain.Post; 6 | import com.hantsylabs.restexample.springmvc.exception.ResourceNotFoundException; 7 | import com.hantsylabs.restexample.springmvc.model.PostDetails; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | import org.junit.After; 14 | import org.junit.Before; 15 | import org.junit.BeforeClass; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.test.web.servlet.MockMvc; 22 | 23 | import com.hantsylabs.restexample.springmvc.model.PostForm; 24 | import com.hantsylabs.restexample.springmvc.service.BlogService; 25 | import com.hantsylabs.restexample.springmvc.test.Fixtures; 26 | import java.util.Arrays; 27 | import java.util.Date; 28 | import java.util.Locale; 29 | import static org.hamcrest.CoreMatchers.hasItem; 30 | import org.junit.AfterClass; 31 | import org.mockito.InjectMocks; 32 | import static org.mockito.Matchers.any; 33 | import static org.mockito.Matchers.anyString; 34 | import org.mockito.Mock; 35 | import org.mockito.Mockito; 36 | import static org.mockito.Mockito.mock; 37 | import static org.mockito.Mockito.times; 38 | import static org.mockito.Mockito.verify; 39 | import static org.mockito.Mockito.verifyNoMoreInteractions; 40 | import static org.mockito.Mockito.when; 41 | import org.mockito.MockitoAnnotations; 42 | import org.mockito.invocation.InvocationOnMock; 43 | import org.mockito.runners.MockitoJUnitRunner; 44 | import org.mockito.stubbing.Answer; 45 | import org.springframework.data.domain.PageImpl; 46 | import org.springframework.data.domain.PageRequest; 47 | import org.springframework.data.domain.Pageable; 48 | import org.springframework.data.domain.Sort.Direction; 49 | import org.springframework.data.web.PageableHandlerMethodArgumentResolver; 50 | import org.springframework.test.web.servlet.MvcResult; 51 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 52 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 53 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; 54 | import org.springframework.web.servlet.View; 55 | import org.springframework.web.servlet.ViewResolver; 56 | import org.springframework.web.servlet.view.json.MappingJackson2JsonView; 57 | 58 | @RunWith(MockitoJUnitRunner.class) 59 | public class MockPostControllerTest { 60 | 61 | private static final Logger log = LoggerFactory.getLogger(MockPostControllerTest.class); 62 | 63 | private MockMvc mvc; 64 | 65 | ObjectMapper objectMapper = new ObjectMapper(); 66 | 67 | @Mock 68 | private BlogService blogService; 69 | 70 | @Mock 71 | Pageable pageable = mock(PageRequest.class); 72 | 73 | @InjectMocks 74 | PostController postController; 75 | 76 | @BeforeClass 77 | public static void beforeClass() { 78 | log.debug("==================before class========================="); 79 | } 80 | 81 | @AfterClass 82 | public static void afterClass() { 83 | log.debug("==================after class========================="); 84 | } 85 | 86 | @Before 87 | public void setup() { 88 | log.debug("==================before test case========================="); 89 | Mockito.reset(); 90 | MockitoAnnotations.initMocks(this); 91 | mvc = standaloneSetup(postController) 92 | .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver()) 93 | .setViewResolvers(new ViewResolver() { 94 | @Override 95 | public View resolveViewName(String viewName, Locale locale) throws Exception { 96 | return new MappingJackson2JsonView(); 97 | } 98 | }) 99 | .build(); 100 | } 101 | 102 | @After 103 | public void tearDown() { 104 | log.debug("==================after test case========================="); 105 | } 106 | 107 | @Test 108 | public void savePost() throws Exception { 109 | PostForm post = Fixtures.createPostForm("First Post", "Content of my first post!"); 110 | 111 | when(blogService.savePost(any(PostForm.class))).thenAnswer(new Answer() { 112 | @Override 113 | public PostDetails answer(InvocationOnMock invocation) throws Throwable { 114 | PostForm fm = (PostForm) invocation.getArgumentAt(0, PostForm.class); 115 | 116 | PostDetails result = new PostDetails(); 117 | result.setId(1L); 118 | result.setTitle(fm.getTitle()); 119 | result.setContent(fm.getContent()); 120 | result.setCreatedDate(new Date()); 121 | 122 | return result; 123 | } 124 | }); 125 | mvc.perform(post("/api/posts").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(post))) 126 | .andExpect(status().isCreated()); 127 | 128 | verify(blogService, times(1)).savePost(any(PostForm.class)); 129 | verifyNoMoreInteractions(blogService); 130 | } 131 | 132 | @Test 133 | public void retrievePosts() throws Exception { 134 | PostDetails post1 = new PostDetails(); 135 | post1.setId(1L); 136 | post1.setTitle("First post"); 137 | post1.setContent("Content of first post"); 138 | post1.setCreatedDate(new Date()); 139 | 140 | PostDetails post2 = new PostDetails(); 141 | post2.setId(2L); 142 | post2.setTitle("Second post"); 143 | post2.setContent("Content of second post"); 144 | post2.setCreatedDate(new Date()); 145 | 146 | when(blogService.searchPostsByCriteria(anyString(), any(Post.Status.class), any(Pageable.class))) 147 | .thenReturn(new PageImpl(Arrays.asList(post1, post2), new PageRequest(0, 10, Direction.DESC, "createdDate"), 2)); 148 | 149 | MvcResult response = mvc.perform(get("/api/posts?q=test&page=0&size=10")) 150 | .andExpect(status().isOk()) 151 | .andExpect(jsonPath("$.content[*].id", hasItem(1))) 152 | .andExpect(jsonPath("$.content[*].title", hasItem("First post"))) 153 | .andReturn(); 154 | 155 | verify(blogService, times(1)) 156 | .searchPostsByCriteria(anyString(), any(Post.Status.class), any(Pageable.class)); 157 | verifyNoMoreInteractions(blogService); 158 | log.debug("get posts result @" + response.getResponse().getContentAsString()); 159 | } 160 | 161 | @Test 162 | public void retrieveSinglePost() throws Exception { 163 | 164 | PostDetails post1 = new PostDetails(); 165 | post1.setId(1L); 166 | post1.setTitle("First post"); 167 | post1.setContent("Content of first post"); 168 | post1.setCreatedDate(new Date()); 169 | 170 | when(blogService.findPostById(1L)).thenReturn(post1); 171 | 172 | mvc.perform(get("/api/posts/1").accept(MediaType.APPLICATION_JSON)) 173 | .andExpect(status().isOk()) 174 | .andExpect(content().contentType("application/json;charset=UTF-8")) 175 | .andExpect(jsonPath("id").isNumber()); 176 | 177 | verify(blogService, times(1)).findPostById(1L); 178 | verifyNoMoreInteractions(blogService); 179 | } 180 | 181 | @Test 182 | public void removePost() throws Exception { 183 | when(blogService.deletePostById(1L)).thenReturn(true); 184 | mvc.perform(delete("/api/posts/{id}", 1L)) 185 | .andExpect(status().isNoContent()); 186 | 187 | verify(blogService, times(1)).deletePostById(1L); 188 | verifyNoMoreInteractions(blogService); 189 | } 190 | 191 | @Test() 192 | public void notFound() { 193 | when(blogService.findPostById(1000L)).thenThrow(new ResourceNotFoundException(1000L)); 194 | try { 195 | mvc.perform(get("/api/posts/1000").accept(MediaType.APPLICATION_JSON)) 196 | .andExpect(status().isNotFound()); 197 | } catch (Exception ex) { 198 | log.debug("exception caught @" + ex); 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/test/java/com/hantsylabs/restexample/springmvc/test/web/PostControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.hantsylabs.restexample.springmvc.test.web; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hantsylabs.restexample.springmvc.config.AppConfig; 5 | import com.hantsylabs.restexample.springmvc.config.DataJpaConfig; 6 | import com.hantsylabs.restexample.springmvc.config.DataSourceConfig; 7 | import com.hantsylabs.restexample.springmvc.config.Jackson2ObjectMapperConfig; 8 | import com.hantsylabs.restexample.springmvc.config.JpaConfig; 9 | import com.hantsylabs.restexample.springmvc.config.WebConfig; 10 | import com.hantsylabs.restexample.springmvc.domain.Post; 11 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | 16 | import org.junit.After; 17 | import org.junit.Before; 18 | import org.junit.BeforeClass; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.http.MediaType; 24 | import org.springframework.test.web.servlet.MockMvc; 25 | 26 | import com.hantsylabs.restexample.springmvc.model.PostForm; 27 | import com.hantsylabs.restexample.springmvc.repository.PostRepository; 28 | import com.hantsylabs.restexample.springmvc.test.Fixtures; 29 | import javax.inject.Inject; 30 | import static org.hamcrest.CoreMatchers.is; 31 | import org.junit.AfterClass; 32 | import org.springframework.test.context.ContextConfiguration; 33 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 34 | import org.springframework.test.context.web.WebAppConfiguration; 35 | import org.springframework.test.web.servlet.MvcResult; 36 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 37 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 38 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; 39 | import org.springframework.web.context.WebApplicationContext; 40 | 41 | @RunWith(SpringJUnit4ClassRunner.class) 42 | @ContextConfiguration(classes = {AppConfig.class, Jackson2ObjectMapperConfig.class, DataSourceConfig.class, JpaConfig.class, DataJpaConfig.class, WebConfig.class}) 43 | @WebAppConfiguration 44 | public class PostControllerTest { 45 | 46 | private static final Logger log = LoggerFactory.getLogger(PostControllerTest.class); 47 | 48 | @Inject 49 | WebApplicationContext wac; 50 | 51 | @Inject 52 | ObjectMapper objectMapper; 53 | 54 | @Inject 55 | private PostRepository postRepository; 56 | 57 | private MockMvc mvc; 58 | 59 | private Post post; 60 | 61 | @BeforeClass 62 | public static void beforeClass() { 63 | log.debug("==================before class========================="); 64 | } 65 | 66 | @AfterClass 67 | public static void afterClass() { 68 | log.debug("==================after class========================="); 69 | } 70 | 71 | @Before 72 | public void setup() { 73 | log.debug("==================before test case========================="); 74 | mvc = webAppContextSetup(this.wac).build(); 75 | 76 | postRepository.deleteAll(); 77 | post = postRepository.save(Fixtures.createPost("My first post", "content of my first post")); 78 | } 79 | 80 | @After 81 | public void tearDown() { 82 | log.debug("==================after test case========================="); 83 | } 84 | 85 | @Test 86 | public void savePost() throws Exception { 87 | PostForm post = Fixtures.createPostForm("First Post", "Content of my first post!"); 88 | 89 | mvc.perform(post("/api/posts").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(post))) 90 | .andExpect(status().isCreated()); 91 | 92 | } 93 | 94 | @Test 95 | public void retrievePosts() throws Exception { 96 | 97 | MvcResult response = mvc.perform(get("/api/posts?q=first&page=0&size=10")) 98 | .andExpect(status().isOk()) 99 | .andExpect(jsonPath("$.content[0].id", is(post.getId().intValue()))) 100 | .andExpect(jsonPath("$.content[0].title", is("My first post"))) 101 | .andReturn(); 102 | 103 | log.debug("get posts result @" + response.getResponse().getContentAsString()); 104 | } 105 | 106 | @Test 107 | public void retrieveSinglePost() throws Exception { 108 | 109 | mvc.perform(get("/api/posts/{id}", post.getId()).accept(MediaType.APPLICATION_JSON)) 110 | .andExpect(status().isOk()) 111 | .andExpect(content().contentTypeCompatibleWith("application/json")) 112 | .andExpect(jsonPath("$.id").isNumber()) 113 | .andExpect(jsonPath("$.title", is("My first post"))); 114 | 115 | } 116 | 117 | @Test 118 | public void removePost() throws Exception { 119 | mvc.perform(delete("/api/posts/{id}", post.getId())) 120 | .andExpect(status().isNoContent()); 121 | } 122 | 123 | @Test() 124 | public void notFound() { 125 | try { 126 | mvc.perform(get("/api/posts/1000").accept(MediaType.APPLICATION_JSON)) 127 | .andExpect(status().isNotFound()); 128 | } catch (Exception ex) { 129 | log.debug("exception caught @" + ex); 130 | } 131 | } 132 | 133 | } 134 | --------------------------------------------------------------------------------