diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f331610 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +# No marketplace actions are used here on purpose: runners for this repository cannot +# resolve them ("Failed to resolve action download info. Error: Service Unavailable"), +# so actions/checkout and actions/setup-java fail before the job body ever runs. +# Checkout is done with git directly, and the runner image already ships JDK 21. +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git init -q . + git remote add origin "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" + git fetch -q --depth 1 origin "$GITHUB_SHA" + git checkout -q FETCH_HEAD + + - name: Build and test on JDK 21 + run: | + export JAVA_HOME="$JAVA_HOME_21_X64" + export PATH="$JAVA_HOME/bin:$PATH" + java -version + ./mvnw -B clean verify diff --git a/.gitignore b/.gitignore index 57f1cb2..2d513a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/.idea/ \ No newline at end of file +/.idea/ +/target/ diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index a156ef4..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -gs-spring-boot \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index daa2f90..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index ba88c4b..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_2_3.xml b/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_2_3.xml deleted file mode 100644 index 6fec8f4..0000000 --- a/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_2_3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__ch_qos_logback_logback_core_1_2_3.xml b/.idea/libraries/Maven__ch_qos_logback_logback_core_1_2_3.xml deleted file mode 100644 index 9eb8596..0000000 --- a/.idea/libraries/Maven__ch_qos_logback_logback_core_1_2_3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_classmate_1_3_4.xml b/.idea/libraries/Maven__com_fasterxml_classmate_1_3_4.xml deleted file mode 100644 index 33c30b4..0000000 --- a/.idea/libraries/Maven__com_fasterxml_classmate_1_3_4.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_9_0.xml b/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_9_0.xml deleted file mode 100644 index 06441f4..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_9_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_9_5.xml b/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_9_5.xml deleted file mode 100644 index 1205e0d..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_9_5.xml b/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_9_5.xml deleted file mode 100644 index 467b779..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jdk8_2_9_5.xml b/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jdk8_2_9_5.xml deleted file mode 100644 index 65b251d..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jdk8_2_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jsr310_2_9_5.xml b/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jsr310_2_9_5.xml deleted file mode 100644 index c85f307..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jsr310_2_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_fasterxml_jackson_module_jackson_module_parameter_names_2_9_5.xml b/.idea/libraries/Maven__com_fasterxml_jackson_module_jackson_module_parameter_names_2_9_5.xml deleted file mode 100644 index 4ace22a..0000000 --- a/.idea/libraries/Maven__com_fasterxml_jackson_module_jackson_module_parameter_names_2_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_h2database_h2_1_4_197.xml b/.idea/libraries/Maven__com_h2database_h2_1_4_197.xml deleted file mode 100644 index 87c599a..0000000 --- a/.idea/libraries/Maven__com_h2database_h2_1_4_197.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_zaxxer_HikariCP_2_7_9.xml b/.idea/libraries/Maven__com_zaxxer_HikariCP_2_7_9.xml deleted file mode 100644 index 6d6865c..0000000 --- a/.idea/libraries/Maven__com_zaxxer_HikariCP_2_7_9.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__javax_annotation_javax_annotation_api_1_3_2.xml b/.idea/libraries/Maven__javax_annotation_javax_annotation_api_1_3_2.xml deleted file mode 100644 index e74f3ab..0000000 --- a/.idea/libraries/Maven__javax_annotation_javax_annotation_api_1_3_2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__javax_validation_validation_api_2_0_1_Final.xml b/.idea/libraries/Maven__javax_validation_validation_api_2_0_1_Final.xml deleted file mode 100644 index 6978c0b..0000000 --- a/.idea/libraries/Maven__javax_validation_validation_api_2_0_1_Final.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_logging_log4j_log4j_api_2_10_0.xml b/.idea/libraries/Maven__org_apache_logging_log4j_log4j_api_2_10_0.xml deleted file mode 100644 index cbe5ca7..0000000 --- a/.idea/libraries/Maven__org_apache_logging_log4j_log4j_api_2_10_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_logging_log4j_log4j_to_slf4j_2_10_0.xml b/.idea/libraries/Maven__org_apache_logging_log4j_log4j_to_slf4j_2_10_0.xml deleted file mode 100644 index 1407c58..0000000 --- a/.idea/libraries/Maven__org_apache_logging_log4j_log4j_to_slf4j_2_10_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_core_8_5_31.xml b/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_core_8_5_31.xml deleted file mode 100644 index a6693a2..0000000 --- a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_core_8_5_31.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_el_8_5_31.xml b/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_el_8_5_31.xml deleted file mode 100644 index bc00163..0000000 --- a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_el_8_5_31.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_websocket_8_5_31.xml b/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_websocket_8_5_31.xml deleted file mode 100644 index a47a3be..0000000 --- a/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_websocket_8_5_31.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_hibernate_validator_hibernate_validator_6_0_9_Final.xml b/.idea/libraries/Maven__org_hibernate_validator_hibernate_validator_6_0_9_Final.xml deleted file mode 100644 index 5a26bec..0000000 --- a/.idea/libraries/Maven__org_hibernate_validator_hibernate_validator_6_0_9_Final.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_jboss_logging_jboss_logging_3_3_2_Final.xml b/.idea/libraries/Maven__org_jboss_logging_jboss_logging_3_3_2_Final.xml deleted file mode 100644 index 5f7dd01..0000000 --- a/.idea/libraries/Maven__org_jboss_logging_jboss_logging_3_3_2_Final.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_jul_to_slf4j_1_7_25.xml b/.idea/libraries/Maven__org_slf4j_jul_to_slf4j_1_7_25.xml deleted file mode 100644 index 6073e53..0000000 --- a/.idea/libraries/Maven__org_slf4j_jul_to_slf4j_1_7_25.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml b/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml deleted file mode 100644 index 20e8163..0000000 --- a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_2_0_2_RELEASE.xml deleted file mode 100644 index 216fa5d..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_autoconfigure_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_autoconfigure_2_0_2_RELEASE.xml deleted file mode 100644 index 416393d..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_autoconfigure_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_2_0_2_RELEASE.xml deleted file mode 100644 index f4f17f8..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_jdbc_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_jdbc_2_0_2_RELEASE.xml deleted file mode 100644 index 1fe0fd1..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_jdbc_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_json_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_json_2_0_2_RELEASE.xml deleted file mode 100644 index 7f64d70..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_json_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_logging_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_logging_2_0_2_RELEASE.xml deleted file mode 100644 index 73c5114..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_logging_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_tomcat_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_tomcat_2_0_2_RELEASE.xml deleted file mode 100644 index 15cdbc9..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_tomcat_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_web_2_0_2_RELEASE.xml b/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_web_2_0_2_RELEASE.xml deleted file mode 100644 index ab4ca86..0000000 --- a/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_web_2_0_2_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_aop_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_aop_5_0_6_RELEASE.xml deleted file mode 100644 index 45ed002..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_aop_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_beans_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_beans_5_0_6_RELEASE.xml deleted file mode 100644 index c046b8d..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_beans_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_context_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_context_5_0_6_RELEASE.xml deleted file mode 100644 index 9b830cd..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_context_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_core_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_core_5_0_6_RELEASE.xml deleted file mode 100644 index 597e34e..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_core_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_expression_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_expression_5_0_6_RELEASE.xml deleted file mode 100644 index ffca824..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_expression_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_jcl_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_jcl_5_0_6_RELEASE.xml deleted file mode 100644 index c0a9cc7..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_jcl_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_jdbc_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_jdbc_5_0_6_RELEASE.xml deleted file mode 100644 index 671875f..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_jdbc_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_tx_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_tx_5_0_6_RELEASE.xml deleted file mode 100644 index 6e29e18..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_tx_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_web_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_web_5_0_6_RELEASE.xml deleted file mode 100644 index 521bb85..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_web_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_springframework_spring_webmvc_5_0_6_RELEASE.xml b/.idea/libraries/Maven__org_springframework_spring_webmvc_5_0_6_RELEASE.xml deleted file mode 100644 index e4279b7..0000000 --- a/.idea/libraries/Maven__org_springframework_spring_webmvc_5_0_6_RELEASE.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_yaml_snakeyaml_1_19.xml b/.idea/libraries/Maven__org_yaml_snakeyaml_1_19.xml deleted file mode 100644 index 33ccf19..0000000 --- a/.idea/libraries/Maven__org_yaml_snakeyaml_1_19.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 01b5b5c..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 751276b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index e96534f..0000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index e90adcd..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,943 +0,0 @@ - - - - - - - - - - - - hello.controller.* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HelloController - aslist - compata - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1526804939552 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - file://$PROJECT_DIR$/src/main/java/hello/controller/GreetingController.java - 15 - - - - file://$PROJECT_DIR$/src/main/java/hello/controller/HelloController.java - 55 - - - - - - - - - ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString)) - JAVA - CODE_FRAGMENT - - - ZonedDateTime.of(getLocalDateTime(), getZoneId("UTC-04:00")) - JAVA - CODE_FRAGMENT - - - - - getZoneId(zoneString) - JAVA - EXPRESSION - - - topics.stream().filter(topic -> topic.getId().equals(id)).findFirst().get() - JAVA - EXPRESSION - - - topics.stream().filter(topic -> topic.getId().equals(id)) - JAVA - EXPRESSION - - - topics.stream().filter(topicId -> topicId.equals(id)) - JAVA - EXPRESSION - - - topics.stream().filter(topicId -> topicId.equals(id)).findFirst().get() - JAVA - EXPRESSION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - No facets are configured - - - - - - - - - - - - - - - 1.8 - - - - - - - - gs-spring-boot - - - - - - - - Maven: ch.qos.logback:logback-classic:1.2.3 - - - - - - - - \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 5fd4d50..0000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index c954cec..ffcab66 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,3 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/README.md b/README.md index 2b09400..bc29551 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,150 @@ # springboot-java8 -The project is made on spring boot. The project summarize the new features present in Java 8. -It contain list of harcoded topics list. You can call the apis's with POSTMAN to add,delete,update Topic list -In addition, it uses -1) Java 8 NIO methods -2) String operations -3) Stream operations -4) IntStream functions -5) Functional interface -6) Lambda functions -7) Optional datatype -8) Foreach loops -9) Default and Static methods in interface -10) Java 8 LocalDateTime API -11) Pattern - - - -## Getting Started -1) Download or clone the project with link -(https://github.com/RehmanMuradAli/springboot-java8/) - -## Available API's - -Greetings -``` -GET / -``` -Get all Topics in List -``` -GET /topic -``` -Get Topic of given ID -``` -GET /topic/{id} -``` -Add Topic in List -``` -POST /topic -``` -Update Topic of given ID -``` -PUT /topic/{id} -``` -Delete Topic of given ID -``` -DELETE /topic/{id} -``` +A small Spring Boot REST service whose purpose is to **demonstrate Java language features** — originally the Java 8 +feature set, which is still what the code shows off. The name stuck, but the project itself now builds and runs on +**Java 21 with Spring Boot 3.5.16**. -Get all Topics whose ID's length is greater than minLength -``` -GET /topic/minimum/length/{minLength} -``` +The domain is deliberately trivial: an in-memory, hardcoded list of `Topic` objects that you can create, read, update +and delete over HTTP, plus a few endpoints that exist purely to print the result of stream / string / file operations. -Get all Topics sorted by ID -``` -GET /topic/sort -``` +## Java 8 features demonstrated -String Operations on Topic List -``` -GET /topic/string/operation -``` +1. NIO.2 file APIs (`Files.list`, `Files.find`, `Files.walk`, `Files.newBufferedReader`) +2. String operations (`String.join`, `String.chars`) +3. Stream operations (`filter`, `map`, `sorted`, `Collectors.joining`) +4. `IntStream` (`IntStream.range` for index lookups) +5. Functional interfaces (`CustomPredicate`) +6. Lambda expressions +7. `Optional` / `OptionalInt` +8. `forEach` loops +9. `default` and `static` methods on interfaces (`TimeClient`) +10. The `java.time` API (`LocalDateTime`, `ZonedDateTime`, `ChronoUnit`) +11. `Pattern` as a stream source and as a predicate (`splitAsStream`, `asPredicate`) +12. try-with-resources over streams of paths -File Operations on Topic List -``` -GET /topic/file/operation -``` -Java 8 Date Time example +## Java 21 idioms in use + +The application was migrated from Spring Boot 2.0.2 / Java 8 to Spring Boot 3.5.16 / Java 21, and the presentation +layer has been rewritten to use the modern language features that replace what the old code did by hand: + +- **Text blocks** (Java 15) for the multi-part responses of `/datetime`, `/topic/string/operation` and + `/topic/file/operation`, using `\` line-continuation escapes so the responses stay on a single line. +- **`String.formatted(...)`** (Java 15) instead of `String.format(...)` and `+` concatenation. +- Label templates are `private static final` constants rather than mutable instance fields. + +## Requirements + +- JDK 21 (the build sets `21`) +- Maven is **not** required — use the bundled Maven Wrapper (`./mvnw`, Maven 3.9.9) +- `curl`, HTTPie or Postman to call the API + +## Getting started + +```bash +git clone https://github.com/COG-GTM/springboot-java8.git +cd springboot-java8 + +# compile, run the test suite and package the jar +./mvnw clean verify + +# run the application (listens on http://localhost:8080) +./mvnw spring-boot:run ``` -GET /datetime + +You can also run the packaged jar directly: + +```bash +java -jar target/gs-spring-boot-0.1.0.jar ``` +> The Gradle build has been removed; Maven is the only supported build. + +Every push and pull request is built by GitHub Actions with `./mvnw -B clean verify` on JDK 21 +(see `.github/workflows/ci.yml`). + +### Expected startup noise +- On startup the app creates an in-memory **H2** `customers` table with `JdbcTemplate` and logs a few rows — this is + part of the demo, not an error. +- It also tries to fetch a random quote from `gturnquist-quoters.cfapps.io`, a demo service that no longer exists. + The resulting `WARN Could not fetch a quote from ...` is expected and must never fail startup. -### Prerequisites +## API -1) Java sdk -2) POSTMAN +All endpoints are served from `http://localhost:8080`. -### Installing +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/` | Greeting; accepts an optional `?name=` parameter (defaults to `World`) | +| `GET` | `/topic` | All topics | +| `GET` | `/topic/{id}` | A single topic by id | +| `POST` | `/topic` | Add a topic (JSON body) | +| `PUT` | `/topic/{id}` | Replace the topic with the given id | +| `DELETE` | `/topic/{id}` | Delete the topic with the given id | +| `GET` | `/topic/sort` | All topics sorted by id | +| `GET` | `/topic/minimum/length/{minLength}` | Topics whose id is longer than `minLength` | +| `GET` | `/topic/string/operation` | Plain-text dump of the string/stream/regex examples | +| `GET` | `/topic/file/operation` | Plain-text dump of the NIO.2 file examples | +| `GET` | `/datetime` | Plain-text dump of the `java.time` examples | +The topic list lives in memory, so `POST` / `PUT` / `DELETE` changes are lost when the application restarts. +### Examples +```bash +$ curl 'http://localhost:8080/?name=Devin' +{"id":1,"content":"Hello, Devin!"} + +$ curl http://localhost:8080/topic +[{"id":"spring","subjectName":"Spring Framework","subjectDescription":"Spring Framework Description"}, ...] + +$ curl -X POST http://localhost:8080/topic \ + -H 'Content-Type: application/json' \ + -d '{"id":"kotlin","subjectName":"Kotlin","subjectDescription":"Kotlin Description"}' ``` -1) Download or clone -2) Import the project -3) Run on location machine -4) Open Postman, to call API's ( localhost:8080 ) + +The `/topic/file/operation` endpoint resolves paths relative to the working directory the application was started +from, so its output depends on where you run it. + +## Project layout + +``` +src/main/java/hello +├── Application.java # entry point + CommandLineRunner (H2 seeding, quote fetch) +├── config/ # RestTemplate bean +├── controller/ # GreetingController, TopicController, HelloController +├── declaration/ # CustomPredicate, TimeClient (default/static interface methods) +├── model/ # Topic, Greeting, Customer, Quote, Value, SimpleTimeClient +└── service/ # TopicService — where most of the Java 8 examples live ``` +## Helpful links -## Helpful Links Spring: https://spring.io/guides -Java 8: +Java 8: http://www.baeldung.com/java-8-functional-interfaces -http://winterbe.com/posts/2015/05/22/java8-concurrency-tutorial-atomic-concurrent-map-examples/ - https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ -http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html - https://docs.oracle.com/javase/tutorial/essential/io/pathOps.html -https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html - -http://www.baeldung.com/foreach-java +Java 21: -http://winterbe.com/posts/2015/03/25/java8-examples-string-number-math-files/ +https://docs.oracle.com/en/java/javase/21/text-blocks/index.html -http://www.baeldung.com/java-8-comparator-comparing +https://docs.spring.io/spring-boot/docs/current/reference/html/ -http://www.baeldung.com/java-8-sort-lambda +## Built with -https://dzone.com/articles/java-8-friday-goodies-new-new - - -## Built With - -* [Maven](https://maven.apache.org/) - Dependency Management +* [Maven](https://maven.apache.org/) — build and dependency management +* [Spring Boot](https://spring.io/projects/spring-boot) 3.5.16 ## Authors -* **Rehman Murad Ali** - - +* **Rehman Murad Ali** — original author diff --git a/application.properties b/application.properties deleted file mode 100644 index d3baf26..0000000 --- a/application.properties +++ /dev/null @@ -1 +0,0 @@ -#server.port = 8081 \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 09f1083..0000000 --- a/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.2.RELEASE") - } -} - -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'idea' -apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' - -bootJar { - baseName = 'gs-spring-boot' - version = '0.1.0' -} - -repositories { - mavenCentral() -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -dependencies { - compile("org.springframework.boot:spring-boot-starter-web") - testCompile("junit:junit") -} - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ca78035..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a2ff3cc..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Mar 01 09:01:15 CST 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip diff --git a/gradlew b/gradlew deleted file mode 100644 index 27309d9..0000000 --- a/gradlew +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 832fdb6..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/gs-spring-boot.iml b/gs-spring-boot.iml deleted file mode 100644 index ad195a8..0000000 --- a/gs-spring-boot.iml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 index a1ba1bf..bd8896b --- a/mvnw +++ b/mvnw @@ -19,215 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi +} -fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # - # Look for the Apple JDKs first to preserve the existing behaviour, and then look - # for the new JDKs provided by Oracle. - # - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then - # - # Oracle JDKs - # - export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then - # - # Apple JDKs - # - export JAVA_HOME=`/usr/libexec/java_home` - fi - ;; -esac +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi +die() { + printf %s\\n "$1" >&2 + exit 1 +} -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" - saveddir=`pwd` +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - M2_HOME=`dirname "$PRG"`/.. +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} - cd "$saveddir" - # echo Using m2 at $M2_HOME +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - JAVACMD="`which java`" + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - local basedir=$(pwd) - local wdir=$(pwd) - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi fi - wdir=$(cd "$wdir/.."; pwd) done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + set -f +fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 2b934e8..5761d94 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,145 +1,189 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -set MAVEN_CMD_LINE_ARGS=%* - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% \ No newline at end of file +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 63f5cbd..5aa53f2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,21 +5,16 @@ org.springframework gs-spring-boot - pom + jar 0.1.0 org.springframework.boot spring-boot-starter-parent - 2.0.2.RELEASE + 3.5.16 - - org.springframework.boot - spring-boot-properties-migrator - runtime - org.springframework.boot spring-boot-starter-web @@ -32,10 +27,15 @@ com.h2database h2 + + org.springframework.boot + spring-boot-starter-test + test + - 1.8 + 21 diff --git a/src/main/java/hello/Application.java b/src/main/java/hello/Application.java index 7cf8faf..560a9bf 100644 --- a/src/main/java/hello/Application.java +++ b/src/main/java/hello/Application.java @@ -2,7 +2,7 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import java.util.stream.Stream; import hello.model.Customer; import hello.model.Quote; @@ -12,65 +12,51 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; @SpringBootApplication public class Application implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(Application.class); + private static final String QUOTE_URL = "http://gturnquist-quoters.cfapps.io/api/random"; + public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(Application.class, args); - + System.out.println("Let's inspect the beans provided by Spring Boot:"); - + String[] beanNames = ctx.getBeanDefinitionNames(); Arrays.sort(beanNames); for (String beanName : beanNames) { System.out.println(beanName); } - - RestTemplate restTemplate = new RestTemplate(); - Quote quote = restTemplate.getForObject("http://gturnquist-quoters.cfapps.io/api/random", Quote.class); - log.info(quote.toString()); - } - - - @Bean - public RestTemplate restTemplate(RestTemplateBuilder builder) { - return builder.build(); - } - - @Bean - public CommandLineRunner run(RestTemplate restTemplate) throws Exception { - return args -> { - Quote quote = restTemplate.getForObject( - "http://gturnquist-quoters.cfapps.io/api/random", Quote.class); - log.info(quote.toString()); - }; } @Autowired JdbcTemplate jdbcTemplate; + @Autowired + RestClient restClient; + @Override public void run(String... args) throws Exception { + logRandomQuote(); + log.info("Creating tables"); jdbcTemplate.execute("DROP TABLE customers IF EXISTS"); jdbcTemplate.execute("CREATE TABLE customers(id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))"); // Split up the array of whole names into an array of first/last names - List splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long") - .stream() - .map(name -> name.split(" ")) - .collect(Collectors.toList()); + List splitUpNames = Stream.of("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long") + .map(name -> name.split(" ")) + .toList(); // Use a Java 8 stream to print out each tuple of the list splitUpNames.forEach(name -> log.info(String.format("Inserting customer record for %s %s", name[0], name[1]))); @@ -80,9 +66,26 @@ public void run(String... args) throws Exception { log.info("Querying for customer records where first_name = 'Josh':"); jdbcTemplate.query( - "SELECT id, first_name, last_name FROM customers WHERE first_name = ?", new Object[]{"Josh"}, - (rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")) + "SELECT id, first_name, last_name FROM customers WHERE first_name = ?", + (rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")), + "Josh" ).forEach(customer -> log.info(customer.toString())); } + + /** + * The upstream demo quote service is not always reachable, so a failure here + * must never prevent the application from starting. + */ + private void logRandomQuote() { + try { + Quote quote = restClient.get() + .uri(QUOTE_URL) + .retrieve() + .body(Quote.class); + log.info(String.valueOf(quote)); + } catch (RestClientException e) { + log.warn("Could not fetch a quote from {}: {}", QUOTE_URL, e.getMessage()); + } + } } diff --git a/src/main/java/hello/config/RestClientConfig.java b/src/main/java/hello/config/RestClientConfig.java new file mode 100644 index 0000000..9cef05a --- /dev/null +++ b/src/main/java/hello/config/RestClientConfig.java @@ -0,0 +1,14 @@ +package hello.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(RestClient.Builder builder) { + return builder.build(); + } +} diff --git a/src/main/java/hello/controller/GreetingController.java b/src/main/java/hello/controller/GreetingController.java index cf52f8d..9f5cfbe 100644 --- a/src/main/java/hello/controller/GreetingController.java +++ b/src/main/java/hello/controller/GreetingController.java @@ -9,12 +9,11 @@ @RestController public class GreetingController { - private static final String template = "Hello, %s!"; + private static final String TEMPLATE = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @RequestMapping("/") public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { - return new Greeting(counter.incrementAndGet(), - String.format(template, name)); + return new Greeting(counter.incrementAndGet(), TEMPLATE.formatted(name)); } } diff --git a/src/main/java/hello/controller/HelloController.java b/src/main/java/hello/controller/HelloController.java index f3602fa..b599c6b 100644 --- a/src/main/java/hello/controller/HelloController.java +++ b/src/main/java/hello/controller/HelloController.java @@ -2,38 +2,37 @@ import hello.declaration.TimeClient; import hello.model.SimpleTimeClient; -import hello.model.Topic; import hello.service.TopicService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.ZoneId; -import java.time.chrono.ChronoPeriod; import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.regex.Pattern; -import java.util.stream.Collector; -import java.util.stream.Collectors; @RestController public class HelloController { - - String joinTemplate = "Joining All String ID's with JOIN method: "; - String makeDistinctAndSortCharactersTemplate = "-------------Get all ID characters, select distict and sort with ID= "; - String splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoinTemplate = "-------------Split All Id With Colon," + - "Select ID With \"Java\" Keyword," + - " Then Sort Then Join "; - String findIdHavingCharacterTemplate = "-------------Return All ID having character \'g\' in it: "; - String findAllFilesInPathAndSortTemplate = "---------Find all files in path and sort: "; - String findParticularFileInPathAndSortTemplate = "----------Find File in present directory which strats with \"grad\",provided maximum depth=25 and sort : "; - String findParticularFileInPathAndSortWithWalkFunctionTemplate = "----------Find File in present directory which strats with \"grad\",provided maximum depth=25 and sort : with walk function"; - String readFileWithStreamFunctionTemplate = "---------Read \"temp.txt\" file with stream functions, having \"print\" witin it: "; - + private static final String JOIN_TEMPLATE = "Joining All String ID's with JOIN method: "; + private static final String MAKE_DISTINCT_AND_SORT_CHARACTERS_TEMPLATE = "-------------Get all ID characters, select distict and sort with ID= "; + private static final String SPLIT_ALL_ID_WITH_COLON_SELECT_ID_WITH_JAVA_KEYWORD_THEN_SORT_THEN_JOIN_TEMPLATE = """ + -------------Split All Id With Colon,\ + Select ID With "Java" Keyword,\ + Then Sort Then Join\s"""; + private static final String FIND_ID_HAVING_CHARACTER_TEMPLATE = "-------------Return All ID having character 'g' in it: "; + private static final String FIND_ALL_FILES_IN_PATH_AND_SORT_TEMPLATE = "---------Find all files in path and sort: "; + private static final String FIND_PARTICULAR_FILE_IN_PATH_AND_SORT_TEMPLATE = "----------Find File in present directory which strats with \"grad\",provided maximum depth=25 and sort : "; + private static final String FIND_PARTICULAR_FILE_IN_PATH_AND_SORT_WITH_WALK_FUNCTION_TEMPLATE = "----------Find File in present directory which strats with \"grad\",provided maximum depth=25 and sort : with walk function"; + private static final String READ_FILE_WITH_STREAM_FUNCTION_TEMPLATE = "---------Read \"temp.txt\" file with stream functions, having \"print\" witin it: "; + + /** Responses are single line, hence the trailing line-continuation escapes. */ + private static final String LABELLED_SECTIONS_FORMAT = """ + %s%s\ + %s%s\ + %s%s\ + %s%s\ + """; @Autowired private TopicService topicService; @@ -46,18 +45,23 @@ public class HelloController { @RequestMapping("/datetime") public String index() { TimeClient myTimeClient = new SimpleTimeClient(); - LocalDateTime localDateTime = LocalDateTime.now(); - return "Greetings from Spring Boot! ----------------------" + - "Datetime now is " + String.valueOf(myTimeClient.toString()) + "----------------------" + - "Datetime tomorrow will be " + String.valueOf(myTimeClient.getLocalDateTime().plusDays(1)) + "----------------------" + - "Datetime of previous month was " + String.valueOf(myTimeClient.getLocalDateTime().minus(1, ChronoUnit.MONTHS)) + "----------------------" + - "Is this a leap year ? " + String.valueOf(LocalDate.now().isLeapYear()) + "----------------------" + - "Default system zone id " + String.valueOf(ZoneId.systemDefault()) + "-------------------" + - "Time in California: " + myTimeClient.getZonedDateTime("Canada/Central").toString(); - + return """ + Greetings from Spring Boot! ----------------------\ + Datetime now is %s----------------------\ + Datetime tomorrow will be %s----------------------\ + Datetime of previous month was %s----------------------\ + Is this a leap year ? %s----------------------\ + Default system zone id %s-------------------\ + Time in California: %s\ + """.formatted( + myTimeClient, + myTimeClient.getLocalDateTime().plusDays(1), + myTimeClient.getLocalDateTime().minus(1, ChronoUnit.MONTHS), + LocalDate.now().isLeapYear(), + ZoneId.systemDefault(), + myTimeClient.getZonedDateTime("Canada/Central")); } - /** * String Operations in Java 8 * @@ -72,16 +76,16 @@ public String showStringOperation() { .splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin(join); String findIdHavingCharacter = topicService.findIdHavingCharacter(); - return joinTemplate + join - + makeDistinctAndSortCharactersTemplate + makeDistinctAndSortCharacters - + splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoinTemplate + splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin - + findIdHavingCharacterTemplate + findIdHavingCharacter; - + return LABELLED_SECTIONS_FORMAT.formatted( + JOIN_TEMPLATE, join, + MAKE_DISTINCT_AND_SORT_CHARACTERS_TEMPLATE, makeDistinctAndSortCharacters, + SPLIT_ALL_ID_WITH_COLON_SELECT_ID_WITH_JAVA_KEYWORD_THEN_SORT_THEN_JOIN_TEMPLATE, splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin, + FIND_ID_HAVING_CHARACTER_TEMPLATE, findIdHavingCharacter); } - /** * File Operation in Java 8 + * * @return */ @RequestMapping("/topic/file/operation") @@ -90,12 +94,12 @@ public String showFileOperation() { String findParticularFileInPathAndSort = topicService.findParticularFileInPathAndSort(); String findParticularFileInPathAndSortWithWalkFunction = topicService.findParticularFileInPathAndSortWithWalkFunction(); String readFileWithStreamFunction = topicService.readFileWithStreamFunction(); - return findAllFilesInPathAndSortTemplate + findAllFilesInPathAndSort - + findParticularFileInPathAndSortTemplate + findParticularFileInPathAndSort - + findParticularFileInPathAndSortWithWalkFunctionTemplate + findParticularFileInPathAndSortWithWalkFunction - + readFileWithStreamFunctionTemplate + readFileWithStreamFunction; - } - + return LABELLED_SECTIONS_FORMAT.formatted( + FIND_ALL_FILES_IN_PATH_AND_SORT_TEMPLATE, findAllFilesInPathAndSort, + FIND_PARTICULAR_FILE_IN_PATH_AND_SORT_TEMPLATE, findParticularFileInPathAndSort, + FIND_PARTICULAR_FILE_IN_PATH_AND_SORT_WITH_WALK_FUNCTION_TEMPLATE, findParticularFileInPathAndSortWithWalkFunction, + READ_FILE_WITH_STREAM_FUNCTION_TEMPLATE, readFileWithStreamFunction); + } } diff --git a/src/main/java/hello/controller/TopicController.java b/src/main/java/hello/controller/TopicController.java index c2e8273..c4e322c 100644 --- a/src/main/java/hello/controller/TopicController.java +++ b/src/main/java/hello/controller/TopicController.java @@ -3,6 +3,7 @@ import hello.model.Topic; import hello.service.TopicService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -29,8 +30,10 @@ public List getAllTopics() { * @return */ @RequestMapping("/topic/{id}") - public Topic getTopicWithID(@PathVariable String id) { - return topicService.getTopicWithId(id); + public ResponseEntity getTopicWithID(@PathVariable String id) { + return topicService.getTopicWithId(id) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); } /** diff --git a/src/main/java/hello/declaration/CustomPredicate.java b/src/main/java/hello/declaration/CustomPredicate.java index 46f0c44..af828ac 100644 --- a/src/main/java/hello/declaration/CustomPredicate.java +++ b/src/main/java/hello/declaration/CustomPredicate.java @@ -4,6 +4,7 @@ * Use as a Functional Interface * @param */ +@FunctionalInterface public interface CustomPredicate { boolean test(T t); } diff --git a/src/main/java/hello/model/Customer.java b/src/main/java/hello/model/Customer.java index a6df80e..640db05 100644 --- a/src/main/java/hello/model/Customer.java +++ b/src/main/java/hello/model/Customer.java @@ -1,45 +1,4 @@ package hello.model; -public class Customer { - private long id; - private String firstName, lastName; - - @Override - public String toString() { - return "Customer{" + - "id=" + id + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - '}'; - } - - public Customer(long id, String firstName, String lastName) { - this.id = id; - this.firstName = firstName; - this.lastName = lastName; - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } +public record Customer(long id, String firstName, String lastName) { } diff --git a/src/main/java/hello/model/Greeting.java b/src/main/java/hello/model/Greeting.java index f2e7772..135b2fb 100644 --- a/src/main/java/hello/model/Greeting.java +++ b/src/main/java/hello/model/Greeting.java @@ -1,21 +1,4 @@ package hello.model; -public class Greeting { - private final long id; - private final String content; - - public Greeting(long id, String content) { - this.id = id; - this.content = content; - } - - - public long getId() { - - return id; - } - - public String getContent() { - return content; - } +public record Greeting(long id, String content) { } diff --git a/src/main/java/hello/model/Quote.java b/src/main/java/hello/model/Quote.java index 2178a6d..b548a67 100644 --- a/src/main/java/hello/model/Quote.java +++ b/src/main/java/hello/model/Quote.java @@ -1,34 +1,4 @@ package hello.model; -public class Quote { - private String type; - private Value value; - - @Override - public String toString() { - return "Quote{" + - "type='" + type + '\'' + - ", value=" + value + - '}'; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Value getValue() { - return value; - } - - public void setValue(Value value) { - this.value = value; - } - - public Quote() { - - } +public record Quote(String type, Value value) { } diff --git a/src/main/java/hello/model/Topic.java b/src/main/java/hello/model/Topic.java index 483a4a7..59740dc 100644 --- a/src/main/java/hello/model/Topic.java +++ b/src/main/java/hello/model/Topic.java @@ -1,43 +1,4 @@ package hello.model; - -public class Topic { - private String id; - private String subjectName; - private String subjectDescription; - - public Topic() { - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getSubjectName() { - return subjectName; - } - - public void setSubjectName(String subjectName) { - this.subjectName = subjectName; - } - - public String getSubjectDescription() { - return subjectDescription; - } - - public void setSubjectDescription(String subjectDescription) { - this.subjectDescription = subjectDescription; - } - - public Topic(String id, String subjectName, String subjectDescription) { - super(); - this.id = id; - this.subjectName = subjectName; - this.subjectDescription = subjectDescription; - } - +public record Topic(String id, String subjectName, String subjectDescription) { } diff --git a/src/main/java/hello/model/Value.java b/src/main/java/hello/model/Value.java index 272be70..b795170 100644 --- a/src/main/java/hello/model/Value.java +++ b/src/main/java/hello/model/Value.java @@ -1,33 +1,4 @@ package hello.model; -public class Value { - private Long id; - private String quote; - - public Value() { - } - - @Override - public String toString() { - return "Value{" + - "id=" + id + - ", quote='" + quote + '\'' + - '}'; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getQuote() { - return quote; - } - - public void setQuote(String quote) { - this.quote = quote; - } +public record Value(Long id, String quote) { } diff --git a/src/main/java/hello/service/TopicService.java b/src/main/java/hello/service/TopicService.java index feffb92..d0a5706 100644 --- a/src/main/java/hello/service/TopicService.java +++ b/src/main/java/hello/service/TopicService.java @@ -35,8 +35,8 @@ public List getAllTopics() { * @param id * @return */ - public Topic getTopicWithId(String id) { - return topics.stream().filter(topic -> topic.getId().equals(id)).findFirst().get(); + public Optional getTopicWithId(String id) { + return topics.stream().filter(topic -> topic.id().equals(id)).findFirst(); } public void addTopic(Topic topic) { @@ -50,7 +50,7 @@ public void addTopic(Topic topic) { * @param topic */ public void updateTopic(String id, Topic topic) { - OptionalInt indexOfElement = IntStream.range(0, topics.size()).filter(index -> id.equals(topics.get(index).getId())).findFirst(); + OptionalInt indexOfElement = IntStream.range(0, topics.size()).filter(index -> id.equals(topics.get(index).id())).findFirst(); if (indexOfElement.isPresent()) topics.set(indexOfElement.getAsInt(), topic); } @@ -60,7 +60,7 @@ public void updateTopic(String id, Topic topic) { * @param id */ public void deleteTopic(String id) { - topics.removeIf(topic -> topic.getId().equals(id)); + topics.removeIf(topic -> topic.id().equals(id)); } /** @@ -70,7 +70,7 @@ public void deleteTopic(String id) { * @return */ public List filterMinimumLengthForId(Integer minLength) { - return printTopicsWithPredicate(topics, topic -> topic.getId().length() > minLength); + return printTopicsWithPredicate(topics, topic -> topic.id().length() > minLength); } @@ -96,7 +96,7 @@ private static List printTopicsWithPredicate(List topicList, Custo * @return */ public List sortTopicsWithID() { - topics.sort(Comparator.comparing(Topic::getId)); + topics.sort(Comparator.comparing(Topic::id)); return topics; } @@ -107,7 +107,7 @@ public List sortTopicsWithID() { * @return */ public String returnAllTopicIDWithStringSlicing() { - List topicIds = topics.stream().map(topic -> topic.getId()).collect(Collectors.toList()); + List topicIds = topics.stream().map(Topic::id).toList(); return String.join(":", topicIds); } @@ -147,13 +147,11 @@ public String splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin(String */ public String findIdHavingCharacter() { Pattern pattern = Pattern.compile(".*g.*"); - Object[] topicIdObjectList = topics.stream().map(topic -> topic.getId()).collect(Collectors.toList()).toArray(); - - String[] topicIdList = Arrays.stream(topicIdObjectList).toArray(String[]::new); + String[] topicIdList = topics.stream().map(Topic::id).toArray(String[]::new); return Stream.of(topicIdList) .filter(pattern.asPredicate()) - .collect(Collectors.toList()) + .toList() .toString(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..c4a0209 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +# Serve each request on a virtual thread (Java 21 + Spring Boot 3.2+). +spring.threads.virtual.enabled=true + +#server.port = 8081 diff --git a/src/test/java/hello/ApplicationContextLoadTest.java b/src/test/java/hello/ApplicationContextLoadTest.java new file mode 100644 index 0000000..d52b7dd --- /dev/null +++ b/src/test/java/hello/ApplicationContextLoadTest.java @@ -0,0 +1,49 @@ +package hello; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.client.RestClient; + +import hello.controller.GreetingController; +import hello.controller.HelloController; +import hello.controller.TopicController; +import hello.service.TopicService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Starts the whole application context, including the {@link org.springframework.boot.CommandLineRunner} + * on {@link Application}. The runner contacts a demo quote service that no longer exists; the resulting + * WARN must not fail startup, so reaching the assertions below is itself part of what is being verified. + */ +@SpringBootTest +class ApplicationContextLoadTest { + + @Autowired + private ApplicationContext context; + + @Test + void contextLoads() { + assertThat(context).isNotNull(); + } + + @Test + void exposesTheExpectedBeans() { + assertThat(context.getBean(TopicService.class)).isNotNull(); + assertThat(context.getBean(TopicController.class)).isNotNull(); + assertThat(context.getBean(HelloController.class)).isNotNull(); + assertThat(context.getBean(GreetingController.class)).isNotNull(); + assertThat(context.getBean(RestClient.class)).isNotNull(); + assertThat(context.getBean(JdbcTemplate.class)).isNotNull(); + } + + @Test + void commandLineRunnerPopulatedTheCustomersTable() { + Integer count = context.getBean(JdbcTemplate.class) + .queryForObject("SELECT COUNT(*) FROM customers", Integer.class); + assertThat(count).isEqualTo(4); + } +} diff --git a/src/test/java/hello/controller/GreetingControllerWebTest.java b/src/test/java/hello/controller/GreetingControllerWebTest.java new file mode 100644 index 0000000..ad8fd25 --- /dev/null +++ b/src/test/java/hello/controller/GreetingControllerWebTest.java @@ -0,0 +1,61 @@ +package hello.controller; + +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Pins the JSON contract of {@code /}: {@code {"id": , "content": }}. + */ +@SpringBootTest +@AutoConfigureMockMvc +class GreetingControllerWebTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void greetsTheWorldByDefault() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.*", hasSize(2))) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.content").value("Hello, World!")); + } + + @Test + void greetsTheRequestedName() throws Exception { + mockMvc.perform(get("/").param("name", "Devin")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("Hello, Devin!")); + } + + @Test + void everyGreetingGetsItsOwnId() throws Exception { + long first = greetingId(); + long second = greetingId(); + + assertThat(second).isEqualTo(first + 1); + } + + private long greetingId() throws Exception { + String json = mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + return JsonPath.parse(json).read("$.id", Long.class); + } +} diff --git a/src/test/java/hello/controller/HelloControllerWebTest.java b/src/test/java/hello/controller/HelloControllerWebTest.java new file mode 100644 index 0000000..e99f741 --- /dev/null +++ b/src/test/java/hello/controller/HelloControllerWebTest.java @@ -0,0 +1,63 @@ +package hello.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Covers the plain-text endpoints. {@code /topic/file/operation} walks the process working directory + * and reads {@code temp.txt} from it, so its exact output changes whenever a file is added to or + * removed from the repository; only the stable parts of the response are asserted. + */ +@SpringBootTest +@AutoConfigureMockMvc +class HelloControllerWebTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void datetimeEndpointRendersTheJavaTimeShowcase() throws Exception { + mockMvc.perform(get("/datetime")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Greetings from Spring Boot!"))) + .andExpect(content().string(containsString("Datetime now is "))) + .andExpect(content().string(containsString("Is this a leap year ?"))) + .andExpect(content().string(containsString("Default system zone id"))) + .andExpect(content().string(containsString("Time in California: "))) + // "Datetime now is " + .andExpect(content().string(matchesRegex("(?s).*Datetime now is \\d{4}-\\d{2}-\\d{2}T[\\d:.]+.*"))); + } + + @Test + void stringOperationEndpointRendersTheStreamShowcase() throws Exception { + mockMvc.perform(get("/topic/string/operation")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Joining All String ID's with JOIN method: "))) + // The distinct-and-sorted characters and the ":"-split are both sorted, so they are + // stable even if another test has re-ordered the singleton's topic list. + .andExpect(content().string(containsString(":acgijnprstv"))) + .andExpect(content().string(containsString("java:javascript"))) + .andExpect(content().string(containsString("[spring]"))); + } + + @Test + void fileOperationEndpointRendersTheNioShowcase() throws Exception { + mockMvc.perform(get("/topic/file/operation")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Find all files in path and sort:"))) + .andExpect(content().string(containsString("pom.xml"))) + .andExpect(content().string(containsString("Read \"temp.txt\" file with stream functions"))) + .andExpect(content().string(not(containsString("Error in IO")))) + .andExpect(content().string(not(containsString("IO exception")))); + } +} diff --git a/src/test/java/hello/controller/TopicControllerWebTest.java b/src/test/java/hello/controller/TopicControllerWebTest.java new file mode 100644 index 0000000..05147be --- /dev/null +++ b/src/test/java/hello/controller/TopicControllerWebTest.java @@ -0,0 +1,125 @@ +package hello.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.MockMvc; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * End-to-end coverage of the JSON contract of the topic endpoints. + * + *

