├── .gitignore ├── README.adoc ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── restfulapi-data-schema.png ├── restfulapi-documents.png └── src ├── main ├── asciidoc │ ├── abstract.adoc │ ├── index.adoc │ ├── overview │ │ ├── overview-authentication.adoc │ │ ├── overview-client-errors.adoc │ │ ├── overview-conditional-requests.adoc │ │ ├── overview-cors.adoc │ │ ├── overview-current-version.adoc │ │ ├── overview-http-redirects.adoc │ │ ├── overview-http-verbs.adoc │ │ ├── overview-hypermedia.adoc │ │ ├── overview-json-p-callbacks.adoc │ │ ├── overview-pagination.adoc │ │ ├── overview-parameters.adoc │ │ ├── overview-rate-limiting.adoc │ │ ├── overview-root-endpoint.adoc │ │ ├── overview-schema.adoc │ │ ├── overview-timezones.adoc │ │ └── overview-user-agent-required.adoc │ ├── resources │ │ ├── resources-doctors.adoc │ │ ├── resources-index.adoc │ │ ├── resources-patients.adoc │ │ └── resources-schedules.adoc │ ├── static │ │ └── css │ │ │ └── nanum.css │ └── v1.adoc ├── java │ ├── .gitkeep │ └── io │ │ └── example │ │ ├── SampleBootRunner.java │ │ ├── common │ │ ├── AbstractPersistable.java │ │ ├── GlobalControllerExceptionHandler.java │ │ ├── LocalDateToDateConverter.java │ │ ├── LocalTimeToTimeConverter.java │ │ ├── NestedContentResource.java │ │ └── ResourceNotFoundException.java │ │ ├── config │ │ ├── loading │ │ │ └── LoadingDataService.java │ │ ├── mapper │ │ │ ├── AutoMapper.java │ │ │ ├── AutoMapperConfiguration.java │ │ │ ├── AutoMapperConfigurer.java │ │ │ ├── AutoMapperFactoryBean.java │ │ │ └── PropertyMapConfigurerSupport.java │ │ └── web │ │ │ └── WebBootConfig.java │ │ ├── doctor │ │ ├── Doctor.java │ │ ├── DoctorInput.java │ │ ├── DoctorJpaRepository.java │ │ ├── DoctorResourceAssembler.java │ │ └── DoctorRestController.java │ │ ├── index │ │ └── IndexRestController.java │ │ ├── patient │ │ ├── Patient.java │ │ ├── PatientInput.java │ │ ├── PatientJpaRepository.java │ │ ├── PatientResourceAssembler.java │ │ └── PatientRestController.java │ │ └── schedule │ │ ├── Schedule.java │ │ ├── ScheduleInput.java │ │ ├── ScheduleJpaRepository.java │ │ ├── ScheduleResourceAssembler.java │ │ └── ScheduleRestController.java └── resources │ ├── .gitkeep │ ├── application.yml │ └── banner.txt └── test ├── java ├── .gitkeep └── io │ └── example │ ├── TestBootConfig.java │ ├── TestDoctorsRestController.java │ ├── TestErrorRestController.java │ ├── TestIndexRestController.java │ ├── TestPatientsRestController.java │ ├── TestSchedulesRestController.java │ └── TestSchemaRestController.java └── resources └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | *.class 15 | 16 | # Package Files # 17 | *.ear 18 | /build 19 | /bin 20 | **/build 21 | **/bin 22 | 23 | # IDE Files # 24 | *.iml 25 | /.idea 26 | .classpath 27 | /.settings 28 | .project 29 | */.settings 30 | 31 | # Gradle Files # 32 | /.gradle 33 | 34 | # JRevel Files # 35 | rebel.xml 36 | 37 | # ETC Files # 38 | .DS_Store 39 | *.log 40 | /.apt_generated/ 41 | src/main/resources/static/ 42 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :seminar-link: http://www.ksug.org/seminar/20151024 2 | :seminar-slide-link: http://slides.com/gmind7/spirngrestdocs 3 | :lombok-link: https://projectlombok.org 4 | 5 | = Modern Java web application with Spring 6 | 7 | == Overview 8 | 9 | {seminar-link}[Modern Java web application with Spring] 세미나를 통해 공유한 10 | 11 | Making your API easy to document with Spring REST Docs 섹션 샘플링 프로젝트 입니다. 12 | 13 | === Example RESTful API Documents 14 | 15 | image::restfulapi-documents.png[] 16 | 17 | === Example RESTful API Data Schema 18 | 19 | image::restfulapi-data-schema.png[] 20 | 21 | == Learning more 22 | 23 | {seminar-slide-link}[Slides] 자료 24 | 25 | == Building from source 26 | 27 | requires Java8 28 | 29 | * gradle spring boot WAS Start 30 | 31 | [source,groovy,indent=0] 32 | ---- 33 | gradlew copyAsciidocResources bootRun 34 | ---- 35 | 36 | API : http://127.0.0.1:8080 37 | 38 | Doc : http://127.0.0.1:8080/docs 39 | 40 | * IDEA, STS, Eclipse WAS Start 41 | 42 | requires {lombok-link}[Lombok] 43 | 44 | gradlew copyAsciidocResources task 실행하면 *.adoc -> *.html 만들어 진 후 src/main/resource/static/docs 폴더 밑에 copy 되어 집니다. 45 | 46 | [source,groovy,indent=0] 47 | ---- 48 | gradlew copyAsciidocResources 49 | ---- 50 | 51 | src/main/resource/static/docs/*.html 만들어 졌으면 MainClass로 서버를 시작 합니다. 52 | 53 | MainClass : io.example.SampleBootRunner.class 54 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' 7 | classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE' 8 | classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.2' 9 | classpath 'org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.9' 10 | classpath 'org.kordamp.gradle:livereload-gradle-plugin:0.2.1' 11 | } 12 | ext { 13 | snippetsDir = file('build/generated-snippets') 14 | } 15 | } 16 | 17 | apply plugin: 'eclipse' 18 | apply plugin: 'idea' 19 | apply plugin: 'java' 20 | apply plugin: 'spring-boot' 21 | apply plugin: 'io.spring.dependency-management' 22 | apply plugin: 'org.asciidoctor.convert' 23 | apply plugin: 'org.kordamp.gradle.livereload' 24 | 25 | [compileJava, compileTestJava, javadoc]*.options*.encoding = "UTF-8" 26 | 27 | group = 'io.example' 28 | 29 | mainClassName = 'io.example.SampleBootRunner' 30 | 31 | sourceCompatibility = 1.8 32 | targetCompatibility = 1.8 33 | 34 | repositories { 35 | mavenLocal() 36 | maven { url 'https://repo.spring.io/libs-snapshot' } 37 | jcenter() 38 | } 39 | 40 | dependencyManagement { 41 | imports { 42 | mavenBom "io.spring.platform:platform-bom:1.1.3.RELEASE" 43 | } 44 | dependencies { 45 | dependency 'org.modelmapper:modelmapper:0.7.5' 46 | } 47 | testCompile { 48 | dependencies { 49 | dependency 'org.springframework.restdocs:spring-restdocs-mockmvc:1.0.0.RELEASE' 50 | } 51 | } 52 | } 53 | 54 | configurations { 55 | all { 56 | exclude group: 'commons-logging' 57 | exclude module: 'slf4j-log4j12' 58 | resolutionStrategy { 59 | cacheChangingModulesFor 0, 'seconds' 60 | cacheDynamicVersionsFor 10 * 60, 'seconds' 61 | } 62 | } 63 | } 64 | 65 | dependencies { 66 | compile 'org.springframework.boot:spring-boot-starter-data-jpa' 67 | compile 'org.springframework.boot:spring-boot-starter-hateoas' 68 | 69 | compile 'org.modelmapper:modelmapper' 70 | compile 'com.google.guava:guava' 71 | compile 'commons-beanutils:commons-beanutils' 72 | compile 'org.apache.commons:commons-lang3' 73 | compile 'org.projectlombok:lombok' 74 | compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' 75 | 76 | runtime 'org.hsqldb:hsqldb' 77 | runtime 'mysql:mysql-connector-java' 78 | runtime 'org.atteo:evo-inflector' 79 | 80 | testCompile 'com.jayway.jsonpath:json-path' 81 | testCompile 'org.springframework.boot:spring-boot-starter-test' 82 | testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' 83 | } 84 | 85 | test { 86 | outputs.dir snippetsDir 87 | } 88 | 89 | asciidoctor { 90 | backends 'html5' 91 | sourceDir 'src/main/asciidoc' 92 | attributes 'snippets': snippetsDir, 93 | 'icons': 'font', 94 | 'setanchors':'true', 95 | 'idprefix':'', 96 | 'idseparator':'-', 97 | 'doctype': 'book', 98 | 'docinfo1':'true', 99 | 'source-highlighter': 'highlightjs' 100 | inputs.dir snippetsDir 101 | dependsOn test 102 | } 103 | 104 | liveReload { 105 | docRoot asciidoctor.outputDir.canonicalPath 106 | } 107 | 108 | task copyAsciidocResources(dependsOn: asciidoctor, group: 'Documentation', description: 'Converts AsciiDoc files and copies the output files and related resources to the build directory.') { 109 | file("src/main/resources/static/docs").deleteDir() 110 | doLast { 111 | copy { 112 | from "${asciidoctor.outputDir}/html5" 113 | into "${project.getProjectDir()}/src/main/resources/static/docs" 114 | } 115 | } 116 | } 117 | 118 | jar { 119 | dependsOn copyAsciidocResources 120 | // from ("${asciidoctor.outputDir}/html5") { 121 | // into 'static/docs' 122 | // } 123 | } 124 | 125 | eclipse { 126 | classpath { 127 | downloadJavadoc = true 128 | downloadSources=true 129 | } 130 | } 131 | 132 | idea { 133 | module { 134 | inheritOutputDirs = false 135 | outputDir = file("$buildDir/classes/main/") 136 | jdkName = '1.8' 137 | downloadJavadoc = true 138 | downloadSources = true 139 | } 140 | } 141 | 142 | eclipseJdt.onlyIf { false } 143 | cleanEclipseJdt.onlyIf { false } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Oct 19 12:09:53 KST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /restfulapi-data-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/restfulapi-data-schema.png -------------------------------------------------------------------------------- /restfulapi-documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/restfulapi-documents.png -------------------------------------------------------------------------------- /src/main/asciidoc/abstract.adoc: -------------------------------------------------------------------------------- 1 | 본 문서는 병원 진료 예약 관리 RESTful API에 대한 개발자 가이드 문서 입니다. 2 | 3 | CAUTION: 샘플 프로젝트로 이해를 돕기 위해 Overview가 작성 되어 일부 내용은 정상 동작 되지 않습니다. -------------------------------------------------------------------------------- /src/main/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = Appointment RESTful Developer API Documents 2 | Modern Java web application with Spring 3 | :stylesheet : static/css/nanum.css 4 | 5 | [[abstract]] 6 | 7 | include::abstract.adoc[] 8 | 9 | |=== 10 | | RELEASE | STATUS | DOCUMENTATION 11 | 12 | | `V2` 13 | | CURRENT 14 | | link:v1.html[Reference] 15 | 16 | | `V1` 17 | | CURRENT 18 | | link:v1.html[Reference] 19 | |=== 20 | 21 | :last-update-label!: -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-authentication.adoc: -------------------------------------------------------------------------------- 1 | [[overview-authentication]] 2 | === Authentication 3 | 4 | 인증을 위해서는..... 5 | 6 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-client-errors.adoc: -------------------------------------------------------------------------------- 1 | [[overview-client-errors]] 2 | === Client Errors 3 | 4 | 오류 응답 (상태 코드> = 400)가 리턴 될 때마다, 오류 정보에 대한 내용이 JSON 객체로 전달 되어 지며 데이터 형태는 다음과 같습니다. 5 | 6 | include::{snippets}/client-error-bad-request/response-fields.adoc[] 7 | 8 | 예를 들어, 존재 하지 않는 리소스에 대한 시도 요청시 `(404) Not Found` 대한 응답 9 | 10 | include::{snippets}/client-error-bad-request/http-response.adoc[] 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-conditional-requests.adoc: -------------------------------------------------------------------------------- 1 | [[overview-conditional-requests]] 2 | === Conditional Requests 3 | 4 | `ETag` 와 같은 조건부 요청에 대해서는 ... 5 | 6 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-cors.adoc: -------------------------------------------------------------------------------- 1 | [[overview-cors]] 2 | === Cross Origin Resource Sharing 3 | 4 | CORS는 현재..... 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-current-version.adoc: -------------------------------------------------------------------------------- 1 | [[overview-current-version]] 2 | === Current Version 3 | 4 | 모든 요청을 기본적으로 v1 버전의 API로 받게 되어 있지만 명시적으로 수락 `HEADER` 에 Accept 버전을 요청하는 것이 좋습니다. 5 | 6 | [source,bash,indent=0] 7 | ---- 8 | Accept: application/vnd.le.v1+json 9 | ---- -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-http-redirects.adoc: -------------------------------------------------------------------------------- 1 | [[overview-http-redirects]] 2 | === HTTP Redirects 3 | 4 | API의 변경, 업데이트, 장애로 인한 리다이렉션 응답을 받을 경우 아래와 같은 리다이렉션을 행동을 따라야 합니다. 5 | 6 | |=== 7 | | Status Code | Description 8 | 9 | | `301` 10 | | 영구 리다이렉션. 요청한 URI 자원에 대한 이후의 모든 요청은 새로운 URI로 이동합니다 11 | 12 | | `302,307` 13 | | 임시 리다이렉션. 요청에 URI 자원에 대해 일시적으로 새로운 RUI로 이동하지만 나중에 원래의 URI로 이동 합니다. 14 | |=== 15 | 16 | 다른 리다이렉션은 HTTP 1.1 spec 상태 코드를 준수 합니다. -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-http-verbs.adoc: -------------------------------------------------------------------------------- 1 | [[overview-http-verbs]] 2 | === HTTP Verbs 3 | 4 | API 정보 조회는 다음과 같은 자원을 통해서 요청 할 수 있습니다. 5 | 6 | |=== 7 | | Verb | Description 8 | 9 | | `HEAD` 10 | | HTTP 헤더 정보를 필요로 하는 자원에 대해 서는 요청 될 수도 있습니다 11 | 12 | | `GET` 13 | | 자원 검색시 Parameters가 PathVariable, MatrixVariable로 요청이 가능한 경우에 사용 됩니다. 14 | 15 | | `POST` 16 | | 자원 검색시 Parameters의 복잡도가 높은 경우에 사용 됩니다. 17 | 18 | | `PUT` 19 | | 신규 자원을 저장 하고자 할 때 사용 됩니다. 20 | 21 | | `PATCH` 22 | | 기존 자원을 수정 하고자 할 때 사용 됩니다. 23 | 24 | | `DELETE` 25 | | 기존 자원을 삭제 하고자 할 때 사용 됩니다. 26 | 27 | | `OPTIONS` 28 | | Returns the HTTP methods that the server supports 29 | 30 | | `CONNECT` 31 | | Converts the request connection to a transparent TCP/IP tunnel 32 | |=== 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-hypermedia.adoc: -------------------------------------------------------------------------------- 1 | [[overview-hypermedia]] 2 | === Hypermedia 3 | 4 | 모든 자원은 하나 이상 있을 수 있습니다._links는 self, other 자원에 대해 리소스 링크 속성을 제공하여 클라이언트가 URL을 구성 할 필요가 없도록 5 | 명시적으로 URL을 지원 하여 개발자가 향후에 쉽게 API를 업그레이드를 할 수 있도록 하기 위함 입니다. 6 | 7 | * Atom – 응답 되는 컨텐츠 타입이 `application/json` 과 호환 될 때 `links`의 구성은 순수 데이터 리스트가 될 것 입니다. 8 | * HAL - 응답 되는 컨텐츠 타입이 `application/hal+json` 과 호환 될 때 `links`의 구성은 데이터+정보 맵 정보가 될 것 입니다. 9 | 10 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-json-p-callbacks.adoc: -------------------------------------------------------------------------------- 1 | [[overview-json-p-callbacks]] 2 | === JSON-P Callbacks 3 | 4 | JSON-P을 요청을 위해서는.... 5 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-pagination.adoc: -------------------------------------------------------------------------------- 1 | [[overview-pagination]] 2 | === Pagination 3 | 4 | 리스트 항목에 대한 요청은 기본값으로(0 page/50 size)로 반환 되어 지며 페이지 파라미터 변수를 통해 검색 범위에 대해 요청 할 수 있습니다. 5 | 예를 들어 의사 목록의 1 page의 10개의 데이터 리스트 항목은 다음과 같이 요청 할 수 있습니다. 6 | 7 | [source,bash,indent=0] 8 | ---- 9 | $ curl 'http://root-endpoint/doctors?page=1&size=10' 10 | ---- 11 | 12 | 모든 리스트 항목에 대해 페이지 파라미터 변수는 동일하게 지원 됩니다. 13 | 14 | |=== 15 | | Params | Description 16 | 17 | | `page` 18 | | 페이지 번호 19 | 20 | | `size` 21 | | 페이지 번호에서의 목록 사이즈 22 | |=== 23 | 24 | [source,json,indent=0] 25 | ---- 26 | { 27 | "_links": { 28 | "self": { 29 | "href": "http://localhost:8080/doctors?page=1&size=10{&sort}", 30 | "templated": true 31 | }, 32 | "next": { 33 | "href": "http://localhost:8080/doctors?page=2&size=10{&sort}", 34 | "templated": true 35 | }, 36 | "prev": { 37 | "href": "http://localhost:8080/doctors?page=0&size=10{&sort}", 38 | "templated": true 39 | } 40 | }, 41 | "_embedded" : { 42 | "doctors": [ 43 | ................................... 44 | ] 45 | }, 46 | "page": { 47 | "size": 10, 48 | "totalElements": 30, 49 | "totalPages": 3, 50 | "number": 1 51 | } 52 | } 53 | 54 | ---- -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-parameters.adoc: -------------------------------------------------------------------------------- 1 | [[overview-parameters]] 2 | === Parameters 3 | 4 | GET, POST 요청 경로의 세그먼트로 지정된 PathVariable/MatrixVariable 매개 변수와 HTTP 쿼리 문자열 매개 변수로 전달 할 수 있습니다. 5 | 6 | * PathVariable 7 | [source,bash,indent=0] 8 | ---- 9 | $ curl 'http://root-endpoint/doctors/{id}/schedules/{scheduleId}' 10 | ---- 11 | 12 | * MatrixVariable 13 | [source,bash,indent=0] 14 | ---- 15 | $ curl 'http://root-endpoint/doctors/1;name=kim/schedules/1;startTime=1200' 16 | ---- 17 | 18 | * QueryString 19 | [source,bash,indent=0] 20 | ---- 21 | $ curl 'http://root-endpoint/doctors?page=2&size=50' 22 | ---- -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-rate-limiting.adoc: -------------------------------------------------------------------------------- 1 | [[overview-rate-limiting]] 2 | === Rate Limiting 3 | 4 | 클라이언트는 5,000/시간, 60/초당 요청을 할 수 있습니다. 모든 요청 제한 상태는 HTTP HEADER를 통해 반환 되며 확인 할 수 있습니다. 5 | 6 | [source,bash,indent=0] 7 | ---- 8 | $ curl 'http://root-endpoint' -i 9 | ---- 10 | 11 | [source,http] 12 | ---- 13 | HTTP/1.1 200 OK 14 | 15 | Date: Mon, 01 Jul 2015 17:27:06 GMT 16 | 17 | Status: 200 OK 18 | 19 | X-RateLimit-Limit: 5000 20 | 21 | X-RateLimit-Remaining: 4987 22 | 23 | X-RateLimit-Reset: 1350085394 24 | ---- 25 | 26 | |=== 27 | | Verb | Description 28 | 29 | | `X-RateLimit-Limit` 30 | | 시간 당 허용되는 최대 요청 수 31 | 32 | | `X-RateLimit-Remaining` 33 | | 현재 나머지 요청 가능 횟수 34 | 35 | | `X-RateLimit-Reset` 36 | | 현재 요청 제한 횟수가 재설정되는 시간 37 | |=== 38 | 39 | 요청 제한에 걸려 이동하면 다음과 같은 오류 응답을 받게 됩니다. 40 | 41 | [source,http] 42 | ---- 43 | HTTP/1.1 403 Forbidden 44 | 45 | Date: Mon, 01 Jul 2015 17:27:06 GMT 46 | 47 | Status: 403 Forbidden 48 | 49 | X-RateLimit-Limit: 60 50 | 51 | X-RateLimit-Remaining: 0 52 | 53 | X-RateLimit-Reset: 1350085394 54 | 55 | { 56 | "timestamp" : "2015-09-17 11:40:30", 57 | "status" : 403, 58 | "error" : "Forbidden", 59 | "message" : "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.", 60 | "path" : "http://root-endpoint/doctors" 61 | } 62 | ---- 63 | 64 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-root-endpoint.adoc: -------------------------------------------------------------------------------- 1 | [[overview-root-endpoint]] 2 | === Root Endpoint 3 | 4 | 모든 엔드 포인트 카테고리들의 API 정보를 제공하는 망별 루트 엔트 포인트는 다음과 같습니다. 5 | 6 | |=== 7 | | Networks | URL 8 | 9 | | `DEV` 10 | | http://127.0.0.1:8090 11 | 12 | | `RC` 13 | | http://127.0.0.1:8090 14 | 15 | | `LIVE` 16 | | http://127.0.0.1:8090 17 | |=== 18 | 19 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-schema.adoc: -------------------------------------------------------------------------------- 1 | [[overview-schema]] 2 | === Schema 3 | 4 | 모든 API의 엑세스는 HTTP를 통해 허용 되며 모든 데이터는 JSON 객체 형태로 요청/수신 되어 집니다. 5 | 6 | [source,bash,indent=0] 7 | ---- 8 | $ curl 'http://root-endpoint' -i 9 | ---- 10 | 11 | [source,bash,indent=0] 12 | ---- 13 | HTTP/1.1 200 OK 14 | 15 | Server: nginx 16 | 17 | Date: Fri, 12 Oct 2015 23:33:14 GMT 18 | 19 | Content-Type: application/hal+json; charset=utf-8 20 | 21 | Connection: keep-alive 22 | 23 | Status: 200 OK 24 | 25 | ETag: "a00049ba79152d03380c34652f2cb612" 26 | 27 | X-LE-Media-Type: le.v1 28 | 29 | X-RateLimit-Limit: 5000 30 | 31 | X-RateLimit-Remaining: 4987 32 | 33 | X-RateLimit-Reset: 1350085394 34 | 35 | Content-Length: 5 36 | 37 | Cache-Control: max-age=0, private, must-revalidate 38 | 39 | X-Content-Type-Options: nosniff 40 | 41 | [] 42 | ---- 43 | 44 | 빈 필드가 포함되어 있는 경우 `null` 대신 생략 되어 지며 모든 타임 스탬프는 ISO 8601 형식으로 반환됩니다 45 | 46 | [source,bash,indent=0] 47 | ---- 48 | YYYY-MM-DDTHH:MM:SSZ 49 | ---- 50 | 51 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-timezones.adoc: -------------------------------------------------------------------------------- 1 | [[overview-timezones]] 2 | === Timezones 3 | 4 | 명시적으로 `Timezones` 정보와 `ISO 8601` 타임 스탬프를 제공 합니다. `Timezones`을 재정의 하려면 다음과 같이... 5 | -------------------------------------------------------------------------------- /src/main/asciidoc/overview/overview-user-agent-required.adoc: -------------------------------------------------------------------------------- 1 | [[overview-user-agent-required]] 2 | === User Agent Required 3 | 4 | 모든 API의 요청에는 `User-Agent` 가 포함되어야 하며 유효해야 합니다. `User-Agent` 포함되지 않은 경우 거부 되며 요청할 대상의 `User-Agent` 는 이미 협의 된 응용 프로그램의 이름을 사용해야 합니다. 5 | 문제가 있는 경우 다음과 같이 응답 되어 집니다. 6 | 7 | [source,bash,indent=0] 8 | ---- 9 | curl -iH 'User-Agent: ' http://root-endpoint 10 | ---- 11 | 12 | [source,http] 13 | ---- 14 | HTTP/1.1 403 Forbidden 15 | 16 | Connection: close 17 | 18 | Content-Type: text/html 19 | ---- 20 | 21 | -------------------------------------------------------------------------------- /src/main/asciidoc/resources/resources-doctors.adoc: -------------------------------------------------------------------------------- 1 | [[resources-doctors]] 2 | === Doctors 3 | 4 | 의사 목록과 상세 정보, 생성, 수정, 삭제를 요청 하는데 사용 합니다. 5 | 6 | [[resources-doctors-show-all]] 7 | ==== /doctors [small silver]*[GET]* 8 | 9 | `GET` 요청은 의사 리스트를 검색 합니다. 10 | 11 | include::{snippets}/doctors-show-all/http-request.adoc[] 12 | 13 | ===== Request Structure 14 | 15 | ====== request-parameters 16 | 17 | include::{snippets}/doctors-show-all/request-parameters.adoc[] 18 | 19 | ===== Response Structure 20 | 21 | ====== response-fields 22 | 23 | include::{snippets}/doctors-show-all/response-fields.adoc[] 24 | 25 | ===== Example Response 26 | 27 | include::{snippets}/doctors-show-all/http-response.adoc[] 28 | 29 | [[resources-index-links]] 30 | ===== Links 31 | 32 | include::{snippets}/doctors-show-all/links.adoc[] 33 | 34 | 35 | [[resources-doctors-show-one]] 36 | ==== /doctors/{id} [small silver]*[GET]* 37 | 38 | `GET` 요청은 의사 정보를 검색 합니다. 39 | 40 | include::{snippets}/doctors-show-one/http-request.adoc[] 41 | 42 | ===== Request Structure 43 | 44 | ====== path-parameters 45 | 46 | include::{snippets}/doctors-show-one/path-parameters.adoc[] 47 | 48 | ===== Response Structure 49 | 50 | ====== response-fields 51 | [[resources-doctors-show-one-response-fields]] 52 | 53 | include::{snippets}/doctors-show-one/response-fields.adoc[] 54 | 55 | ===== Example Response 56 | 57 | include::{snippets}/doctors-show-one/http-response.adoc[] 58 | 59 | [[resources-index-links]] 60 | ===== Links 61 | 62 | include::{snippets}/doctors-show-one/links.adoc[] 63 | 64 | 65 | [[resources-doctors-create]] 66 | ==== /doctors/{id} [small silver]*[PUT]* 67 | 68 | `PUT` 요청은 의사 정보를 생성 합니다. 69 | 70 | include::{snippets}/doctors-create/http-request.adoc[] 71 | 72 | ===== Request Structure 73 | 74 | ====== request-fields 75 | 76 | include::{snippets}/doctors-create/request-fields.adoc[] 77 | 78 | ===== Response Structure 79 | 80 | ====== response-headers 81 | 82 | include::{snippets}/doctors-create/response-headers.adoc[] 83 | 84 | ===== Example Response 85 | 86 | include::{snippets}/doctors-create/http-response.adoc[] 87 | 88 | 89 | [[resources-doctors-update]] 90 | ==== /doctors/{id} [small silver]*[PATCH]* 91 | 92 | `PATCH` 요청은 의사 정보를 수정 합니다. 93 | 94 | include::{snippets}/doctors-update/http-request.adoc[] 95 | 96 | ===== Request Structure 97 | 98 | ====== path-parameters 99 | 100 | include::{snippets}/doctors-delete/path-parameters.adoc[] 101 | 102 | ====== request-fields 103 | 104 | include::{snippets}/doctors-create/request-fields.adoc[] 105 | 106 | ===== Example Response 107 | 108 | include::{snippets}/doctors-update/http-response.adoc[] 109 | 110 | 111 | [[resources-doctors-delete]] 112 | ==== /doctors/{id} [small silver]*[DELETE]* 113 | 114 | `DELETE` 요청은 의사 정보를 삭제 합니다. 115 | 116 | include::{snippets}/doctors-delete/http-request.adoc[] 117 | 118 | ===== Request Structure 119 | 120 | ====== path-parameters 121 | 122 | include::{snippets}/doctors-delete/path-parameters.adoc[] 123 | 124 | ===== Example Response 125 | 126 | include::{snippets}/doctors-delete/http-response.adoc[] 127 | -------------------------------------------------------------------------------- /src/main/asciidoc/resources/resources-index.adoc: -------------------------------------------------------------------------------- 1 | [[resources-index]] 2 | === Index 3 | 4 | API의 정보 리스트의 인덱스 진입 점을 제공 합니다. 5 | 6 | [[resources-index-access]] 7 | ==== / [small silver]*[GET]* 8 | 9 | `GET` 요청은 인덱스를 엑세스 하는데 사용 됩니다. 10 | 11 | ===== Response Structure 12 | 13 | include::{snippets}/index/response-fields.adoc[] 14 | 15 | ===== Example Response 16 | 17 | include::{snippets}/index/http-response.adoc[] 18 | 19 | [[resources-index-links]] 20 | ===== Links 21 | 22 | include::{snippets}/index/links.adoc[] 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/asciidoc/resources/resources-patients.adoc: -------------------------------------------------------------------------------- 1 | [[resources-patients]] 2 | === Patients 3 | 4 | 환자 목록과 상세 정보, 생성, 수정, 삭제를 요청 하는데 사용 합니다. 5 | 6 | [[resources-patients-show-all]] 7 | ==== /patients [small silver]*[GET]* 8 | 9 | `GET` 요청은 환자 리스트를 검색 합니다. 10 | 11 | include::{snippets}/patients-show-all/http-request.adoc[] 12 | 13 | ===== Request Structure 14 | 15 | ====== request-parameters 16 | 17 | include::{snippets}/patients-show-all/request-parameters.adoc[] 18 | 19 | ===== Response Structure 20 | 21 | ====== response-fields 22 | 23 | include::{snippets}/patients-show-all/response-fields.adoc[] 24 | 25 | ===== Example Response 26 | 27 | include::{snippets}/patients-show-all/http-response.adoc[] 28 | 29 | [[resources-index-links]] 30 | ===== Links 31 | 32 | include::{snippets}/patients-show-all/links.adoc[] 33 | 34 | 35 | [[resources-patients-show-one]] 36 | ==== /patients/{id} [small silver]*[GET]* 37 | 38 | `GET` 요청은 환자 정보를 검색 합니다. 39 | 40 | include::{snippets}/patients-show-one/http-request.adoc[] 41 | 42 | ===== Request Structure 43 | 44 | ====== path-parameters 45 | 46 | include::{snippets}/patients-show-one/path-parameters.adoc[] 47 | 48 | ===== Response Structure 49 | 50 | ====== response-fields 51 | [[resources-patients-show-one-response-fields]] 52 | 53 | include::{snippets}/patients-show-one/response-fields.adoc[] 54 | 55 | ===== Example Response 56 | 57 | include::{snippets}/patients-show-one/http-response.adoc[] 58 | 59 | [[resources-index-links]] 60 | ===== Links 61 | 62 | include::{snippets}/patients-show-one/links.adoc[] 63 | 64 | 65 | [[resources-patients-create]] 66 | ==== /patients/{id} [small silver]*[PUT]* 67 | 68 | `PUT` 요청은 환자 정보를 생성 합니다. 69 | 70 | include::{snippets}/patients-create/http-request.adoc[] 71 | 72 | ===== Request Structure 73 | 74 | ====== request-fields 75 | 76 | include::{snippets}/patients-create/request-fields.adoc[] 77 | 78 | ===== Response Structure 79 | 80 | ====== response-headers 81 | 82 | include::{snippets}/patients-create/response-headers.adoc[] 83 | 84 | ===== Example Response 85 | 86 | include::{snippets}/patients-create/http-response.adoc[] 87 | 88 | 89 | [[resources-patients-update]] 90 | ==== /patients/{id} [small silver]*[PATCH]* 91 | 92 | `PATCH` 요청은 환자 정보를 수정 합니다. 93 | 94 | include::{snippets}/patients-update/http-request.adoc[] 95 | 96 | ===== Request Structure 97 | 98 | ====== path-parameters 99 | 100 | include::{snippets}/patients-delete/path-parameters.adoc[] 101 | 102 | ====== request-fields 103 | 104 | include::{snippets}/patients-create/request-fields.adoc[] 105 | 106 | ===== Example Response 107 | 108 | include::{snippets}/patients-update/http-response.adoc[] 109 | 110 | 111 | [[resources-patients-delete]] 112 | ==== /patients/{id} [small silver]*[DELETE]* 113 | 114 | `DELETE` 요청은 환자 정보를 삭제 합니다. 115 | 116 | include::{snippets}/patients-delete/http-request.adoc[] 117 | 118 | ===== Request Structure 119 | 120 | ====== path-parameters 121 | 122 | include::{snippets}/patients-delete/path-parameters.adoc[] 123 | 124 | ===== Example Response 125 | 126 | include::{snippets}/patients-delete/http-response.adoc[] 127 | -------------------------------------------------------------------------------- /src/main/asciidoc/resources/resources-schedules.adoc: -------------------------------------------------------------------------------- 1 | [[resources-schedules]] 2 | === Schedules 3 | 4 | 진료 스케쥴 목록과 상세 정보, 생성, 수정, 삭제를 요청 하는데 사용 합니다. 5 | 6 | [[resources-schedules-show-all]] 7 | ==== /schedules [small silver]*[GET]* 8 | 9 | `GET` 요청은 진료 스케쥴 리스트를 검색 합니다. 10 | 11 | include::{snippets}/schedules-show-all/http-request.adoc[] 12 | 13 | ===== Request Structure 14 | 15 | ====== request-parameters 16 | 17 | include::{snippets}/schedules-show-all/request-parameters.adoc[] 18 | 19 | ===== Response Structure 20 | 21 | ====== response-fields 22 | [[resources-schedules-show-all-response-fields]] 23 | 24 | include::{snippets}/schedules-show-all/response-fields.adoc[] 25 | 26 | ===== Example Response 27 | 28 | include::{snippets}/schedules-show-all/http-response.adoc[] 29 | 30 | [[resources-index-links]] 31 | ===== Links 32 | 33 | include::{snippets}/schedules-show-all/links.adoc[] 34 | 35 | 36 | [[resources-schedules-show-one]] 37 | ==== /schedules/{id} [small silver]*[GET]* 38 | 39 | `GET` 요청은 진료 스케쥴 정보를 검색 합니다. 40 | 41 | include::{snippets}/schedules-show-one/http-request.adoc[] 42 | 43 | ===== Request Structure 44 | 45 | ====== path-parameters 46 | 47 | include::{snippets}/schedules-show-one/path-parameters.adoc[] 48 | 49 | ===== Response Structure 50 | 51 | ====== response-fields 52 | 53 | include::{snippets}/schedules-show-one/response-fields.adoc[] 54 | 55 | ===== Example Response 56 | 57 | include::{snippets}/schedules-show-one/http-response.adoc[] 58 | 59 | [[resources-index-links]] 60 | ===== Links 61 | 62 | include::{snippets}/schedules-show-one/links.adoc[] 63 | 64 | 65 | [[resources-schedules-create]] 66 | ==== /schedules/{id} [small silver]*[PUT]* 67 | 68 | `PUT` 요청은 진료 스케쥴 정보를 생성 합니다. 69 | 70 | include::{snippets}/schedules-create/http-request.adoc[] 71 | 72 | ===== Request Structure 73 | 74 | ====== request-fields 75 | 76 | include::{snippets}/schedules-create/request-fields.adoc[] 77 | 78 | ===== Response Structure 79 | 80 | ====== response-headers 81 | 82 | include::{snippets}/schedules-create/response-headers.adoc[] 83 | 84 | ===== Example Response 85 | 86 | include::{snippets}/schedules-create/http-response.adoc[] 87 | 88 | 89 | [[resources-schedules-update]] 90 | ==== /schedules/{id} [small silver]*[PATCH]* 91 | 92 | `PATCH` 요청은 진료 스케쥴 정보를 수정 합니다. 93 | 94 | include::{snippets}/schedules-update/http-request.adoc[] 95 | 96 | ===== Request Structure 97 | 98 | ====== path-parameters 99 | 100 | include::{snippets}/schedules-delete/path-parameters.adoc[] 101 | 102 | ====== request-fields 103 | 104 | include::{snippets}/schedules-create/request-fields.adoc[] 105 | 106 | ===== Example Response 107 | 108 | include::{snippets}/schedules-update/http-response.adoc[] 109 | 110 | 111 | [[resources-schedules-delete]] 112 | ==== /schedules/{id} [small silver]*[DELETE]* 113 | 114 | `DELETE` 요청은 진료 스케쥴 정보를 삭제 합니다. 115 | 116 | include::{snippets}/schedules-delete/http-request.adoc[] 117 | 118 | ===== Request Structure 119 | 120 | ====== path-parameters 121 | 122 | include::{snippets}/schedules-delete/path-parameters.adoc[] 123 | 124 | ===== Example Response 125 | 126 | include::{snippets}/schedules-delete/http-response.adoc[] 127 | -------------------------------------------------------------------------------- /src/main/asciidoc/static/css/nanum.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/earlyaccess/nanumgothic.css); 2 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400); 3 | /* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ 4 | /* Remove the comments around the @import statement below when using this as a custom stylesheet */ 5 | /*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400";*/ 6 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} 7 | audio,canvas,video{display:inline-block} 8 | audio:not([controls]){display:none;height:0} 9 | [hidden],template{display:none} 10 | script{display:none!important} 11 | html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} 12 | body{margin:0} 13 | a{background:transparent} 14 | a:focus{outline:thin dotted} 15 | a:active,a:hover{outline:0} 16 | h1{font-size:2em;margin:.67em 0} 17 | abbr[title]{border-bottom:1px dotted} 18 | b,strong{font-weight:bold} 19 | dfn{font-style:italic} 20 | hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} 21 | mark{background:#ff0;color:#000} 22 | code,kbd,pre,samp{font-family:monospace;font-size:1em} 23 | pre{white-space:pre-wrap} 24 | q{quotes:"\201C" "\201D" "\2018" "\2019"} 25 | small{font-size:80%} 26 | sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} 27 | sup{top:-.5em} 28 | sub{bottom:-.25em} 29 | img{border:0} 30 | svg:not(:root){overflow:hidden} 31 | figure{margin:0} 32 | fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} 33 | legend{border:0;padding:0} 34 | button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} 35 | button,input{line-height:normal} 36 | button,select{text-transform:none} 37 | button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} 38 | button[disabled],html input[disabled]{cursor:default} 39 | input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} 40 | input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} 41 | input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} 42 | button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} 43 | textarea{overflow:auto;vertical-align:top} 44 | table{border-collapse:collapse;border-spacing:0} 45 | *,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} 46 | html,body{font-size:100%} 47 | body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Nanum gothic","Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto} 48 | a:hover{cursor:pointer} 49 | img,object,embed{max-width:100%;height:auto} 50 | object,embed{height:100%} 51 | img{-ms-interpolation-mode:bicubic} 52 | #map_canvas img,#map_canvas embed,#map_canvas object,.map_canvas img,.map_canvas embed,.map_canvas object{max-width:none!important} 53 | .left{float:left!important} 54 | .right{float:right!important} 55 | .text-left{text-align:left!important} 56 | .text-right{text-align:right!important} 57 | .text-center{text-align:center!important} 58 | .text-justify{text-align:justify!important} 59 | .hide{display:none} 60 | .antialiased,body{-webkit-font-smoothing:antialiased} 61 | img{display:inline-block;vertical-align:middle} 62 | textarea{height:auto;min-height:50px} 63 | select{width:100%} 64 | p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6} 65 | .subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} 66 | div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} 67 | a{color:#2156a5;text-decoration:underline;line-height:inherit} 68 | a:hover,a:focus{color:#1d4b8f} 69 | a img{border:none} 70 | p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} 71 | p aside{font-size:.875em;line-height:1.35;font-style:italic} 72 | h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} 73 | h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} 74 | h1{font-size:2.125em} 75 | h2{font-size:1.6875em} 76 | h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} 77 | h4,h5{font-size:1.125em} 78 | h6{font-size:1em} 79 | hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} 80 | em,i{font-style:italic;line-height:inherit} 81 | strong,b{font-weight:bold;line-height:inherit} 82 | small{font-size:60%;line-height:inherit} 83 | code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} 84 | ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} 85 | ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em} 86 | ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} 87 | ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} 88 | ul.square{list-style-type:square} 89 | ul.circle{list-style-type:circle} 90 | ul.disc{list-style-type:disc} 91 | ul.no-bullet{list-style:none} 92 | ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} 93 | dl dt{margin-bottom:.3125em;font-weight:bold} 94 | dl dd{margin-bottom:1.25em} 95 | abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} 96 | abbr{text-transform:none} 97 | blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} 98 | blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} 99 | blockquote cite:before{content:"\2014 \0020"} 100 | blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} 101 | blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} 102 | @media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} 103 | h1{font-size:2.75em} 104 | h2{font-size:2.3125em} 105 | h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} 106 | h4{font-size:1.4375em}}table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} 107 | table thead,table tfoot{background:#f7f8f7;font-weight:bold} 108 | table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} 109 | table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} 110 | table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} 111 | table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} 112 | h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} 113 | h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} 114 | .clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} 115 | .clearfix:after,.float-group:after{clear:both} 116 | *:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} 117 | pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} 118 | .keyseq{color:rgba(51,51,51,.8)} 119 | kbd{display:inline-block;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.4;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:-.15em .15em 0 .15em;padding:.2em .6em .2em .5em;vertical-align:middle;white-space:nowrap} 120 | .keyseq kbd:first-child{margin-left:0} 121 | .keyseq kbd:last-child{margin-right:0} 122 | .menuseq,.menu{color:rgba(0,0,0,.8)} 123 | b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} 124 | b.button:before{content:"[";padding:0 3px 0 2px} 125 | b.button:after{content:"]";padding:0 2px 0 3px} 126 | p a>code:hover{color:rgba(0,0,0,.9)} 127 | #header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} 128 | #header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} 129 | #header:after,#content:after,#footnotes:after,#footer:after{clear:both} 130 | #content{margin-top:1.25em} 131 | #content:before{content:none} 132 | #header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} 133 | #header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8} 134 | #header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px} 135 | #header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} 136 | #header .details span:first-child{margin-left:-.125em} 137 | #header .details span.email a{color:rgba(0,0,0,.85)} 138 | #header .details br{display:none} 139 | #header .details br+span:before{content:"\00a0\2013\00a0"} 140 | #header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} 141 | #header .details br+span#revremark:before{content:"\00a0|\00a0"} 142 | #header #revnumber{text-transform:capitalize} 143 | #header #revnumber:after{content:"\00a0"} 144 | #content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} 145 | #toc{border-bottom:1px solid #efefed;padding-bottom:.5em} 146 | #toc>ul{margin-left:.125em} 147 | #toc ul.sectlevel0>li>a{font-style:italic} 148 | #toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} 149 | #toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} 150 | #toc a{text-decoration:none} 151 | #toc a:active{text-decoration:underline} 152 | #toctitle{color:#7a2518;font-size:1.2em} 153 | @media only screen and (min-width:768px){#toctitle{font-size:1.375em} 154 | body.toc2{padding-left:15em;padding-right:0} 155 | #toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} 156 | #toc.toc2 #toctitle{margin-top:0;font-size:1.2em} 157 | #toc.toc2>ul{font-size:.9em;margin-bottom:0} 158 | #toc.toc2 ul ul{margin-left:0;padding-left:1em} 159 | #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} 160 | body.toc2.toc-right{padding-left:0;padding-right:15em} 161 | body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} 162 | #toc.toc2{width:20em} 163 | #toc.toc2 #toctitle{font-size:1.375em} 164 | #toc.toc2>ul{font-size:.95em} 165 | #toc.toc2 ul ul{padding-left:1.25em} 166 | body.toc2.toc-right{padding-left:0;padding-right:20em}}#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} 167 | #content #toc>:first-child{margin-top:0} 168 | #content #toc>:last-child{margin-bottom:0} 169 | #footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} 170 | #footer-text{color:rgba(255,255,255,.8);line-height:1.44} 171 | .sect1{padding-bottom:.625em} 172 | @media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}.sect1+.sect1{border-top:1px solid #efefed} 173 | #content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} 174 | #content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} 175 | #content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} 176 | #content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} 177 | #content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} 178 | .audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} 179 | .admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Nanum gothic","Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} 180 | table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0} 181 | .paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)} 182 | table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit} 183 | .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} 184 | .admonitionblock>table td.icon{text-align:center;width:80px} 185 | .admonitionblock>table td.icon img{max-width:none} 186 | .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} 187 | .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} 188 | .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} 189 | .exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} 190 | .exampleblock>.content>:first-child{margin-top:0} 191 | .exampleblock>.content>:last-child{margin-bottom:0} 192 | .sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} 193 | .sidebarblock>:first-child{margin-top:0} 194 | .sidebarblock>:last-child{margin-bottom:0} 195 | .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} 196 | .exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} 197 | .literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} 198 | .sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} 199 | .literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em} 200 | .literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal} 201 | @media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} 202 | .listingblock pre.highlightjs{padding:0} 203 | .listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} 204 | .listingblock pre.prettyprint{border-width:0} 205 | .listingblock>.content{position:relative} 206 | .listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} 207 | .listingblock:hover code[data-lang]:before{display:block} 208 | .listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} 209 | .listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} 210 | table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} 211 | table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0} 212 | table.pyhltable td.code{padding-left:.75em;padding-right:0} 213 | pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} 214 | pre.pygments .lineno{display:inline-block;margin-right:.25em} 215 | table.pyhltable .linenodiv{background:none!important;padding-right:0!important} 216 | .quoteblock{margin:0 1em 1.25em 1.5em;display:table} 217 | .quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} 218 | .quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} 219 | .quoteblock blockquote{margin:0;padding:0;border:0} 220 | .quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} 221 | .quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} 222 | .quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right} 223 | .quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} 224 | .quoteblock .quoteblock blockquote{padding:0 0 0 .75em} 225 | .quoteblock .quoteblock blockquote:before{display:none} 226 | .verseblock{margin:0 1em 1.25em 1em} 227 | .verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} 228 | .verseblock pre strong{font-weight:400} 229 | .verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} 230 | .quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} 231 | .quoteblock .attribution br,.verseblock .attribution br{display:none} 232 | .quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.05em;color:rgba(0,0,0,.6)} 233 | .quoteblock.abstract{margin:0 0 1.25em 0;display:block} 234 | .quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} 235 | .quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} 236 | table.tableblock{max-width:100%;border-collapse:separate} 237 | table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0} 238 | table.spread{width:100%} 239 | table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} 240 | table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} 241 | table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0} 242 | table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} 243 | table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0} 244 | table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0} 245 | table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0} 246 | table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0} 247 | table.frame-all{border-width:1px} 248 | table.frame-sides{border-width:0 1px} 249 | table.frame-topbot{border-width:1px 0} 250 | th.halign-left,td.halign-left{text-align:left} 251 | th.halign-right,td.halign-right{text-align:right} 252 | th.halign-center,td.halign-center{text-align:center} 253 | th.valign-top,td.valign-top{vertical-align:top} 254 | th.valign-bottom,td.valign-bottom{vertical-align:bottom} 255 | th.valign-middle,td.valign-middle{vertical-align:middle} 256 | table thead th,table tfoot th{font-weight:bold} 257 | tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} 258 | tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} 259 | p.tableblock>code:only-child{background:none;padding:0} 260 | p.tableblock{font-size:1em} 261 | td>div.verse{white-space:pre} 262 | ol{margin-left:1.75em} 263 | ul li ol{margin-left:1.5em} 264 | dl dd{margin-left:1.125em} 265 | dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} 266 | ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} 267 | ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} 268 | ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} 269 | ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em} 270 | ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px} 271 | ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} 272 | ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block} 273 | ul.inline>li>*{display:block} 274 | .unstyled dl dt{font-weight:400;font-style:normal} 275 | ol.arabic{list-style-type:decimal} 276 | ol.decimal{list-style-type:decimal-leading-zero} 277 | ol.loweralpha{list-style-type:lower-alpha} 278 | ol.upperalpha{list-style-type:upper-alpha} 279 | ol.lowerroman{list-style-type:lower-roman} 280 | ol.upperroman{list-style-type:upper-roman} 281 | ol.lowergreek{list-style-type:lower-greek} 282 | .hdlist>table,.colist>table{border:0;background:none} 283 | .hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} 284 | td.hdlist1{padding-right:.75em;font-weight:bold} 285 | td.hdlist1,td.hdlist2{vertical-align:top} 286 | .literalblock+.colist,.listingblock+.colist{margin-top:-.5em} 287 | .colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} 288 | .colist>table tr>td:last-of-type{padding:.25em 0} 289 | .thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} 290 | .imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} 291 | .imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} 292 | .imageblock>.title{margin-bottom:0} 293 | .imageblock.thumb,.imageblock.th{border-width:6px} 294 | .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} 295 | .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} 296 | .image.left{margin-right:.625em} 297 | .image.right{margin-left:.625em} 298 | a.image{text-decoration:none} 299 | span.footnote,span.footnoteref{vertical-align:super;font-size:.875em} 300 | span.footnote a,span.footnoteref a{text-decoration:none} 301 | span.footnote a:active,span.footnoteref a:active{text-decoration:underline} 302 | #footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} 303 | #footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0} 304 | #footnotes .footnote{padding:0 .375em;line-height:1.3;font-size:.875em;margin-left:1.2em;text-indent:-1.2em;margin-bottom:.2em} 305 | #footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none} 306 | #footnotes .footnote:last-of-type{margin-bottom:0} 307 | #content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} 308 | .gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} 309 | .gist .file-data>table td.line-data{width:99%} 310 | div.unbreakable{page-break-inside:avoid} 311 | .big{font-size:larger} 312 | .small{font-size:smaller} 313 | .underline{text-decoration:underline} 314 | .overline{text-decoration:overline} 315 | .line-through{text-decoration:line-through} 316 | .aqua{color:#00bfbf} 317 | .aqua-background{background-color:#00fafa} 318 | .black{color:#000} 319 | .black-background{background-color:#000} 320 | .blue{color:#0000bf} 321 | .blue-background{background-color:#0000fa} 322 | .fuchsia{color:#bf00bf} 323 | .fuchsia-background{background-color:#fa00fa} 324 | .gray{color:#606060} 325 | .gray-background{background-color:#7d7d7d} 326 | .green{color:#006000} 327 | .green-background{background-color:#007d00} 328 | .lime{color:#00bf00} 329 | .lime-background{background-color:#00fa00} 330 | .maroon{color:#600000} 331 | .maroon-background{background-color:#7d0000} 332 | .navy{color:#000060} 333 | .navy-background{background-color:#00007d} 334 | .olive{color:#606000} 335 | .olive-background{background-color:#7d7d00} 336 | .purple{color:#600060} 337 | .purple-background{background-color:#7d007d} 338 | .red{color:#bf0000} 339 | .red-background{background-color:#fa0000} 340 | .silver{color:#909090} 341 | .silver-background{background-color:#bcbcbc} 342 | .teal{color:#006060} 343 | .teal-background{background-color:#007d7d} 344 | .white{color:#bfbfbf} 345 | .white-background{background-color:#fafafa} 346 | .yellow{color:#bfbf00} 347 | .yellow-background{background-color:#fafa00} 348 | span.icon>.fa{cursor:default} 349 | .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} 350 | .admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c} 351 | .admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} 352 | .admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900} 353 | .admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} 354 | .admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} 355 | .conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} 356 | .conum[data-value] *{color:#fff!important} 357 | .conum[data-value]+b{display:none} 358 | .conum[data-value]:after{content:attr(data-value)} 359 | pre .conum[data-value]{position:relative;top:-.125em} 360 | b.conum *{color:inherit!important} 361 | .conum:not([data-value]):empty{display:none} 362 | h1,h2{letter-spacing:-.01em} 363 | dt,th.tableblock,td.content{text-rendering:optimizeLegibility} 364 | p,td.content{letter-spacing:-.01em} 365 | p strong,td.content strong{letter-spacing:-.005em} 366 | p,blockquote,dt,td.content{font-size:1.0625rem} 367 | p{margin-bottom:1.25rem} 368 | .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} 369 | .exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} 370 | .print-only{display:none!important} 371 | @media print{@page{margin:1.25cm .75cm} 372 | *{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} 373 | a{color:inherit!important;text-decoration:underline!important} 374 | a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} 375 | a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} 376 | abbr[title]:after{content:" (" attr(title) ")"} 377 | pre,blockquote,tr,img{page-break-inside:avoid} 378 | thead{display:table-header-group} 379 | img{max-width:100%!important} 380 | p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} 381 | h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} 382 | #toc,.sidebarblock,.exampleblock>.content{background:none!important} 383 | #toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important} 384 | .sect1{padding-bottom:0!important} 385 | .sect1+.sect1{border:0!important} 386 | #header>h1:first-child{margin-top:1.25rem} 387 | body.book #header{text-align:center} 388 | body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0} 389 | body.book #header .details{border:0!important;display:block;padding:0!important} 390 | body.book #header .details span:first-child{margin-left:0!important} 391 | body.book #header .details br{display:block} 392 | body.book #header .details br+span:before{content:none!important} 393 | body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} 394 | body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} 395 | .listingblock code[data-lang]:before{display:block} 396 | #footer{background:none!important;padding:0 .9375em} 397 | #footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em} 398 | .hide-on-print{display:none!important} 399 | .print-only{display:block!important} 400 | .hide-for-print{display:none!important} 401 | .show-for-print{display:inherit!important}} 402 | -------------------------------------------------------------------------------- /src/main/asciidoc/v1.adoc: -------------------------------------------------------------------------------- 1 | = Appointment RESTful Developer API Documents 2 | Modern Java web application with Spring 3 | v1.0, 2014-01-01 4 | :toc: left 5 | :toclevels: 3 6 | :stylesheet : static/css/nanum.css 7 | 8 | [[abstract]] 9 | 10 | include::abstract.adoc[] 11 | 12 | [[overview]] 13 | == Overview 14 | 15 | include::overview/overview-current-version.adoc[] 16 | include::overview/overview-schema.adoc[] 17 | include::overview/overview-parameters.adoc[] 18 | include::overview/overview-root-endpoint.adoc[] 19 | include::overview/overview-client-errors.adoc[] 20 | include::overview/overview-http-redirects.adoc[] 21 | include::overview/overview-http-verbs.adoc[] 22 | include::overview/overview-authentication.adoc[] 23 | include::overview/overview-hypermedia.adoc[] 24 | include::overview/overview-pagination.adoc[] 25 | include::overview/overview-rate-limiting.adoc[] 26 | include::overview/overview-user-agent-required.adoc[] 27 | include::overview/overview-conditional-requests.adoc[] 28 | include::overview/overview-cors.adoc[] 29 | include::overview/overview-json-p-callbacks.adoc[] 30 | include::overview/overview-timezones.adoc[] 31 | 32 | [[resources]] 33 | == Resources 34 | 35 | include::resources/resources-index.adoc[] 36 | include::resources/resources-doctors.adoc[] 37 | include::resources/resources-patients.adoc[] 38 | include::resources/resources-schedules.adoc[] -------------------------------------------------------------------------------- /src/main/java/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/src/main/java/.gitkeep -------------------------------------------------------------------------------- /src/main/java/io/example/SampleBootRunner.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import io.example.config.loading.LoadingDataService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 10 | 11 | @EnableAutoConfiguration 12 | @ComponentScan 13 | @EnableJpaRepositories 14 | public class SampleBootRunner implements CommandLineRunner { 15 | 16 | @Autowired 17 | private LoadingDataService loadingDataService; 18 | 19 | public static void main(String[] args) throws Exception { 20 | SpringApplication.run(SampleBootRunner.class, args); 21 | } 22 | 23 | @Override 24 | public void run(String... args) throws Exception { 25 | loadingDataService.createDoctor(0, 29); 26 | loadingDataService.createPatient(0, 29); 27 | loadingDataService.createSchedule(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/io/example/common/AbstractPersistable.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 5 | import lombok.Data; 6 | import org.springframework.data.domain.Persistable; 7 | 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.Id; 10 | import javax.persistence.MappedSuperclass; 11 | import javax.persistence.Transient; 12 | import java.io.Serializable; 13 | 14 | /** 15 | * Created by gmind on 2015-10-07. 16 | */ 17 | @Data 18 | @MappedSuperclass 19 | public abstract class AbstractPersistable implements Persistable { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Id 24 | @GeneratedValue 25 | private PK id; 26 | 27 | @Transient 28 | @JsonIgnore 29 | public boolean isNew() { 30 | return null == getId(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/example/common/GlobalControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ControllerAdvice; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | 10 | @ControllerAdvice 11 | public class GlobalControllerExceptionHandler { 12 | 13 | @ExceptionHandler(ResourceNotFoundException.class) 14 | public void handleResourceNotFoundException(HttpServletResponse response, ResourceNotFoundException ex) throws IOException { 15 | response.sendError(HttpStatus.BAD_REQUEST.value(), " " + ex.getMessage()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/example/common/LocalDateToDateConverter.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import java.time.LocalDate; 4 | import javax.persistence.AttributeConverter; 5 | import javax.persistence.Converter; 6 | 7 | /** 8 | * Created by gmind on 2015-10-07. 9 | */ 10 | @Converter 11 | public class LocalDateToDateConverter implements AttributeConverter { 12 | 13 | @Override 14 | public java.sql.Date convertToDatabaseColumn(LocalDate entityValue) { 15 | if (entityValue != null) { 16 | return java.sql.Date.valueOf(entityValue); 17 | } 18 | return null; 19 | } 20 | 21 | @Override 22 | public LocalDate convertToEntityAttribute(java.sql.Date databaseValue) { 23 | if (databaseValue != null) { 24 | return databaseValue.toLocalDate(); 25 | } 26 | return null; 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/io/example/common/LocalTimeToTimeConverter.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import javax.persistence.AttributeConverter; 4 | import javax.persistence.Converter; 5 | import java.time.LocalTime; 6 | 7 | /** 8 | * Created by gmind on 2015-10-07. 9 | */ 10 | @Converter 11 | public class LocalTimeToTimeConverter implements AttributeConverter { 12 | 13 | @Override 14 | public java.sql.Time convertToDatabaseColumn(LocalTime entityValue) { 15 | if (entityValue != null) { 16 | return java.sql.Time.valueOf(entityValue); 17 | } 18 | return null; 19 | } 20 | 21 | @Override 22 | public LocalTime convertToEntityAttribute(java.sql.Time databaseValue) { 23 | if (databaseValue != null) { 24 | return databaseValue.toLocalTime(); 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/example/common/NestedContentResource.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | import org.springframework.hateoas.ResourceSupport; 5 | import org.springframework.hateoas.Resources; 6 | 7 | /** 8 | * Created by gmind on 2015-10-05. 9 | */ 10 | public class NestedContentResource extends ResourceSupport { 11 | 12 | private final Resources nested; 13 | 14 | public NestedContentResource(Iterable toNested) { 15 | this.nested = new Resources(toNested); 16 | } 17 | 18 | @JsonUnwrapped 19 | public Resources getNested() { 20 | return this.nested; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/example/common/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.example.common; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Created by KDS on 2015-10-09. 8 | */ 9 | @ResponseStatus(HttpStatus.BAD_REQUEST) 10 | public final class ResourceNotFoundException extends RuntimeException { 11 | 12 | private static final String _PREFIX_MESSAGE = "Please try again and with a non empty as "; 13 | 14 | public ResourceNotFoundException() { 15 | super(_PREFIX_MESSAGE+"id"); 16 | } 17 | 18 | public ResourceNotFoundException(String emptyName, Throwable cause) { 19 | super(_PREFIX_MESSAGE+emptyName, cause); 20 | } 21 | 22 | public ResourceNotFoundException(String emptyName) { 23 | super(_PREFIX_MESSAGE+emptyName); 24 | } 25 | 26 | public ResourceNotFoundException(Throwable cause) { 27 | super(cause); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/loading/LoadingDataService.java: -------------------------------------------------------------------------------- 1 | package io.example.config.loading; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.example.doctor.Doctor; 5 | import io.example.doctor.DoctorJpaRepository; 6 | import io.example.patient.Patient; 7 | import io.example.patient.PatientJpaRepository; 8 | import io.example.schedule.Schedule; 9 | import io.example.schedule.ScheduleJpaRepository; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.time.LocalDate; 15 | import java.time.LocalDateTime; 16 | import java.time.Month; 17 | import java.util.List; 18 | import java.util.Random; 19 | import java.util.stream.IntStream; 20 | 21 | /** 22 | * Created by gmind on 2015-10-07. 23 | */ 24 | @Slf4j 25 | @Service 26 | public class LoadingDataService { 27 | 28 | @Autowired 29 | private DoctorJpaRepository doctorJpaRepository; 30 | 31 | @Autowired 32 | private PatientJpaRepository patientJpaRepository; 33 | 34 | @Autowired 35 | private ScheduleJpaRepository scheduleJpaRepository; 36 | 37 | public void createDoctor(int startInclusive, int endExclusive) { 38 | log.debug("Loading Create Doctor Data"); 39 | this.doctorJpaRepository.deleteAll(); 40 | IntStream.range(startInclusive, endExclusive).forEach(x -> { 41 | Doctor doctor = new Doctor(); 42 | doctor.setName("doctor_name_"+x); 43 | this.doctorJpaRepository.save(doctor); 44 | }); 45 | } 46 | 47 | public void createPatient(int startInclusive, int endExclusive) { 48 | log.debug("Loading Create Patient Data"); 49 | this.patientJpaRepository.deleteAll(); 50 | IntStream.range(startInclusive, endExclusive).forEach(x -> { 51 | Patient patient = new Patient(); 52 | patient.setName("patient_name_"+x); 53 | patient.setBirthDate(LocalDate.now().minusYears(x).minusMonths(x).minusDays(x)); 54 | this.patientJpaRepository.save(patient); 55 | }); 56 | } 57 | 58 | public void createSchedule() { 59 | log.debug("Loading Create Schedule Data"); 60 | this.scheduleJpaRepository.deleteAll(); 61 | 62 | List scheduleCalendar = scheduleCalendar(); 63 | 64 | List doctors = this.doctorJpaRepository.findAll(); 65 | List patients = this.patientJpaRepository.findAll(); 66 | 67 | doctors.forEach(doctor -> { 68 | scheduleCalendar.forEach(dateTime -> { 69 | Schedule schedule = new Schedule(); 70 | schedule.setAppointmentDay(dateTime.toLocalDate()); 71 | schedule.setStartTime(dateTime.toLocalTime()); 72 | schedule.setEndTime(dateTime.plusMinutes(30).toLocalTime()); 73 | schedule.setDoctor(doctor); 74 | if(dateTime.getHour()%2==0) { 75 | schedule.setPatient(patients.get(new Random().nextInt(patients.size()))); 76 | } 77 | this.scheduleJpaRepository.save(schedule); 78 | }); 79 | }); 80 | } 81 | 82 | protected List scheduleCalendar() { 83 | 84 | List scheduleCalendar = Lists.newArrayList(); 85 | 86 | LocalDateTime currentDateTime = LocalDateTime.now(); 87 | 88 | int currentYear = currentDateTime.getYear(); 89 | Month currentMonth = currentDateTime.getMonth(); 90 | int currentDayOfMonth = currentDateTime.getDayOfMonth(); 91 | int currentLastDayOfMonth = currentMonth.maxLength(); 92 | 93 | IntStream.rangeClosed(currentDayOfMonth, currentLastDayOfMonth).forEach(day -> { 94 | LocalDateTime startDateTime = LocalDateTime.of(currentYear, currentMonth, day, 9, 0, 0); 95 | LocalDateTime finishDateTime = LocalDateTime.of(currentYear, currentMonth, day, 17, 30, 0); 96 | scheduleCalendar.add(startDateTime); 97 | while(!startDateTime.equals(finishDateTime)) { 98 | startDateTime = startDateTime.plusMinutes(30); 99 | scheduleCalendar.add(startDateTime); 100 | } 101 | }); 102 | 103 | return scheduleCalendar; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/mapper/AutoMapper.java: -------------------------------------------------------------------------------- 1 | package io.example.config.mapper; 2 | 3 | import org.apache.commons.beanutils.PropertyUtils; 4 | import org.apache.commons.lang3.math.NumberUtils; 5 | import org.modelmapper.ModelMapper; 6 | import org.springframework.util.StringUtils; 7 | 8 | import java.beans.PropertyDescriptor; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class AutoMapper extends ModelMapper { 15 | 16 | private static Map descriptorsMap = new HashMap(); 17 | 18 | public D map(Object sourceObject, Object targetObject, Class destinationType) { 19 | Object resultObject = null; 20 | try { 21 | if (sourceObject == null || targetObject == null) { 22 | throw new NullPointerException("A null paramter was passed into targetObject"); 23 | } 24 | resultObject = targetObject.getClass().newInstance(); 25 | Class sourceClass = sourceObject.getClass(); 26 | Class targetClass = targetObject.getClass(); 27 | if (!sourceClass.equals(targetClass)) { 28 | throw new IllegalArgumentException("Received parameters are not the same type of class, but must be"); 29 | } 30 | 31 | PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(targetClass); 32 | if (descriptors == null) { 33 | descriptors = PropertyUtils.getPropertyDescriptors(targetClass); 34 | descriptorsMap.put(targetClass, descriptors); 35 | } 36 | 37 | for (PropertyDescriptor descriptor : descriptors) { 38 | if (PropertyUtils.isReadable(targetObject, descriptor.getName()) && PropertyUtils.isWriteable(targetObject, descriptor.getName())) { 39 | Method readMethod = descriptor.getReadMethod(); 40 | Object sourceValue = readMethod.invoke(sourceObject); 41 | Object targetValue = readMethod.invoke(targetObject); 42 | Object resultValue = targetValue; 43 | if (sourceValue != null || sourceValue == targetValue) { 44 | resultValue = sourceValue; 45 | } 46 | Method writeMethod = descriptor.getWriteMethod(); 47 | writeMethod.invoke(resultObject, resultValue); 48 | } 49 | } 50 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 51 | e.printStackTrace(); 52 | } 53 | return destinationType.cast(resultObject); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/io/example/config/mapper/AutoMapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.example.config.mapper; 2 | 3 | import org.modelmapper.ModelMapper; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @ConditionalOnClass(ModelMapper.class) 11 | public class AutoMapperConfiguration { 12 | 13 | @Bean 14 | @ConditionalOnMissingBean(AutoMapperFactoryBean.class) 15 | public AutoMapperFactoryBean modelMapperFactoryBean() { 16 | return new AutoMapperFactoryBean(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/mapper/AutoMapperConfigurer.java: -------------------------------------------------------------------------------- 1 | package io.example.config.mapper; 2 | 3 | public interface AutoMapperConfigurer { 4 | 5 | void configure(AutoMapper autoMapper); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/mapper/AutoMapperFactoryBean.java: -------------------------------------------------------------------------------- 1 | package io.example.config.mapper; 2 | 3 | import org.modelmapper.config.Configuration; 4 | import org.modelmapper.convention.MatchingStrategies; 5 | import org.springframework.beans.factory.FactoryBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import java.util.List; 9 | 10 | public class AutoMapperFactoryBean implements FactoryBean { 11 | 12 | private static final Class AUTO_MAPPER_CLASS = AutoMapper.class; 13 | 14 | @Autowired(required = false) 15 | private List configurers; 16 | 17 | @Override 18 | public AutoMapper getObject() throws Exception { 19 | final AutoMapper autoMapper = new AutoMapper(); 20 | autoMapper.getConfiguration() 21 | .setFieldMatchingEnabled(true) 22 | .setFieldAccessLevel(Configuration.AccessLevel.PACKAGE_PRIVATE) 23 | .setMatchingStrategy(MatchingStrategies.LOOSE); 24 | configure(autoMapper); 25 | return autoMapper; 26 | } 27 | 28 | @Override 29 | public Class getObjectType() { 30 | return AUTO_MAPPER_CLASS; 31 | } 32 | 33 | @Override 34 | public boolean isSingleton() { 35 | return true; 36 | } 37 | 38 | private void configure(AutoMapper autoMapper) { 39 | if (configurers != null) { 40 | for (AutoMapperConfigurer autoMapperConfigurer : configurers) { 41 | autoMapperConfigurer.configure(autoMapper); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/mapper/PropertyMapConfigurerSupport.java: -------------------------------------------------------------------------------- 1 | package io.example.config.mapper; 2 | 3 | import org.modelmapper.PropertyMap; 4 | 5 | public abstract class PropertyMapConfigurerSupport implements AutoMapperConfigurer { 6 | 7 | public abstract PropertyMap mapping(); 8 | 9 | @Override 10 | public void configure(AutoMapper autoMapper) { 11 | autoMapper.addMappings(mapping()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/example/config/web/WebBootConfig.java: -------------------------------------------------------------------------------- 1 | package io.example.config.web; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | 7 | /** 8 | * Created by KDS on 2015-10-10. 9 | */ 10 | @Configuration 11 | public class WebBootConfig extends WebMvcConfigurerAdapter { 12 | 13 | @Override 14 | public void addViewControllers(ViewControllerRegistry registry) { 15 | registry.addViewController("/docs").setViewName("redirect:docs/index.html"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/example/doctor/Doctor.java: -------------------------------------------------------------------------------- 1 | package io.example.doctor; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 5 | import io.example.schedule.Schedule; 6 | import io.example.common.AbstractPersistable; 7 | 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | import javax.persistence.CascadeType; 14 | import javax.persistence.Column; 15 | import javax.persistence.Entity; 16 | import javax.persistence.OneToMany; 17 | import java.io.Serializable; 18 | import java.util.List; 19 | 20 | /** 21 | * Created by gmind on 2015-10-05. 22 | */ 23 | @Data 24 | @NoArgsConstructor 25 | @EqualsAndHashCode(callSuper = true, exclude = "schedules") 26 | @ToString(callSuper = true, exclude = "schedules") 27 | @Entity 28 | public class Doctor extends AbstractPersistable implements Serializable { 29 | 30 | private static final long serialVersionUID = 1L; 31 | 32 | public Doctor(Long id) { 33 | this.setId(id); 34 | } 35 | 36 | @Column(nullable = false) 37 | private String name; 38 | 39 | @JsonIgnore 40 | @OneToMany(mappedBy = "doctor", cascade = CascadeType.REMOVE) 41 | private List schedules; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/example/doctor/DoctorInput.java: -------------------------------------------------------------------------------- 1 | package io.example.doctor; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | import lombok.*; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Created by gmind on 2015-10-05. 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | @RequiredArgsConstructor 16 | public class DoctorInput implements Serializable { 17 | 18 | private static final long serialVersionUID = 1L; 19 | 20 | @JsonUnwrapped 21 | private Long id; 22 | 23 | @NonNull 24 | private String name; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/example/doctor/DoctorJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.example.doctor; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | /** 7 | * Created by gmind on 2015-10-05. 8 | */ 9 | @Transactional(readOnly = true) 10 | public interface DoctorJpaRepository extends JpaRepository { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/io/example/doctor/DoctorResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.example.doctor; 2 | 3 | import io.example.common.ResourceNotFoundException; 4 | import org.springframework.hateoas.Resource; 5 | import org.springframework.hateoas.mvc.ResourceAssemblerSupport; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | import static io.example.doctor.DoctorResourceAssembler.DoctorResource; 11 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 12 | /** 13 | * Created by gmind on 2015-10-05. 14 | */ 15 | @Component 16 | public class DoctorResourceAssembler extends ResourceAssemblerSupport { 17 | 18 | public DoctorResourceAssembler() { 19 | super(DoctorRestController.class, DoctorResource.class); 20 | } 21 | 22 | @Override 23 | public DoctorResource toResource(Doctor doctor) { 24 | if(doctor.getId()==null) { 25 | throw new ResourceNotFoundException("doctor"); 26 | } 27 | DoctorResource resource = createResourceWithId(doctor.getId(), doctor); 28 | resource.add(linkTo(DoctorRestController.class, LocalDateTime.now().toLocalDate()).slash(doctor.getId()).slash("schedules").withRel("doctor_schedules")); 29 | return resource; 30 | } 31 | 32 | @Override 33 | protected DoctorResource instantiateResource(Doctor doctor) { 34 | return new DoctorResource(doctor); 35 | } 36 | 37 | public static class DoctorResource extends Resource { 38 | public DoctorResource(Doctor doctor) { 39 | super(doctor); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/example/doctor/DoctorRestController.java: -------------------------------------------------------------------------------- 1 | package io.example.doctor; 2 | 3 | import io.example.config.mapper.AutoMapper; 4 | import io.example.schedule.Schedule; 5 | import io.example.schedule.ScheduleJpaRepository; 6 | import io.example.schedule.ScheduleResourceAssembler; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.PageImpl; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.web.PageableDefault; 12 | import org.springframework.data.web.PagedResourcesAssembler; 13 | import org.springframework.format.annotation.DateTimeFormat; 14 | import org.springframework.hateoas.PagedResources; 15 | import org.springframework.hateoas.Resource; 16 | import org.springframework.http.HttpHeaders; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.web.bind.annotation.*; 19 | 20 | import java.time.LocalDate; 21 | import java.time.LocalDateTime; 22 | 23 | import static io.example.doctor.DoctorResourceAssembler.DoctorResource; 24 | import static io.example.schedule.ScheduleResourceAssembler.ScheduleResource; 25 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 26 | 27 | /** 28 | * Created by gmind on 2015-10-05. 29 | */ 30 | @RestController 31 | @RequestMapping(value = "/doctors") 32 | public class DoctorRestController { 33 | 34 | @Autowired 35 | private DoctorJpaRepository doctorJpaRepository; 36 | 37 | @Autowired 38 | private ScheduleJpaRepository scheduleJpaRepository; 39 | 40 | @Autowired 41 | private DoctorResourceAssembler doctorResourceAssembler; 42 | 43 | @Autowired 44 | private ScheduleResourceAssembler scheduleResourceAssembler; 45 | 46 | @Autowired 47 | private PagedResourcesAssembler pagedResourcesAssembler; 48 | 49 | @Autowired 50 | private AutoMapper autoMapper; 51 | 52 | @RequestMapping(method = RequestMethod.GET) 53 | public PagedResources showAll(@PageableDefault Pageable pageable) { 54 | Page doctors = this.doctorJpaRepository.findAll(pageable); 55 | return this.pagedResourcesAssembler.toResource(doctors, doctorResourceAssembler); 56 | } 57 | 58 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 59 | public Resource showOne(@PathVariable("id") Long id) { 60 | Doctor entity = this.doctorJpaRepository.findOne(id); 61 | return this.doctorResourceAssembler.toResource(entity); 62 | } 63 | 64 | @ResponseStatus(HttpStatus.CREATED) 65 | @RequestMapping(method = RequestMethod.PUT) 66 | public HttpHeaders create(@RequestBody DoctorInput doctorInput) { 67 | Doctor mapping = this.autoMapper.map(doctorInput, Doctor.class); 68 | Doctor entity = this.doctorJpaRepository.save(mapping); 69 | HttpHeaders httpHeaders = new HttpHeaders(); 70 | httpHeaders.setLocation(linkTo(DoctorRestController.class).slash(entity.getId()).toUri()); 71 | return httpHeaders; 72 | } 73 | 74 | @ResponseStatus(HttpStatus.NO_CONTENT) 75 | @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) 76 | public void update(@PathVariable("id") Long id, @RequestBody DoctorInput doctorInput) { 77 | Doctor source = this.autoMapper.map(doctorInput, Doctor.class); 78 | Doctor target = this.doctorJpaRepository.findOne(id); 79 | Doctor mapping = this.autoMapper.map(source, target, Doctor.class); 80 | this.doctorJpaRepository.save(mapping); 81 | } 82 | 83 | @ResponseStatus(HttpStatus.OK) 84 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 85 | public void delete(@PathVariable("id") Long id) { 86 | this.doctorJpaRepository.delete(id); 87 | } 88 | 89 | @RequestMapping(value = "/{id}/schedules", method = RequestMethod.GET) 90 | public PagedResources showSchdules(@PathVariable("id") Long id, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate appointmentDay, @PageableDefault(size = 50) Pageable pageable) { 91 | appointmentDay = (appointmentDay==null) ? LocalDateTime.now().toLocalDate() : appointmentDay; 92 | Page schedules = this.scheduleJpaRepository.findByDoctorIdAndAppointmentDay(id, appointmentDay, pageable); 93 | return this.pagedResourcesAssembler.toResource(schedules, scheduleResourceAssembler); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/io/example/index/IndexRestController.java: -------------------------------------------------------------------------------- 1 | package io.example.index; 2 | 3 | import io.example.doctor.DoctorRestController; 4 | import io.example.patient.PatientRestController; 5 | import io.example.schedule.ScheduleRestController; 6 | import org.springframework.hateoas.ResourceSupport; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 12 | 13 | /** 14 | * Created by gmind on 2015-10-06. 15 | */ 16 | @RestController 17 | @RequestMapping(value = "/") 18 | public class IndexRestController { 19 | 20 | @RequestMapping(method= RequestMethod.GET) 21 | public ResourceSupport index() { 22 | ResourceSupport index = new ResourceSupport(); 23 | index.add(linkTo(DoctorRestController.class).withRel("doctors")); 24 | index.add(linkTo(PatientRestController.class).withRel("patient")); 25 | index.add(linkTo(ScheduleRestController.class).withRel("schedule")); 26 | return index; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/example/patient/Patient.java: -------------------------------------------------------------------------------- 1 | package io.example.patient; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import io.example.common.LocalDateToDateConverter; 5 | import io.example.schedule.Schedule; 6 | import io.example.common.AbstractPersistable; 7 | 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | import javax.persistence.*; 14 | import java.io.Serializable; 15 | import java.time.LocalDate; 16 | import java.util.List; 17 | 18 | /** 19 | * Created by gmind on 2015-10-05. 20 | */ 21 | @Data 22 | @NoArgsConstructor 23 | @EqualsAndHashCode(callSuper = true, exclude = "schedules") 24 | @ToString(callSuper = true, exclude = "schedules") 25 | @Entity 26 | public class Patient extends AbstractPersistable implements Serializable { 27 | 28 | private static final long serialVersionUID = 1L; 29 | 30 | public Patient(Long id) { 31 | this.setId(id); 32 | } 33 | 34 | @Column(nullable = false) 35 | private String name; 36 | 37 | @Column(nullable = false) 38 | @Convert(converter = LocalDateToDateConverter.class) 39 | private LocalDate birthDate; 40 | 41 | @JsonIgnore 42 | @OneToMany(mappedBy = "patient", cascade = CascadeType.ALL) 43 | private List schedules; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/example/patient/PatientInput.java: -------------------------------------------------------------------------------- 1 | package io.example.patient; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 7 | import lombok.*; 8 | import org.springframework.format.annotation.DateTimeFormat; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDate; 12 | 13 | /** 14 | * Created by gmind on 2015-10-05. 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @RequiredArgsConstructor 20 | public class PatientInput implements Serializable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | @JsonUnwrapped 25 | private Long id; 26 | 27 | @NonNull 28 | private String name; 29 | 30 | @NonNull 31 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 32 | private LocalDate birthDate; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/example/patient/PatientJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.example.patient; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | /** 7 | * Created by gmind on 2015-10-05. 8 | */ 9 | @Transactional(readOnly = true) 10 | public interface PatientJpaRepository extends JpaRepository { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/example/patient/PatientResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.example.patient; 2 | 3 | import io.example.common.ResourceNotFoundException; 4 | import io.example.doctor.Doctor; 5 | import org.springframework.hateoas.Resource; 6 | import org.springframework.hateoas.mvc.ResourceAssemblerSupport; 7 | import org.springframework.stereotype.Component; 8 | 9 | import static io.example.patient.PatientResourceAssembler.PatientResource; 10 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 11 | 12 | /** 13 | * Created by gmind on 2015-10-05. 14 | */ 15 | @Component 16 | public class PatientResourceAssembler extends ResourceAssemblerSupport { 17 | 18 | public PatientResourceAssembler() { 19 | super(PatientRestController.class, PatientResource.class); 20 | } 21 | 22 | @Override 23 | public PatientResource toResource(Patient patient) { 24 | if(patient.getId()==null) { 25 | throw new ResourceNotFoundException("patient"); 26 | } 27 | PatientResource resource = createResourceWithId(patient.getId(), patient); 28 | resource.add(linkTo(PatientRestController.class).slash(patient.getId()).slash("schedules").withRel("patient_schedules")); 29 | return resource; 30 | } 31 | 32 | @Override 33 | protected PatientResource instantiateResource(Patient patient) { 34 | return new PatientResource(patient); 35 | } 36 | 37 | public static class PatientResource extends Resource { 38 | public PatientResource(Patient patient) { 39 | super(patient); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/example/patient/PatientRestController.java: -------------------------------------------------------------------------------- 1 | package io.example.patient; 2 | 3 | import io.example.config.mapper.AutoMapper; 4 | import io.example.schedule.Schedule; 5 | import io.example.schedule.ScheduleJpaRepository; 6 | import io.example.schedule.ScheduleResourceAssembler; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.data.web.PageableDefault; 11 | import org.springframework.data.web.PagedResourcesAssembler; 12 | import org.springframework.hateoas.PagedResources; 13 | import org.springframework.hateoas.Resource; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import static io.example.patient.PatientResourceAssembler.PatientResource; 19 | import static io.example.schedule.ScheduleResourceAssembler.ScheduleResource; 20 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 21 | 22 | /** 23 | * Created by gmind on 2015-10-05. 24 | */ 25 | @RestController 26 | @RequestMapping(value = "/patients") 27 | public class PatientRestController { 28 | 29 | @Autowired 30 | private PatientJpaRepository patientJpaRepository; 31 | 32 | @Autowired 33 | private ScheduleJpaRepository scheduleJpaRepository; 34 | 35 | @Autowired 36 | private PatientResourceAssembler patientResourceAssembler; 37 | 38 | @Autowired 39 | private ScheduleResourceAssembler scheduleResourceAssembler; 40 | 41 | @Autowired 42 | private PagedResourcesAssembler pagedResourcesAssembler; 43 | 44 | @Autowired 45 | private AutoMapper autoMapper; 46 | 47 | @RequestMapping(method = RequestMethod.GET) 48 | public PagedResources showAll(@PageableDefault Pageable pageable) { 49 | Page patients = this.patientJpaRepository.findAll(pageable); 50 | return this.pagedResourcesAssembler.toResource(patients, patientResourceAssembler); 51 | } 52 | 53 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 54 | public Resource showOne(@PathVariable("id") Long id) { 55 | Patient entity = this.patientJpaRepository.findOne(id); 56 | return this.patientResourceAssembler.toResource(entity); 57 | } 58 | 59 | @ResponseStatus(HttpStatus.CREATED) 60 | @RequestMapping(method = RequestMethod.PUT) 61 | public HttpHeaders create(@RequestBody PatientInput patientInput) { 62 | Patient mapping = this.autoMapper.map(patientInput, Patient.class); 63 | Patient entity = this.patientJpaRepository.save(mapping); 64 | HttpHeaders httpHeaders = new HttpHeaders(); 65 | httpHeaders.setLocation(linkTo(PatientRestController.class).slash(entity.getId()).toUri()); 66 | return httpHeaders; 67 | } 68 | 69 | @ResponseStatus(HttpStatus.NO_CONTENT) 70 | @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) 71 | public void update(@PathVariable("id") Long id, @RequestBody PatientInput patientInput) { 72 | Patient source = this.autoMapper.map(patientInput, Patient.class); 73 | Patient target = this.patientJpaRepository.findOne(id); 74 | Patient mapping = this.autoMapper.map(source, target, Patient.class); 75 | this.patientJpaRepository.save(mapping); 76 | } 77 | 78 | @ResponseStatus(HttpStatus.OK) 79 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 80 | public void delete(@PathVariable("id") Long id) { 81 | this.patientJpaRepository.delete(id); 82 | } 83 | 84 | @RequestMapping(value = "/{id}/schedules", method = RequestMethod.GET) 85 | public PagedResources showSchdules(@PathVariable("id") Long id, @PageableDefault Pageable pageable) { 86 | Page schedules = this.scheduleJpaRepository.findByPatientId(id, pageable); 87 | return this.pagedResourcesAssembler.toResource(schedules, scheduleResourceAssembler); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/example/schedule/Schedule.java: -------------------------------------------------------------------------------- 1 | package io.example.schedule; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import io.example.common.AbstractPersistable; 5 | import io.example.common.LocalDateToDateConverter; 6 | import io.example.common.LocalTimeToTimeConverter; 7 | import io.example.doctor.Doctor; 8 | import io.example.patient.Patient; 9 | import lombok.Data; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | 14 | import javax.persistence.*; 15 | import java.io.Serializable; 16 | import java.time.LocalDate; 17 | import java.time.LocalTime; 18 | 19 | /** 20 | * Created by gmind on 2015-10-05. 21 | */ 22 | @Data 23 | @NoArgsConstructor 24 | @EqualsAndHashCode(callSuper = true, exclude = {"doctor","patient"}) 25 | @ToString(callSuper = true, exclude = {"doctor","patient"}) 26 | @Entity 27 | public class Schedule extends AbstractPersistable implements Serializable { 28 | 29 | private static final long serialVersionUID = 1L; 30 | 31 | public Schedule(Long id) { 32 | this.setId(id); 33 | } 34 | 35 | @Column(nullable = false) 36 | @Convert(converter = LocalDateToDateConverter.class) 37 | private LocalDate appointmentDay; 38 | 39 | @Column(nullable = false) 40 | @Convert(converter = LocalTimeToTimeConverter.class) 41 | private LocalTime startTime; 42 | 43 | @Column(nullable = false) 44 | @Convert(converter = LocalTimeToTimeConverter.class) 45 | private LocalTime endTime; 46 | 47 | @JsonIgnore 48 | @ManyToOne(optional = false) 49 | private Doctor doctor; 50 | 51 | @JsonIgnore 52 | @ManyToOne 53 | private Patient patient; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/example/schedule/ScheduleInput.java: -------------------------------------------------------------------------------- 1 | package io.example.schedule; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | import lombok.*; 5 | import org.springframework.format.annotation.DateTimeFormat; 6 | 7 | import java.io.Serializable; 8 | import java.time.LocalDate; 9 | import java.time.LocalTime; 10 | 11 | /** 12 | * Created by gmind on 2015-10-05. 13 | */ 14 | @Getter 15 | @Setter 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class ScheduleInput implements Serializable { 19 | 20 | private static final long serialVersionUID = 1L; 21 | 22 | @JsonUnwrapped 23 | private Long id; 24 | 25 | @NonNull 26 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 27 | private LocalDate appointmentDay; 28 | 29 | @NonNull 30 | @DateTimeFormat(pattern = "HH:mm") 31 | private LocalTime startTime; 32 | 33 | @NonNull 34 | @DateTimeFormat(pattern = "HH:mm") 35 | private LocalTime endTime; 36 | 37 | @NonNull 38 | private Long doctorId; 39 | 40 | private Long patientId; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/example/schedule/ScheduleJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.example.schedule; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import java.time.LocalDate; 9 | 10 | /** 11 | * Created by gmind on 2015-10-05. 12 | */ 13 | @Transactional(readOnly = true) 14 | public interface ScheduleJpaRepository extends JpaRepository { 15 | 16 | Page findByDoctorIdAndAppointmentDay(Long doctorId, LocalDate appointmentDay, Pageable pageable); 17 | 18 | Page findByPatientId(Long patientId, Pageable pageable); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/example/schedule/ScheduleResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.example.schedule; 2 | 3 | import io.example.common.ResourceNotFoundException; 4 | import org.springframework.hateoas.Resource; 5 | import org.springframework.hateoas.mvc.ResourceAssemblerSupport; 6 | import org.springframework.stereotype.Component; 7 | 8 | import static io.example.schedule.ScheduleResourceAssembler.ScheduleResource; 9 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 10 | 11 | /** 12 | * Created by gmind on 2015-10-05. 13 | */ 14 | @Component 15 | public class ScheduleResourceAssembler extends ResourceAssemblerSupport { 16 | 17 | public ScheduleResourceAssembler() { 18 | super(ScheduleRestController.class, ScheduleResource.class); 19 | } 20 | 21 | @Override 22 | public ScheduleResource toResource(Schedule schedule) { 23 | if(schedule.getId()==null) { 24 | throw new ResourceNotFoundException("schedule"); 25 | } 26 | ScheduleResource resource = createResourceWithId(schedule.getId(), schedule); 27 | resource.add(linkTo(ScheduleRestController.class).slash(schedule.getId()).slash("doctor").withRel("doctor")); 28 | if(schedule.getPatient()!=null) { 29 | resource.add(linkTo(ScheduleRestController.class).slash(schedule.getId()).slash("patient").withRel("patient")); 30 | } 31 | return resource; 32 | } 33 | 34 | @Override 35 | protected ScheduleResource instantiateResource(Schedule schedule) { 36 | return new ScheduleResource(schedule); 37 | } 38 | 39 | public static class ScheduleResource extends Resource { 40 | public ScheduleResource(Schedule schedule) { 41 | super(schedule); 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/example/schedule/ScheduleRestController.java: -------------------------------------------------------------------------------- 1 | package io.example.schedule; 2 | 3 | import io.example.common.ResourceNotFoundException; 4 | import io.example.config.mapper.AutoMapper; 5 | import io.example.doctor.Doctor; 6 | import io.example.doctor.DoctorRestController; 7 | import io.example.patient.Patient; 8 | import io.example.patient.PatientRestController; 9 | import org.apache.commons.lang3.ObjectUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.data.web.PageableDefault; 14 | import org.springframework.data.web.PagedResourcesAssembler; 15 | import org.springframework.hateoas.PagedResources; 16 | import org.springframework.hateoas.Resource; 17 | import org.springframework.http.HttpHeaders; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.web.bind.annotation.*; 20 | 21 | import javax.print.Doc; 22 | 23 | import static io.example.schedule.ScheduleResourceAssembler.ScheduleResource; 24 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 25 | 26 | /** 27 | * Created by gmind on 2015-10-05. 28 | */ 29 | @RestController 30 | @RequestMapping(value = "/schedules") 31 | public class ScheduleRestController { 32 | 33 | @Autowired 34 | private ScheduleJpaRepository scheduleJpaRepository; 35 | 36 | @Autowired 37 | private ScheduleResourceAssembler scheduleResourceAssembler; 38 | 39 | @Autowired 40 | private PagedResourcesAssembler pagedResourcesAssembler; 41 | 42 | @Autowired 43 | private DoctorRestController doctorRestController; 44 | 45 | @Autowired 46 | private PatientRestController patientRestController; 47 | 48 | @Autowired 49 | private AutoMapper autoMapper; 50 | 51 | @RequestMapping(method = RequestMethod.GET) 52 | public PagedResources showAll(@PageableDefault Pageable pageable) { 53 | Page schedules = this.scheduleJpaRepository.findAll(pageable); 54 | return this.pagedResourcesAssembler.toResource(schedules, scheduleResourceAssembler); 55 | } 56 | 57 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 58 | public Resource showOne(@PathVariable("id") Long id) { 59 | Schedule entity = this.scheduleJpaRepository.findOne(id); 60 | return this.scheduleResourceAssembler.toResource(entity); 61 | } 62 | 63 | @ResponseStatus(HttpStatus.CREATED) 64 | @RequestMapping(method = RequestMethod.PUT) 65 | public HttpHeaders create(@RequestBody ScheduleInput scheduleInput) { 66 | Schedule mapping = this.autoMapper.map(scheduleInput, Schedule.class); 67 | Schedule entity = this.scheduleJpaRepository.save(mapping); 68 | HttpHeaders httpHeaders = new HttpHeaders(); 69 | httpHeaders.setLocation(linkTo(ScheduleRestController.class).slash(entity.getId()).toUri()); 70 | return httpHeaders; 71 | } 72 | 73 | @ResponseStatus(HttpStatus.NO_CONTENT) 74 | @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) 75 | public void update(@PathVariable("id") Long id, @RequestBody ScheduleInput scheduleInput) { 76 | Schedule source = this.autoMapper.map(scheduleInput, Schedule.class); 77 | Schedule target = this.scheduleJpaRepository.findOne(id); 78 | Schedule mapping = this.autoMapper.map(source, target, Schedule.class); 79 | this.scheduleJpaRepository.save(mapping); 80 | } 81 | 82 | @ResponseStatus(HttpStatus.OK) 83 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 84 | public void delete(@PathVariable("id") Long id) { 85 | this.scheduleJpaRepository.delete(id); 86 | } 87 | 88 | @RequestMapping(value = "/{id}/doctor", method = RequestMethod.GET) 89 | public Resource showDoctor(@PathVariable("id") Long id) { 90 | Doctor doctor = this.scheduleJpaRepository.findOne(id).getDoctor(); 91 | if(doctor==null) { 92 | throw new ResourceNotFoundException("doctor"); 93 | } 94 | Resource doctorResource = this.doctorRestController.showOne(doctor.getId()); 95 | return doctorResource; 96 | } 97 | 98 | @RequestMapping(value = "/{id}/patient", method = RequestMethod.GET) 99 | public Resource showPatient(@PathVariable("id") Long id) { 100 | Patient patient = this.scheduleJpaRepository.findOne(id).getPatient(); 101 | if(patient==null) { 102 | throw new ResourceNotFoundException("patient"); 103 | } 104 | Resource patientResource = this.patientRestController.showOne(patient.getId()); 105 | return patientResource; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/src/main/resources/.gitkeep -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring: 3 | profiles: default,test 4 | datasource: 5 | url: jdbc:hsqldb:mem:spring-restdocs:charaset=UTF-8 6 | username: sa 7 | password: 8 | driver-class-name: org.hsqldb.jdbcDriver 9 | jpa: 10 | show-sql: true 11 | mvc: 12 | favicon: 13 | enabled: false 14 | mustach: 15 | check-template-location: false 16 | jackson: 17 | date-format: yyyy-MM-DD HH:mm:ss 18 | serialization-inclusion: NON_NULL 19 | serialization: 20 | indent_output: true 21 | http: 22 | encoding: 23 | charset: UTF-8 24 | enabled: true 25 | force: true 26 | logging: 27 | level: 28 | # org.hibernate: INFO 29 | # org.hibernate.type : TRACE 30 | io.example: DEBUG 31 | 32 | --- 33 | spring: 34 | profiles: mysql 35 | datasource: 36 | url: jdbc:mysql://127.0.0.1:3306/spring-restdocs 37 | username: restdocs 38 | password: restdocs 39 | driver-class-name: com.mysql.jdbc.Driver 40 | jpa: 41 | properties: 42 | hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect 43 | hibernate.hbm2ddl.auto: create-drop 44 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ███╗ ███╗ ██████╗ ██████╗ ███████╗██████╗ ███╗ ██╗ ██╗ █████╗ ██╗ ██╗ █████╗ ██╗ ██╗███████╗██████╗ 2 | ████╗ ████║██╔═══██╗██╔══██╗██╔════╝██╔══██╗████╗ ██║ ██║██╔══██╗██║ ██║██╔══██╗ ██║ ██║██╔════╝██╔══██╗ 3 | ██╔████╔██║██║ ██║██║ ██║█████╗ ██████╔╝██╔██╗ ██║ ██║███████║██║ ██║███████║ ██║ █╗ ██║█████╗ ██████╔╝ 4 | ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██╔══██╗██║╚██╗██║ ██ ██║██╔══██║╚██╗ ██╔╝██╔══██║ ██║███╗██║██╔══╝ ██╔══██╗ 5 | ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗██║ ██║██║ ╚████║ ╚█████╔╝██║ ██║ ╚████╔╝ ██║ ██║ ╚███╔███╔╝███████╗██████╔╝ 6 | ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚══════╝╚═════╝ -------------------------------------------------------------------------------- /src/test/java/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/src/test/java/.gitkeep -------------------------------------------------------------------------------- /src/test/java/io/example/TestBootConfig.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.junit.*; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.MethodSorters; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.boot.test.SpringApplicationConfiguration; 11 | import org.springframework.restdocs.RestDocumentation; 12 | import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; 13 | import org.springframework.test.context.ActiveProfiles; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | import org.springframework.test.context.web.WebAppConfiguration; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | import org.springframework.web.context.WebApplicationContext; 19 | 20 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; 21 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; 22 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; 23 | 24 | /** 25 | * Created by gmind on 2015-10-06. 26 | */ 27 | @RunWith(SpringJUnit4ClassRunner.class) 28 | @SpringApplicationConfiguration(classes = SampleBootRunner.class) 29 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 30 | @WebAppConfiguration 31 | public class TestBootConfig { 32 | 33 | @Rule 34 | public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); 35 | 36 | @Autowired 37 | private WebApplicationContext context; 38 | 39 | public RestDocumentationResultHandler document; 40 | 41 | public MockMvc mockMvc; 42 | 43 | @Before 44 | public void setUp() { 45 | this.document = document("{method-name}", 46 | preprocessRequest(prettyPrint()), 47 | preprocessResponse(prettyPrint())); 48 | this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) 49 | .apply(documentationConfiguration(this.restDocumentation) 50 | .uris() 51 | .withScheme("http") 52 | .withHost("root-endpoint") 53 | .and() 54 | .snippets() 55 | .withEncoding("UTF-8")) 56 | .alwaysDo(this.document) 57 | .build(); 58 | } 59 | 60 | @Test 61 | public void bulkSkipTest() { 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestDoctorsRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.Maps; 5 | import io.example.doctor.Doctor; 6 | import io.example.doctor.DoctorJpaRepository; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.hateoas.MediaTypes; 10 | import org.springframework.restdocs.hypermedia.LinkDescriptor; 11 | import org.springframework.restdocs.payload.JsonFieldType; 12 | import org.springframework.test.web.servlet.ResultHandler; 13 | 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.Matchers.notNullValue; 17 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; 18 | import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; 19 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 20 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; 21 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; 22 | import static org.springframework.restdocs.payload.PayloadDocumentation.*; 23 | import static org.springframework.restdocs.request.RequestDocumentation.*; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | 27 | /** 28 | * Created by gmind on 2015-10-06. 29 | */ 30 | public class TestDoctorsRestController extends TestBootConfig { 31 | 32 | @Autowired 33 | private DoctorJpaRepository doctorJpaRepository; 34 | 35 | @Autowired 36 | private ObjectMapper objectMapper; 37 | 38 | @Test 39 | public void doctorsShowAll() throws Exception { 40 | this.mockMvc.perform(get("/doctors").param("page","2").param("size","10")) 41 | .andExpect(status().isOk()) 42 | .andDo(this.document.snippets( 43 | links( 44 | linkWithRel("next").optional().description("다음페이지"), 45 | linkWithRel("prev").optional().description("이전페이지"), 46 | linkWithRel("self").description("현재페이지")), 47 | requestParameters( 48 | parameterWithName("page").description("페이지 번호"), 49 | parameterWithName("size").description("리스트 사이즈")), 50 | responseFields( 51 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("<> Resources"), 52 | fieldWithPath("_embedded.doctors").type(JsonFieldType.OBJECT).description("<> Resource").optional(), 53 | fieldWithPath("page").type(JsonFieldType.OBJECT).description("Information On <>")))); 54 | } 55 | 56 | @Test 57 | public void doctorsShowOne() throws Exception { 58 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 59 | this.mockMvc.perform(get("/doctors/{id}", doctor.getId())) 60 | .andExpect(status().isOk()) 61 | .andDo(createDoctorsResultHandler( 62 | linkWithRel("self").description("Self Rel Href"), 63 | linkWithRel("doctor_schedules").description("<> Rel Href"))); 64 | } 65 | 66 | @Test 67 | public void doctorsCreate() throws Exception { 68 | Map create = Maps.newHashMap(); 69 | create.put("name", "doctor_name_create"); 70 | this.mockMvc.perform( 71 | put("/doctors") 72 | .contentType(MediaTypes.HAL_JSON) 73 | .content(this.objectMapper.writeValueAsString(create))) 74 | .andExpect(header().string("Location", notNullValue())) 75 | .andDo(this.document.snippets( 76 | requestFields( 77 | fieldWithPath("name").type(JsonFieldType.STRING).description("의사명")), 78 | responseHeaders(headerWithName("Location").description("신규 생성된 자원 주소")))) 79 | .andReturn().getResponse().getHeader("Location"); 80 | } 81 | 82 | @Test 83 | public void doctorsUpdate() throws Exception { 84 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 85 | Map update = Maps.newHashMap(); 86 | update.put("name", doctor.getName()+"_update"); 87 | this.mockMvc.perform( 88 | patch("/doctors/{id}", doctor.getId()) 89 | .contentType(MediaTypes.HAL_JSON) 90 | .content(this.objectMapper.writeValueAsString(update))) 91 | .andDo(this.document.snippets( 92 | requestFields( 93 | fieldWithPath("name").type(JsonFieldType.STRING).description("의사명")), 94 | pathParameters(parameterWithName("id").description("의사아이디")))) 95 | .andExpect(status().isNoContent()); 96 | } 97 | 98 | @Test 99 | public void doctorsDelete() throws Exception { 100 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 101 | this.mockMvc.perform( 102 | delete("/doctors/{id}", doctor.getId()) 103 | .contentType(MediaTypes.HAL_JSON)) 104 | .andDo(this.document.snippets(pathParameters(parameterWithName("id").description("의사아이디")))) 105 | .andExpect(status().isOk()); 106 | } 107 | 108 | @Test 109 | public void doctorsSchdules() throws Exception { 110 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 111 | this.mockMvc.perform(get("/doctors/{id}/schedules", doctor.getId())) 112 | .andExpect(status().isOk()); 113 | } 114 | 115 | public ResultHandler createDoctorsResultHandler(LinkDescriptor... linkDescriptors) { 116 | return this.document.snippets( 117 | links(linkDescriptors), 118 | pathParameters( 119 | parameterWithName("id").description("의사아이디")), 120 | responseFields( 121 | fieldWithPath("id").type(JsonFieldType.NUMBER).description("의사아이디"), 122 | fieldWithPath("name").type(JsonFieldType.STRING).description("의사명"), 123 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("의사정보"))); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestErrorRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import org.junit.Test; 4 | import org.springframework.hateoas.MediaTypes; 5 | 6 | import javax.servlet.RequestDispatcher; 7 | 8 | import static org.hamcrest.Matchers.is; 9 | import static org.hamcrest.Matchers.notNullValue; 10 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; 11 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 12 | import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | /** 18 | * Created by gmind on 2015-10-06. 19 | */ 20 | public class TestErrorRestController extends TestBootConfig { 21 | 22 | @Test 23 | public void clientErrorBadRequest() throws Exception { 24 | this.document.snippets(responseFields( 25 | fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), 26 | fieldWithPath("message").description("A description of the cause of the error"), 27 | fieldWithPath("path").description("The path to which the request was made"), 28 | fieldWithPath("status").description("The HTTP status code, e.g. `400`"), 29 | fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))); 30 | 31 | this.mockMvc 32 | .perform(get("/error").accept(MediaTypes.HAL_JSON) 33 | .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) 34 | .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, "/doctors/123") 35 | .requestAttr(RequestDispatcher.ERROR_MESSAGE, "The tag 'http://localhost:8080/doctors/123' does not exist")) 36 | .andDo(print()).andExpect(status().isBadRequest()) 37 | .andExpect(jsonPath("error", is("Bad Request"))) 38 | .andExpect(jsonPath("timestamp", is(notNullValue()))) 39 | .andExpect(jsonPath("status", is(400))) 40 | .andExpect(jsonPath("path", is(notNullValue()))); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestIndexRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import org.junit.Test; 4 | import org.springframework.hateoas.MediaTypes; 5 | 6 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 7 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; 8 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; 9 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 10 | import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | /** 13 | * Created by gmind on 2015-10-06. 14 | */ 15 | public class TestIndexRestController extends TestBootConfig { 16 | 17 | @Test 18 | public void index() throws Exception { 19 | this.document.snippets( 20 | links( 21 | linkWithRel("doctors").description("The <>"), 22 | linkWithRel("patient").description("The <>"), 23 | linkWithRel("schedule").description("The <>")), 24 | responseFields( 25 | fieldWithPath("_links").description("<> to other resources"))); 26 | 27 | this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) 28 | .andExpect(status().isOk()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestPatientsRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.Maps; 5 | import io.example.patient.Patient; 6 | import io.example.patient.PatientJpaRepository; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.hateoas.MediaTypes; 10 | import org.springframework.restdocs.hypermedia.LinkDescriptor; 11 | import org.springframework.restdocs.payload.JsonFieldType; 12 | import org.springframework.test.web.servlet.ResultHandler; 13 | 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.Matchers.notNullValue; 17 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; 18 | import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; 19 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 20 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; 21 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; 22 | import static org.springframework.restdocs.payload.PayloadDocumentation.*; 23 | import static org.springframework.restdocs.request.RequestDocumentation.*; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | 27 | /** 28 | * Created by gmind on 2015-10-06. 29 | */ 30 | public class TestPatientsRestController extends TestBootConfig { 31 | 32 | @Autowired 33 | private PatientJpaRepository patientJpaRepository; 34 | 35 | @Autowired 36 | private ObjectMapper objectMapper; 37 | 38 | @Test 39 | public void patientsShowAll() throws Exception { 40 | this.mockMvc.perform(get("/patients").param("page","2").param("size", "10")) 41 | .andExpect(status().isOk()) 42 | .andDo(this.document.snippets( 43 | links( 44 | linkWithRel("next").optional().description("다음페이지"), 45 | linkWithRel("prev").optional().description("이전페이지"), 46 | linkWithRel("self").description("현재페이지")), 47 | requestParameters( 48 | parameterWithName("page").description("페이지 번호"), 49 | parameterWithName("size").description("리스트 사이즈")), 50 | responseFields( 51 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("<> Resources"), 52 | fieldWithPath("_embedded.patients").type(JsonFieldType.OBJECT).description("<> Resource").optional(), 53 | fieldWithPath("page").type(JsonFieldType.OBJECT).description("Information On <>")))); 54 | } 55 | 56 | @Test 57 | public void patientsShowOne() throws Exception { 58 | Patient patient = this.patientJpaRepository.findAll().stream().filter(x -> x.getSchedules()!=null).findFirst().get(); 59 | this.mockMvc.perform(get("/patients/{id}", patient.getId())) 60 | .andExpect(status().isOk()) 61 | .andDo(createPatientsResultHandler( 62 | linkWithRel("self").description("Self Rel Href"), 63 | linkWithRel("patient_schedules").optional().description("<> Rel Href"))); 64 | } 65 | 66 | @Test 67 | public void patientsCreate() throws Exception { 68 | Map create = Maps.newHashMap(); 69 | create.put("name", "patient_name_create"); 70 | create.put("birthDate", "2015-10-01"); 71 | this.mockMvc.perform( 72 | put("/patients") 73 | .contentType(MediaTypes.HAL_JSON) 74 | .content(this.objectMapper.writeValueAsString(create))) 75 | .andExpect(header().string("Location", notNullValue())) 76 | .andDo(this.document.snippets( 77 | requestFields( 78 | fieldWithPath("name").type(JsonFieldType.STRING).description("환자명"), 79 | fieldWithPath("birthDate").type(JsonFieldType.STRING).description("생년월일(yyyy-MM-dd)")), 80 | responseHeaders( 81 | headerWithName("Location").description("신규 생성된 자원 주소")))) 82 | .andReturn().getResponse().getHeader("Location"); 83 | } 84 | 85 | @Test 86 | public void patientsUpdate() throws Exception { 87 | Patient patient = this.patientJpaRepository.findAll().get(0); 88 | Map update = Maps.newHashMap(); 89 | update.put("name", patient.getName()+"_update"); 90 | update.put("birthDate", "2015-10-02"); 91 | this.mockMvc.perform( 92 | patch("/patients/{id}", patient.getId()) 93 | .contentType(MediaTypes.HAL_JSON) 94 | .content(this.objectMapper.writeValueAsString(update))) 95 | .andDo(this.document.snippets( 96 | requestFields( 97 | fieldWithPath("name").type(JsonFieldType.STRING).description("환자명"), 98 | fieldWithPath("birthDate").type(JsonFieldType.STRING).description("생년월일(yyyy-MM-dd)")), 99 | pathParameters(parameterWithName("id").description("환자아이디")))) 100 | .andExpect(status().isNoContent()); 101 | } 102 | 103 | @Test 104 | public void patientsDelete() throws Exception { 105 | Patient patient = this.patientJpaRepository.findAll().get(0); 106 | this.mockMvc.perform( 107 | delete("/patients/{id}", patient.getId()) 108 | .contentType(MediaTypes.HAL_JSON)) 109 | .andDo(this.document.snippets(pathParameters(parameterWithName("id").description("환자아이디")))) 110 | .andExpect(status().isOk()); 111 | } 112 | 113 | @Test 114 | public void patientsSchdules() throws Exception { 115 | Patient patient = this.patientJpaRepository.findAll().get(0); 116 | this.mockMvc.perform(get("/patients/{id}/schedules", patient.getId())) 117 | .andExpect(status().isOk()); 118 | } 119 | 120 | public ResultHandler createPatientsResultHandler(LinkDescriptor... linkDescriptors) { 121 | return this.document.snippets( 122 | links(linkDescriptors), 123 | pathParameters( 124 | parameterWithName("id").description("환자아이디")), 125 | responseFields( 126 | fieldWithPath("id").type(JsonFieldType.NUMBER).description("환자아이디"), 127 | fieldWithPath("name").type(JsonFieldType.STRING).description("환자명"), 128 | fieldWithPath("birthDate").type(JsonFieldType.OBJECT).description("환자생년월일"), 129 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("환자정보"))); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestSchedulesRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.Maps; 5 | import io.example.doctor.Doctor; 6 | import io.example.doctor.DoctorJpaRepository; 7 | import io.example.patient.Patient; 8 | import io.example.patient.PatientJpaRepository; 9 | import io.example.schedule.Schedule; 10 | import io.example.schedule.ScheduleJpaRepository; 11 | import org.junit.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.hateoas.MediaTypes; 14 | import org.springframework.restdocs.hypermedia.LinkDescriptor; 15 | import org.springframework.restdocs.payload.JsonFieldType; 16 | import org.springframework.test.web.servlet.ResultHandler; 17 | 18 | import java.util.Map; 19 | 20 | import static org.hamcrest.Matchers.is; 21 | import static org.hamcrest.Matchers.notNullValue; 22 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; 23 | import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; 24 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 25 | import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; 26 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; 27 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 28 | import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; 29 | import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 30 | import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; 31 | import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; 32 | import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 36 | 37 | /** 38 | * Created by gmind on 2015-10-06. 39 | */ 40 | public class TestSchedulesRestController extends TestBootConfig { 41 | 42 | @Autowired 43 | private DoctorJpaRepository doctorJpaRepository; 44 | 45 | @Autowired 46 | private PatientJpaRepository patientJpaRepository; 47 | 48 | @Autowired 49 | private ScheduleJpaRepository scheduleJpaRepository; 50 | 51 | @Autowired 52 | private ObjectMapper objectMapper; 53 | 54 | @Test 55 | public void schedulesShowAll() throws Exception { 56 | this.mockMvc.perform(get("/schedules").param("page","2").param("size", "10")) 57 | .andExpect(status().isOk()) 58 | .andDo(this.document.snippets( 59 | links( 60 | linkWithRel("next").optional().description("다음페이지"), 61 | linkWithRel("prev").optional().description("이전페이지"), 62 | linkWithRel("self").description("현재페이지")), 63 | requestParameters( 64 | parameterWithName("page").description("페이지 번호"), 65 | parameterWithName("size").description("리스트 사이즈")), 66 | responseFields( 67 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("<> Resources"), 68 | fieldWithPath("_embedded.schedules").type(JsonFieldType.OBJECT).description("<> Resource").optional(), 69 | fieldWithPath("page").type(JsonFieldType.OBJECT).description("Information On <>")))); 70 | } 71 | 72 | @Test 73 | public void schedulesShowOne() throws Exception { 74 | Schedule schedule = this.scheduleJpaRepository.findAll().stream().filter(x -> x.getPatient()!=null).findFirst().get(); 75 | this.mockMvc.perform(get("/schedules/{id}", schedule.getId())) 76 | .andExpect(status().isOk()) 77 | .andDo(createSchedulesResultHandler( 78 | linkWithRel("self").description("Self Rel Href"), 79 | linkWithRel("doctor").description("<> Rel Href"), 80 | linkWithRel("patient").optional().description("<> Rel Href"))); 81 | } 82 | 83 | @Test 84 | public void schedulesCreate() throws Exception { 85 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 86 | Patient patient = this.patientJpaRepository.findAll().get(0); 87 | Map create = Maps.newHashMap(); 88 | create.put("appointmentDay", "9991-12-31"); 89 | create.put("startTime", "09:00"); 90 | create.put("endTime", "09:30"); 91 | create.put("doctorId", Long.toString(doctor.getId())); 92 | create.put("patientId", Long.toString(patient.getId())); 93 | this.mockMvc.perform( 94 | put("/schedules") 95 | .contentType(MediaTypes.HAL_JSON) 96 | .content(this.objectMapper.writeValueAsString(create))) 97 | .andExpect(header().string("Location", notNullValue())) 98 | .andDo(this.document.snippets( 99 | requestFields( 100 | fieldWithPath("appointmentDay").type(JsonFieldType.STRING).description("진료일(yyyy-MM-DD)"), 101 | fieldWithPath("startTime").type(JsonFieldType.STRING).description("진료시작시간(HH:mm)"), 102 | fieldWithPath("endTime").type(JsonFieldType.STRING).description("진료종료시간(HH:mm)"), 103 | fieldWithPath("doctorId").type(JsonFieldType.STRING).description("의사아이디"), 104 | fieldWithPath("patientId").type(JsonFieldType.STRING).optional().description("환자아이디")), 105 | responseHeaders( 106 | headerWithName("Location").description("신규 생성된 자원 주소")))) 107 | .andReturn().getResponse().getHeader("Location"); 108 | } 109 | 110 | @Test 111 | public void schedulesUpdate() throws Exception { 112 | Doctor doctor = this.doctorJpaRepository.findAll().get(0); 113 | Patient patient = this.patientJpaRepository.findAll().get(0); 114 | Schedule schedule = this.scheduleJpaRepository.findAll().get(0); 115 | Map update = Maps.newHashMap(); 116 | update.put("appointmentDay", "9991-12-31"); 117 | update.put("startTime", "10:00"); 118 | update.put("endTime", "10:30"); 119 | update.put("doctorId", Long.toString(doctor.getId())); 120 | update.put("patientId", Long.toString(patient.getId())); 121 | this.mockMvc.perform( 122 | patch("/schedules/{id}", schedule.getId()) 123 | .contentType(MediaTypes.HAL_JSON) 124 | .content(this.objectMapper.writeValueAsString(update))) 125 | .andDo(this.document.snippets( 126 | requestFields( 127 | fieldWithPath("appointmentDay").type(JsonFieldType.STRING).description("진료일(yyyy-MM-DD)"), 128 | fieldWithPath("startTime").type(JsonFieldType.STRING).description("진료시작시간(HH:mm)"), 129 | fieldWithPath("endTime").type(JsonFieldType.STRING).description("진료종료시간(HH:mm)"), 130 | fieldWithPath("doctorId").type(JsonFieldType.STRING).description("의사아이디"), 131 | fieldWithPath("patientId").type(JsonFieldType.STRING).optional().description("환자아이디")), 132 | pathParameters(parameterWithName("id").description("스케쥴아이디")))) 133 | .andExpect(status().isNoContent()); 134 | } 135 | 136 | @Test 137 | public void schedulesDelete() throws Exception { 138 | Schedule schedule = this.scheduleJpaRepository.findAll().get(0); 139 | this.mockMvc.perform( 140 | delete("/schedules/{id}", schedule.getId()) 141 | .contentType(MediaTypes.HAL_JSON)) 142 | .andDo(this.document.snippets(pathParameters(parameterWithName("id").description("스케쥴아이디")))) 143 | .andExpect(status().isOk()); 144 | } 145 | 146 | @Test 147 | public void schedulesDoctor() throws Exception { 148 | Schedule schedule = this.scheduleJpaRepository.findAll().get(0); 149 | this.mockMvc.perform(get("/schedules/{id}/doctor", schedule.getId())) 150 | .andExpect(status().isOk()); 151 | } 152 | 153 | @Test 154 | public void schedulesPatient() throws Exception { 155 | Schedule schedule = this.scheduleJpaRepository.findAll().get(0); 156 | this.mockMvc.perform(get("/schedules/{id}/patient", schedule.getId())) 157 | .andExpect(status().isOk()); 158 | } 159 | 160 | public ResultHandler createSchedulesResultHandler(LinkDescriptor... linkDescriptors) { 161 | return this.document.snippets( 162 | links(linkDescriptors), 163 | pathParameters( 164 | parameterWithName("id").description("스케쥴아이디")), 165 | responseFields( 166 | fieldWithPath("id").type(JsonFieldType.NUMBER).description("스케쥴아이디"), 167 | fieldWithPath("appointmentDay").type(JsonFieldType.STRING).description("진료일"), 168 | fieldWithPath("startTime").type(JsonFieldType.STRING).description("진료시작시간"), 169 | fieldWithPath("endTime").type(JsonFieldType.STRING).description("진료종료시간"), 170 | fieldWithPath("_links").type(JsonFieldType.OBJECT).description("스케쥴정보"))); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/test/java/io/example/TestSchemaRestController.java: -------------------------------------------------------------------------------- 1 | package io.example; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; 6 | import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; 7 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 9 | /** 10 | * Created by gmind on 2015-10-08. 11 | */ 12 | public class TestSchemaRestController extends TestBootConfig { 13 | 14 | @Test 15 | public void schema() throws Exception { 16 | this.document.snippets(responseHeaders( 17 | headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); 18 | this.mockMvc.perform(get("/")) 19 | .andExpect(status().isOk()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmind7/spring-restdocs-seminar/db07c3929f2343d188d6d9097a5a9c97ea2d3e99/src/test/resources/.gitkeep --------------------------------------------------------------------------------