├── .gitignore ├── README.md ├── build.gradle ├── buildSrc ├── build.gradle └── src │ └── main │ └── groovy │ ├── com.github.dreamhead.coverage.gradle │ ├── com.github.dreamhead.idea.gradle │ ├── com.github.dreamhead.style.gradle │ └── com.github.dreamhead.todo.gradle ├── gradle.properties ├── gradle ├── config │ ├── checkstyle │ │ └── sun_checks.xml │ └── migration │ │ └── V2021.07.25.18.28__Create_todo_item_table.sql └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── settings.gradle ├── todo-api ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── dreamhead │ │ │ └── todo │ │ │ └── api │ │ │ ├── AddTodoItemRequest.java │ │ │ ├── Bootstrap.java │ │ │ ├── MarkAsDoneRequest.java │ │ │ ├── TodoItemResource.java │ │ │ ├── TodoItemResponse.java │ │ │ └── repository │ │ │ └── TodoItemJpaRepository.java │ └── resources │ │ └── application.properties │ └── test │ ├── java │ └── com │ │ └── github │ │ └── dreamhead │ │ └── todo │ │ └── api │ │ ├── CucumberIntegrationTest.java │ │ ├── SpringIntegrationTest.java │ │ ├── TodoItemRepositoryTest.java │ │ ├── TodoItemResourceTest.java │ │ └── stepdef │ │ └── TodoItemStepDefinitions.java │ └── resources │ ├── cucumber.properties │ ├── features │ └── todo.feature │ └── test.properties ├── todo-cli ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── github │ │ └── dreamhead │ │ └── todo │ │ ├── cli │ │ ├── Bootstrap.java │ │ ├── ObjectFactory.java │ │ ├── TodoCommand.java │ │ └── file │ │ │ └── FileTodoItemRepository.java │ │ └── util │ │ └── Jsons.java │ └── test │ └── java │ └── com │ └── github │ └── dreamhead │ └── todo │ └── cli │ ├── TodoCommandTest.java │ └── file │ └── FileTodoItemRepositoryTest.java └── todo-core ├── build.gradle └── src ├── main └── java │ └── com │ └── github │ └── dreamhead │ └── todo │ └── core │ ├── TodoException.java │ ├── TodoIndexParameter.java │ ├── TodoItem.java │ ├── TodoItemRepository.java │ ├── TodoItemService.java │ └── TodoParameter.java └── test └── java └── com └── github └── dreamhead └── todo └── core ├── TodoExceptionTest.java ├── TodoIndexParameterTest.java ├── TodoItemServiceTest.java └── TodoParameterTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipr 2 | *.iml 3 | *.iws 4 | out 5 | build 6 | .gradle -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geektime Todo 2 | 3 | ## 简介 4 | 5 | 这是《极客时间》专栏的 Todo 项目示例。 6 | 7 | ## 基本用法 8 | 9 | * 生成 IDEA 工程 10 | 11 | ```shell 12 | ./gradlew idea 13 | ``` 14 | 15 | * 检查 16 | 17 | ```shell 18 | ./gradlew check 19 | ``` 20 | 21 | * 数据库迁移 22 | 23 | ```shell 24 | ./gradlew migrate 25 | ``` 26 | 27 | * 生成构建产物 28 | 29 | ```shell 30 | ./gradlew build 31 | ``` 32 | 33 | * 生成发布包 34 | 35 | 对于 CLI 项目,运行如下命令 36 | ```shell 37 | ./gradlew uberJar 38 | ``` 39 | 40 | 在 todo-cli/build/libs 下就会生成一个 Uber JAR,它是可以独立运行的。 41 | 42 | ```shell 43 | java -jar todo-uber-.jar 44 | ``` 45 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.github.dreamhead.idea" 3 | } 4 | 5 | wrapper { 6 | gradleVersion = '8.2.1' 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy-gradle-plugin' 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/com.github.dreamhead.coverage.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'jacoco' 3 | } 4 | 5 | class JacocoExtension { 6 | boolean enabled = true 7 | List excludePackages; 8 | List excludeClasses; 9 | } 10 | 11 | project.extensions.create('coverage', JacocoExtension) 12 | 13 | test.finalizedBy jacocoTestReport 14 | jacocoTestReport.dependsOn test 15 | jacocoTestCoverageVerification.dependsOn compileJava, processResources 16 | check.dependsOn jacocoTestCoverageVerification 17 | 18 | jacoco.toolVersion = jacocoVersion 19 | 20 | jacocoTestCoverageVerification { 21 | afterEvaluate { 22 | def excludeClasses = (coverage.excludeClasses ?: []).collect { 23 | "${it.replaceAll('\\.', '/') + '.class'}" 24 | } 25 | 26 | def excludePackageClasses = (coverage.excludePackages ?: []).collect { 27 | "${it.replaceAll('\\.', '/') + '/*'}" 28 | } 29 | 30 | def excludes = excludeClasses + excludePackageClasses 31 | classDirectories.setFrom(files(classDirectories.files.collect { 32 | fileTree(dir: it, exclude: excludes) 33 | })) 34 | 35 | test.jacoco.enabled = coverage.enabled 36 | } 37 | 38 | violationRules { 39 | rule { 40 | enabled = coverage.enabled 41 | limit { 42 | counter = 'LINE' 43 | value = 'COVEREDRATIO' 44 | minimum = 1.0 45 | } 46 | limit { 47 | counter = 'BRANCH' 48 | value = 'COVEREDRATIO' 49 | minimum = 1.0 50 | } 51 | limit { 52 | counter = 'CLASS' 53 | value = 'COVEREDRATIO' 54 | minimum = 1.0 55 | } 56 | limit { 57 | counter = 'INSTRUCTION' 58 | value = 'COVEREDRATIO' 59 | minimum = 1.0 60 | } 61 | limit { 62 | counter = 'METHOD' 63 | value = 'COVEREDRATIO' 64 | minimum = 1.0 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/com.github.dreamhead.idea.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'idea' 3 | } 4 | 5 | idea { 6 | project { 7 | jdkName = JavaVersion.VERSION_1_8 8 | languageLevel = JavaVersion.VERSION_1_8 9 | 10 | vcs = "Git" 11 | } 12 | 13 | workspace.iws.withXml { provider -> 14 | def junitDefaults = provider.node.component.find { it.@name == 'RunManager' }.configuration.find { 15 | it.@type == 'JUnit' 16 | } 17 | junitDefaults.option.find { it.@name == 'WORKING_DIRECTORY' }.@value = '$MODULE_DIR$' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/com.github.dreamhead.style.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'checkstyle' 3 | } 4 | 5 | 6 | class StyleExtension { 7 | List excludePackages; 8 | List excludeClasses; 9 | } 10 | 11 | project.extensions.create('style', StyleExtension) 12 | 13 | checkstyle { 14 | configFile = file("$rootDir/gradle/config/checkstyle/sun_checks.xml") 15 | toolVersion = checkstyleVersion 16 | } 17 | 18 | checkstyleTest.enabled = false 19 | 20 | checkstyleMain.doFirst { 21 | style.excludePackages.each() { 22 | checkstyleMain.exclude "${'**/' + it.replaceAll('\\.', '/') + '/*'}" 23 | } 24 | 25 | style.excludeClasses.each() { 26 | checkstyleMain.exclude "${'**/' + it.replaceAll('\\.', '/') + '.java'}" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/com.github.dreamhead.todo.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'maven-publish' 4 | id 'idea' 5 | id 'com.github.dreamhead.style' 6 | id 'com.github.dreamhead.coverage' 7 | } 8 | 9 | repositories { 10 | mavenLocal() 11 | mavenCentral() 12 | maven { 13 | url = uri('https://repo.maven.apache.org/maven2/') 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation("com.google.guava:guava:$guavaVersion") 19 | compileOnly("org.projectlombok:lombok:$lombokVersion") 20 | annotationProcessor("org.projectlombok:lombok:$lombokVersion") 21 | implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") 22 | implementation("jakarta.persistence:jakarta.persistence-api:$persistenceVersion") 23 | implementation("org.springframework:spring-context:$springVersion") 24 | implementation("org.springframework.data:spring-data-commons:$springDataVersion") 25 | 26 | testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") 27 | testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") 28 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") 29 | testImplementation("org.assertj:assertj-core:$assertJVersion") 30 | testImplementation("org.mockito:mockito-core:$mockitoVersion") 31 | } 32 | 33 | test { 34 | useJUnitPlatform() 35 | } 36 | 37 | test { 38 | testLogging { 39 | afterSuite { desc, result -> 40 | if (!desc.parent) { 41 | println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" 42 | } 43 | } 44 | } 45 | } 46 | 47 | group = 'com.github.dreamhead' 48 | version = '1.0-SNAPSHOT' 49 | sourceCompatibility = JavaVersion.VERSION_1_8 50 | targetCompatibility = JavaVersion.VERSION_1_8 51 | 52 | publishing { 53 | publications { 54 | maven(MavenPublication) { 55 | from(components.java) 56 | } 57 | } 58 | } 59 | 60 | tasks.withType(JavaCompile) { 61 | options.encoding = 'UTF-8' 62 | options.compilerArgs << "-Xlint:unchecked" 63 | options.compilerArgs << "-Xlint:deprecation" 64 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | guavaVersion=32.1.1-jre 2 | springBootVersion=2.7.13 3 | springVersion=5.3.28 4 | springDataVersion=2.7.13 5 | lombokVersion=1.18.28 6 | mysqlVersion=8.0.33 7 | junitVersion=5.9.3 8 | assertJVersion=3.24.2 9 | mockitoVersion=4.11.0 10 | flywayVersion=8.5.13 11 | jacocoVersion=0.8.10 12 | checkstyleVersion=9.3 13 | jacksonVersion=2.15.2 14 | picocliVersion=4.7.4 15 | persistenceVersion=2.2.3 16 | cucumberVersion=7.13.0 17 | -------------------------------------------------------------------------------- /gradle/config/checkstyle/sun_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /gradle/config/migration/V2021.07.25.18.28__Create_todo_item_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE todo_items ( 2 | `id` int auto_increment, 3 | `content` varchar(255) not null, 4 | `done` tinyint not null default 0, 5 | primary key (`id`) 6 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamhead/geektime-todo/161b570f8a4cff45f61102db8736320f9d3e5366/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 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 %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 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 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | lombok.setter.flagUsage = error 3 | lombok.data.flagUsage = error -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'geektime-todo' 2 | 3 | include 'todo-core' 4 | include 'todo-cli' 5 | include 'todo-api' -------------------------------------------------------------------------------- /todo-api/build.gradle: -------------------------------------------------------------------------------- 1 | import org.flywaydb.gradle.task.* 2 | 3 | buildscript { 4 | dependencies { 5 | classpath "com.mysql:mysql-connector-j:$mysqlVersion" 6 | classpath "org.flywaydb:flyway-mysql:$flywayVersion" 7 | } 8 | } 9 | 10 | plugins { 11 | id 'org.springframework.boot' version "${springBootVersion}" 12 | id "org.flywaydb.flyway" version "${flywayVersion}" 13 | id 'com.github.dreamhead.todo' 14 | } 15 | 16 | style { 17 | excludePackages = [ 18 | ] 19 | excludeClasses = [ 20 | "com.github.dreamhead.todo.api.Bootstrap" 21 | ] 22 | } 23 | 24 | coverage { 25 | excludePackages = [ 26 | ] 27 | excludeClasses = [ 28 | "com.github.dreamhead.todo.api.Bootstrap" 29 | ] 30 | } 31 | 32 | dependencies { 33 | implementation(project(":todo-core")) 34 | implementation("org.springframework.boot:spring-boot-starter:$springBootVersion") 35 | implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") 36 | implementation("org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion") 37 | runtimeOnly("com.mysql:mysql-connector-j:$mysqlVersion") 38 | testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") 39 | testImplementation("io.cucumber:cucumber-java8:$cucumberVersion") 40 | testImplementation("io.cucumber:cucumber-junit:$cucumberVersion") 41 | testImplementation("io.cucumber:cucumber-junit-platform-engine:$cucumberVersion") 42 | testImplementation("io.cucumber:cucumber-spring:$cucumberVersion") 43 | } 44 | 45 | configurations { 46 | cucumberRuntime { 47 | extendsFrom testImplementation, runtimeOnly 48 | } 49 | } 50 | 51 | task cucumber() { 52 | dependsOn assemble, testClasses 53 | doLast { 54 | javaexec { 55 | main = "io.cucumber.core.cli.Main" 56 | classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output 57 | args = ['--plugin', 'pretty', '--glue', 'com.github.dreamhead.todo.api.stepdef', 'src/test/resources'] 58 | } 59 | } 60 | } 61 | 62 | task migrateToDev(type: FlywayMigrateTask) { 63 | url = 'jdbc:mysql://localhost:3306/todo_dev?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true' 64 | user = 'todo' 65 | password = 'geektime' 66 | locations = ["filesystem:$rootDir/gradle/config/migration"] 67 | } 68 | 69 | task migrateToTest(type: FlywayMigrateTask) { 70 | url = 'jdbc:mysql://localhost:3306/todo_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true' 71 | user = 'todo' 72 | password = 'geektime' 73 | locations = ["filesystem:$rootDir/gradle/config/migration"] 74 | } 75 | 76 | task migrate { 77 | } 78 | 79 | migrate.dependsOn migrateToDev, migrateToTest 80 | 81 | springBoot { 82 | mainClass = 'com.github.dreamhead.todo.api.Bootstrap' 83 | } 84 | 85 | bootJar { 86 | archiveClassifier = 'boot' 87 | 88 | // https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-Notes#jersey-classpath-scanning-limitations 89 | requiresUnpack '**/todo-*.jar' 90 | 91 | launchScript() 92 | } -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/AddTodoItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Getter; 6 | 7 | public class AddTodoItemRequest { 8 | @Getter 9 | private String content; 10 | 11 | @JsonCreator 12 | public AddTodoItemRequest(@JsonProperty("content") final String content) { 13 | this.content = content; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 9 | 10 | @SpringBootApplication 11 | @EnableJpaRepositories("com.github.dreamhead.todo.api.repository") 12 | @EntityScan("com.github.dreamhead.todo.core") 13 | @ComponentScan("com.github.dreamhead.todo") 14 | public class Bootstrap { 15 | private static ConfigurableApplicationContext context; 16 | 17 | public static void main(final String[] args) { 18 | context = SpringApplication.run(Bootstrap.class, args); 19 | } 20 | 21 | public static void shutdown() { 22 | context.close(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/MarkAsDoneRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Getter; 6 | 7 | public class MarkAsDoneRequest { 8 | @Getter 9 | private boolean done; 10 | 11 | @JsonCreator 12 | public MarkAsDoneRequest(@JsonProperty("done") final boolean done) { 13 | this.done = done; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/TodoItemResource.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.github.dreamhead.todo.core.TodoIndexParameter; 4 | import com.github.dreamhead.todo.core.TodoItem; 5 | import com.github.dreamhead.todo.core.TodoItemService; 6 | import com.github.dreamhead.todo.core.TodoParameter; 7 | import com.google.common.base.Strings; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 19 | 20 | import java.net.URI; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.stream.Collectors; 24 | 25 | @RestController 26 | @RequestMapping("/todo-items") 27 | public class TodoItemResource { 28 | private TodoItemService service; 29 | 30 | @Autowired 31 | public TodoItemResource(final TodoItemService service) { 32 | this.service = service; 33 | } 34 | 35 | @PostMapping 36 | public ResponseEntity addTodoItem(@RequestBody final AddTodoItemRequest request) { 37 | if (Strings.isNullOrEmpty(request.getContent())) { 38 | return ResponseEntity.badRequest().build(); 39 | } 40 | 41 | final TodoParameter parameter = TodoParameter.of(request.getContent()); 42 | final TodoItem todoItem = this.service.addTodoItem(parameter); 43 | 44 | final URI uri = ServletUriComponentsBuilder 45 | .fromCurrentRequest() 46 | .path("/{id}") 47 | .buildAndExpand(todoItem.getIndex()) 48 | .toUri(); 49 | return ResponseEntity.created(uri).build(); 50 | } 51 | 52 | @PutMapping("/{id}") 53 | public ResponseEntity markAsDone(@PathVariable("id") final int id, 54 | @RequestBody final MarkAsDoneRequest request) { 55 | if (id <= 0) { 56 | return ResponseEntity.badRequest().build(); 57 | } 58 | 59 | if (!request.isDone()) { 60 | return ResponseEntity.badRequest().build(); 61 | } 62 | 63 | final Optional todoItem = this.service.markTodoItemDone(TodoIndexParameter.of(id)); 64 | 65 | if (todoItem.isPresent()) { 66 | return ResponseEntity.ok().build(); 67 | } 68 | 69 | return ResponseEntity.badRequest().build(); 70 | } 71 | 72 | @GetMapping 73 | public List list(@RequestParam(value = "all", defaultValue = "false") final boolean all) { 74 | final List items = this.service.list(all); 75 | return items.stream().map(TodoItemResponse::new).collect(Collectors.toList()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/TodoItemResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.github.dreamhead.todo.core.TodoItem; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 10 | @NoArgsConstructor 11 | @Getter 12 | public class TodoItemResponse { 13 | @JsonProperty 14 | private long index; 15 | @JsonProperty 16 | private String content; 17 | @JsonProperty 18 | private boolean done; 19 | 20 | public TodoItemResponse(final TodoItem item) { 21 | this.index = item.getIndex(); 22 | this.content = item.getContent(); 23 | this.done = item.isDone(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /todo-api/src/main/java/com/github/dreamhead/todo/api/repository/TodoItemJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api.repository; 2 | 3 | import com.github.dreamhead.todo.core.TodoItemRepository; 4 | 5 | public interface TodoItemJpaRepository extends TodoItemRepository { 6 | void deleteAll(); 7 | } 8 | -------------------------------------------------------------------------------- /todo-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://localhost:3306/todo_dev?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true 2 | spring.datasource.username=todo 3 | spring.datasource.password=geektime 4 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 5 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect -------------------------------------------------------------------------------- /todo-api/src/test/java/com/github/dreamhead/todo/api/CucumberIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions(features = "src/test/resources") 9 | public class CucumberIntegrationTest extends SpringIntegrationTest { 10 | } 11 | -------------------------------------------------------------------------------- /todo-api/src/test/java/com/github/dreamhead/todo/api/SpringIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.github.dreamhead.todo.api.repository.TodoItemJpaRepository; 4 | import io.cucumber.spring.CucumberContextConfiguration; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.annotation.Rollback; 8 | import org.springframework.test.context.TestPropertySource; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | @CucumberContextConfiguration 12 | @SpringBootTest(classes = Bootstrap.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 13 | @Transactional 14 | @Rollback 15 | @TestPropertySource("classpath:test.properties") 16 | public class SpringIntegrationTest { 17 | @Autowired 18 | protected TodoItemJpaRepository repository; 19 | } 20 | -------------------------------------------------------------------------------- /todo-api/src/test/java/com/github/dreamhead/todo/api/TodoItemRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.github.dreamhead.todo.core.TodoItem; 4 | import com.github.dreamhead.todo.core.TodoItemRepository; 5 | import com.google.common.collect.Iterables; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 10 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 11 | import org.springframework.dao.InvalidDataAccessApiUsageException; 12 | import org.springframework.test.context.TestPropertySource; 13 | import org.springframework.test.context.junit.jupiter.SpringExtension; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 17 | 18 | @ExtendWith(SpringExtension.class) 19 | @DataJpaTest 20 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 21 | @TestPropertySource("classpath:test.properties") 22 | public class TodoItemRepositoryTest { 23 | @Autowired 24 | private TodoItemRepository repository; 25 | 26 | @Test 27 | public void should_find_nothing_for_empty_repository() { 28 | final Iterable items = repository.findAll(); 29 | assertThat(items).hasSize(0); 30 | } 31 | 32 | @Test 33 | public void should_find_saved_items() { 34 | repository.save(new TodoItem("foo")); 35 | repository.save(new TodoItem("bar")); 36 | final Iterable items = repository.findAll(); 37 | assertThat(items).hasSize(2); 38 | final TodoItem firstItem = Iterables.get(items, 0); 39 | assertThat(firstItem.getContent()).isEqualTo("foo"); 40 | final TodoItem secondItem = Iterables.get(items, 1); 41 | assertThat(secondItem.getContent()).isEqualTo("bar"); 42 | assertThat(secondItem.getIndex()).isEqualTo(firstItem.getIndex() + 1); 43 | } 44 | 45 | @Test 46 | public void should_update_saved_items() { 47 | repository.save(new TodoItem("foo")); 48 | repository.save(new TodoItem("bar")); 49 | final Iterable items = repository.findAll(); 50 | final TodoItem toUpdate = Iterables.get(items, 0); 51 | toUpdate.markDone(); 52 | 53 | repository.save(toUpdate); 54 | 55 | final Iterable updated = repository.findAll(); 56 | assertThat(updated).hasSize(2); 57 | assertThat(Iterables.get(items, 0).isDone()).isTrue(); 58 | } 59 | 60 | @Test 61 | public void should_not_save_null_todo_item() { 62 | assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) 63 | .isThrownBy(() -> repository.save(null)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /todo-api/src/test/java/com/github/dreamhead/todo/api/TodoItemResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.type.CollectionType; 5 | import com.fasterxml.jackson.databind.type.TypeFactory; 6 | import com.github.dreamhead.todo.core.TodoItem; 7 | import com.github.dreamhead.todo.core.TodoItemRepository; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.test.context.TestPropertySource; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.springframework.test.web.servlet.MvcResult; 16 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | import java.util.List; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | 24 | @SpringBootTest 25 | @AutoConfigureMockMvc 26 | @Transactional 27 | @TestPropertySource("classpath:test.properties") 28 | public class TodoItemResourceTest { 29 | @Autowired 30 | private MockMvc mockMvc; 31 | 32 | @Autowired 33 | private TodoItemRepository repository; 34 | 35 | @Autowired 36 | private ObjectMapper mapper; 37 | 38 | private TypeFactory factory = TypeFactory.defaultInstance(); 39 | 40 | @Test 41 | public void should_add_item() throws Exception { 42 | String todoItem = "{ " + 43 | "\"content\": \"foo\"" + 44 | "}"; 45 | 46 | mockMvc.perform(MockMvcRequestBuilders.post("/todo-items") 47 | .contentType(MediaType.APPLICATION_JSON) 48 | .content(todoItem)) 49 | .andExpect(status().isCreated()); 50 | 51 | assertThat(repository.findAll()).anyMatch(item -> item.getContent().equals("foo")); 52 | } 53 | 54 | @Test 55 | public void should_fail_to_add_empty_item() throws Exception { 56 | String todoItem = "{ " + 57 | "\"content\": \"\"" + 58 | "}"; 59 | 60 | mockMvc.perform(MockMvcRequestBuilders.post("/todo-items") 61 | .contentType(MediaType.APPLICATION_JSON) 62 | .content(todoItem)) 63 | .andExpect(status().isBadRequest()); 64 | } 65 | 66 | @Test 67 | public void should_fail_to_add_unknown_request() throws Exception { 68 | String todoItem = ""; 69 | 70 | mockMvc.perform(MockMvcRequestBuilders.post("/todo-items") 71 | .contentType(MediaType.APPLICATION_JSON) 72 | .content(todoItem)) 73 | .andExpect(status().isBadRequest()); 74 | } 75 | 76 | @Test 77 | public void should_mark_as_done() throws Exception { 78 | final TodoItem newItem = repository.save(new TodoItem("foo")); 79 | 80 | String done = "{ \"done\": true }"; 81 | 82 | System.out.println("Before mark as done:" + newItem.getIndex()); 83 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/" + newItem.getIndex()) 84 | .contentType(MediaType.APPLICATION_JSON) 85 | .content(done)) 86 | .andExpect(status().isOk()); 87 | 88 | assertThat(repository.findAll()).anyMatch(item -> item.getContent().equals("foo") && item.isDone()); 89 | } 90 | 91 | @Test 92 | public void should_fail_to_mark_with_illegal_index() throws Exception { 93 | String done = "{ \"done\": true }"; 94 | 95 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/-1") 96 | .contentType(MediaType.APPLICATION_JSON) 97 | .content(done)) 98 | .andExpect(status().isBadRequest()); 99 | } 100 | 101 | @Test 102 | public void should_fail_to_mark_with_0_index() throws Exception { 103 | String done = "{ \"done\": true }"; 104 | 105 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/0") 106 | .contentType(MediaType.APPLICATION_JSON) 107 | .content(done)) 108 | .andExpect(status().isBadRequest()); 109 | } 110 | 111 | @Test 112 | public void should_fail_to_mark_unknown_todo_item() throws Exception { 113 | String done = "{ \"done\": true }"; 114 | 115 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/" + (Integer.MAX_VALUE - 1)) 116 | .contentType(MediaType.APPLICATION_JSON) 117 | .content(done)) 118 | .andExpect(status().isBadRequest()); 119 | } 120 | 121 | @Test 122 | public void should_fail_to_mark_todo_item_for_wrong_content() throws Exception { 123 | String done = "{ \"done\": false }"; 124 | 125 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/" + (Integer.MAX_VALUE - 1)) 126 | .contentType(MediaType.APPLICATION_JSON) 127 | .content(done)) 128 | .andExpect(status().isBadRequest()); 129 | } 130 | 131 | @Test 132 | public void should_fail_to_mark_with_unknown_request() throws Exception { 133 | String done = ""; 134 | 135 | mockMvc.perform(MockMvcRequestBuilders.put("/todo-items/1") 136 | .contentType(MediaType.APPLICATION_JSON) 137 | .content(done)) 138 | .andExpect(status().isBadRequest()); 139 | } 140 | 141 | @Test 142 | public void should_list_without_done() throws Exception { 143 | repository.save(new TodoItem("foo")); 144 | repository.save(doneItem("bar")); 145 | repository.save(new TodoItem("blah")); 146 | 147 | final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/todo-items") 148 | .contentType(MediaType.APPLICATION_JSON)) 149 | .andExpect(status().isOk()) 150 | .andReturn(); 151 | 152 | final CollectionType type = factory.constructCollectionType(List.class, TodoItemResponse.class); 153 | List responses = mapper.readValue(result.getResponse().getContentAsString(), type); 154 | assertThat(responses).hasSize(2); 155 | } 156 | 157 | @Test 158 | public void should_list_with_done() throws Exception { 159 | repository.save(new TodoItem("foo")); 160 | repository.save(doneItem("bar")); 161 | repository.save(new TodoItem("blah")); 162 | 163 | final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/todo-items?all=true") 164 | .contentType(MediaType.APPLICATION_JSON)) 165 | .andExpect(status().isOk()) 166 | .andReturn(); 167 | 168 | final CollectionType type = factory.constructCollectionType(List.class, TodoItemResponse.class); 169 | List responses = mapper.readValue(result.getResponse().getContentAsString(), type); 170 | assertThat(responses).hasSize(3); 171 | } 172 | 173 | private TodoItem doneItem(final String content) { 174 | final TodoItem item = new TodoItem(content); 175 | item.markDone(); 176 | return item; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /todo-api/src/test/java/com/github/dreamhead/todo/api/stepdef/TodoItemStepDefinitions.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.api.stepdef; 2 | 3 | import com.github.dreamhead.todo.api.AddTodoItemRequest; 4 | import com.github.dreamhead.todo.api.MarkAsDoneRequest; 5 | import com.github.dreamhead.todo.api.SpringIntegrationTest; 6 | import com.github.dreamhead.todo.api.TodoItemResponse; 7 | import io.cucumber.java8.En; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import java.util.Arrays; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | public class TodoItemStepDefinitions extends SpringIntegrationTest implements En { 20 | private RestTemplate restTemplate; 21 | private TodoItemResponse[] responses; 22 | private Map todoItems; 23 | 24 | public TodoItemStepDefinitions() { 25 | Before(() -> { 26 | this.restTemplate = new RestTemplate(); 27 | this.todoItems = new HashMap<>(); 28 | }); 29 | 30 | After(() -> { 31 | this.repository.deleteAll(); 32 | }); 33 | 34 | Given("todo item {string} is added", (String content) -> 35 | addTodoItem(content) 36 | ); 37 | 38 | When("list todo items", () -> { 39 | responses = listTodoItems(); 40 | }); 41 | 42 | When("mark todo item {string} as done", (String content) -> { 43 | final String id = this.todoItems.get(content); 44 | assertThat(id).isNotNull(); 45 | markTodoItemAsDone(id); 46 | }); 47 | 48 | Then("todo item {string} should be contained", (String content) -> { 49 | assertThat(Arrays.stream(responses) 50 | .anyMatch(item -> item.getContent().equals(content))).isTrue(); 51 | }); 52 | 53 | Then("todo item {string} should not be contained", (String content) -> { 54 | assertThat(Arrays.stream(responses) 55 | .noneMatch(item -> item.getContent().equals(content))).isTrue(); 56 | }); 57 | } 58 | 59 | private TodoItemResponse[] listTodoItems() { 60 | return restTemplate.getForObject("http://localhost:8080/todo-items", TodoItemResponse[].class); 61 | } 62 | 63 | private void addTodoItem(final String content) { 64 | AddTodoItemRequest request = new AddTodoItemRequest(content); 65 | final ResponseEntity entity = 66 | restTemplate.postForEntity("http://localhost:8080/todo-items", request, String.class); 67 | assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 68 | final List locations = entity.getHeaders().get("Location"); 69 | final String location = locations.get(0); 70 | final int index = location.lastIndexOf("/"); 71 | final String id = location.substring(index + 1); 72 | this.todoItems.put(content, id); 73 | } 74 | 75 | private void markTodoItemAsDone(final String id) { 76 | final MarkAsDoneRequest request = new MarkAsDoneRequest(true); 77 | restTemplate.put("http://localhost:8080/todo-items/" + id, request); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /todo-api/src/test/resources/cucumber.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true -------------------------------------------------------------------------------- /todo-api/src/test/resources/features/todo.feature: -------------------------------------------------------------------------------- 1 | Feature: Todo Item 2 | 3 | Scenario: List todo item 4 | Given todo item "foo" is added 5 | And todo item "bar" is added 6 | When list todo items 7 | Then todo item "foo" should be contained 8 | And todo item "bar" should be contained 9 | 10 | Scenario: Mark todo item as done 11 | Given todo item "foo" is added 12 | And todo item "bar" is added 13 | When mark todo item "foo" as done 14 | And list todo items 15 | Then todo item "foo" should not be contained 16 | And todo item "bar" should be contained 17 | -------------------------------------------------------------------------------- /todo-api/src/test/resources/test.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://localhost:3306/todo_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true 2 | spring.datasource.username=todo 3 | spring.datasource.password=geektime 4 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -------------------------------------------------------------------------------- /todo-cli/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.dreamhead.todo' 3 | 4 | } 5 | 6 | style { 7 | excludePackages = [ 8 | ] 9 | excludeClasses = [ 10 | "com.github.dreamhead.todo.cli.Bootstrap" 11 | ] 12 | } 13 | 14 | coverage { 15 | excludePackages = [ 16 | ] 17 | excludeClasses = [ 18 | "com.github.dreamhead.todo.cli.Bootstrap", 19 | "com.github.dreamhead.todo.util.Jsons" 20 | ] 21 | } 22 | 23 | task uberJar(type: Jar) { 24 | manifest { 25 | attributes 'Main-Class': 'com.github.dreamhead.todo.cli.Bootstrap' 26 | } 27 | archiveBaseName = 'todo-uber' 28 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 29 | from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } 30 | with jar 31 | } 32 | 33 | project.dependencies { 34 | implementation("info.picocli:picocli:$picocliVersion") 35 | implementation(project(':todo-core')) 36 | } -------------------------------------------------------------------------------- /todo-cli/src/main/java/com/github/dreamhead/todo/cli/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli; 2 | 3 | import picocli.CommandLine; 4 | 5 | import java.io.File; 6 | 7 | public class Bootstrap { 8 | public static void main(final String[] args) { 9 | final File todoRepository = new File(todoHome(), "repository.json"); 10 | ObjectFactory factory = new ObjectFactory(); 11 | final CommandLine commandLine = factory.createCommandLine(todoRepository); 12 | commandLine.execute(args); 13 | } 14 | 15 | private static File todoHome() { 16 | final File homeDirectory = new File(System.getProperty("user.home")); 17 | final File todoHome = new File(homeDirectory, ".todo"); 18 | if (!todoHome.exists()) { 19 | todoHome.mkdirs(); 20 | } 21 | return todoHome; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /todo-cli/src/main/java/com/github/dreamhead/todo/cli/ObjectFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli; 2 | 3 | import com.github.dreamhead.todo.cli.file.FileTodoItemRepository; 4 | import com.github.dreamhead.todo.core.TodoItemRepository; 5 | import com.github.dreamhead.todo.core.TodoItemService; 6 | import picocli.CommandLine; 7 | 8 | import java.io.File; 9 | 10 | public class ObjectFactory { 11 | public CommandLine createCommandLine(final File repositoryFile) { 12 | return new CommandLine(createTodoCommand(repositoryFile)); 13 | } 14 | 15 | private TodoCommand createTodoCommand(final File repositoryFile) { 16 | final TodoItemService service = createService(repositoryFile); 17 | return new TodoCommand(service); 18 | } 19 | 20 | public TodoItemService createService(final File repositoryFile) { 21 | final TodoItemRepository repository = new FileTodoItemRepository(repositoryFile); 22 | return new TodoItemService(repository); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /todo-cli/src/main/java/com/github/dreamhead/todo/cli/TodoCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli; 2 | 3 | import com.github.dreamhead.todo.core.TodoIndexParameter; 4 | import com.github.dreamhead.todo.core.TodoItem; 5 | import com.github.dreamhead.todo.core.TodoItemService; 6 | import com.github.dreamhead.todo.core.TodoParameter; 7 | import com.google.common.base.Strings; 8 | import picocli.CommandLine; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @CommandLine.Command(name = "todo") 14 | public class TodoCommand { 15 | private final TodoItemService service; 16 | 17 | @CommandLine.Spec 18 | private CommandLine.Model.CommandSpec spec; 19 | 20 | public TodoCommand(final TodoItemService service) { 21 | this.service = service; 22 | } 23 | 24 | @CommandLine.Command(name = "add") 25 | public int add(@CommandLine.Parameters(index = "0") final String item) { 26 | if (Strings.isNullOrEmpty(item)) { 27 | throw new CommandLine.ParameterException(spec.commandLine(), "empty item is not allowed"); 28 | } 29 | 30 | final TodoItem todoItem = this.service.addTodoItem(TodoParameter.of(item)); 31 | System.out.printf("%d. %s%n", todoItem.getIndex(), todoItem.getContent()); 32 | System.out.printf("Item <%d> added%n", todoItem.getIndex()); 33 | 34 | return 0; 35 | } 36 | 37 | @CommandLine.Command(name = "done") 38 | public int markAsDone(@CommandLine.Parameters(index = "0") final int index) { 39 | if (index <= 0) { 40 | throw new CommandLine.ParameterException(spec.commandLine(), "index should be greater than 0"); 41 | } 42 | 43 | final Optional item = this.service.markTodoItemDone(TodoIndexParameter.of(index)); 44 | if (!item.isPresent()) { 45 | throw new CommandLine.ParameterException(spec.commandLine(), "unknown todo item index"); 46 | } 47 | 48 | final TodoItem actual = item.get(); 49 | System.out.printf("Item <%d> done%n", actual.getIndex()); 50 | return 0; 51 | } 52 | 53 | @CommandLine.Command(name = "list") 54 | public int list(@CommandLine.Option(names = "--all") final boolean all) { 55 | final List items = this.service.list(all); 56 | 57 | items.stream() 58 | .map(this::formatItem) 59 | .forEach(System.out::println); 60 | 61 | if (all) { 62 | final long doneCount = items.stream() 63 | .filter(TodoItem::isDone) 64 | .count(); 65 | 66 | System.out.println(formatTotal(items.size(), doneCount)); 67 | return 0; 68 | } 69 | 70 | System.out.println(formatTotal(items.size())); 71 | return 0; 72 | } 73 | 74 | private String formatTotal(final int size, final long doneCount) { 75 | return "Total: " 76 | + size + unit(size) + ", " 77 | + doneCount + unit(doneCount); 78 | } 79 | 80 | private String formatTotal(final int size) { 81 | return "Total: " + size + unit(size); 82 | } 83 | 84 | private String unit(final long count) { 85 | if (count > 1) { 86 | return " items"; 87 | } 88 | 89 | return " item"; 90 | } 91 | 92 | private String formatItem(final TodoItem todoItem) { 93 | if (todoItem.isDone()) { 94 | return String.format("%d. [done] %s", todoItem.getIndex(), todoItem.getContent()); 95 | } 96 | 97 | return String.format("%d. %s", todoItem.getIndex(), todoItem.getContent()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /todo-cli/src/main/java/com/github/dreamhead/todo/cli/file/FileTodoItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli.file; 2 | 3 | import com.github.dreamhead.todo.core.TodoItem; 4 | import com.github.dreamhead.todo.core.TodoItemRepository; 5 | import com.github.dreamhead.todo.util.Jsons; 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.common.collect.Iterables; 8 | 9 | import java.io.File; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.StreamSupport; 12 | 13 | public class FileTodoItemRepository implements TodoItemRepository { 14 | private final File file; 15 | 16 | public FileTodoItemRepository(final File file) { 17 | this.file = file; 18 | } 19 | 20 | @Override 21 | public TodoItem save(final TodoItem item) { 22 | if (item == null) { 23 | throw new IllegalArgumentException("Todo item should not be null"); 24 | } 25 | 26 | final Iterable items = findAll(); 27 | final Iterable newContent = toSaveContent(item, items); 28 | Jsons.writeObjects(this.file, newContent); 29 | return item; 30 | } 31 | 32 | private Iterable toSaveContent(final TodoItem newItem, final Iterable items) { 33 | if (newItem.getIndex() == 0) { 34 | long newIndex = Iterables.size(items) + 1; 35 | newItem.assignIndex(newIndex); 36 | 37 | return ImmutableList.builder() 38 | .addAll(items) 39 | .add(newItem) 40 | .build(); 41 | } 42 | 43 | return StreamSupport.stream(items.spliterator(), false) 44 | .map(item -> updateItem(newItem, item)) 45 | .collect(Collectors.toList()); 46 | } 47 | 48 | private TodoItem updateItem(final TodoItem newItem, final TodoItem item) { 49 | if (item.getIndex() == newItem.getIndex()) { 50 | return newItem; 51 | } 52 | 53 | return item; 54 | } 55 | 56 | @Override 57 | public Iterable findAll() { 58 | if (this.file.length() == 0) { 59 | return ImmutableList.of(); 60 | } 61 | 62 | return Jsons.toObjects(this.file); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /todo-cli/src/main/java/com/github/dreamhead/todo/util/Jsons.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.util; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.type.CollectionType; 5 | import com.fasterxml.jackson.databind.type.TypeFactory; 6 | import com.github.dreamhead.todo.core.TodoException; 7 | import com.github.dreamhead.todo.core.TodoItem; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | public final class Jsons { 14 | private static final TypeFactory FACTORY = TypeFactory.defaultInstance(); 15 | private static final ObjectMapper MAPPER = new ObjectMapper(); 16 | 17 | public static Iterable toObjects(final File file) { 18 | final CollectionType type = FACTORY.constructCollectionType(List.class, TodoItem.class); 19 | try { 20 | return MAPPER.readValue(file, type); 21 | } catch (IOException e) { 22 | throw new TodoException("Fail to read objects", e); 23 | } 24 | } 25 | 26 | public static void writeObjects(final File file, final Iterable objects) { 27 | try { 28 | MAPPER.writeValue(file, objects); 29 | } catch (IOException e) { 30 | throw new TodoException("Fail to write objects", e); 31 | } 32 | } 33 | 34 | 35 | private Jsons() { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /todo-cli/src/test/java/com/github/dreamhead/todo/cli/TodoCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli; 2 | 3 | import com.github.dreamhead.todo.core.TodoIndexParameter; 4 | import com.github.dreamhead.todo.core.TodoItem; 5 | import com.github.dreamhead.todo.core.TodoItemService; 6 | import com.github.dreamhead.todo.core.TodoParameter; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.io.TempDir; 10 | import picocli.CommandLine; 11 | 12 | import java.io.File; 13 | import java.util.List; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | class TodoCommandTest { 18 | @TempDir 19 | File tempDir; 20 | private TodoItemService service; 21 | private CommandLine cli; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | final ObjectFactory factory = new ObjectFactory(); 26 | final File repositoryFile = new File(tempDir, "repository.json"); 27 | this.service = factory.createService(repositoryFile); 28 | cli = factory.createCommandLine(repositoryFile); 29 | } 30 | 31 | @Test 32 | public void should_add_todo_item() { 33 | final int result = cli.execute("add", "foo"); 34 | 35 | assertThat(result).isEqualTo(0); 36 | final List items = service.list(true); 37 | assertThat(items).hasSize(1); 38 | assertThat(items.get(0).getContent()).isEqualTo("foo"); 39 | } 40 | 41 | @Test 42 | public void should_fail_to_add_empty_todo() { 43 | assertThat(cli.execute("add", "")).isNotEqualTo(0); 44 | } 45 | 46 | @Test 47 | public void should_mark_as_done() { 48 | service.addTodoItem(TodoParameter.of("foo")); 49 | 50 | cli.execute("done", "1"); 51 | 52 | final List items = service.list(true); 53 | assertThat(items.get(0).isDone()).isTrue(); 54 | } 55 | 56 | @Test 57 | public void should_fail_to_mark_with_illegal_index() { 58 | assertThat(cli.execute("done", "-1")).isNotEqualTo(0); 59 | assertThat(cli.execute("done", "0")).isNotEqualTo(0); 60 | } 61 | 62 | @Test 63 | public void should_fail_to_mark_unknown_todo_item() { 64 | assertThat(cli.execute("done", "1")).isNotEqualTo(0); 65 | } 66 | 67 | @Test 68 | public void should_list_without_done() { 69 | service.addTodoItem(TodoParameter.of("foo")); 70 | service.addTodoItem(TodoParameter.of("bar")); 71 | service.addTodoItem(TodoParameter.of("blah")); 72 | service.markTodoItemDone(TodoIndexParameter.of(2)); 73 | 74 | assertThat(cli.execute("list")).isEqualTo(0); 75 | } 76 | 77 | @Test 78 | public void should_list_with_done() { 79 | service.addTodoItem(TodoParameter.of("foo")); 80 | service.addTodoItem(TodoParameter.of("bar")); 81 | service.addTodoItem(TodoParameter.of("blah")); 82 | service.markTodoItemDone(TodoIndexParameter.of(2)); 83 | 84 | assertThat(cli.execute("list", "--all")).isEqualTo(0); 85 | } 86 | } -------------------------------------------------------------------------------- /todo-cli/src/test/java/com/github/dreamhead/todo/cli/file/FileTodoItemRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.cli.file; 2 | 3 | import com.github.dreamhead.todo.core.TodoItem; 4 | import com.google.common.collect.Iterables; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.io.TempDir; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 14 | 15 | class FileTodoItemRepositoryTest { 16 | @TempDir 17 | File tempDir; 18 | private File tempFile; 19 | private FileTodoItemRepository repository; 20 | 21 | @BeforeEach 22 | void setUp() throws IOException { 23 | this.tempFile = File.createTempFile("file", "", tempDir); 24 | this.repository = new FileTodoItemRepository(this.tempFile); 25 | } 26 | 27 | @Test 28 | public void should_find_nothing_for_empty_repository() throws IOException { 29 | final Iterable items = repository.findAll(); 30 | assertThat(items).hasSize(0); 31 | } 32 | 33 | @Test 34 | public void should_find_saved_items() { 35 | repository.save(new TodoItem("foo")); 36 | repository.save(new TodoItem("bar")); 37 | final Iterable items = repository.findAll(); 38 | assertThat(items).hasSize(2); 39 | final TodoItem firstItem = Iterables.get(items, 0); 40 | assertThat(firstItem.getContent()).isEqualTo("foo"); 41 | assertThat(firstItem.getIndex()).isEqualTo(1); 42 | final TodoItem secondItem = Iterables.get(items, 1); 43 | assertThat(secondItem.getContent()).isEqualTo("bar"); 44 | assertThat(secondItem.getIndex()).isEqualTo(2); 45 | } 46 | 47 | @Test 48 | public void should_update_saved_items() { 49 | repository.save(new TodoItem("foo")); 50 | repository.save(new TodoItem("bar")); 51 | final Iterable items = repository.findAll(); 52 | final TodoItem toUpdate = Iterables.get(items, 0); 53 | toUpdate.markDone(); 54 | 55 | repository.save(toUpdate); 56 | 57 | final Iterable updated = repository.findAll(); 58 | assertThat(updated).hasSize(2); 59 | assertThat(Iterables.get(items, 0).isDone()).isTrue(); 60 | } 61 | 62 | @Test 63 | public void should_not_save_null_todo_item() { 64 | assertThatExceptionOfType(IllegalArgumentException.class) 65 | .isThrownBy(() -> repository.save(null)); 66 | } 67 | } -------------------------------------------------------------------------------- /todo-core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.dreamhead.todo' 3 | } 4 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoException.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | public class TodoException extends RuntimeException { 4 | public TodoException(final String message, final Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoIndexParameter.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import lombok.Getter; 4 | 5 | public final class TodoIndexParameter { 6 | @Getter 7 | private final int index; 8 | 9 | private TodoIndexParameter(final int index) { 10 | this.index = index; 11 | } 12 | 13 | public static TodoIndexParameter of(final int index) { 14 | if (index <= 0) { 15 | throw new IllegalArgumentException("Todo index should be greater than 0"); 16 | } 17 | 18 | return new TodoIndexParameter(index); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoItem.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import lombok.AccessLevel; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | import javax.persistence.Column; 11 | import javax.persistence.Entity; 12 | import javax.persistence.GeneratedValue; 13 | import javax.persistence.GenerationType; 14 | import javax.persistence.Id; 15 | import javax.persistence.Table; 16 | 17 | @EqualsAndHashCode 18 | @Getter 19 | @ToString 20 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 21 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 22 | @Entity 23 | @Table(name = "todo_items") 24 | public class TodoItem { 25 | @Id 26 | @Column(name = "id") 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private long index; 29 | 30 | @Column 31 | private String content; 32 | 33 | @Column 34 | private boolean done; 35 | 36 | public TodoItem(final String content) { 37 | this.content = content; 38 | this.done = false; 39 | } 40 | 41 | public void assignIndex(final long index) { 42 | this.index = index; 43 | } 44 | 45 | public void markDone() { 46 | this.done = true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import org.springframework.data.repository.Repository; 4 | 5 | public interface TodoItemRepository extends Repository { 6 | TodoItem save(TodoItem item); 7 | 8 | Iterable findAll(); 9 | } 10 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoItemService.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import com.google.common.collect.Streams; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.StreamSupport; 11 | 12 | @Service 13 | public class TodoItemService { 14 | private final TodoItemRepository repository; 15 | 16 | @Autowired 17 | public TodoItemService(final TodoItemRepository repository) { 18 | this.repository = repository; 19 | } 20 | 21 | public TodoItem addTodoItem(final TodoParameter todoParameter) { 22 | if (todoParameter == null) { 23 | throw new IllegalArgumentException("Null or empty content is not allowed"); 24 | } 25 | 26 | final TodoItem item = new TodoItem(todoParameter.getContent()); 27 | return this.repository.save(item); 28 | } 29 | 30 | public Optional markTodoItemDone(final TodoIndexParameter index) { 31 | final Iterable all = this.repository.findAll(); 32 | 33 | final Optional optionalItem = StreamSupport.stream(all.spliterator(), false) 34 | .filter(element -> element.getIndex() == index.getIndex()) 35 | .findFirst(); 36 | return optionalItem.flatMap(this::doMarkAsDone); 37 | } 38 | 39 | private Optional doMarkAsDone(final TodoItem todoItem) { 40 | todoItem.markDone(); 41 | return Optional.of(this.repository.save(todoItem)); 42 | } 43 | 44 | public List list(final boolean all) { 45 | return Streams.stream(this.repository.findAll()) 46 | .filter(item -> all || !item.isDone()) 47 | .collect(Collectors.toList()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /todo-core/src/main/java/com/github/dreamhead/todo/core/TodoParameter.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import com.google.common.base.Strings; 4 | import lombok.Getter; 5 | 6 | public final class TodoParameter { 7 | @Getter 8 | private final String content; 9 | 10 | private TodoParameter(final String content) { 11 | this.content = content; 12 | } 13 | 14 | public static TodoParameter of(final String content) { 15 | if (Strings.isNullOrEmpty(content)) { 16 | throw new IllegalArgumentException("Empty content is not allowed"); 17 | } 18 | 19 | return new TodoParameter(content); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /todo-core/src/test/java/com/github/dreamhead/todo/core/TodoExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class TodoExceptionTest { 8 | @Test 9 | public void should_create_todo_exception() { 10 | final TodoException exception = new TodoException("foo", new IllegalArgumentException()); 11 | assertThat(exception).hasMessage("foo"); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /todo-core/src/test/java/com/github/dreamhead/todo/core/TodoIndexParameterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 7 | 8 | class TodoIndexParameterTest { 9 | @Test 10 | public void should_create_index_parameter() { 11 | final TodoIndexParameter parameter = TodoIndexParameter.of(1); 12 | assertThat(parameter.getIndex()).isEqualTo(1); 13 | } 14 | 15 | @Test 16 | public void should_not_create_index_parameter() { 17 | assertThatExceptionOfType(IllegalArgumentException.class) 18 | .isThrownBy(() -> TodoIndexParameter.of(0)); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /todo-core/src/test/java/com/github/dreamhead/todo/core/TodoItemServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 12 | import static org.mockito.AdditionalAnswers.returnsFirstArg; 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class TodoItemServiceTest { 19 | private TodoItemService service; 20 | private TodoItemRepository repository; 21 | 22 | @BeforeEach 23 | public void setUp() { 24 | this.repository = mock(TodoItemRepository.class); 25 | this.service = new TodoItemService(this.repository); 26 | } 27 | 28 | @Test 29 | public void should_add_todo_item() { 30 | when(repository.save(any())).then(returnsFirstArg()); 31 | TodoItem item = service.addTodoItem(TodoParameter.of("foo")); 32 | assertThat(item.getContent()).isEqualTo("foo"); 33 | 34 | verify(repository).save(any()); 35 | } 36 | 37 | @Test 38 | public void should_throw_exception_for_null_todo_item() { 39 | assertThatExceptionOfType(IllegalArgumentException.class) 40 | .isThrownBy(() -> service.addTodoItem(null)); 41 | } 42 | 43 | @Test 44 | public void should_mark_todo_item_as_done() { 45 | final TodoItem foo = new TodoItem("foo"); 46 | foo.assignIndex(1); 47 | when(repository.findAll()).thenReturn(ImmutableList.of(foo)); 48 | when(repository.save(any())).then(returnsFirstArg()); 49 | 50 | final Optional todoItem = service.markTodoItemDone(TodoIndexParameter.of(1)); 51 | 52 | assertThat(todoItem).isPresent(); 53 | final TodoItem actual = todoItem.get(); 54 | assertThat(actual.isDone()).isTrue(); 55 | } 56 | 57 | @Test 58 | public void should_not_mark_todo_item_for_out_of_scope_index() { 59 | when(repository.findAll()).thenReturn(ImmutableList.of(new TodoItem("foo"))); 60 | final Optional todoItem = service.markTodoItemDone(TodoIndexParameter.of(2)); 61 | assertThat(todoItem).isEmpty(); 62 | } 63 | 64 | @Test 65 | public void should_list_all() { 66 | final TodoItem item = new TodoItem("foo"); 67 | item.assignIndex(1); 68 | when(repository.findAll()).thenReturn(ImmutableList.of(item)); 69 | 70 | List items = service.list(true); 71 | assertThat(items).hasSize(1); 72 | final TodoItem result = items.get(0); 73 | assertThat(result.getIndex()).isEqualTo(1); 74 | assertThat(result.getContent()).isEqualTo("foo"); 75 | } 76 | 77 | @Test 78 | public void should_not_list_without_item() { 79 | when(repository.findAll()).thenReturn(ImmutableList.of()); 80 | 81 | List items = service.list(true); 82 | assertThat(items).hasSize(0); 83 | } 84 | 85 | @Test 86 | public void should_list_all_without_done() { 87 | final TodoItem doneItem = new TodoItem("foo"); 88 | doneItem.assignIndex(1); 89 | doneItem.markDone(); 90 | final TodoItem regularItem = new TodoItem("bar"); 91 | regularItem.assignIndex(2); 92 | 93 | when(repository.findAll()).thenReturn(ImmutableList.of(doneItem, regularItem)); 94 | 95 | List items = service.list(false); 96 | assertThat(items).hasSize(1); 97 | final TodoItem item = items.get(0); 98 | assertThat(item.getIndex()).isEqualTo(2); 99 | assertThat(item.getContent()).isEqualTo("bar"); 100 | } 101 | 102 | @Test 103 | public void should_not_list_without_done_item() { 104 | final TodoItem doneItem = new TodoItem("foo"); 105 | doneItem.assignIndex(1); 106 | doneItem.markDone(); 107 | 108 | when(repository.findAll()).thenReturn(ImmutableList.of(doneItem)); 109 | 110 | List items = service.list(false); 111 | assertThat(items).hasSize(0); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /todo-core/src/test/java/com/github/dreamhead/todo/core/TodoParameterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dreamhead.todo.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 7 | 8 | class TodoParameterTest { 9 | @Test 10 | public void should_create_todo_parameter() { 11 | final TodoParameter parameter = TodoParameter.of("foo"); 12 | assertThat(parameter.getContent()).isEqualTo("foo"); 13 | } 14 | 15 | @Test 16 | public void should_not_create_todo_parameter() { 17 | assertThatExceptionOfType(IllegalArgumentException.class) 18 | .isThrownBy(() -> TodoParameter.of("")); 19 | 20 | assertThatExceptionOfType(IllegalArgumentException.class) 21 | .isThrownBy(() -> TodoParameter.of(null)); 22 | } 23 | 24 | } --------------------------------------------------------------------------------