{@code TopicService} is a singleton holding a mutable list, so anything that writes to it leaks + * into the next test. Rather than depending on test order, the two tests that mutate the list + * ({@code /topic} writes and the in-place sort performed by {@code /topic/sort}) drop the context + * afterwards with {@link DirtiesContext}; every other test can then assume the seeded state. + * + *

Assertions go through {@code jsonPath} rather than through model getters on purpose: the JSON + * field names are the actual contract and they survive the ongoing conversion of the models to records. + */ +@SpringBootTest +@AutoConfigureMockMvc +class TopicControllerWebTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void getAllTopicsReturnsTheSeededTopics() throws Exception { + mockMvc.perform(get("/topic")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[*].id", containsInAnyOrder("spring", "java", "javascript"))) + .andExpect(jsonPath("$[?(@.id=='spring')].subjectName", contains("Spring Framework"))) + .andExpect(jsonPath("$[?(@.id=='spring')].subjectDescription", contains("Spring Framework Description"))); + } + + @Test + void aTopicSerialisesWithExactlyIdSubjectNameAndSubjectDescription() throws Exception { + mockMvc.perform(get("/topic/java")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.*", hasSize(3))) + .andExpect(jsonPath("$.id").value("java")) + .andExpect(jsonPath("$.subjectName").value("Core Java")) + .andExpect(jsonPath("$.subjectDescription").value("Java Description")); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void supportsTheFullCreateReadUpdateDeleteLifecycle() throws Exception { + mockMvc.perform(post("/topic") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"id":"kotlin","subjectName":"Kotlin","subjectDescription":"Kotlin Description"}""")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/topic")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(4))) + .andExpect(jsonPath("$[*].id", containsInAnyOrder("spring", "java", "javascript", "kotlin"))); + + mockMvc.perform(get("/topic/kotlin")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.subjectName").value("Kotlin")) + .andExpect(jsonPath("$.subjectDescription").value("Kotlin Description")); + + mockMvc.perform(put("/topic/kotlin") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"id":"kotlin","subjectName":"Kotlin Updated","subjectDescription":"Updated Description"}""")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/topic/kotlin")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.subjectName").value("Kotlin Updated")) + .andExpect(jsonPath("$.subjectDescription").value("Updated Description")); + + mockMvc.perform(delete("/topic/kotlin")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/topic")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[*].id", not(hasItem("kotlin")))); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void sortEndpointReturnsTopicsOrderedById() throws Exception { + mockMvc.perform(get("/topic/sort")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[*].id", contains("java", "javascript", "spring"))); + } + + @Test + void minimumLengthEndpointFiltersOnIdLength() throws Exception { + mockMvc.perform(get("/topic/minimum/length/4")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].id", containsInAnyOrder("spring", "javascript"))); + + mockMvc.perform(get("/topic/minimum/length/6")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].id", contains("javascript"))); + + mockMvc.perform(get("/topic/minimum/length/20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(0))); + } +} diff --git a/src/test/java/hello/controller/TopicNotFoundStatusTest.java b/src/test/java/hello/controller/TopicNotFoundStatusTest.java new file mode 100644 index 0000000..86e888b --- /dev/null +++ b/src/test/java/hello/controller/TopicNotFoundStatusTest.java @@ -0,0 +1,43 @@ +package hello.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Requesting an unknown topic id is the one place where the HTTP status carries real information, and + * MockMvc rethrows unhandled exceptions instead of running the error dispatch, so this one case is + * driven over a real connection. + * + *

{@code TopicService.getTopicWithId} returns an empty {@link java.util.Optional} on a miss, which + * the controller translates into a 404. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class TopicNotFoundStatusTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void unknownTopicIdReturnsNotFound() { + ResponseEntity response = restTemplate.getForEntity("/topic/does-not-exist", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void knownTopicIdReturnsOkWithTheUnchangedJsonShape() { + ResponseEntity response = restTemplate.getForEntity("/topic/spring", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()) + .contains("\"id\":\"spring\"") + .contains("\"subjectName\":\"Spring Framework\"") + .contains("\"subjectDescription\":\"Spring Framework Description\""); + } +} diff --git a/src/test/java/hello/declaration/TimeClientTest.java b/src/test/java/hello/declaration/TimeClientTest.java new file mode 100644 index 0000000..3778f26 --- /dev/null +++ b/src/test/java/hello/declaration/TimeClientTest.java @@ -0,0 +1,80 @@ +package hello.declaration; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +import hello.model.SimpleTimeClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Covers the {@code static} and {@code default} methods on the interface and the + * {@link SimpleTimeClient} implementation of them. + */ +class TimeClientTest { + + @Test + void getZoneIdResolvesAKnownZone() { + assertThat(TimeClient.getZoneId("Canada/Central")).isEqualTo(ZoneId.of("Canada/Central")); + } + + @Test + void getZoneIdFallsBackToTheSystemZoneForAnInvalidZone() { + assertThat(TimeClient.getZoneId("Not/A/Zone")).isEqualTo(ZoneId.systemDefault()); + assertThat(TimeClient.getZoneId("")).isEqualTo(ZoneId.systemDefault()); + } + + @Test + void getZonedDateTimeCombinesTheLocalDateTimeWithTheRequestedZone() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2020, 5, 17, 13, 45, 30); + + ZonedDateTime zoned = client.getZonedDateTime("Canada/Central"); + + assertThat(zoned.toLocalDateTime()).isEqualTo(LocalDateTime.of(2020, 5, 17, 13, 45, 30)); + assertThat(zoned.getZone()).isEqualTo(ZoneId.of("Canada/Central")); + } + + @Test + void getZonedDateTimeUsesTheSystemZoneWhenTheZoneIsInvalid() { + SimpleTimeClient client = new SimpleTimeClient(); + + assertThat(client.getZonedDateTime("Middle/Earth").getZone()).isEqualTo(ZoneId.systemDefault()); + } + + @Test + void newClientIsInitialisedWithTheCurrentDateTime() { + LocalDateTime before = LocalDateTime.now().minusMinutes(1); + + SimpleTimeClient client = new SimpleTimeClient(); + + assertThat(client.getLocalDateTime()).isAfter(before); + assertThat(client.toString()).isEqualTo(client.getLocalDateTime().toString()); + } + + @Test + void setTimeKeepsTheDateAndReplacesTheTime() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2020, 5, 17, 1, 2, 3); + + client.setTime(23, 59, 58); + + assertThat(client.getLocalDateTime()).isEqualTo(LocalDateTime.of(2020, 5, 17, 23, 59, 58)); + } + + @Test + void setDateKeepsTheTimeAndReplacesTheDate() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2020, 5, 17, 8, 30, 0); + + // setDate/setDateAndTime declare their parameters as (day, month, year) but delegate to + // LocalDate.of(..) positionally, so the first argument is really the year. Asserted as + // implemented rather than as named. + client.setDate(2021, 12, 25); + + assertThat(client.getLocalDateTime()).isEqualTo(LocalDateTime.of(2021, 12, 25, 8, 30, 0)); + } +} diff --git a/src/test/java/hello/service/TopicServiceTest.java b/src/test/java/hello/service/TopicServiceTest.java new file mode 100644 index 0000000..3bae0f7 --- /dev/null +++ b/src/test/java/hello/service/TopicServiceTest.java @@ -0,0 +1,181 @@ +package hello.service; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import hello.model.Topic; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * {@link TopicService} keeps its topics in a mutable instance field, so as a Spring singleton its + * state leaks between tests. These unit tests therefore build a brand new service in {@link BeforeEach} + * instead of injecting the bean: every test starts from the three seeded topics and nothing has to be + * undone afterwards. + */ +class TopicServiceTest { + + private TopicService topicService; + + @BeforeEach + void createFreshService() { + topicService = new TopicService(); + } + + /** + * Single place where a {@link Topic} accessor is used, so the parallel conversion of the models to + * records only has to touch one line in this class. + */ + private static List idsOf(List topics) { + return topics.stream().map(Topic::id).collect(Collectors.toList()); + } + + @Test + void seedsThreeTopics() { + assertThat(idsOf(topicService.getAllTopics())) + .containsExactly("spring", "java", "javascript"); + } + + @Test + void getTopicWithIdReturnsTheMatchingTopic() { + Topic topic = topicService.getTopicWithId("java").orElseThrow(); + + assertThat(idsOf(List.of(topic))).containsExactly("java"); + assertThat(topic.subjectName()).isEqualTo("Core Java"); + } + + @Test + void getTopicWithIdReportsAMissingTopicWithoutThrowing() { + assertThatCode(() -> topicService.getTopicWithId("does-not-exist")).doesNotThrowAnyException(); + assertThat(topicService.getTopicWithId("does-not-exist")).isEmpty(); + } + + @Test + void addTopicAppendsToTheList() { + topicService.addTopic(new Topic("kotlin", "Kotlin", "Kotlin Description")); + + assertThat(idsOf(topicService.getAllTopics())).containsExactly("spring", "java", "javascript", "kotlin"); + } + + @Test + void updateTopicReplacesTheTopicInPlace() { + topicService.updateTopic("java", new Topic("java", "Updated Java", "Updated Description")); + + List topics = topicService.getAllTopics(); + assertThat(idsOf(topics)).containsExactly("spring", "java", "javascript"); + assertThat(topics.get(1).subjectName()).isEqualTo("Updated Java"); + } + + @Test + void updateTopicIsANoOpForAnUnknownId() { + topicService.updateTopic("kotlin", new Topic("kotlin", "Kotlin", "Kotlin Description")); + + assertThat(idsOf(topicService.getAllTopics())).containsExactly("spring", "java", "javascript"); + } + + @Test + void deleteTopicRemovesOnlyTheMatchingTopic() { + topicService.deleteTopic("java"); + + assertThat(idsOf(topicService.getAllTopics())).containsExactly("spring", "javascript"); + } + + @Test + void deleteTopicIsANoOpForAnUnknownId() { + topicService.deleteTopic("kotlin"); + + assertThat(idsOf(topicService.getAllTopics())).hasSize(3); + } + + @Test + void filterMinimumLengthForIdKeepsIdsStrictlyLongerThanTheGivenLength() { + assertThat(idsOf(topicService.filterMinimumLengthForId(4))) + .containsExactly("spring", "javascript"); + assertThat(idsOf(topicService.filterMinimumLengthForId(6))) + .containsExactly("javascript"); + assertThat(topicService.filterMinimumLengthForId(20)).isEmpty(); + } + + @Test + void sortTopicsWithIDSortsTheUnderlyingListById() { + assertThat(idsOf(topicService.sortTopicsWithID())) + .containsExactly("java", "javascript", "spring"); + + // The sort mutates the service's own list, which is exactly why these tests use a fresh instance. + assertThat(idsOf(topicService.getAllTopics())) + .containsExactly("java", "javascript", "spring"); + } + + @Test + void returnAllTopicIDWithStringSlicingJoinsIdsWithAColon() { + assertThat(topicService.returnAllTopicIDWithStringSlicing()).isEqualTo("spring:java:javascript"); + } + + @Test + void makeDistinctAndSortCharactersReturnsSortedDistinctCharacters() { + assertThat(topicService.makeDistinctAndSortCharacters("spring:java:javascript")) + .isEqualTo(":acgijnprstv"); + assertThat(topicService.makeDistinctAndSortCharacters("")).isEmpty(); + } + + @Test + void splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoinKeepsOnlyJavaIds() { + assertThat(topicService.splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin("spring:java:javascript")) + .isEqualTo("java:javascript"); + assertThat(topicService.splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin("spring:kotlin")) + .isEmpty(); + } + + @Test + void findIdHavingCharacterMatchesIdsContainingG() { + assertThat(topicService.findIdHavingCharacter()).isEqualTo("[spring]"); + } + + // The file operations below walk the process working directory (the module base directory when run + // by Surefire) and read temp.txt from it. Their exact output changes whenever a file is added or + // removed from the repository, so they are asserted loosely on purpose. + + @Test + void findAllFilesInPathAndSortListsVisibleFilesInTheWorkingDirectory() { + String listed = topicService.findAllFilesInPathAndSort(); + + assertThat(listed).isNotNull().isNotEqualTo(" Error in IO"); + assertThat(listed).contains("pom.xml"); + assertThat(listed).doesNotContain(".git;"); + } + + @Test + void findParticularFileInPathAndSortReturnsAStringWithoutFailing() { + assertThat(topicService.findParticularFileInPathAndSort()) + .isNotNull() + .isNotEqualTo(" IO exception "); + } + + @Test + void findParticularFileInPathAndSortWithWalkFunctionReturnsAStringWithoutFailing() { + assertThat(topicService.findParticularFileInPathAndSortWithWalkFunction()) + .isNotNull() + .isNotEqualTo(" IO exception "); + } + + @Test + void readFileWithStreamFunctionReturnsTheLinesStartingWithPrint() { + Assumptions.assumeTrue(Files.exists(Paths.get("temp.txt")), "temp.txt is read relative to the working directory"); + + String read = topicService.readFileWithStreamFunction(); + + assertThat(read).isNotNull().isNotEqualTo(" IO exception "); + assertThat(read).contains(" Hello"); + assertThat(read).doesNotContain("print"); + } +} diff --git a/target/classes/hello/Application.class b/target/classes/hello/Application.class deleted file mode 100644 index 245a020..0000000 Binary files a/target/classes/hello/Application.class and /dev/null differ diff --git a/target/classes/hello/controller/GreetingController.class b/target/classes/hello/controller/GreetingController.class deleted file mode 100644 index 1149660..0000000 Binary files a/target/classes/hello/controller/GreetingController.class and /dev/null differ diff --git a/target/classes/hello/controller/HelloController.class b/target/classes/hello/controller/HelloController.class deleted file mode 100644 index 79b82ce..0000000 Binary files a/target/classes/hello/controller/HelloController.class and /dev/null differ diff --git a/target/classes/hello/controller/TopicController.class b/target/classes/hello/controller/TopicController.class deleted file mode 100644 index c32cad5..0000000 Binary files a/target/classes/hello/controller/TopicController.class and /dev/null differ diff --git a/target/classes/hello/declaration/CustomPredicate.class b/target/classes/hello/declaration/CustomPredicate.class deleted file mode 100644 index e35803f..0000000 Binary files a/target/classes/hello/declaration/CustomPredicate.class and /dev/null differ diff --git a/target/classes/hello/declaration/TimeClient.class b/target/classes/hello/declaration/TimeClient.class deleted file mode 100644 index a077c3b..0000000 Binary files a/target/classes/hello/declaration/TimeClient.class and /dev/null differ diff --git a/target/classes/hello/model/Customer.class b/target/classes/hello/model/Customer.class deleted file mode 100644 index 06e7307..0000000 Binary files a/target/classes/hello/model/Customer.class and /dev/null differ diff --git a/target/classes/hello/model/Greeting.class b/target/classes/hello/model/Greeting.class deleted file mode 100644 index f875277..0000000 Binary files a/target/classes/hello/model/Greeting.class and /dev/null differ diff --git a/target/classes/hello/model/Quote.class b/target/classes/hello/model/Quote.class deleted file mode 100644 index 3ec6c6c..0000000 Binary files a/target/classes/hello/model/Quote.class and /dev/null differ diff --git a/target/classes/hello/model/SimpleTimeClient.class b/target/classes/hello/model/SimpleTimeClient.class deleted file mode 100644 index f9bdddd..0000000 Binary files a/target/classes/hello/model/SimpleTimeClient.class and /dev/null differ diff --git a/target/classes/hello/model/Topic.class b/target/classes/hello/model/Topic.class deleted file mode 100644 index 3a53aa5..0000000 Binary files a/target/classes/hello/model/Topic.class and /dev/null differ diff --git a/target/classes/hello/model/Value.class b/target/classes/hello/model/Value.class deleted file mode 100644 index 862a68f..0000000 Binary files a/target/classes/hello/model/Value.class and /dev/null differ diff --git a/target/classes/hello/service/TopicService.class b/target/classes/hello/service/TopicService.class deleted file mode 100644 index b9f4992..0000000 Binary files a/target/classes/hello/service/TopicService.class and /dev/null differ diff --git a/target/classes/public/index.html b/target/classes/public/index.html deleted file mode 100644 index 566549b..0000000 --- a/target/classes/public/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - \ No newline at end of file