Repository: phauer/blog-related Branch: master Commit: 8a63126e0845 Files: 363 Total size: 505.3 KB Directory structure: gitextract_2r1gvc7i/ ├── README.md ├── cairosvg-on-alpine/ │ ├── .gitignore │ ├── Pipfile │ ├── README.md │ ├── docker-compose.yml │ └── src/ │ ├── Dockerfile │ └── svg-converter-service.py ├── cleaner-code-with-kotlin/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ └── main/ │ └── kotlin/ │ └── functions/ │ ├── BeAware.kt │ ├── Expressions.kt │ ├── Immutability.kt │ ├── Nullability.kt │ ├── ProductClient.java │ └── ProductClientKotlin.kt ├── compare-payloads/ │ ├── .gitignore │ ├── README.md │ ├── compare-scripts/ │ │ ├── compare-all-final.sh │ │ ├── compare-by-sorting.sh │ │ ├── compare-json-payload.sh │ │ └── compare-xml-payload.sh │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ ├── BlogPost.java │ │ ├── BlogPost2.java │ │ └── ComparePayloadApplication.java │ └── resources/ │ └── application.properties ├── continuation-token/ │ ├── .gitignore │ ├── README.md │ ├── continuation-token/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ └── pagination/ │ │ │ ├── ContinuationTokenParser.kt │ │ │ ├── Model.kt │ │ │ └── Pagination.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ └── pagination/ │ │ ├── ContinuationTokenParserTest.kt │ │ └── PaginationTest.kt │ └── demo-kotlin/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ ├── DesignDAO.kt │ │ │ ├── DesignEntity.kt │ │ │ ├── DesignResource.kt │ │ │ ├── Main.kt │ │ │ └── util/ │ │ │ ├── DesignCreator.kt │ │ │ └── FunctionsMySQL.kt │ │ └── resources/ │ │ └── create-designs-table.sql │ └── test/ │ └── kotlin/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── DesignResourceTest.kt ├── development-productivity-vaadin-spring-boot/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── Makefile │ ├── README.md │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── springloaded-1.2.9.BUILD-20170831.190856-1.jar │ └── src/ │ └── main/ │ ├── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ └── devproductivity/ │ │ ├── DevProductivityApplication.java │ │ ├── WebSecurityConfiguration.java │ │ ├── model/ │ │ │ ├── Role.java │ │ │ └── User.java │ │ ├── rest/ │ │ │ └── AdminResource.java │ │ └── ui/ │ │ └── MyAppUI.java │ ├── resources/ │ │ └── application.properties │ └── webapp/ │ └── VAADIN/ │ └── themes/ │ └── mytheme/ │ ├── addons.scss │ ├── mytheme.scss │ └── styles.scss ├── dont-use-in-memory-databases-tests/ │ ├── db-container-managed-by-gradle/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── docker-compose.yml │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle │ │ └── src/ │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ └── MyTest.java │ ├── db-container-managed-by-maven/ │ │ ├── .gitignore │ │ ├── docker-compose.yml │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ └── MyIT.java │ └── db-container-managed-by-the-test/ │ ├── .gitignore │ ├── build.gradle │ ├── docker-compose.yml │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src/ │ └── test/ │ └── java/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── MyTest.java ├── framework-beats-generator/ │ ├── .gitignore │ ├── pom.xml │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── philipphauer/ │ │ │ │ ├── h2/ │ │ │ │ │ └── H2WebConsole.java │ │ │ │ ├── jpa/ │ │ │ │ │ ├── Article.java │ │ │ │ │ └── ArticleDAO.java │ │ │ │ └── mongojack/ │ │ │ │ ├── Product.java │ │ │ │ └── ProductDAO.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── persistence.xml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ ├── jpa/ │ │ │ ├── ArticleDAOTest.java │ │ │ └── H2Test.java │ │ └── mongojack/ │ │ └── ProductDAOTest.java │ └── startMongoDBLocally.bat ├── kotlin-examples/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ ├── javaVariant/ │ │ ├── 1DefineAndMapBeans.java │ │ └── 2ConditionsAndTypeSwitch.java │ └── kotlinVariant/ │ ├── 1DefineAndMapBeans.kt │ └── 2ConditionsAndTypeSwitch.kt ├── kotlin-idiomatic/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ └── main/ │ └── kotlin/ │ └── idiomaticKotlin/ │ ├── Apply.kt │ ├── DefaultArgs.kt │ ├── Destruction.kt │ ├── FunctionalProgramming.kt │ ├── InitBlock.kt │ ├── Mapping.kt │ ├── NamedArgs.kt │ ├── Nullability.kt │ ├── ObjectForStatelessFWImpls.kt │ ├── Structs.kt │ ├── TopLevelExtensionFunctions.kt │ └── ValueObjects.kt ├── kotlin-spring-boot-vaadin-scaffolding/ │ ├── .gitignore │ ├── README.md │ ├── TODO.md │ ├── docker-compose.yml │ ├── myapp.yaml │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ ├── misc/ │ │ │ │ ├── PuttingTogether.kt │ │ │ │ ├── ValueObjects.kt │ │ │ │ ├── constructorinjection/ │ │ │ │ │ ├── CRMClient.java │ │ │ │ │ ├── ConstructorInjection.kt │ │ │ │ │ ├── CustomerRepository.java │ │ │ │ │ └── CustomerResource.java │ │ │ │ └── vaadin/ │ │ │ │ ├── ActionListenerLambdaExample.kt │ │ │ │ └── ActionListenerLambdaExampleJava.java │ │ │ └── scaffolding/ │ │ │ ├── DummyDataCreator.kt │ │ │ ├── MyApplication.kt │ │ │ ├── SpringConfiguration.kt │ │ │ ├── YamlConfigProps.kt │ │ │ ├── db/ │ │ │ │ ├── Entities.kt │ │ │ │ └── SnippetRepository.kt │ │ │ ├── rest/ │ │ │ │ └── AdminResource.kt │ │ │ └── ui/ │ │ │ ├── Beans.kt │ │ │ ├── DetailsWindow.kt │ │ │ ├── EntityToBeanMapper.kt │ │ │ ├── MainViewDisplay.kt │ │ │ ├── MyAppUI.kt │ │ │ ├── NavigationPresenter.kt │ │ │ ├── UiMisc.kt │ │ │ └── views/ │ │ │ ├── CreateSnippetView.kt │ │ │ ├── ErrorView.kt │ │ │ └── OverviewView.kt │ │ └── resources/ │ │ ├── VAADIN/ │ │ │ └── themes/ │ │ │ └── mytheme/ │ │ │ ├── mytheme.scss │ │ │ └── styles.scss │ │ ├── application.properties │ │ └── banner.txt │ └── test/ │ └── kotlin/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── scaffolding/ │ └── DummyDataCreatorTest.kt ├── modern-best-practices-testing-java/ │ ├── .gitignore │ ├── docker-compose.yml │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── phauer/ │ │ │ └── modernunittesting/ │ │ │ ├── FuturePlayground.java │ │ │ ├── MainLayout.java │ │ │ ├── ModernUnitTestingApplication.java │ │ │ ├── PriceCalculator.java │ │ │ ├── ProductController.java │ │ │ ├── ProductDAO.java │ │ │ ├── ProductDTO.java │ │ │ ├── ProductEntity.java │ │ │ ├── ProductModel.java │ │ │ ├── ProductView.java │ │ │ ├── SchemaCreator.java │ │ │ ├── TaxServiceClient.java │ │ │ └── TaxServiceResponseDTO.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── java/ │ └── com/ │ └── phauer/ │ └── modernunittesting/ │ ├── AssertJTest.java │ ├── AwaitilityTest.java │ ├── DesignControllerTest.java │ ├── DisplayNameTest.java │ ├── HelperFunctions.java │ ├── ParameterTest.java │ ├── Product.java │ ├── ProductControllerITest.java │ ├── ProductControllerITest2.java │ ├── ProductViewITest.java │ └── RandomizedValues.java ├── modern-integration-testing/ │ ├── .gitignore │ ├── docker-compose.yml │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── phauer/ │ │ │ └── modernunittesting/ │ │ │ ├── MainLayout.java │ │ │ ├── ModernUnitTestingApplication.java │ │ │ ├── PriceCalculator.java │ │ │ ├── ProductController.java │ │ │ ├── ProductDAO.java │ │ │ ├── ProductDTO.java │ │ │ ├── ProductEntity.java │ │ │ ├── ProductModel.java │ │ │ ├── ProductView.java │ │ │ ├── SchemaCreator.java │ │ │ ├── TaxServiceClient.java │ │ │ └── TaxServiceResponseDTO.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── java/ │ └── com/ │ └── phauer/ │ └── modernunittesting/ │ ├── AssertJTest.java │ ├── Product.java │ ├── ProductControllerITest.java │ ├── ProductControllerITest2.java │ └── ProductViewITest.java ├── mongodb-practice/ │ ├── connection-strings.sh │ ├── docker-compose.yml │ ├── example.json │ └── local-dev/ │ └── mongo-seeding/ │ ├── Dockerfile │ └── seedMongo.py ├── python-demo/ │ ├── .gitignore │ ├── 1concise-powerful.py │ ├── 2collections.py │ ├── 3functions.py │ ├── 4classes.py │ └── 5operator-overloading.py ├── rest-api-doc-jaxrs-swagger-asciidoc/ │ ├── .gitignore │ ├── README.md │ ├── config.yml │ ├── generate-documentation.sh │ ├── pom.xml │ ├── src/ │ │ ├── docs/ │ │ │ └── asciidoc/ │ │ │ ├── custom.css │ │ │ ├── general-remarks.adoc │ │ │ ├── index.adoc │ │ │ └── usage.adoc │ │ └── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ ├── RestApiDocApplication.java │ │ │ ├── RestApiDocConfiguration.java │ │ │ ├── apiDocGen/ │ │ │ │ └── SwaggerAndAsciiDocGenerator.java │ │ │ └── resources/ │ │ │ ├── BandResource.java │ │ │ ├── CORSFilter.java │ │ │ ├── DocumentationResource.java │ │ │ └── dto/ │ │ │ ├── BandCreationDTO.java │ │ │ └── BandRetrievalDTO.java │ │ └── resources/ │ │ └── banner.txt │ └── swagger-ui/ │ └── docker-compose.yml ├── sealedclasses/ │ ├── .gitignore │ ├── docker-compose.yml │ ├── pom.xml │ ├── service-stub/ │ │ ├── Dockerfile │ │ └── service-stub.py │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── phauer/ │ │ ├── HttpUserProfileClient.kt │ │ ├── ImageAvailabilityClient.kt │ │ ├── LdapDAO.kt │ │ └── common/ │ │ └── Common.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── phauer/ │ └── HelloTest.kt ├── smooth-local-dev-docker/ │ ├── .gitignore │ ├── Pipfile │ ├── bla.conf │ ├── docker-compose.yml │ ├── local-dev/ │ │ ├── external-service-stub/ │ │ │ ├── Dockerfile │ │ │ ├── Pipfile │ │ │ ├── external-service-stub.py │ │ │ └── static-user-response.json │ │ ├── external-service-wrapped/ │ │ │ ├── Dockerfile │ │ │ └── config.yaml │ │ ├── mongo-seeding/ │ │ │ ├── Dockerfile │ │ │ ├── Pipfile │ │ │ └── seed-mongo.py │ │ └── mysql-seeding/ │ │ ├── Dockerfile │ │ ├── Pipfile │ │ └── seed-mysql.py │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ ├── ExternalServiceUserClient.kt │ │ ├── Main.kt │ │ ├── MongoDesignDAO.kt │ │ └── MySqlUserDAO.kt │ └── test/ │ └── kotlin/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── HelloTest.kt ├── testingrestservice/ │ ├── integration-tests/ │ │ ├── .gitignore │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ └── testingrestservice/ │ │ └── integrationtests/ │ │ ├── BlogsTest.java │ │ ├── dto/ │ │ │ ├── BlogDTO.java │ │ │ └── BlogListDTO.java │ │ └── dtoKotlin/ │ │ ├── BlogDTOKotlin.kt │ │ └── BlogsKotlinTest.kt │ └── service/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ └── testingrestservice/ │ │ │ └── service/ │ │ │ ├── BlogApplication.java │ │ │ ├── dataaccess/ │ │ │ │ ├── BlogRepository.java │ │ │ │ ├── DatabaseInitializer.java │ │ │ │ ├── PostRepository.java │ │ │ │ └── entities/ │ │ │ │ ├── BlogEntity.java │ │ │ │ ├── CommentEntity.java │ │ │ │ └── PostEntity.java │ │ │ ├── rest/ │ │ │ │ ├── BlogsResource.java │ │ │ │ └── dto/ │ │ │ │ ├── BlogDTO.java │ │ │ │ ├── BlogsDTO.java │ │ │ │ └── ReferenceDTO.java │ │ │ └── servicecall/ │ │ │ ├── ImageReference.java │ │ │ └── ImageServiceClient.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── java/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── testingrestservice/ │ └── service/ │ └── servicecall/ │ └── ImageReferenceServiceClientTest.java ├── ti-continuation-token/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── README.md │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── de/ │ │ │ └── philipphauer/ │ │ │ └── blog/ │ │ │ └── pagination/ │ │ │ ├── DesignDAO.kt │ │ │ ├── DesignEntity.kt │ │ │ ├── DesignResource.kt │ │ │ ├── Main.kt │ │ │ ├── token/ │ │ │ │ ├── Model.kt │ │ │ │ └── Pagination.kt │ │ │ └── util/ │ │ │ ├── DesignDatabaseUtil.kt │ │ │ └── FunctionsMySQL.kt │ │ └── resources/ │ │ └── create-designs-table.sql │ └── test/ │ └── kotlin/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── pagination/ │ ├── Common.kt │ ├── DesignResourceTest.kt │ ├── PaginationClient.kt │ └── token/ │ └── PaginationTest.kt ├── unit-tests-kotlin/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── phauer/ │ │ └── unittestkotlin/ │ │ └── MongoDAO.kt │ └── test/ │ ├── kotlin/ │ │ └── com/ │ │ └── phauer/ │ │ └── unittestkotlin/ │ │ ├── BackticksAndNestedClasses.kt │ │ ├── DataClassAssertions.kt │ │ ├── HandlingState.kt │ │ ├── IntroductionExample.kt │ │ ├── KGenericContainer.kt │ │ ├── MockHandling.kt │ │ ├── MongoDAOTestJUnit4.kt │ │ ├── MongoDAOTestJUnit5.kt │ │ ├── ParseTest.kt │ │ ├── ParseTestKotest.kt │ │ ├── TestSpecificExtFunctions.kt │ │ ├── assertAllOrSomeFields/ │ │ │ └── AssertAllOrSomeFields.kt │ │ ├── foo/ │ │ │ ├── CreationHelper.kt │ │ │ └── MockK.kt │ │ └── mockk/ │ │ ├── UserScheduler.kt │ │ └── UserSchedulerTest_MockK.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMakerXXX ├── uuid-mysql-hibernate/ │ ├── .gitignore │ ├── README.md │ ├── docker-compose.yml │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── de/ │ │ └── philipphauer/ │ │ └── blog/ │ │ ├── ProductsResource.java │ │ ├── UuidMysqlHibernateApplication.java │ │ └── model/ │ │ └── Product.java │ └── resources/ │ └── application.properties ├── vaadin-10-sass-cssrefresh/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── phauer/ │ │ └── vaadin10sasscssrefresh/ │ │ ├── CustomVaadinServiceListener.java │ │ ├── ExampleView.java │ │ ├── MainLayout.java │ │ └── Vaadin10SassCssrefreshApplication.java │ └── resources/ │ ├── META-INF/ │ │ └── resources/ │ │ ├── frontend/ │ │ │ └── styles/ │ │ │ ├── exampleView.scss │ │ │ ├── main.scss │ │ │ └── variables.scss │ │ └── js/ │ │ └── cssrefresh.js │ └── application.properties └── versioning-continuous-delivery/ ├── .gitignore ├── README.md ├── docker-compose.yml ├── pom.xml └── src/ └── main/ ├── java/ │ └── de/ │ └── philipphauer/ │ └── blog/ │ └── VersioningContinuousDeliveryApplication.java └── resources/ └── application.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # blog-related ================================================ FILE: cairosvg-on-alpine/.gitignore ================================================ .vscode/ ================================================ FILE: cairosvg-on-alpine/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] flask = "==0.12.2" CairoSVG = "==2.1.3" [dev-packages] [requires] python_version = "3.6" ================================================ FILE: cairosvg-on-alpine/README.md ================================================ # Alpine Image with CairoSVG ```bash # build and start the docker container docker-compose up # trigger the svg convertion curl http://localhost:5000/image ``` For details, check out the `src/Dockerfile`. # Run the Script on Ubuntu ```bash # pip install pipenv pipenv install pipenv shell cd src python svg-converter-service.py ``` That should do the trick (tested on Ubuntu 17.04). However, somehow I used to get the error `ModuleNotFoundError: No module named 'PIL'`. - Fix a) add the dependency `Pillow = "==5.1.0"` to the Pipfile - or Fix b) ```bash sudo apt-get install python3-dev python3-setuptools sudo apt-get install python3-dev python3-setuptools libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev ``` ================================================ FILE: cairosvg-on-alpine/docker-compose.yml ================================================ version: '3' services: svg-converter-service: build: ./src ports: - "5000:5000" ================================================ FILE: cairosvg-on-alpine/src/Dockerfile ================================================ FROM python:3.6.4-alpine3.7 RUN apk add --no-cache \ build-base cairo-dev cairo cairo-tools \ # pillow dependencies jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev RUN pip install "flask==1.0.1" "CairoSVG==2.1.3" COPY circle.svg / COPY svg-converter-service.py / CMD python3 /svg-converter-service.py ================================================ FILE: cairosvg-on-alpine/src/svg-converter-service.py ================================================ import cairosvg from flask import Flask, Response app = Flask(__name__) @app.route('/image') def convert_image(): png_data = cairosvg.svg2png(url="circle.svg", parent_width=300, parent_height=300) return Response(png_data, mimetype='image/png') if __name__ == '__main__': app.run(debug=True, port=5000, host='0.0.0.0') ================================================ FILE: cleaner-code-with-kotlin/.gitignore ================================================ .idea/ target/ *.iml ================================================ FILE: cleaner-code-with-kotlin/pom.xml ================================================ 4.0.0 de.philipphauer.blog cleaner-code-with-kotlin 1.0-SNAPSHOT 1.1.1 1.8 3.6.0 org.jetbrains.kotlin kotlin-stdlib-jre8 ${kotlin.version} org.jetbrains.kotlin kotlin-test ${kotlin.version} test org.json json 20160810 org.jetbrains.kotlin kotlin-stdlib-jre8 ${kotlin.version} com.squareup.okhttp3 okhttp ${okhttp.version} com.squareup.okhttp3 logging-interceptor ${okhttp.version} org.assertj assertj-core 3.6.2 test junit junit 4.12 test ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} compile compile compile test-compile test-compile test-compile org.apache.maven.plugins maven-compiler-plugin 3.5.1 ${java.version} ${java.version} default-compile none default-testCompile none java-compile compile compile java-test-compile test-compile testCompile ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/BeAware.kt ================================================ package functions fun add(value: String?){ val map = mutableMapOf() value?.emptyToNull()?.let { map.put("bla", it) } if (value?.isNotEmpty() ?: false){ map.put("key", value!!) } //KISS if (!value.isNullOrEmpty()){ map.put("key", value!!) } if (value != null && value.isNotEmpty()){ map.put("key", value) } // or with smart-cast/without null-assertion } fun String.emptyToNull() = if (this.isEmpty()) null else this ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/Expressions.kt ================================================ package functions import org.json.JSONException import org.json.JSONObject val url = "http://bla.de?asdf" val delimiterIndex = url.indexOf("?") val strippedUrl = if (delimiterIndex > 0) { url.substring(0, delimiterIndex) } else { url } val json = """{"message":"HELLO"}""" val message = try { JSONObject(json).getString("message") } catch (ex: JSONException) { json } fun getMessage(json: String): String { val message: String = try { JSONObject(json).getString("message") } catch (ex: JSONException) { json } return message } fun getMessage2(json: String) = try { JSONObject(json).getString("message") } catch (ex: JSONException) { json } ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/Immutability.kt ================================================ package functions fun varVal(){ val id = 1 // id = 2 var id2 = 1 id2 = 2 println(id) println(id2) } fun collections() { val list = listOf(1,2,3,4) //list.add(1) val evenList = list.filter { it % 2 == 0 } } data class DesignMetaData( val id: Int, val fileName: String, val uploaderId: Int, val width: Int = 0, val height: Int = 0 ) val design = DesignMetaData(id = 1, fileName = "cat.jpg", uploaderId = 2) val id = design.id // design.id = 2 val design2 = design.copy(fileName = "dog.jpg") enum class DesignType {PIXEL, VECTOR} ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/Nullability.kt ================================================ package functions val value: String = "Clean Code" //val value2: String = null val nullValue: String? = "Clean Code" val nullValue2: String? = null //val assign1: String = nullValue val assign2: String = if (nullValue == null) "default" else nullValue //smart-cast val assign3: String = nullValue ?: "default" data class Order(val customer: Customer?) data class Customer(val address: Address?) data class Address(val city: String) fun ship(order: Order?){ //every time you do if-null-checks, hold on. if (order == null || order.customer == null || order.customer.address == null){ throw IllegalArgumentException("Invalid Order") } val city = order.customer.address.city } fun ship2(order: Order?){ // Often, you can use null-safe call (?.) or the elvis operator (?:) instead. val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order") } interface Service class CustomerService : Service { fun getCustomer() {} } fun getMetrics(service: Service){ // also hold on for if-type-checks if (service !is CustomerService) { throw IllegalArgumentException("No CustomerService") } service.getCustomer() } fun getMetrics2(service: Service){ //check type, (smart-)cast it and throw exception if the type is not the expected one. all in one expression! service as? CustomerService ?: throw IllegalArgumentException("No CustomerService") service.getCustomer() } fun foo(order: Order?){ // avoid yelling !! where every possible. search for better solutions by verifying the variable up front and handle nulls. (quote book) order!!.customer!!.address!!.city } fun findOrder(): Order? { return null } fun dun(customer: Customer?){ } fun handle(){ // Don't val order: Order? = findOrder() if (order != null){ dun(order.customer) } // with let(), there is no need for an extra variable // can write as one expression findOrder()?.let { dun(it.customer) } findOrder()?.customer?.let(::dun) } ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/ProductClient.java ================================================ package functions; import okhttp3.Response; import okhttp3.ResponseBody; public class ProductClient { public Product parseProductFromHttpBody(Response response){ if (response == null){ throw new ProductClientException("Response is null!"); } int code = response.code(); if (code == 200 || code == 201){ return mapToDTO(response.body()); } if (code >= 400 && code <= 499){ throw new ProductClientException("Send an invalid request."); } if (code >= 500 && code <= 599){ throw new ProductClientException("Server error."); } throw new ProductClientException("Unknown code " + code); } private Product mapToDTO(ResponseBody body) { return null; } public static class Product{ } public static class ProductClientException extends RuntimeException{ public ProductClientException(String message) { super(message); } } } ================================================ FILE: cleaner-code-with-kotlin/src/main/kotlin/functions/ProductClientKotlin.kt ================================================ package functions import functions.ProductClient.Product import functions.ProductClient.ProductClientException import okhttp3.Response import okhttp3.ResponseBody class ProductClientKotlin { fun parseProductFromHttpBody(response: Response?) = when (response?.code()){ null -> throw ProductClientException("Response is null!") 200, 201 -> mapToDTO(response.body()) in 400..499 -> throw ProductClientException("Send an invalid request.") in 500..599 -> throw ProductClientException("Server error.") else -> throw ProductClientException("Error. Code ${response.code()}") } private fun mapToDTO(body: ResponseBody?): Product { return Product() } fun parseProductFromHttpBody2(response: Response?): Product { val product = when (response?.code()) { null -> throw ProductClientException("Response is null!") 200, 201 -> mapToDTO(response.body()) in 400..499 -> throw ProductClientException("Send an invalid request to server.") in 500..599 -> throw ProductClientException("Server error.") else -> throw ProductClientException("Error. Code ${response.code()}") } return product } } ================================================ FILE: compare-payloads/.gitignore ================================================ target .idea *iml compare-scripts/payload-output ================================================ FILE: compare-payloads/README.md ================================================ ``` $ mvn package $ java -jar target/compare-payloads_1.jar # TODO $ cd compare-scripts $ sudo apt install httpie libxml2-utils jq meld $ ./compare-json-payload.jh $ ./compare-xml-payload.sh $ ... ``` ================================================ FILE: compare-payloads/compare-scripts/compare-all-final.sh ================================================ #!/usr/bin/env bash if [ -z "$2" ]; then fileName=$(basename "$0") printf "Paths are not provided! Pattern: \n./$fileName \"http://localhost:8080/blogposts\" \"http://localhost:8080/blogposts2\"\n" exit fi outputFolder="payload-output" if [[ ! -e "$outputFolder" ]]; then mkdir "$outputFolder" fi resource1="$1" resource2="$2" compare-payloads(){ format="$1" payload1="$outputFolder/payload1.$format" payload2="$outputFolder/payload2.$format" if [ "$format" = "json" ]; then http "$resource1" Accept:application/json --pretty=none | jq -S . > "$payload1" http "$resource2" Accept:application/json --pretty=none | jq -S . > "$payload2" else http "$resource1" Accept:application/xml --pretty=none | xmllint -c14n - | xmllint --format - > "$payload1" http "$resource2" Accept:application/xml --pretty=none | xmllint -c14n - | xmllint --format - > "$payload2" fi if [ "$(cat "$payload1")" = "$(cat "$payload2")" ]; then echo "$format payloads are equal." else echo "!!!$format payloads are NOT EQUAL!!!" # if the payloads are not equal, create a diff and show it to the developer. Let him decide. echo "Showing a diff via meld..." meld "$payload1" "$payload2" fi } #TODO pipe output of http to variable, so this can be outside of the if-body. this also prevents redundant echos for the requests. "calling ...". #TODO xml nodes are not sorted! #TODO compare files... using cat? compare-payloads "json" compare-payloads "xml" ================================================ FILE: compare-payloads/compare-scripts/compare-by-sorting.sh ================================================ #!/usr/bin/env bash if [[ ! -e "payload-output" ]]; then mkdir "payload-output" fi http http://localhost:8080/blogposts --pretty=none > payload-output/version1.json http http://localhost:8080/blogposts2 --pretty=none > payload-output/version2.json # heuristic: sort all characters of the payload alphanumerical. This leads to rubbish. # But we can compare the rubbish to determine equality with a high probability. version1Sorted=$(grep -o . "payload-output/version1.json" | sort | tr -d "\n") version2Sorted=$(grep -o . "payload-output/version2.json" | sort | tr -d "\n") if [ "$version1Sorted" = "$version2Sorted" ]; then echo "JSON payloads are equal." else echo "!!!JSON payloads are NOT EQUAL!!!" meld payload-output/version1.json payload-output/version2.json fi ================================================ FILE: compare-payloads/compare-scripts/compare-json-payload.sh ================================================ #!/usr/bin/env bash if [[ ! -e "payload-output" ]]; then mkdir "payload-output" fi http http://localhost:8080/blogposts --pretty=none | jq -S . > payload-output/version1.json http http://localhost:8080/blogposts2 --pretty=none | jq -S . > payload-output/version2.json meld payload-output/version1.json payload-output/version2.json ================================================ FILE: compare-payloads/compare-scripts/compare-xml-payload.sh ================================================ #!/usr/bin/env bash if [[ ! -e "payload-output" ]]; then mkdir "payload-output" fi http http://localhost:8080/blogposts Accept:application/xml --pretty=none | xmllint -c14n - | xmllint --format - > payload-output/version1.xml http http://localhost:8080/blogposts2 Accept:application/xml --pretty=none | xmllint -c14n - | xmllint --format - > payload-output/version2.xml meld payload-output/version1.xml payload-output/version2.xml #TODO xml nodes are not sorted! #TODO difference between canonical format 1.0 and 1.1 ================================================ FILE: compare-payloads/pom.xml ================================================ 4.0.0 de.philipphauer.blog compare-payloads 1 jar compare-payloads org.springframework.boot spring-boot-starter-parent 1.3.6.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-web com.fasterxml.jackson.dataformat jackson-dataformat-xml com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.6.1 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ================================================ FILE: compare-payloads/src/main/java/de/philipphauer/blog/BlogPost.java ================================================ package de.philipphauer.blog; import javax.xml.bind.annotation.XmlRootElement; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZonedDateTime; public class BlogPost { private String author; private String content; private Instant created; public String getAuthor() { return author; } public BlogPost setAuthor(String author) { this.author = author; return this; } public String getContent() { return content; } public BlogPost setContent(String content) { this.content = content; return this; } public Instant getCreated() { return created; } public BlogPost setCreated(Instant created) { this.created = created; return this; } } ================================================ FILE: compare-payloads/src/main/java/de/philipphauer/blog/BlogPost2.java ================================================ package de.philipphauer.blog; import javax.xml.bind.annotation.XmlRootElement; public class BlogPost2 { private String authorName; private String created; private String content; public String getAuthorName() { return authorName; } public BlogPost2 setAuthorName(String authorName) { this.authorName = authorName; return this; } public String getContent() { return content; } public BlogPost2 setContent(String content) { this.content = content; return this; } public String getCreated() { return created; } public BlogPost2 setCreated(String created) { this.created = created; return this; } } ================================================ FILE: compare-payloads/src/main/java/de/philipphauer/blog/ComparePayloadApplication.java ================================================ package de.philipphauer.blog; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @SpringBootApplication @RestController public class ComparePayloadApplication { public static void main(String[] args) { SpringApplication.run(ComparePayloadApplication.class, args); } List blogPosts = new ArrayList<>(); List blogPosts2 = new ArrayList<>(); public ComparePayloadApplication(){ Instant now = Instant.now(); blogPosts.add(new BlogPost().setAuthor("Philipp").setContent("Super Post").setCreated(now)); blogPosts.add(new BlogPost().setAuthor("Peter").setContent("Nice Post").setCreated(now.minusSeconds(60))); blogPosts.add(new BlogPost().setAuthor("Albert").setContent("Great Post").setCreated(now.minusSeconds(60*60))); blogPosts2.add(new BlogPost2().setAuthorName("Philipp").setContent("Super Post").setCreated(format(now))); blogPosts2.add(new BlogPost2().setAuthorName("Peter").setContent("Nice Post").setCreated(format(now.minusSeconds(60)))); blogPosts2.add(new BlogPost2().setAuthorName("Albert").setContent("Great Post").setCreated(format(now.minusSeconds(60*60)))); } @RequestMapping(value = "/blogposts", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) public List getBlogPosts() { return blogPosts; } @RequestMapping(value = "/blogposts2", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) public List getBlogPosts2() { return blogPosts2; } public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; private String format(Instant now) { ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(now, ZoneOffset.UTC); return FORMATTER.format(zonedDateTime); } } ================================================ FILE: compare-payloads/src/main/resources/application.properties ================================================ ================================================ FILE: continuation-token/.gitignore ================================================ .idea/ *.iml ================================================ FILE: continuation-token/README.md ================================================ # REST API Pagination Example Application # Getting Started ```bash cd continuation-token ./mvnw install cd ../demo-kotlin ./mvnw package && java -jar target/demo-kotlin*.jar ``` Open `http://localhost:8000/designs?pageSize=3` in your browser an click on the URL in the `nextPage` field in the json payload. # The Application It's a lightweight HTTP service written in Kotlin and powered by [HTTP4K](https://www.http4k.org/). It starts within 600 ms. ;-) ================================================ FILE: continuation-token/continuation-token/.gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar .idea/ *.iml ================================================ FILE: continuation-token/continuation-token/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip ================================================ FILE: continuation-token/continuation-token/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # 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 # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # 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 # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home 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 saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME 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"` 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? 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 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" fi else JAVACMD="`which java`" 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." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} echo $MAVEN_PROJECTBASEDIR MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # 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"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: continuation-token/continuation-token/mvnw.cmd ================================================ @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 @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="%MAVEN_PROJECTBASEDIR%\.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_CONFIG% %* 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% ================================================ FILE: continuation-token/continuation-token/pom.xml ================================================ 4.0.0 de.philipphauer.blog continuation-token 1.0.0-SNAPSHOT jar Continuation-Token Handy library to implement fast and reliable API pagination MIT License http://www.opensource.org/licenses/mit-license.php UTF-8 1.1.60 5.0.2 org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.junit.jupiter junit-jupiter-api ${junit5.version} test org.junit.jupiter junit-jupiter-params ${junit5.version} test org.assertj assertj-core 3.8.0 test src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} compile compile compile test-compile test-compile test-compile maven-surefire-plugin 2.19 org.junit.platform junit-platform-surefire-provider 1.0.2 org.junit.jupiter junit-jupiter-engine ${junit5.version} org.apache.maven.plugins maven-source-plugin 3.0.1 attach-sources jar org.jetbrains.dokka dokka-maven-plugin 0.9.15 pre-site javadocJar jcenter JCenter https://jcenter.bintray.com/ ================================================ FILE: continuation-token/continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/ContinuationTokenParser.kt ================================================ package de.philipphauer.blog.pagination object ContinuationTokenParser { val DELIMITER = ":" fun parse(tokenString: String): ContinuationToken{ val parts = tokenString.split(DELIMITER) try { return ContinuationToken( timestamp = parts[0].toLong(), offset = parts[1].toInt(), checksum = parts[2].toLong() ) } catch (ex: Exception){ throw ContinuationTokenParseException("Invalid token '$tokenString'.", ex) } } } class ContinuationTokenParseException(message: String, cause: Exception) : RuntimeException(message, cause) ================================================ FILE: continuation-token/continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/Model.kt ================================================ package de.philipphauer.blog.pagination import de.philipphauer.blog.pagination.ContinuationTokenParser.DELIMITER /** a token points to the last element of the current page. "last" usually means "highest timestamp". **/ data class ContinuationToken( /** timestamp of the highest entity in the last page. */ val timestamp: Long, /** offset = amount of entities with the highest timestamp in the last page, that have the same timestamp */ val offset: Int, /** used to detect modifications during pagination */ val checksum: Long ) { override fun toString() = "$timestamp$DELIMITER$offset$DELIMITER$checksum" } data class QueryAdvice( /** use this with >= in the WHERE clause (the equals is important!) */ val timestamp: Long, val limit: Int ) data class Page( val entities: List, val currentToken: ContinuationToken? ) interface Pageable { fun getID(): String fun getTimestamp(): Long } ================================================ FILE: continuation-token/continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/Pagination.kt ================================================ package de.philipphauer.blog.pagination import java.util.zip.CRC32 object Pagination{ //TODO implement checksum fallback fun createPage(entitiesSinceIncludingTs: List, oldToken: ContinuationToken?, requiredPageSize: Int): Page { if (entitiesSinceIncludingTs.isEmpty()){ return Page(entities = listOf(), currentToken = null) } if (oldToken == null || currentPageStartsWithADifferentTimestampThanInToken(entitiesSinceIncludingTs, oldToken)){ //don't skip val token = createTokenForPage(entitiesSinceIncludingTs, entitiesSinceIncludingTs, requiredPageSize) return Page(entities = entitiesSinceIncludingTs, currentToken = token) } else { val entitiesForNextPage = skipOffset(entitiesSinceIncludingTs, oldToken) val token = createTokenForPage(entitiesSinceIncludingTs, entitiesForNextPage, requiredPageSize) return Page(entities = entitiesForNextPage, currentToken = token) } } private fun fillUpWholePage(entities: List, requiredPageSize: Int): Boolean = entities.size >= requiredPageSize private fun currentPageStartsWithADifferentTimestampThanInToken(allEntitiesSinceIncludingTs: List, oldToken: ContinuationToken): Boolean { val timestampOfFirstElement = allEntitiesSinceIncludingTs.first().getTimestamp() return timestampOfFirstElement != oldToken.timestamp } fun calculateQueryAdvice(token: ContinuationToken?, pageSize: Int): QueryAdvice { if (token == null){ return QueryAdvice(limit = pageSize, timestamp = 0) } return QueryAdvice(limit = token.offset + pageSize, timestamp = token.timestamp) } private fun skipOffset(entitiesSinceIncludingTs: List, currentToken: ContinuationToken) = entitiesSinceIncludingTs.subList(currentToken.offset, entitiesSinceIncludingTs.size) /** * @param entitiesForNextPage includes skip/offset */ internal fun createTokenForPage(allEntitiesSinceIncludingTs: List, entitiesForNextPage: List, requiredPageSize: Int): ContinuationToken? { if (allEntitiesSinceIncludingTs.isEmpty()){ return null } if (!fillUpWholePage(entitiesForNextPage, requiredPageSize)){ return null // no next token required } val highestEntities = getEntitiesWithHighestTimestamp(allEntitiesSinceIncludingTs) val highestTimestamp = highestEntities.last().getTimestamp() val ids = highestEntities.map(Pageable::getID) val checksum = createCRC32Checksum(ids) return ContinuationToken( timestamp = highestTimestamp, offset = highestEntities.size, checksum = checksum ) } private fun createCRC32Checksum(ids: List): Long { val hash = CRC32() hash.update(ids.joinToString("_").toByteArray()) return hash.value } internal fun getEntitiesWithHighestTimestamp(entities: List): List { if (entities.isEmpty()){ return listOf() } val highestTimestamp = entities.last().getTimestamp() val entitiesWithHighestTimestamp = mutableListOf() val lastIndex = entities.size - 1 var i = lastIndex while (i >= 0 && highestTimestamp == entities[i].getTimestamp()) { entitiesWithHighestTimestamp.add(entities[i]) i-- } return entitiesWithHighestTimestamp.reversed() } } ================================================ FILE: continuation-token/continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/ContinuationTokenParserTest.kt ================================================ package de.philipphauer.blog.pagination import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ContinuationTokenParserTest { private val parser = ContinuationTokenParser @Test fun valid() { assertThat(parser.parse("1511443755:2:1842515611")) .isEqualTo(ContinuationToken(timestamp = 1511443755, offset = 2, checksum = 1842515611)) assertThat(parser.parse("1511443755:1:1842521611")) .isEqualTo(ContinuationToken(timestamp = 1511443755, offset = 1, checksum = 1842521611)) //also support timestamps with millisecond precision assertThat(parser.parse("1511443755999:1:1842521611")) .isEqualTo(ContinuationToken(timestamp = 1511443755999, offset = 1, checksum = 1842521611)) } @ParameterizedTest @MethodSource("invalidTokenProvider") fun invalid(invalidToken: String) { val exception = assertThrows(ContinuationTokenParseException::class.java){ parser.parse(invalidToken) } assertThat(exception.message).isEqualTo("Invalid token '$invalidToken'.") } private fun invalidTokenProvider(): Stream = Stream.of( "asdf:1:1842521611" , "1511443755:sadfasd:1842521611" , "1511443755:1:sadfasd" , "1511443755:1" , "1511443755:1:" , "" , "::" , "12::" , "12::213" , ":1231:213" ) } ================================================ FILE: continuation-token/continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/PaginationTest.kt ================================================ package de.philipphauer.blog.pagination import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import java.util.zip.CRC32 @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class PaginationTest{ @Nested inner class `createPage` { @Test fun `|1,2,3|4,5,6| different keys`() { val allEntries = listOf( TestPageable(1), TestPageable(2), TestPageable(3), TestPageable(4), TestPageable(5), TestPageable(6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 4) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable(4), TestPageable(5), TestPageable(6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,2,3|3,5,6| key 3 overlaps two pages`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3), TestPageable("4", 3), TestPageable("5", 5), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 4) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 3), TestPageable("5", 5), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,1,1|1,1,1| all have same key`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 1), TestPageable("3", 1), TestPageable("4", 1), TestPageable("5", 1), TestPageable("6", 1) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 1), TestPageable("3", 1) ), currentToken = ContinuationToken(timestamp = 1, offset = 3, checksum = checksum("1", "2", "3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 1, limit = 6) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 1), TestPageable("5", 1), TestPageable("6", 1) ), currentToken = ContinuationToken(timestamp = 1, offset = 6, checksum = checksum("1", "2", "3", "4", "5", "6")) )) } @Test fun `|1,2,3|| although it's the last page it fits right into the page size so we can't tell if this is the last page and have to pass a next token`() { val allEntries = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 3) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf(), currentToken = null )) } @Test fun `|1,2| no next token if there are less elements than page size`() { val allEntries = listOf( TestPageable(1), TestPageable(2) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable(1), TestPageable(2) ), currentToken = null )) } @Test fun `|1,2,3|4| still skip correctly even if there are less elements than page size`() { val allEntries = listOf( TestPageable(1), TestPageable(2), TestPageable(3), TestPageable(4) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 3) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable(4) ), currentToken = null )) } @Test fun `|1,2,3|4,5| second page is not full so no next token`() { val allEntries = listOf( TestPageable(1), TestPageable(2), TestPageable(3), TestPageable(4), TestPageable(5) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 3) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable(4), TestPageable(5) ), currentToken = null )) } @Test fun `|| empty page`() { val page = Pagination.createPage(listOf(), null, 3) assertThat(page).isEqualTo(Page( entities = listOf(), currentToken = null )) } @Test fun `|1,3,3|4,5,6|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3), TestPageable("4", 4), TestPageable("5", 5), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 2, checksum = checksum("2", "3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 5) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 4), TestPageable("5", 5), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,3,3|3,5,6|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3), TestPageable("4", 3), TestPageable("5", 5), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 2, checksum = checksum("2", "3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 5) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 3), TestPageable("5", 5), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,3,3|3,3,6|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3), TestPageable("4", 3), TestPageable("5", 3), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 3), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 2, checksum = checksum("2", "3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 5) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 3), TestPageable("5", 3), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,2,3|3,3,6|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3), TestPageable("4", 3), TestPageable("5", 3), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 5) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 3), TestPageable("5", 3), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,2,3|4,4,6|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3), TestPageable("4", 4), TestPageable("5", 4), TestPageable("6", 6) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 2), TestPageable("3", 3) ), currentToken = ContinuationToken(timestamp = 3, offset = 1, checksum = checksum("3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 3, limit = 5) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 4), TestPageable("5", 4), TestPageable("6", 6) ), currentToken = ContinuationToken(timestamp = 6, offset = 1, checksum = checksum("6")) )) } @Test fun `|1,1,1|1,1,2|`() { val allEntries = listOf( TestPageable("1", 1), TestPageable("2", 1), TestPageable("3", 1), TestPageable("4", 1), TestPageable("5", 1), TestPageable("6", 2) ) val firstPage = allEntries.getEntriesSinceIncluding(timestamp = 0, limit = 3) val page = Pagination.createPage(firstPage, null, 3) assertThat(page).isEqualTo(Page( entities = listOf( TestPageable("1", 1), TestPageable("2", 1), TestPageable("3", 1) ), currentToken = ContinuationToken(timestamp = 1, offset = 3, checksum = checksum("1", "2", "3")) )) val entriesSinceKey = allEntries.getEntriesSinceIncluding(timestamp = 1, limit = 6) val page2 = Pagination.createPage(entriesSinceKey, page.currentToken, 3) assertThat(page2).isEqualTo(Page( entities = listOf( TestPageable("4", 1), TestPageable("5", 1), TestPageable("6", 2) ), currentToken = ContinuationToken(timestamp = 2, offset = 1, checksum = checksum("6")) )) } private fun List.getEntriesSinceIncluding(timestamp: Int, limit: Int) = this.filter { it.getTimestamp() >= timestamp }.take(limit) } @Nested inner class `createToken` { @Test fun `only one entity with highest timestamp`() { val pageables = listOf( TestPageable(1), TestPageable(2), TestPageable(3), TestPageable(4) ) val token = Pagination.createTokenForPage(pageables, pageables, 4) assertThat(token).isEqualTo(ContinuationToken(timestamp = 4, offset = 1, checksum = checksum("4"))) } @Test fun `two entities with highest timestamp`() { val pageables = listOf( TestPageable(1), TestPageable(2), TestPageable("3", 3), TestPageable("4", 3) ) val token = Pagination.createTokenForPage(pageables, pageables, 4) assertThat(token).isEqualTo(ContinuationToken(timestamp = 3, offset = 2, checksum = checksum("3", "4"))) } @Test fun `all elements have same timestamp`() { val pageables = listOf( TestPageable("1",1), TestPageable("2",1), TestPageable("3",1) ) val token = Pagination.createTokenForPage(pageables, pageables, 3) assertThat(token).isEqualTo(ContinuationToken(timestamp = 1, offset = 3, checksum = checksum("1", "2", "3"))) } @Test fun `one element list`() { val pageables = listOf( TestPageable(1) ) val token = Pagination.createTokenForPage(pageables, pageables, 1) assertThat(token).isEqualTo(ContinuationToken(timestamp = 1, offset = 1, checksum = checksum("1"))) } @Test fun `empty list`() { val token = Pagination.createTokenForPage(listOf(), listOf(), 10) assertThat(token).isNull() } //TODO test varying pagesize! } @Nested inner class `calculateQueryAdvice`{ @Test fun `no token provided`(){ val advice = Pagination.calculateQueryAdvice(token = null, pageSize = 5) assertThat(advice).isEqualTo(QueryAdvice(timestamp = 0, limit = 5)) } @Test fun `there was one element with timestamp 20 in the last page`(){ val token = ContinuationToken(timestamp = 20, offset = 1, checksum = 999) val advice = Pagination.calculateQueryAdvice(token, pageSize = 5) assertThat(advice).isEqualTo(QueryAdvice(timestamp = 20, limit = 6)) } @Test fun `there were 3 elements with timestamp 20 in the last page`(){ val token = ContinuationToken(timestamp = 20, offset = 3, checksum = 999) val advice = Pagination.calculateQueryAdvice(token, pageSize = 5) assertThat(advice).isEqualTo(QueryAdvice(timestamp = 20, limit = 8)) } } @Nested inner class `getEntitiesWithHighestKey`{ @Test fun `all have different keys`(){ val pageables = listOf( TestPageable(1), TestPageable(2), TestPageable(3) ) val entities = Pagination.getEntitiesWithHighestTimestamp(pageables) assertThat(entities).containsExactly(TestPageable(3)) } @Test fun `some with the same key`(){ val pageables = listOf( TestPageable(1), TestPageable(2), TestPageable("4",3), TestPageable("5",3) ) val entities = Pagination.getEntitiesWithHighestTimestamp(pageables) assertThat(entities).containsExactly(TestPageable("4",3), TestPageable("5",3)) } @Test fun `all with the same key`(){ val pageables = listOf( TestPageable("1",1), TestPageable("2",1), TestPageable("3",1) ) val entities = Pagination.getEntitiesWithHighestTimestamp(pageables) assertThat(entities).containsExactly(TestPageable("1",1), TestPageable("2",1), TestPageable("3",1)) } @Test fun `empty list`(){ val entities = Pagination.getEntitiesWithHighestTimestamp(listOf()) assertThat(entities).isEmpty() } @Test fun `only one element`(){ val pageables = listOf(TestPageable(1)) val entities = Pagination.getEntitiesWithHighestTimestamp(pageables) assertThat(entities).containsExactly(TestPageable("1",1)) } } } private fun checksum(vararg ids: String): Long{ val hash = CRC32() hash.update(ids.joinToString("_").toByteArray()) return hash.value } data class TestPageable( private val id: String, private val timestamp: Long ): Pageable { constructor(timestamp: Long): this(timestamp.toString(), timestamp) override fun getID() = id override fun getTimestamp() = timestamp } ================================================ FILE: continuation-token/demo-kotlin/.gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar .idea/ *.iml dependency-reduced-pom.xml ================================================ FILE: continuation-token/demo-kotlin/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip ================================================ FILE: continuation-token/demo-kotlin/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # 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 # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # 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 # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home 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 saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME 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"` 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? 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 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" fi else JAVACMD="`which java`" 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." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} echo $MAVEN_PROJECTBASEDIR MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # 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"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: continuation-token/demo-kotlin/mvnw.cmd ================================================ @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 @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="%MAVEN_PROJECTBASEDIR%\.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_CONFIG% %* 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% ================================================ FILE: continuation-token/demo-kotlin/pom.xml ================================================ 4.0.0 de.philipphauer.blog demo-kotlin 1.0-SNAPSHOT jar UTF-8 1.1.60 5.0.2 3.0.1 1.0.0-SNAPSHOT de.philipphauer.blog continuation-token ${continuation-token.version} org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.http4k http4k-core ${http4k.version} org.http4k http4k-server-jetty ${http4k.version} org.http4k http4k-format-jackson ${http4k.version} org.springframework spring-jdbc 5.0.1.RELEASE com.h2database h2 1.4.196 org.junit.jupiter junit-jupiter-api ${junit5.version} test org.assertj assertj-core 3.8.0 test src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} compile compile compile test-compile test-compile test-compile maven-surefire-plugin 2.19 org.junit.platform junit-platform-surefire-provider 1.0.2 org.junit.jupiter junit-jupiter-engine ${junit5.version} org.apache.maven.plugins maven-shade-plugin 3.1.0 de.philipphauer.blog.MainKt package shade ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/DesignDAO.kt ================================================ package de.philipphauer.blog import de.philipphauer.blog.pagination.ContinuationToken import de.philipphauer.blog.pagination.Pagination import org.springframework.jdbc.core.JdbcTemplate import java.sql.ResultSet import javax.sql.DataSource class DesignDAO(dataSource: DataSource){ private val template = JdbcTemplate(dataSource) fun getDesigns(token: ContinuationToken?, pageSize: Int): DesignPageEntity { val queryAdvice = Pagination.calculateQueryAdvice(token, pageSize) val sql = """SELECT * FROM designs WHERE UNIX_TIMESTAMP(dateModified) >= ${queryAdvice.timestamp} ORDER BY dateModified asc, id asc LIMIT ${queryAdvice.limit};""" val designs = template.query(sql, this::mapToDesign) val nextPage = Pagination.createPage(designs, token, pageSize) return DesignPageEntity(nextPage.entities as List, nextPage.currentToken) } private fun mapToDesign(rs: ResultSet, rowNum: Int) = DesignEntity( id = rs.getString("id"), title = rs.getString("title"), imageUrl = rs.getString("imageUrl"), dateModified = rs.getTimestamp("dateModified").toInstant() ) } data class DesignPageEntity( val designs: List, val token: ContinuationToken? ) ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/DesignEntity.kt ================================================ package de.philipphauer.blog import de.philipphauer.blog.pagination.Pageable import java.time.Instant data class DesignEntity( val id: String, val title: String, val imageUrl: String, val dateModified: Instant ): Pageable { override fun getID() = id override fun getTimestamp() = dateModified.epochSecond } ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/DesignResource.kt ================================================ package de.philipphauer.blog import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import de.philipphauer.blog.pagination.ContinuationTokenParser import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status class DesignResource(val dao: DesignDAO) { //TODO no next page on last page //TODO checksum fun getDesigns(request: Request): Response { val token = request.query("continue")?.let {ContinuationTokenParser.parse(it)} val pageSize = request.query("pageSize")?.toInt() ?: 100 val daoResult = dao.getDesigns(token, pageSize) val dto = PageDTO( results = daoResult.designs.map(::mapToDTO), nextPage = if (daoResult.token == null) null else "http://localhost:8000/designs?pageSize=$pageSize&continue=${daoResult.token}" ) return Response(Status.OK) .header("Content-Type", "application/json;charset=UTF-8") .body(dto.toJson()) } } private fun mapToDTO(entity: DesignEntity) = DesignDTO( id = entity.id, title = entity.title, imageUrl = entity.imageUrl, dateModified = entity.dateModified.epochSecond ) data class DesignDTO( val id: String, val title: String, val imageUrl: String, val dateModified: Long ) data class PageDTO( val results: List, val nextPage: String? ) private val mapper = jacksonObjectMapper() private fun Any.toJson() = mapper.writeValueAsString(this) ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/Main.kt ================================================ package de.philipphauer.blog import de.philipphauer.blog.util.DesignCreator import de.philipphauer.blog.util.FunctionsMySQL import org.eclipse.jetty.server.NCSARequestLog import org.eclipse.jetty.server.Server import org.h2.jdbcx.JdbcDataSource import org.http4k.core.Method import org.http4k.routing.bind import org.http4k.routing.routes import org.http4k.server.Jetty import org.http4k.server.asServer import org.springframework.core.io.ClassPathResource import org.springframework.jdbc.datasource.init.ScriptUtils fun main(args: Array) { val resource = bootstrapDesignResource() val routingHandler = routes( "/designs" bind Method.GET to resource::getDesigns ) val jetty = Server(8000).apply { requestLog = NCSARequestLog() } val server = routingHandler.asServer(Jetty(jetty)).start() server.block() } private fun bootstrapDesignResource(): DesignResource { val dataSource = JdbcDataSource().apply { user = "sa" password = "" setURL("jdbc:h2:mem:access;MODE=MySQL;DB_CLOSE_DELAY=-1") } FunctionsMySQL.register(dataSource.connection) ScriptUtils.executeSqlScript(dataSource.connection, ClassPathResource("create-designs-table.sql")) DesignCreator(dataSource).createDesigns(amount = 20) val dao = DesignDAO(dataSource) return DesignResource(dao) } ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/util/DesignCreator.kt ================================================ package de.philipphauer.blog.util import org.springframework.jdbc.core.JdbcTemplate import java.time.Instant import javax.sql.DataSource class DesignCreator(dataSource: DataSource) { private val utilTemplate = JdbcTemplate(dataSource) fun createDesigns(amount: Int) { val now = Instant.now() val values = (1..amount).mapIndexed{ i, _ -> arrayOf( i, "Cat $i", "http://domain.de/cat$i.jpg", now.plusSeconds(i.toLong()).epochSecond ) } utilTemplate.batchUpdate("INSERT INTO designs (id, title, imageUrl, dateModified) VALUES (?, ?, ?, FROM_UNIXTIME(?))", values) } } ================================================ FILE: continuation-token/demo-kotlin/src/main/kotlin/de/philipphauer/blog/util/FunctionsMySQL.kt ================================================ package de.philipphauer.blog.util import org.h2.util.StringUtils import java.sql.Connection import java.sql.SQLException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale /** * https://github.com/h2database/h2database/blob/master/h2/src/main/org/h2/mode/FunctionsMySQL.java * This class implements some MySQL-specific functions. * * @author Jason Brittain * @author Thomas Mueller */ object FunctionsMySQL { /** * The date format of a MySQL formatted date/time. * Example: 2008-09-25 08:40:59 */ private val DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" /** * Format replacements for MySQL date formats. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-format */ private val FORMAT_REPLACE = arrayOf("%a", "EEE", "%b", "MMM", "%c", "MM", "%d", "dd", "%e", "d", "%H", "HH", "%h", "hh", "%I", "hh", "%i", "mm", "%j", "DDD", "%k", "H", "%l", "h", "%M", "MMMM", "%m", "MM", "%p", "a", "%r", "hh:mm:ss a", "%S", "ss", "%s", "ss", "%T", "HH:mm:ss", "%W", "EEEE", "%w", "F", "%Y", "yyyy", "%y", "yy", "%%", "%") /** * Register the functionality in the database. * Nothing happens if the functions are already registered. * * @param conn the connection */ @Throws(SQLException::class) fun register(conn: Connection) { val init = arrayOf("UNIX_TIMESTAMP", "unixTimestamp", "FROM_UNIXTIME", "fromUnixTime", "DATE", "date") val stat = conn.createStatement() var i = 0 while (i < init.size) { val alias = init[i] val method = init[i + 1] stat.execute( "CREATE ALIAS IF NOT EXISTS " + alias + " FOR \"" + FunctionsMySQL::class.java!!.name + "." + method + "\"") i += 2 } } /** * Get the seconds since 1970-01-01 00:00:00 UTC. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_unix-timestamp * * @return the current timestamp in seconds (not milliseconds). */ @JvmStatic fun unixTimestamp(): Int { return (System.currentTimeMillis() / 1000L).toInt() } /** * Get the seconds since 1970-01-01 00:00:00 UTC of the given timestamp. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_unix-timestamp * * @param timestamp the timestamp * @return the current timestamp in seconds (not milliseconds). */ @JvmStatic fun unixTimestamp(timestamp: java.sql.Timestamp): Int { return (timestamp.time / 1000L).toInt() } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_from-unixtime * * @param seconds The current timestamp in seconds. * @return a formatted date/time String in the format "yyyy-MM-dd HH:mm:ss". */ @JvmStatic fun fromUnixTime(seconds: Int): String { val formatter = SimpleDateFormat(DATE_TIME_FORMAT, Locale.ENGLISH) return formatter.format(Date(seconds * 1000L)) } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_from-unixtime * * @param seconds The current timestamp in seconds. * @param format The format of the date/time String to return. * @return a formatted date/time String in the given format. */ @JvmStatic fun fromUnixTime(seconds: Int, format: String): String { var format = format format = convertToSimpleDateFormat(format) val formatter = SimpleDateFormat(format, Locale.ENGLISH) return formatter.format(Date(seconds * 1000L)) } private fun convertToSimpleDateFormat(format: String): String { var format = format val replace = FORMAT_REPLACE var i = 0 while (i < replace.size) { format = StringUtils.replaceAll(format, replace[i], replace[i + 1]) i += 2 } return format } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date * This function is dependent on the exact formatting of the MySQL date/time * string. * * @param dateTime The date/time String from which to extract just the date * part. * @return the date part of the given date/time String argument. */ @JvmStatic fun date(dateTime: String?): String? { if (dateTime == null) { return null } val index = dateTime!!.indexOf(' ') return if (index != -1) { dateTime!!.substring(0, index) } else dateTime } } ================================================ FILE: continuation-token/demo-kotlin/src/main/resources/create-designs-table.sql ================================================ CREATE TABLE designs ( id int AUTO_INCREMENT PRIMARY KEY, title varchar(100) NOT NULL, imageUrl varchar(100) NOT NULL, dateModified TIMESTAMP NOT NULL ); ================================================ FILE: continuation-token/demo-kotlin/src/test/kotlin/de/philipphauer/blog/DesignResourceTest.kt ================================================ package de.philipphauer.blog import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import de.philipphauer.blog.util.DesignCreator import de.philipphauer.blog.util.FunctionsMySQL import org.h2.jdbcx.JdbcDataSource import org.http4k.core.Method import org.http4k.core.Request import org.http4k.core.Response import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.springframework.core.io.ClassPathResource import org.springframework.jdbc.datasource.init.ScriptUtils @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DesignResourceTest { private val resource = initDesignResource() private val creator = DesignCreator(dataSource) @Test fun `happy path`() { creator.createDesigns(amount = 10) val response = resource.getDesigns(Request(Method.GET, "/designs?pageSize=3")) println(response) // assertThat(response.toPageable().nextPage).contains("?continue=TODO") } //TODO test: find better test abstraction. e.g. PaginationTest // page with same key/ts // final page (amount < page size and = page size) // first element of next page: a) is first one with new key, b) not the same one // empty result // correct page size // no `nextPage` in last page // checksum usage private fun initDesignResource(): DesignResource { val dao = DesignDAO(dataSource) FunctionsMySQL.register(dataSource.connection) ScriptUtils.executeSqlScript(dataSource.connection, ClassPathResource("create-designs-table.sql")) return DesignResource(dao) } } private val dataSource = JdbcDataSource().apply { user = "sa" password = "" setURL("jdbc:h2:mem:access;MODE=MySQL;DB_CLOSE_DELAY=-1") } private val mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } private fun Response.toPageable() = mapper.readValue(bodyString(), PageableResponse::class.java) data class PageableResponse( val nextPage: String ) ================================================ FILE: development-productivity-vaadin-spring-boot/.gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ build/ nbbuild/ dist/ nbdist/ .nb-gradle/ src/main/webapp/VAADIN/themes/mytheme/styles.css src/main/webapp/VAADIN/themes/mytheme/styles.scss.cache src/main/resources/VAADIN/themes/mytheme/styles.css ================================================ FILE: development-productivity-vaadin-spring-boot/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip ================================================ FILE: development-productivity-vaadin-spring-boot/Makefile ================================================ .PHONY: all watch SHELL:=/bin/bash BROWSERSYNC:=/usr/local/bin/browser-sync all: watch watch: $(BROWSERSYNC) browser-sync start --proxy 'localhost:8080' --files 'src/main/webapp/VAADIN/themes/mytheme/*.scss' $(BROWSERSYNC): sudo npm install -g browser-sync ================================================ FILE: development-productivity-vaadin-spring-boot/README.md ================================================ An example application with some bootstrapping (spring security, JDBC auto configuration, h2 console, actuator endpoints, vaadin view discovery) to keep the application busy during startup. this way, we can better see the impact of our optimizations. Build and Run ```bash ./mvnw clean package && java -jar target/development-productivity-vaadin-spring-boot*.jar ``` Search in created jar ```bash ./mvnw clean package unzip -l target/development-productivity-vaadin-spring-boot*.jar | grep css ``` Compile Vaadin Theme ```bash ./mvnw vaadin:compile-theme ``` Check out `src/main/resources/application.properties`. # Side Notes Where to put `VAADIN/themes/mytheme`? `src/main/resources` or `src/main/webapp`? Vaadin's on-the-fly compilation works in both cases! (given: there is no `styles.css` (`src` and `target/classes`) and `production-mode=false`) - `webapp`: sass changes are automatically detected and a recompilation is triggered. `styles.scss.cache` is created. no manual action required. But somehow, there is no `styles.css` in the built jar, although everything works fine. - `resources`: after a change (in let's say `mytheme.scss`), you have to hit `Ctrl+Shift+F9` in IDEA. Now, the change is detected and recompilation is triggered. There is no `styles.scss.cache` at all. - Advantages: it's the [recommend location](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-spring-mvc-static-content) of spring boot! Moreover, you can find the generated styles.css in the jar! - Drawback: more uncomfortable theme editing. If somebody can explain this behavior, please approach me! ================================================ FILE: development-productivity-vaadin-spring-boot/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # 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 # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # 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 # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home 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 saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME 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"` 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? 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 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" fi else JAVACMD="`which java`" 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." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} echo $MAVEN_PROJECTBASEDIR MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # 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"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: development-productivity-vaadin-spring-boot/mvnw.cmd ================================================ @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 @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="%MAVEN_PROJECTBASEDIR%\.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_CONFIG% %* 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% ================================================ FILE: development-productivity-vaadin-spring-boot/pom.xml ================================================ 4.0.0 de.philipphauer.blog development-productivity-vaadin-spring-boot 0.0.1-SNAPSHOT jar development-productivity-vaadin-spring-boot Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 1.5.7.RELEASE UTF-8 UTF-8 1.8 8.1.0 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-actuator-docs org.springframework.boot spring-boot-starter-mustache org.springframework.boot spring-boot-starter-security com.vaadin vaadin-spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc com.h2database h2 1.4.196 org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-test test com.vaadin vaadin-bom ${vaadin.version} pom import org.springframework.boot spring-boot-maven-plugin com.vaadin vaadin-maven-plugin ${vaadin.version} resources update-theme compile-theme ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/DevProductivityApplication.java ================================================ package de.philipphauer.blog.devproductivity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DevProductivityApplication { public static void main(String[] args) { SpringApplication.run(DevProductivityApplication.class, args); } } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/WebSecurityConfiguration.java ================================================ package de.philipphauer.blog.devproductivity; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.authorizeRequests().anyRequest().permitAll(); http.httpBasic().disable(); http.formLogin().disable(); } } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/model/Role.java ================================================ package de.philipphauer.blog.devproductivity.model; public enum Role { GUEST, REGISTERED_USER } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/model/User.java ================================================ package de.philipphauer.blog.devproductivity.model; public class User { private int id; private String firstName; private String lastName; private int age; private Role role; private boolean active; public int getId() { return id; } public User setId(int id) { this.id = id; return this; } public String getFirstName() { return firstName; } public User setFirstName(String firstName) { this.firstName = firstName; return this; } public String getLastName() { return lastName; } public User setLastName(String lastName) { this.lastName = lastName; return this; } public int getAge() { return age; } public User setAge(int age) { this.age = age; return this; } public Role getRole() { return role; } public User setRole(Role role) { this.role = role; return this; } public boolean isActive() { return active; } public User setActive(boolean active) { this.active = active; return this; } } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/rest/AdminResource.java ================================================ package de.philipphauer.blog.devproductivity.rest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class AdminResource { @GetMapping("/") public String redirectToUI(){ return "redirect:/ui"; } @GetMapping("favicon.ico") public String favicon(){ return "forward:/VAADIN/themes/sqljunkie/favicon.ico"; } @GetMapping("/customResource") public String customResource(){ return "Hi!"; } } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/java/de/philipphauer/blog/devproductivity/ui/MyAppUI.java ================================================ package de.philipphauer.blog.devproductivity.ui; import com.vaadin.annotations.Theme; import com.vaadin.server.VaadinRequest; import com.vaadin.spring.annotation.SpringUI; import com.vaadin.ui.Grid; import com.vaadin.ui.Label; import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.themes.ValoTheme; import de.philipphauer.blog.devproductivity.model.Role; import de.philipphauer.blog.devproductivity.model.User; import java.util.List; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.Stream; @SpringUI(path = "") @Theme("mytheme") public class MyAppUI extends UI { private Grid table = new Grid<>(User.class); private Label heading = new Label("Development Productivity Demo"); @Override protected void init(VaadinRequest vaadinRequest) { getPage().setTitle("Development Productivity Demo"); table.setSizeFull(); table.setItems(generateDummyUsers()); heading.addStyleName(ValoTheme.LABEL_H1); VerticalLayout layout = new VerticalLayout(); layout.addComponent(heading); layout.addComponentsAndExpand(table); setContent(layout); System.out.println("Session Object ID:"+getSession().hashCode()); } private List generateDummyUsers() { Random random = new Random(); return Stream.generate(() -> random.nextInt(9999)) .limit(50) .map(uuid -> new User() .setId(uuid) .setFirstName("Paul") .setLastName("Stark") .setActive(true) .setRole(Role.REGISTERED_USER) .setActive(true)) .collect(Collectors.toList()); } } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/resources/application.properties ================================================ # but I prefer to control this via program arguments in my IDE's run configuration vaadin.servlet.production-mode=false # https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html spring.devtools.restart.enabled=false spring.devtools.livereload.enabled=false # just some stuff to keep our application busy during startup: spring.h2.console.enabled=true spring.h2.console.path=/h2 spring.datasource.url=jdbc:h2:file:~/test spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver # deactivate default favicon delivery. use my dedicated resource for this. this resources uses the favicon in the vaadin theme. spring.mvc.favicon.enabled=false ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/webapp/VAADIN/themes/mytheme/addons.scss ================================================ /* This file is automatically managed and will be overwritten from time to time. */ /* Do not manually edit this file. */ /* Import and include this mixin into your project theme to include the addon themes */ @mixin addons { } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/webapp/VAADIN/themes/mytheme/mytheme.scss ================================================ @import "../valo/valo.scss"; @mixin mytheme { @include valo; .monospace { font-family: monospace; } //div { // background-color: blue; //} } ================================================ FILE: development-productivity-vaadin-spring-boot/src/main/webapp/VAADIN/themes/mytheme/styles.scss ================================================ @import "addons"; @import "mytheme"; .mytheme { @include addons; @include mytheme; } ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/.gitignore ================================================ .gradle/ .idea/ out/ build/ *.iml ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/build.gradle ================================================ plugins { id "com.chrisgahlert.gradle-dcompose-plugin" version "0.9.1" id 'org.springframework.boot' version '1.4.2.RELEASE' } apply plugin: 'java' group 'de.philipphauer.blog' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile 'org.springframework.boot:spring-boot-starter-jdbc' compile 'mysql:mysql-connector-java:6.0.5' testCompile group: 'junit', name: 'junit', version: '4.12' } def mysqlTestPort = 3306 def mysqlTestPw = 'root' dcompose { database { image = 'mysql:5.5.53' portBindings = ["$mysqlTestPort:3306"] env = ["MYSQL_ROOT_PASSWORD=$mysqlTestPw"] } } test { dependsOn startDatabaseContainer finalizedBy stopDatabaseContainer doFirst { systemProperty 'mysql.port', mysqlTestPort systemProperty 'mysql.pw', mysqlTestPw } } ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/docker-compose.yml ================================================ version: '2' services: mysql: image: mysql:5.5.53 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "testdb" ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Aug 18 18:26:51 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## 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 # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/gradlew.bat ================================================ @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 :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=%* :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 ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/settings.gradle ================================================ rootProject.name = 'db-container-managed-by-gradle' ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-gradle/src/test/java/de/philipphauer/blog/MyTest.java ================================================ package de.philipphauer.blog; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import javax.sql.DataSource; public class MyTest { private static DataSource dataSource; @BeforeClass public static void init() throws InterruptedException{ Thread.sleep(5_000); //wait for the docker container to start String port = System.getProperty("mysql.port", "3306"); String pw = System.getProperty("mysql.pw", "root"); String url = "jdbc:mysql://localhost:" + port + "?autoReconnect=true"; System.out.println("MySQL URL: " + url +" with pw " + pw); dataSource = DataSourceBuilder.create() .url(url) .username("root") .password(pw) .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } @Test public void foo(){ DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); TransactionStatus transaction = txManager.getTransaction(new DefaultTransactionDefinition()); jdbcTemplate.execute("DROP SCHEMA IF EXISTS testdb"); //dcompose reuses container jdbcTemplate.execute("CREATE SCHEMA testdb"); jdbcTemplate.execute("CREATE TABLE testdb.BAR (" + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + "name varchar(200)" + ");"); txManager.commit(transaction); } } ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-maven/.gitignore ================================================ .idea/ target/ ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-maven/docker-compose.yml ================================================ version: '2' services: mysql: image: mysql:5.5.53 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "testdb" ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-maven/pom.xml ================================================ 4.0.0 de.philipphauer.blog db-container-managed-by-maven 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 1.4.2.RELEASE 3306 root org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java 6.0.5 junit junit 4.12 org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 1.8 io.fabric8 docker-maven-plugin 0.21.0 mysql:5.5.53 mysql ${mysql.test.pw} ${mysql.test.port}:3306 start pre-integration-test start stop post-integration-test stop org.apache.maven.plugins maven-failsafe-plugin 2.18.1 **/*IT.* ${mysql.test.port} ${mysql.test.pw} integration-test verify org.apache.maven.plugins maven-surefire-plugin 2.18.1 **/*IT.* ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-maven/src/test/java/de/philipphauer/blog/MyIT.java ================================================ package de.philipphauer.blog; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import javax.sql.DataSource; public class MyIT { private static DataSource dataSource; @BeforeClass public static void init() throws InterruptedException{ // the maven-docker-plugin already waits. no need to do it here. String port = System.getProperty("mysql.port", "3306"); String pw = System.getProperty("mysql.pw", "root"); String url = "jdbc:mysql://localhost:" + port + "?autoReconnect=true"; System.out.println("MySQL URL: " + url +" with pw " + pw); dataSource = DataSourceBuilder.create() .url(url) .username("root") .password(pw) .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } @Test public void foo(){ DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); TransactionStatus transaction = txManager.getTransaction(new DefaultTransactionDefinition()); jdbcTemplate.execute("DROP SCHEMA IF EXISTS testdb"); jdbcTemplate.execute("CREATE SCHEMA testdb"); jdbcTemplate.execute("CREATE TABLE testdb.BAR (" + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + "name varchar(200)" + ");"); txManager.commit(transaction); } } ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/.gitignore ================================================ .gradle/ .idea/ out/ build/ *.iml ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/build.gradle ================================================ plugins { id 'org.springframework.boot' version '1.4.2.RELEASE' } apply plugin: 'java' group 'de.philipphauer.blog' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile 'org.springframework.boot:spring-boot-starter-jdbc' compile 'mysql:mysql-connector-java:6.0.5' testCompile group: 'junit', name: 'junit', version: '4.12' testCompile('org.testcontainers:mysql:1.4.2') } ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/docker-compose.yml ================================================ version: '2' services: mysql: image: mysql:5.5.53 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "testdb" ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Aug 18 18:26:51 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## 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 # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/gradlew.bat ================================================ @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 :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=%* :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 ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/settings.gradle ================================================ rootProject.name = 'db-container-managed-by-gradle' ================================================ FILE: dont-use-in-memory-databases-tests/db-container-managed-by-the-test/src/test/java/de/philipphauer/blog/MyTest.java ================================================ package de.philipphauer.blog; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.testcontainers.containers.MySQLContainer; import javax.sql.DataSource; public class MyTest { private static DataSource dataSource; private static MySQLContainer mysql; @BeforeClass public static void init() throws InterruptedException{ //You can also use the GenericContainer for arbitrary containers //But there are convenient classes for common databases. mysql = new MySQLContainer("mysql:5.5.53"); mysql.start(); dataSource = DataSourceBuilder.create() .url(mysql.getJdbcUrl()) .username(mysql.getUsername()) .password(mysql.getPassword()) .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } @AfterClass public static void destroy(){ mysql.close(); } @Test public void foo(){ DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); TransactionStatus transaction = txManager.getTransaction(new DefaultTransactionDefinition()); jdbcTemplate.execute("CREATE TABLE BAR (" + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + "name varchar(200)" + ");"); txManager.commit(transaction); } } ================================================ FILE: framework-beats-generator/.gitignore ================================================ db # Created by .ignore support plugin (hsz.mobi) ### Maven template target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties ### Example user template template ### Example user template # IntelliJ project files .idea *.iml out gen ================================================ FILE: framework-beats-generator/pom.xml ================================================ 4.0.0 de.philipphauer framework-beats-generator 1.0-SNAPSHOT org.mongojack mongojack 2.5.1 junit junit 4.12 test com.h2database h2 1.4.190 org.hibernate hibernate-entitymanager 5.0.6.Final javax.transaction javax.transaction-api 1.2 org.assertj assertj-core 3.2.0 test com.github.joelittlejohn.embedmongo embedmongo-maven-plugin 0.3.1 3.2.0 127.0.0.1 true ================================================ FILE: framework-beats-generator/src/main/java/de/philipphauer/h2/H2WebConsole.java ================================================ package de.philipphauer.h2; import org.h2.tools.Server; import java.sql.SQLException; public class H2WebConsole { public static void start() { try { Server server = Server.createWebServer(new String[]{}).start(); System.out.println("H2 can be accessed in port: "+server.getPort()); System.out.println("URL: jdbc:h2:./db/repository"); System.out.println("Empty user and pw"); } catch (SQLException e) { throw new RuntimeException(e); } } } ================================================ FILE: framework-beats-generator/src/main/java/de/philipphauer/jpa/Article.java ================================================ package de.philipphauer.jpa; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Article { @Id @GeneratedValue private int id; private String name; public Article(String name){ this.name = name; } public Article(){ } public String getName() { return name; } public int getId() { return id; } } ================================================ FILE: framework-beats-generator/src/main/java/de/philipphauer/jpa/ArticleDAO.java ================================================ package de.philipphauer.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.persistence.Query; import java.util.Collection; public class ArticleDAO { private final EntityManager entityManager; public ArticleDAO() { EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myPersistenceUnit"); this.entityManager = entityManagerFactory.createEntityManager(); } public void save(Article article) { entityManager.getTransaction().begin(); entityManager.persist(article); entityManager.getTransaction().commit(); } public Collection
findAll() { Query query = entityManager.createQuery("SELECT e FROM Article e"); return (Collection
) query.getResultList(); } public void close(){ entityManager.close(); } } ================================================ FILE: framework-beats-generator/src/main/java/de/philipphauer/mongojack/Product.java ================================================ package de.philipphauer.mongojack; import org.mongojack.ObjectId; public class Product { @ObjectId private String id; private String name; private int price; public Product(String name, int price) { this.name = name; this.price = price; } public String getId() { return id; } public String getName() { return name; } public int getPrice() { return price; } } ================================================ FILE: framework-beats-generator/src/main/java/de/philipphauer/mongojack/ProductDAO.java ================================================ package de.philipphauer.mongojack; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.MongoClient; import com.mongodb.ServerAddress; import org.mongojack.JacksonDBCollection; import java.net.UnknownHostException; public class ProductDAO { public void save(Product product) throws UnknownHostException { MongoClient mongoClient = new MongoClient(new ServerAddress("localhost", 27017)); DB db = mongoClient.getDB("test"); DBCollection collection = db.getCollection("products"); JacksonDBCollection productCollection = JacksonDBCollection.wrap(collection, Product.class, String.class); productCollection.insert(product); mongoClient.close(); } } ================================================ FILE: framework-beats-generator/src/main/resources/META-INF/persistence.xml ================================================ org.hibernate.jpa.HibernatePersistenceProvider ================================================ FILE: framework-beats-generator/src/test/java/de/philipphauer/jpa/ArticleDAOTest.java ================================================ package de.philipphauer.jpa; import de.philipphauer.h2.H2WebConsole; import org.junit.Test; public class ArticleDAOTest { @Test public void saveAndLoad() throws Exception { ArticleDAO dao = new ArticleDAO(); Article article = new Article("Car"); dao.save(article); // Collection
articles = dao.findAll(); // Assertions.assertThat(articles).contains(article); System.out.println("Now you can take a look at the h2 web console..."); H2WebConsole.start(); Thread.sleep(40000); } } ================================================ FILE: framework-beats-generator/src/test/java/de/philipphauer/jpa/H2Test.java ================================================ package de.philipphauer.jpa; import org.junit.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class H2Test { @Test public void connection() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); Connection conn = DriverManager.getConnection("jdbc:h2:~/test", "sa", ""); conn.close(); } } ================================================ FILE: framework-beats-generator/src/test/java/de/philipphauer/mongojack/ProductDAOTest.java ================================================ package de.philipphauer.mongojack; import org.junit.Test; public class ProductDAOTest { @Test public void save() throws Exception { ProductDAO dao = new ProductDAO(); dao.save(new Product("Lego Car", 100)); } } ================================================ FILE: framework-beats-generator/startMongoDBLocally.bat ================================================ rem local mongodb installation needed echo "Creating dbpath" mkdir \data\db\ echo "Starting MongoDB..." start mongod echo "Starting MongoDB Client..." start mongo ================================================ FILE: kotlin-examples/.gitignore ================================================ .idea/ target/ *.iml ================================================ FILE: kotlin-examples/pom.xml ================================================ 4.0.0 de.philipphauer.blog kotlin-examples 1.0-SNAPSHOT 1.0.5-2 1.8 org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} compile compile ${project.basedir}/src/main/kotlin ${project.basedir}/src/main/java test-compile test-compile ${project.basedir}/src/main/kotlin ${project.basedir}/src/main/java org.apache.maven.plugins maven-compiler-plugin 3.5.1 ${java.version} ${java.version} default-compile none default-testCompile none java-compile compile compile java-test-compile test-compile testCompile ================================================ FILE: kotlin-examples/src/main/java/javaVariant/1DefineAndMapBeans.java ================================================ package javaVariant; import java.time.Instant; import java.util.List; import java.util.stream.Collectors; //BlogEntity (received from persistence layer) map to BlogDTO (returned by our REST Service) //Struct definition and mapping code are extremely verbose in java! //Besides, null handling is cumbersome. Especially when it comes to nested objects that can be null. //All values can be null. It's easy to run into NullPointerExceptions. This leads to error-prone code. And even if you add null-checks, it's easy to forget a check (because the compiler doesn't help you) and the code becomes very verbose. //Argument lists are hard to read and error prone class BlogEntity { private long id; private String name; private List posts; //Praise my IDE for generating the constructor and getter boilerplate. Otherwise I would drive nuts. //Moreover, equals(), hashCode(), toString() are still missing! //AND you have to maintain these methods when field are added or removed. public BlogEntity(long id, String name, List posts) { this.id = id; this.name = name; this.posts = posts; } public String getName() { return name; } public List getPosts() { return posts; } public long getId() { return id; } } class PostEntity { private long id; private AuthorEntity author; private Instant date; private String text; private List comments; public PostEntity(long id, AuthorEntity author, Instant date, String text, List comments) { this.id = id; this.author = author; this.date = date; this.text = text; this.comments = comments; } public AuthorEntity getAuthor() { return author; } public Instant getDate() { return date; } public String getText() { return text; } public List getComments() { return comments; } public long getId() { return id; } } class AuthorEntity { private String name; private String email; public AuthorEntity(String name, String email) { this.name = name; this.email = email; } public String getName() { return name; } public String getEmail() { return email; } } class CommentEntity { private String text; private AuthorEntity author; private Instant date; public CommentEntity(String text, AuthorEntity author, Instant date) { this.text = text; this.author = author; this.date = date; } public String getText() { return text; } public AuthorEntity getAuthor() { return author; } public Instant getDate() { return date; } } class BlogDTO{ private long id; private String name; private List posts; public BlogDTO(long id, String name, List posts) { this.id = id; this.name = name; this.posts = posts; } public String getName() { return name; } public List getPosts() { return posts; } public long getId() { return id; } } class PostDTO{ private long id; private String date; private String author; private String text; private String commentsHref; public PostDTO(long id, String date, String author, String text, String commentsHref) { this.id = id; this.date = date; this.author = author; this.text = text; this.commentsHref = commentsHref; } public String getAuthor() { return author; } public String getDate() { return date; } public String getText() { return text; } public String getCommentsHref() { return commentsHref; } public long getId() { return id; } } class Mapper { public List mapToBlogDTOs(List entities){ //verbose stream api return entities.stream() .map(this::mapToBlogDTO) .collect(Collectors.toList()); } private BlogDTO mapToBlogDTO(BlogEntity entity){ return new BlogDTO( entity.getId(), entity.getName(), mapToPostDTO(entity.getPosts()) ); } private List mapToPostDTO(List posts) { //what if posts is null?! very unsafe code! return posts.stream() .map(this::mapToPostDTO) .collect(Collectors.toList()); } private PostDTO mapToPostDTO(PostEntity post) { //what if author, date, text or comments are null?! error-prone code! //easy to mess up parameter order (most of them are strings). hard to understand meaning of last parameter. return new PostDTO( post.getId(), post.getDate() != null ? post.getDate().getEpochSecond()+ "" : null, //hard to read. easy to forget null check. getNameOrDefault(post.getAuthor()), post.getText(), "posts/" + post.getId() + "/comments" ); } //null checks bloat code. very verbose. hard to read. private String getNameOrDefault(AuthorEntity author) { if (author != null){ String name = author.getName(); if (name != null){ return name; } } return "Anonymous"; } } ================================================ FILE: kotlin-examples/src/main/java/javaVariant/2ConditionsAndTypeSwitch.java ================================================ package javaVariant; import java.sql.SQLException; import java.util.Locale; class Conditions { public Locale getDefaultLocale(String deliveryArea){ if (deliveryArea.equals("germany") || deliveryArea.equals("austria") || deliveryArea.equals("switzerland")) { return Locale.GERMAN; } if (deliveryArea.equals("usa") || deliveryArea.equals("great britain") || deliveryArea.equals("australia")) { return Locale.ENGLISH; } throw new IllegalArgumentException("Unsupported deliveryArea " + deliveryArea); } //or via switch and fall-through //verbose. annoying type cast. public String getExceptionMessage(Exception exception){ if (exception instanceof MyLabeledException){ return ((MyLabeledException) exception).getLabel(); } else if (exception instanceof SQLException){ return exception.getMessage() + ". state: " + ((SQLException) exception).getSQLState(); } else { return exception.getMessage(); } } } class MyLabeledException extends RuntimeException{ private String label; public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } } ================================================ FILE: kotlin-examples/src/main/java/kotlinVariant/1DefineAndMapBeans.kt ================================================ package kotlinVariant import java.time.Instant //Data classes: Each entity definition in a single line! We get immutability, constructor, hashCode(), equals(), toString() for free. class BlogEntity(val id: Long, val name: String, val posts: List?) class PostEntity(val id: Long, val date: Instant?, val author: AuthorEntity?, val text: String, comments: List?) class AuthorEntity(val name: String, val email: String?) class CommentEntity(val text: String, val author: AuthorEntity, date: Instant) //"val" makes the beans immutable ("var" fields can be modified). //The type "String" can never be null. The compiler enforces this! //Contrarily, the type "String?" is nullable. Null-safe types make the code much safer and avoid bloating null checks. class BlogDTO(val id: Long, val name: String, val posts: List?) class PostDTO(val id: Long, val author: String, val date: String?, val text: String, commentsHref: String) //Usage of "single expression function": No body {} is necessary, if there is only a single expression. //Less boilerplate with the collection API : we can call map() directly on a list and it returns a list. //Implicit variable "it" (= parameter) makes lambda syntax even shorter. but you can also write "para -> mapToBlogDTO(para)". fun mapToBlogDTOs(entities: List) = entities.map { mapToBlogDTO(it) } fun mapToBlogDTO(entity: BlogEntity) = BlogDTO( id = entity.id, name = entity.name, posts = entity.posts?.map { mapToPostDTO(it) } //The Kotlin compiler forces me to consider that posts can be null. //We can't call map() directly on posts, because it can be null. //The null-safe call ("?.") invokes the operation only if posts are not null. Otherwise the whole expression is null. ) fun mapToPostDTO(entity: PostEntity) = PostDTO( //easy to read due to named arguments. id = entity.id, date = entity.date?.epochSecond.toString(), //"?." (null safe call). if date is null, null is assigned. Otherwise the epochSecond is retrieved and assigned. author = entity.author?.name ?: "Anonymous", //The elvis operator ("?:") makes Java's getNameOrDefault() a one-liner! If left side of "?:" is null, the right side is returned. Otherwise the left side is returned. text = entity.text, commentsHref = "posts/${entity.id}/comments" //String interpolation! ) //warp up: kotlin code is... // a) extremely concise (data classes, single expression function, field accessor, compact list operations, null-safe calls, elvis operator), //lines of code: 201 lines (java) vs 18 lines (kotlin)! (and java doesn't include hashCode(), toString(), hashCode()) //=> factor 11+ when it comes to code for structs and mapping!! no boilerplate in Kotlin. // b) more readable (named arguments) and // c) less error-prone (compiler enforced null safety, immutability, no manually written toString(), hashCode(), equals()). // extremely reduced boilerplate. ================================================ FILE: kotlin-examples/src/main/java/kotlinVariant/2ConditionsAndTypeSwitch.kt ================================================ package kotlinVariant import java.sql.SQLException import java.util.Locale //"when" is extremely powerful construct is Kotlin. It's much more than just a switch. fun getDefaultLocale(deliveryArea: String): Locale { when (deliveryArea){ "germany", "austria", "switzerland" -> return Locale.GERMAN "usa", "great britain", "australia" -> return Locale.ENGLISH else -> throw IllegalArgumentException("Unsupported deliveryArea $deliveryArea") //string interpolation } } // or even shorter as a single expression function: fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea){ "germany", "austria", "switzerland" -> Locale.GERMAN "usa", "great britain", "australia" -> Locale.ENGLISH else -> throw IllegalArgumentException("Unsupported deliveryArea $deliveryArea") } fun getExceptionMessage(exception: Exception) = when (exception){ //concise type switches is MyLabeledException -> exception.label //smart cast to MyLabeledException -> we can call label directly. is SQLException -> "${exception.message}. state: ${exception.sqlState}" //string interpolation else -> exception.message } class MyLabeledException(val label: String) : RuntimeException(label) ================================================ FILE: kotlin-idiomatic/.gitignore ================================================ .idea/ target/ *.iml ================================================ FILE: kotlin-idiomatic/pom.xml ================================================ 4.0.0 de.philipphauer.blog kotlin-idiomatic 1.0-SNAPSHOT 1.1.1 1.8 org.jetbrains.kotlin kotlin-stdlib-jre8 ${kotlin.version} com.jayway.jsonpath json-path 2.2.0 com.vaadin vaadin-server 8.0.3 com.vaadin vaadin-client-compiled 8.0.3 org.apache.httpcomponents httpclient 4.5.3 org.jetbrains.kotlin kotlin-test ${kotlin.version} test org.jetbrains.kotlin kotlin-stdlib-jre8 ${kotlin.version} org.apache.commons commons-dbcp2 2.1.1 src/main/kotlin kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} compile compile compile test-compile test-compile test-compile org.apache.maven.plugins maven-compiler-plugin 3.5.1 ${java.version} ${java.version} default-compile none default-testCompile none java-compile compile compile java-test-compile test-compile testCompile ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/Apply.kt ================================================ package idiomaticKotlin import org.apache.commons.dbcp2.BasicDataSource fun blub(){ // Don't val dataSource = BasicDataSource() dataSource.driverClassName = "com.mysql.jdbc.Driver" dataSource.url = "jdbc:mysql://domain:3309/db" dataSource.username = "username" dataSource.password = "password" dataSource.maxTotal = 40 dataSource.maxIdle = 40 dataSource.minIdle = 4 } // Do val dataSource = BasicDataSource().apply { driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://domain:3309/db" username = "username" password = "password" maxTotal = 40 maxIdle = 40 minIdle = 4 } ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/DefaultArgs.kt ================================================ package idiomaticKotlin // don't overload methods and constructors to realize default arguments ("chaining") fun find(name: String){ find(name, true) } fun find(name: String, recursive: Boolean){ } // that are crutches. instead, Kotlin provides use named arguments fun find2(name: String, recursive: Boolean = true){ } // in fact, default arguments removed nearly all use cases for method and constructor overloading. ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/Destruction.kt ================================================ package idiomaticKotlin //destruction useful for //a) returning multiple values from a function. define an own data class or use Pair (but less expressive, no semantics) data class ServiceConfig(val host: String, val port: Int) fun createServiceConfig(): ServiceConfig { return ServiceConfig("api.domain.io", 9389) } fun bla(){ val (host, port) = createServiceConfig() } //b) iterate over maps fun foo(){ val map = mapOf("api.domain.io" to 9389, "localhost" to 8080) for ((host, port) in map){ //... } } ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/FunctionalProgramming.kt ================================================ package idiomaticKotlin import com.jayway.jsonpath.JsonPath import java.util.Locale // # Take advantages of functional programming support in Kotlin (better support then in java due to immutability and expression) // -> reduce side-effects (less error-prone, easier to understand, thread-safe) // (start with an enumeration of the relevant ##-points) // ## use immutability (val for variables and properties, immutable data classes, copy(), kotlin's collection api (read-only)) data class Person(var name: String) //better: data class Person2(val name: String) //var x = "hi" //// better: //val y = "hallo" // ## use pure functions (without side-effects) where ever possible (therefore, use expressions and single expression functions) // ## use if, when, try-catch, single expression function! -> concise, expressive, stateless // expression instead of statements (if, when) -> combine control structure with other expression concisely // Don't: fun getDefaultLocale(deliveryArea: String): Locale { val deliverAreaLower = deliveryArea.toLowerCase() if (deliverAreaLower == "germany" || deliverAreaLower == "austria") { return Locale.GERMAN } if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") { return Locale.ENGLISH } if (deliverAreaLower == "french") { return Locale.FRENCH } return Locale.ENGLISH } // Do: fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) { "germany", "austria" -> Locale.GERMAN "usa", "great britain" -> Locale.ENGLISH "french" -> Locale.FRENCH else -> Locale.ENGLISH } //println(getDefaultLocale("germany")) // in general: consider if an `if` can be replace with a more concise `when` expression. //try-catch is also an expression! val json = """{"message":"HELLO"}""" val message: String = try { JsonPath.parse(json).read("message") } catch (ex: Exception) { json } //println(getMessage("""{"message":"HELLO"}""")) //hello // ## use lambda expression to pass around blocks of code. fun main(args: Array) { println(getDefaultLocale("germany")) println(message) //hello } ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/InitBlock.kt ================================================ package idiomaticKotlin import org.apache.http.client.HttpClient import org.apache.http.impl.client.HttpClientBuilder import java.util.concurrent.TimeUnit // think twice before you define a init block/constructor body // (first, named and default argument can help) // second, you can refer to primary constructor para in property initializers (and not only in the init block) // apply() can help to group initialization code and get along with a single expression. // Don't class UsersClient(baseUrl: String, appName: String) { private val usersUrl: String private val httpClient: HttpClient init { usersUrl = "$baseUrl/users" val builder = HttpClientBuilder.create() builder.setUserAgent(appName) builder.setConnectionTimeToLive(10, TimeUnit.SECONDS) httpClient = builder.build() } fun getUsers(){ //call service using httpClient and usersUrl } } // Do class UsersClient2(baseUrl: String, appName: String) { private val usersUrl = "$baseUrl/users" private val httpClient = HttpClientBuilder.create().apply { setUserAgent(appName) setConnectionTimeToLive(10, TimeUnit.SECONDS) }.build() fun getUsers(){ //call service using httpClient and usersUrl } } //- `with()` returns result of lambda (and it's invoked statically) //- `apply()` returns receiver obj (and it's invoked on receiver obj) ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/Mapping.kt ================================================ package idiomaticKotlin import java.time.Instant data class SnippetDTO( val code: String, val author: String, val date: Instant ) data class SnippetEntity( val code: String, val author: AuthorEntity, val date: Instant ) data class AuthorEntity( val firstName: String, val lastName: String ) // Don't fun mapToDTO(entity: SnippetEntity): SnippetDTO { val dto = SnippetDTO( code = entity.code, date = entity.date, author = "${entity.author.firstName} ${entity.author.lastName}" ) return dto } // Do // DONE easy, concise and readable mapping between value objects with single expression functions and named arguments fun mapToDTO2(entity: SnippetEntity) = SnippetDTO( code = entity.code, date = entity.date, author = "${entity.author.firstName} ${entity.author.lastName}" ) // even better: as an extension function fun SnippetEntity.toDTO() = SnippetDTO( code = code, date = date, author = "${author.firstName} ${author.lastName}" ) //val entity = SnippetEntity() //val dto = entity.toDTO() ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/NamedArgs.kt ================================================ package idiomaticKotlin class SearchConfig { fun setRoot(s: String): SearchConfig { return this } fun setTerm(s: String): SearchConfig { return this } fun setRecursive(s: Boolean): SearchConfig { return this } fun setFollowSymlinks(s: Boolean): SearchConfig { return this } } //Back in Java, fluent setters where used to simulate named and default arguments and avoid huge parameter lists (error-prone and hard to read). val config = SearchConfig() .setRoot("~/folder") .setTerm("kotlin") .setRecursive(true) .setFollowSymlinks(true) //in kotlin, named and default arguments are built into the language val config2 = SearchConfig2( root = "~/folder", term = "kotlin", recursive = true, followSymlinks = true ) //definition: class SearchConfig2( val root: String, val term: String, val recursive: Boolean = false, val followSymlinks: Boolean = false ) ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/Nullability.kt ================================================ package idiomaticKotlin data class Order(val customer: Customer?) data class Customer(val address: Address?) data class Address(val city: String) fun ship(order: Order?){ //every time you do if-null-checks, hold on. if (order == null || order.customer == null || order.customer.address == null){ throw IllegalArgumentException("Invalid Order") } val city = order.customer.address.city } fun ship2(order: Order?){ // Often, you can use null-safe call (?.) or the elvis operator (?:) instead. val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order") } interface Service class CustomerService : Service { fun getCustomer() {} } fun getMetrics(service: Service){ // also hold on for if-type-checks if (service !is CustomerService) { throw IllegalArgumentException("No CustomerService") } service.getCustomer() } fun getMetrics2(service: Service){ //check type, (smart-)cast it and throw exception if the type is not the expected one. all in one expression! service as? CustomerService ?: throw IllegalArgumentException("No CustomerService") service.getCustomer() } fun foo(order: Order?){ // avoid yelling !! where every possible. search for better solutions by verifying the variable up front and handle nulls. (quote book) order!!.customer!!.address!!.city } fun findOrder(): Order? { return null } fun dun(customer: Customer?){ } fun handle(){ // Don't val order: Order? = findOrder() if (order != null){ dun(order.customer) } // with let(), there is no need for an extra variable // can write as one expression findOrder()?.let { dun(it.customer) } findOrder()?.customer?.let(::dun) } ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/ObjectForStatelessFWImpls.kt ================================================ package idiomaticKotlin import com.vaadin.data.Converter import com.vaadin.data.Result import com.vaadin.data.ValueContext import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException import java.util.Locale // use object for singletons or implemention of a framework interface without state. // here: Vaadin 8's Converter interface object StringToInstantConverter : Converter { private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z") .withLocale( Locale.UK ) .withZone( ZoneOffset.UTC ) override fun convertToModel(value: String?, context: ValueContext?) = try { Result.ok(Instant.from(DATE_FORMATTER.parse(value))) } catch (ex: DateTimeParseException) { Result.error(ex.message) } override fun convertToPresentation(value: Instant?, context: ValueContext?) = DATE_FORMATTER.format(value) } fun main(args: Array) { val i = StringToInstantConverter.convertToPresentation(Instant.now(), null) println(i) val x = StringToInstantConverter.convertToModel(i, null) println(x) } ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/Structs.kt ================================================ package idiomaticKotlin // due to `to` infix function to create a Pair and static methods to create lists and maps, defining structs is reasonable in kotlin. // but still not as good as in Python or JavaScript. But it's ok and way better then in Java. val customer = mapOf( "name" to "Clair Grube", "age" to 30, "languages" to listOf("german", "english"), "address" to mapOf( "city" to "Leipzig", "street" to "Karl-Liebknecht-Straße 1", "zipCode" to "04107" ) ) ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/TopLevelExtensionFunctions.kt ================================================ package idiomaticKotlin // In Java, you often create static util methods in util classes. object StringUtil { fun countAmountOfX(string: String): Int{ return string.length - string.replace("x", "").length } } //StringUtil.countAmountOfX("xKotlinxFunx") //3 // In Kotlin, remove the unnecessary wrapping util class and use top-level functions instead // Often, you can additionally use extension functions, which increases readability ("like a story"). fun String.countAmountOfX(): Int { return length - replace("x", "").length } //"xKotlinxFunx".countAmountOfX() //3 ================================================ FILE: kotlin-idiomatic/src/main/kotlin/idiomaticKotlin/ValueObjects.kt ================================================ package idiomaticKotlin //without value object: fun send(target: String){} //expressive, readable, safe fun send(target: EmailAddress){} //with value object: data class EmailAddress(val value: String) ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/.gitignore ================================================ target/ .idea *.iml dev/ ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/README.md ================================================ # Scaffolding for Kotlin, Spring Boot, Vaadin, Spring Data MongoDB ## Development ### Database Start/Stop MongoDB and MySQL ``` $ docker-compose up # -d flag for background $ docker-compose down ``` Access MongoDB ``` $ mongo ``` Access MySQL: 1. a) Use a MySQL client and connect to localhost:3306 with root/root. 2. b) use mysql cli: ``` $ mysql -h localhost -P 3306 --protocol=tcp -D dbname -u root -p dbname enter "root" ``` ### Application Start MyApplication.kt out of your IDE. But don't forget to pass the yml (program arguments): ``` --spring.config.location=myapp.yaml ``` ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/TODO.md ================================================ - tests - screeny ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/docker-compose.yml ================================================ version: '2' services: mongo: image: mongo:3.2.10 volumes: - ./dev/mongodbdata:/data/db ports: - "27017:27017" command: --smallfiles --profile=1 --slowms=0 ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/myapp.yaml ================================================ server: port: 8080 spring: data: mongodb: host: localhost port: 27017 database: test datasource: url: "jdbc:mysql://localhost:3306/dbname" username: "root" password: "root" driverClassName: "com.mysql.cj.jdbc.Driver" vaadin: servlet: productionMode: false myapp: requiredProp: "value" authentication: url: "https://blabla.de" credentials: userName: "asdf" userPassword: "asdf" ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/pom.xml ================================================ 4.0.0 de.philipphauer.blog kotlin-spring-boot-vaadin-scaffolding 0.0.1-SNAPSHOT jar org.springframework.boot spring-boot-starter-parent 1.5.1.RELEASE UTF-8 UTF-8 1.8 1.0.6 org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.jetbrains.kotlin kotlin-test-junit ${kotlin.version} test org.springframework.boot spring-boot-starter-data-mongodb com.vaadin vaadin-spring-boot-starter 1.2.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.google.guava guava 20.0 com.vaadin vaadin-bom 7.7.3 pom import src/main/kotlin src/test/kotlin kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} spring org.jetbrains.kotlin kotlin-maven-allopen ${kotlin.version} compile compile compile test-compile test-compile test-compile org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 3.5.1 default-compile none default-testCompile none java-compile compile compile java-test-compile test-compile testCompile compile compile compile testCompile test-compile testCompile pl.project13.maven git-commit-id-plugin 2.2.1 validate revision 7 git yyyy-MM-dd'T'HH:mm:ss true ${project.basedir}/.git true false false false false false 7 -dirty false vaadin-addons http://maven.vaadin.com/vaadin-addons ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/PuttingTogether.kt ================================================ package de.philipphauer.blog.misc import java.time.Instant data class BlogEntity ( val author: AuthorEntity, val date: Instant, val content: String ) data class AuthorEntity(val firstName: String, val lastName: String) ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/ValueObjects.kt ================================================ package de.philipphauer.blog.misc fun process1(emails: List){} //vs data class EmailAddress(val address: String) fun process2(emails: List){} //=> expressive, readable, safe ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/constructorinjection/CRMClient.java ================================================ package de.philipphauer.blog.misc.constructorinjection; public class CRMClient { } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/constructorinjection/ConstructorInjection.kt ================================================ package de.philipphauer.blog.misc.constructorinjection class CustomerResourceKotlin(private val repo: CustomerRepository, private val client: CRMClient){ //that's it! } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/constructorinjection/CustomerRepository.java ================================================ package de.philipphauer.blog.misc.constructorinjection; public class CustomerRepository { } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/constructorinjection/CustomerResource.java ================================================ package de.philipphauer.blog.misc.constructorinjection; public class CustomerResource { private CustomerRepository repo; private CRMClient client; public CustomerResource(CustomerRepository repo, CRMClient client) { this.repo = repo; this.client = client; } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/vaadin/ActionListenerLambdaExample.kt ================================================ package de.philipphauer.blog.misc.vaadin import com.vaadin.ui.Button import com.vaadin.ui.Notification class ActionListenerLambdasExampleView { private fun init() { val button = Button("Click Me") button.addClickListener(::greet) button.addClickListener{event -> greet(event)} button.addClickListener{greet(it)} } } private fun greet(event: Button.ClickEvent){ Notification.show("Hello!") } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/misc/vaadin/ActionListenerLambdaExampleJava.java ================================================ package de.philipphauer.blog.misc.vaadin; import com.vaadin.ui.Button; import com.vaadin.ui.Notification; public class ActionListenerLambdaExampleJava { private void bla(){ Button b = new Button(); b.addClickListener(this::greet); b.addClickListener(event -> greet(event)); b.addClickListener(event -> {greet(event);}); } private void greet(Button.ClickEvent event){ Notification.show("Hello!"); } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/DummyDataCreator.kt ================================================ package de.philipphauer.blog.scaffolding import de.philipphauer.blog.scaffolding.db.AuthorEntity import de.philipphauer.blog.scaffolding.db.SnippetEntity import de.philipphauer.blog.scaffolding.db.SnippetRepository import de.philipphauer.blog.scaffolding.db.SnippetState import org.slf4j.Logger import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner import org.springframework.stereotype.Component import java.time.Instant @Component class DummyDataCreator(val repo: SnippetRepository, val logger: Logger) : ApplicationRunner { override fun run(args: ApplicationArguments) { val count = repo.count() if (count == 0L){ logger.info("Inserting dummy data...") val entity = SnippetEntity( id = null, //set by db code = "Select * From dual;", date = Instant.now(), state = SnippetState.EXECUTION_SUCCESS, author = AuthorEntity( firstName = "Peter", lastName = "Fischer" ) ) repo.insert(entity) } } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/MyApplication.kt ================================================ package de.philipphauer.blog.scaffolding import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication @EnableAutoConfiguration @EnableScheduling class MyApplication fun main(args: Array) { SpringApplication.run(MyApplication::class.java, *args) } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/SpringConfiguration.kt ================================================ package de.philipphauer.blog.scaffolding import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.InjectionPoint import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Scope @Configuration class SpringConfiguration { @Bean @Scope("prototype") fun logger(injectionPoint: InjectionPoint): Logger { return LoggerFactory.getLogger(injectionPoint.methodParameter.containingClass) } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/YamlConfigProps.kt ================================================ package de.philipphauer.blog.scaffolding import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration import javax.validation.constraints.NotNull @Configuration @ConfigurationProperties(prefix = "myapp") class MyAppProps { @NotNull lateinit var requiredProp: String var optionalProp: String? = null } @Configuration @ConfigurationProperties(prefix = "myapp.authentication") class AuthenticationProps { @NotNull lateinit var url: String var credentials: Credentials? = null } class Credentials{ @NotNull lateinit var userName: String @NotNull lateinit var userPassword: String } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/db/Entities.kt ================================================ package de.philipphauer.blog.scaffolding.db import org.bson.types.ObjectId import org.springframework.data.annotation.Id import org.springframework.data.annotation.PersistenceConstructor import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant @Document(collection="snippets") data class SnippetEntity @PersistenceConstructor constructor( @Id val id: ObjectId? = null, //default parameter value requires @PersistenceConstructor val code: String, val author: AuthorEntity, val date: Instant, val state: SnippetState ) data class AuthorEntity( val firstName: String, val lastName: String ) enum class SnippetState {EXECUTION_SUCCESS, EXECUTION_FAIL} ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/db/SnippetRepository.kt ================================================ package de.philipphauer.blog.scaffolding.db import org.springframework.data.mongodb.repository.MongoRepository interface SnippetRepository : MongoRepository ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/rest/AdminResource.kt ================================================ package de.philipphauer.blog.scaffolding.rest import com.google.common.io.Resources import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ResponseBody import java.nio.charset.StandardCharsets import java.time.Instant import java.util.Properties private const val LAST_MONGO_CHECK_TOLERANCE_MS = 120000L private const val INITIAL_MONGO_CHECK_DELAY_MS = 2000L private const val MONGO_CHECK_INTERVAL_MS = 20000L @Controller class AdminResource(val mongo: MongoTemplate) { val versionDTO: VersionDTO by lazy { createVersionDTO() } @Volatile private var lastSuccessfulStatusCheck: Instant = Instant.now() @Scheduled(initialDelay = INITIAL_MONGO_CHECK_DELAY_MS, fixedDelay = MONGO_CHECK_INTERVAL_MS) fun checkMongoStatus() { val commandResult = mongo.executeCommand("{ping:1}") if (commandResult.ok()) { lastSuccessfulStatusCheck = Instant.now() } } @GetMapping("/status", produces = arrayOf(MediaType.TEXT_PLAIN_VALUE)) fun getStatus(): ResponseEntity = if (lastSuccessfulStatusCheck.isAfter(Instant.now().minusMillis(LAST_MONGO_CHECK_TOLERANCE_MS))) ResponseEntity("OK", HttpStatus.OK) else ResponseEntity("MongoDB is not available since $lastSuccessfulStatusCheck", HttpStatus.INTERNAL_SERVER_ERROR) @GetMapping("/version", produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE)) @ResponseBody fun getVersion() = versionDTO @GetMapping("favicon.ico") fun favicon() = "forward:/VAADIN/themes/mytheme/favicon.ico" @GetMapping("/") fun redirectToUI() = "redirect:/ui" //the values won't be set when you start the application in the IDE. private fun createVersionDTO() = readGitProperties().let { VersionDTO(name = "myapp", version = it.getProperty("api.version"), buildTime = it.getProperty("git.build.time"), commit = CommitVersionDTO( revision = it.getProperty("git.commit.id"), message = it.getProperty("git.commit.message.full"), author = it.getProperty("git.commit.user.name"), time = it.getProperty("git.commit.time"), branch = it.getProperty("git.branch")) ) } private fun readGitProperties(): Properties { val gitPropertyUrl = Resources.getResource("application.properties") val charSource = Resources.asCharSource(gitPropertyUrl, StandardCharsets.UTF_8) return Properties().apply { load(charSource.openBufferedStream()) } } } data class VersionDTO( val name: String, val version: String, val buildTime: String, val commit: CommitVersionDTO ) data class CommitVersionDTO( val revision: String, val message: String, val author: String, val time: String, val branch: String ) ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/Beans.kt ================================================ package de.philipphauer.blog.scaffolding.ui import de.philipphauer.blog.scaffolding.db.SnippetState import java.time.Instant data class SnippetOverviewBean( val code: String, val author: String, val date: Instant, val state: SnippetState ) data class SnippetCreationBean( var code: String? = null, var author: String? = null ) object PropertyIds{ const val CODE = "code" const val AUTHOR = "author" const val DATE = "date" const val STATE = "state" } object Labels{ const val CODE = "Code" const val AUTHOR = "Author" const val DATE = "Date" const val STATE = "State" } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/DetailsWindow.kt ================================================ package de.philipphauer.blog.scaffolding.ui import com.vaadin.server.FontAwesome import com.vaadin.server.Sizeable import com.vaadin.shared.ui.label.ContentMode import com.vaadin.ui.Button import com.vaadin.ui.FormLayout import com.vaadin.ui.Label import com.vaadin.ui.Window class DetailsWindow(snippet: SnippetOverviewBean) : Window(){ init { caption = "Snippet Details" isModal = true val layout = FormLayout().apply { setMargin(true) isSpacing = true val codeLabel = Label().apply { contentMode = ContentMode.HTML caption = Labels.CODE value = snippet.code } val authorLabel = Label().apply { caption = Labels.AUTHOR value = snippet.author } val stateLabel = Label().apply { contentMode = ContentMode.HTML caption = Labels.STATE value = "${snippet.state.toIcon().html} ${snippet.state.toLabel()}" } val closeButton = Button("Close", FontAwesome.CLOSE).apply { addClickListener { close() } } addComponents(codeLabel, authorLabel, stateLabel, closeButton) } setWidth(50F, Sizeable.Unit.PERCENTAGE) setHeight(50F, Sizeable.Unit.PERCENTAGE) center() content = layout } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/EntityToBeanMapper.kt ================================================ package de.philipphauer.blog.scaffolding.ui import de.philipphauer.blog.scaffolding.db.SnippetEntity fun mapToBeans(entities: List) = entities.map(::mapToBean) fun mapToBean(entity: SnippetEntity) = SnippetOverviewBean( code = entity.code, date = entity.date, author = "${entity.author.firstName} ${entity.author.lastName}", state = entity.state ) ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/MainViewDisplay.kt ================================================ package de.philipphauer.blog.scaffolding.ui import com.vaadin.navigator.View import com.vaadin.navigator.ViewDisplay import com.vaadin.spring.annotation.SpringViewDisplay import com.vaadin.ui.Component import com.vaadin.ui.Panel import com.vaadin.ui.themes.ValoTheme @SpringViewDisplay class MainViewDisplay : Panel(), ViewDisplay { init { styleName = ValoTheme.PANEL_BORDERLESS } override fun showView(view: View) { content = view as Component } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/MyAppUI.kt ================================================ package de.philipphauer.blog.scaffolding.ui import com.vaadin.annotations.Theme import com.vaadin.server.FontAwesome import com.vaadin.server.Sizeable import com.vaadin.server.VaadinRequest import com.vaadin.shared.ui.label.ContentMode import com.vaadin.spring.annotation.SpringUI import com.vaadin.spring.navigator.SpringNavigator import com.vaadin.ui.Label import com.vaadin.ui.UI import com.vaadin.ui.VerticalLayout import com.vaadin.ui.themes.ValoTheme @SpringUI(path = "ui") @Theme("mytheme") class MyAppUI(val mainContent: MainViewDisplay, navigator: SpringNavigator) : UI() { val navigationPresenter = NavigationPresenter(navigator) override fun init(request: VaadinRequest) { page.setTitle("Kotlin, Spring Boot, Vaadin") content = VerticalLayout(createHeader(), navigationPresenter.menu, mainContent).apply{ setExpandRatio(mainContent, 1f) setHeight(100f, Sizeable.Unit.PERCENTAGE) setMargin(true) } navigationPresenter.disableCurrentMenuItem() } private fun createHeader(): Label { val heading = Label("${FontAwesome.CODE.html} My Application with Kotlin, Spring Boot and Vaadin").apply { styleName = ValoTheme.LABEL_HUGE contentMode = ContentMode.HTML } return heading } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/NavigationPresenter.kt ================================================ package de.philipphauer.blog.scaffolding.ui import com.vaadin.navigator.View import com.vaadin.server.FontAwesome import com.vaadin.server.Page import com.vaadin.server.Resource import com.vaadin.spring.annotation.SpringView import com.vaadin.spring.navigator.SpringNavigator import com.vaadin.ui.MenuBar import com.vaadin.ui.themes.ValoTheme import de.philipphauer.blog.scaffolding.ui.views.CreateSnippetView import de.philipphauer.blog.scaffolding.ui.views.ErrorView import de.philipphauer.blog.scaffolding.ui.views.OverviewView class NavigationPresenter(val navigator: SpringNavigator){ val menu: MenuBar private val viewNameToMenuBar: Map init { navigator.setErrorView(ErrorView::class.java) menu = MenuBar() menu.addStyleName(ValoTheme.MENUBAR_SMALL) menu.addStyleName(ValoTheme.MENUBAR_BORDERLESS) val overviewItem = createMenuItem(OverviewView.LABEL, OverviewView::class.java, FontAwesome.LIST) val createItem = createMenuItem(CreateSnippetView.LABEL, CreateSnippetView::class.java, FontAwesome.CODE) viewNameToMenuBar = mapOf(OverviewView.VIEW_NAME to overviewItem, CreateSnippetView.VIEW_NAME to createItem) } private fun createMenuItem(label: String, viewClass: Class, icon: Resource): MenuBar.MenuItem { val viewAnnotation = viewClass.getDeclaredAnnotation(SpringView::class.java) return menu.addItem(label, icon, MenuBar.Command { navigateTo(viewAnnotation.name )}) } fun navigateTo(view: String) { navigator.navigateTo(view) disableMenuItem(view) } fun disableMenuItem(view: String) { for ((viewName, menuItem) in viewNameToMenuBar){ menuItem.isEnabled = viewName != view } } fun disableCurrentMenuItem() { when (Page.getCurrent().uriFragment?.removePrefix("!")) { CreateSnippetView.VIEW_NAME -> disableMenuItem(CreateSnippetView.VIEW_NAME) null, OverviewView.VIEW_NAME -> disableMenuItem(OverviewView.VIEW_NAME) } } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/UiMisc.kt ================================================ package de.philipphauer.blog.scaffolding.ui import com.vaadin.server.FontAwesome import de.philipphauer.blog.scaffolding.db.SnippetState fun SnippetState.toIcon() = when (this){ SnippetState.EXECUTION_SUCCESS -> FontAwesome.THUMBS_O_UP SnippetState.EXECUTION_FAIL -> FontAwesome.THUMBS_O_DOWN } fun SnippetState.toLabel() = when (this){ SnippetState.EXECUTION_SUCCESS -> "Successfully executed" SnippetState.EXECUTION_FAIL -> "Execution failed" } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/views/CreateSnippetView.kt ================================================ package de.philipphauer.blog.scaffolding.ui.views import com.vaadin.data.fieldgroup.BeanFieldGroup import com.vaadin.data.fieldgroup.PropertyId import com.vaadin.navigator.View import com.vaadin.navigator.ViewChangeListener import com.vaadin.server.FontAwesome import com.vaadin.server.Sizeable import com.vaadin.spring.annotation.SpringView import com.vaadin.ui.Button import com.vaadin.ui.FormLayout import com.vaadin.ui.Notification import com.vaadin.ui.TextArea import com.vaadin.ui.TextField import com.vaadin.ui.VerticalLayout import de.philipphauer.blog.scaffolding.db.SnippetRepository import de.philipphauer.blog.scaffolding.ui.Labels import de.philipphauer.blog.scaffolding.ui.PropertyIds import de.philipphauer.blog.scaffolding.ui.SnippetCreationBean import javax.annotation.PostConstruct @SpringView(name = CreateSnippetView.VIEW_NAME) class CreateSnippetView(val repo: SnippetRepository) : VerticalLayout(), View { companion object{ const val VIEW_NAME = "create" const val LABEL = "Create Snippet" } lateinit var fieldGroup: BeanFieldGroup override fun enter(event: ViewChangeListener.ViewChangeEvent) { } @PostConstruct internal fun init() { val form = CreateSnippetForm().apply { createButton.addClickListener { createSnippet() } } val emptySnippet = SnippetCreationBean() fieldGroup = BeanFieldGroup.bindFieldsUnbuffered(emptySnippet, form) setSizeFull() addComponent(form) setExpandRatio(form, 1f) } private fun createSnippet() { if (fieldGroup.isValid){ Notification.show("Snippet: ${fieldGroup.itemDataSource.bean}") // Snippet creation should go here... } else { Notification.show("Invalid!", Notification.Type.ERROR_MESSAGE) } } } class CreateSnippetForm : FormLayout() { @PropertyId(PropertyIds.CODE) val code = TextArea(Labels.CODE).apply { nullRepresentation = "" setWidth(100f, Sizeable.Unit.PERCENTAGE) isRequired = true addStyleName("monospace") } @PropertyId(PropertyIds.AUTHOR) val author = TextField(Labels.AUTHOR).apply { nullRepresentation = "" setWidth(100f, Sizeable.Unit.PERCENTAGE) } val createButton = Button("Create Snippet", FontAwesome.CODE) init { setSizeFull() isSpacing = true addComponents(code, author, createButton) } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/views/ErrorView.kt ================================================ package de.philipphauer.blog.scaffolding.ui.views import com.vaadin.navigator.View import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent import com.vaadin.spring.annotation.SpringComponent import com.vaadin.spring.annotation.SpringView import com.vaadin.spring.annotation.UIScope import com.vaadin.ui.Label import com.vaadin.ui.VerticalLayout import javax.annotation.PostConstruct @SpringComponent @UIScope @SpringView class ErrorView : VerticalLayout(), View { override fun enter(event: ViewChangeEvent) { } @PostConstruct internal fun init() { addComponent(Label("Invalid View Fragment in URL!")) } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/kotlin/de/philipphauer/blog/scaffolding/ui/views/OverviewView.kt ================================================ package de.philipphauer.blog.scaffolding.ui.views import com.vaadin.data.util.BeanItem import com.vaadin.data.util.BeanItemContainer import com.vaadin.data.util.converter.Converter import com.vaadin.navigator.View import com.vaadin.navigator.ViewChangeListener import com.vaadin.spring.annotation.SpringView import com.vaadin.ui.Button import com.vaadin.ui.Table import com.vaadin.ui.UI import com.vaadin.ui.VerticalLayout import com.vaadin.ui.themes.ValoTheme import de.philipphauer.blog.scaffolding.MyAppProps import de.philipphauer.blog.scaffolding.db.SnippetRepository import de.philipphauer.blog.scaffolding.ui.DetailsWindow import de.philipphauer.blog.scaffolding.ui.Labels import de.philipphauer.blog.scaffolding.ui.PropertyIds import de.philipphauer.blog.scaffolding.ui.SnippetOverviewBean import de.philipphauer.blog.scaffolding.ui.mapToBeans import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.util.Locale import javax.annotation.PostConstruct @SpringView(name = OverviewView.VIEW_NAME) class OverviewView(val repo: SnippetRepository, val props: MyAppProps) : VerticalLayout(), View { companion object { const val VIEW_NAME = "" const val LABEL = "Overview" } override fun enter(event: ViewChangeListener.ViewChangeEvent) { } @PostConstruct internal fun init() { val snippetEntities = repo.findAll() val snippetBeans = mapToBeans(snippetEntities) val container = BeanItemContainer(SnippetOverviewBean::class.java, snippetBeans) val table = Table(null, container).apply { setSizeFull() setColumnHeader(PropertyIds.CODE, Labels.CODE) setColumnHeader(PropertyIds.AUTHOR, Labels.AUTHOR) setColumnHeader(PropertyIds.DATE, Labels.DATE) setColumnHeader(PropertyIds.STATE, Labels.STATE) sort(arrayOf(PropertyIds.DATE), booleanArrayOf(false)) addGeneratedColumn(PropertyIds.CODE, ShortenedValueColumnGenerator) addGeneratedColumn("Details", ::generateDetailsButton) setConverter(PropertyIds.DATE, StringToInstantConverter) } setSizeFull() addComponent(table) } } //a) ColumnGenerator as singleton object (you want to want to group multiple fields and methods OR when your have more than one method (e.g. Converter)) private object ShortenedValueColumnGenerator : Table.ColumnGenerator { private val MAX_LENGTH = 20 override fun generateCell(source: Table, itemId: Any, columnId: Any): Any?{ val log = source.getItem(itemId).getItemProperty(columnId).value as? String return log?.shortenWithEllipsis() } fun String.shortenWithEllipsis(): String{ if (this.length > MAX_LENGTH){ return "${this.substring(0, MAX_LENGTH)}..." } return this } } //b) ColumnGenerator as top-level function. Pass as method reference. very concise. private fun generateDetailsButton(source: Table, itemId: Any, columnId: Any) = Button("Details").apply { addStyleName(ValoTheme.BUTTON_LINK) addClickListener { val item = source.getItem(itemId) as BeanItem val window = DetailsWindow(item.bean) UI.getCurrent().addWindow(window) } } //"object" singleton useful for interfaces with more than one method. only if stateless. object StringToInstantConverter : Converter { private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z") .withLocale(Locale.UK) .withZone(ZoneOffset.UTC) override fun convertToPresentation(value: Instant?, targetType: Class?, locale: Locale?) = DATE_FORMATTER.format(value)!! override fun convertToModel(value: String?, targetType: Class?, locale: Locale?): Instant { throw UnsupportedOperationException("Not yet implemented") } //enjoy the beauty of the following method definitions: override fun getPresentationType() = String::class.java override fun getModelType() = Instant::class.java } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/resources/VAADIN/themes/mytheme/mytheme.scss ================================================ @import "../valo/valo.scss"; @mixin mytheme { @include valo; .monospace { font-family: monospace; } } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/resources/VAADIN/themes/mytheme/styles.scss ================================================ @import "mytheme"; .mytheme { @include mytheme; } ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/resources/application.properties ================================================ # see http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html # productionMode is configured in yaml vaadin.servlet.heartbeatInterval=60 vaadin.servlet.closeIdleSessions=true # see https://github.com/vaadin/spring/blob/master/vaadin-spring-boot/src/main/java/com/vaadin/spring/boot/internal/VaadinServletConfigurationProperties.java # deactivate default favicon delivery. use my dedicated resource for this. this resources uses the favicon in the vaadin theme. spring.mvc.favicon.enabled=false # git-commit-id-plugin, spring-boot requires special @ delimiter git.tags=@git.tags@ git.branch=@git.branch@ git.dirty=@git.dirty@ git.remote.origin.url=@git.remote.origin.url@ git.commit.id=@git.commit.id@ git.commit.id.abbrev=@git.commit.id.abbrev@ git.commit.id.describe=@git.commit.id.describe@ git.commit.id.describe-short=@git.commit.id.describe-short@ git.commit.user.name=@git.commit.user.name@ git.commit.user.email=@git.commit.user.email@ git.commit.message.full=@git.commit.message.full@ git.commit.message.short=@git.commit.message.short@ git.commit.time=@git.commit.time@ git.closest.tag.name=@git.closest.tag.name@ git.closest.tag.commit.count=@git.closest.tag.commit.count@ git.build.user.name=@git.build.user.name@ git.build.user.email=@git.build.user.email@ git.build.time=@git.build.time@ git.build.host=@git.build.host@ git.build.version=@git.build.version@ # maven built-in api.version=@project.version@ api.buildtime=@buildtime@ ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/main/resources/banner.txt ================================================ _ __ _ _ _ _ | |/ / | | | (_) | | | ' / ___ | |_| |_ _ __ _ __ ___ ___| | _____ | < / _ \| __| | | '_ \ | '__/ _ \ / __| |/ / __| | . \ (_) | |_| | | | | | | | | (_) | (__| <\__ \ |_|\_\___/ \__|_|_|_| |_| |_| \___/ \___|_|\_\___/ ================================================ FILE: kotlin-spring-boot-vaadin-scaffolding/src/test/kotlin/de/philipphauer/blog/scaffolding/DummyDataCreatorTest.kt ================================================ package de.philipphauer.blog.scaffolding class DummyDataCreatorTest ================================================ FILE: modern-best-practices-testing-java/.gitignore ================================================ HELP.md /target/ !.mvn/wrapper/maven-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ /build/ ================================================ FILE: modern-best-practices-testing-java/docker-compose.yml ================================================ version: '3.1' services: db: image: "postgres:11.2-alpine" environment: POSTGRES_PASSWORD: password # user: postgres # pw: password ports: - "5432:5432" adminer: image: adminer restart: always ports: - "90:8080" ================================================ FILE: modern-best-practices-testing-java/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE com.phauer modern-best-practices-testing-java 0.0.1-SNAPSHOT Demo project for Spring Boot UTF-8 UTF-8 11 13.0.0 1.10.7 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jdbc org.springframework.boot spring-boot-starter-data-mongodb com.vaadin vaadin-spring-boot-starter org.postgresql postgresql 42.2.5 org.springframework.boot spring-boot-starter-test test junit junit org.assertj assertj-core 3.11.1 test org.junit.jupiter junit-jupiter 5.4.0 test org.testcontainers testcontainers ${testcontainers.version} test org.testcontainers postgresql ${testcontainers.version} test com.squareup.okhttp3 mockwebserver 3.13.1 test com.github.mvysny.kaributesting karibu-testing-v10 1.1.4 test org.awaitility awaitility 4.0.1 test com.vaadin vaadin-bom ${vaadin.version} pom import org.springframework.boot spring-boot-maven-plugin ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/FuturePlayground.java ================================================ package com.phauer.modernunittesting; import org.springframework.scheduling.annotation.Scheduled; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class FuturePlayground { public void bla() throws ExecutionException, InterruptedException { CompletableFuture usFuture = CompletableFuture.supplyAsync(() -> doBusinessLogic(Locale.US)); CompletableFuture germanyFuture = CompletableFuture.supplyAsync(() -> doBusinessLogic(Locale.GERMANY)); String usResult = usFuture.get(); String germanyResult = germanyFuture.get(); } @Scheduled public void start() { } String doBusinessLogic(Locale locale) { return "asdf"; } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/MainLayout.java ================================================ package com.phauer.modernunittesting; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.page.Push; import com.vaadin.flow.component.page.Viewport; import com.vaadin.flow.router.RouterLayout; @Push @Viewport("width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes") public class MainLayout extends Div implements RouterLayout { } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ModernUnitTestingApplication.java ================================================ package com.phauer.modernunittesting; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ModernUnitTestingApplication { public static void main(String[] args) { SpringApplication.run(ModernUnitTestingApplication.class, args); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/PriceCalculator.java ================================================ package com.phauer.modernunittesting; import org.springframework.stereotype.Component; @Component public class PriceCalculator { } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductController.java ================================================ package com.phauer.modernunittesting; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; @RestController public class ProductController { private final ProductDAO dao; public ProductController(ProductDAO dao, TaxServiceClient client, PriceCalculator calculator) { this.dao = dao; } @GetMapping("/products") public List getProducts() { List products = dao.findProducts(); return toDto(products); } private List toDto(List products) { return products.stream() .map(this::toDto) .collect(Collectors.toList()); } private ProductDTO toDto(ProductEntity entity) { return new ProductDTO() .setId(entity.getId()) .setName(entity.getName()) .setPrice(0.5); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductDAO.java ================================================ package com.phauer.modernunittesting; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @Component public class ProductDAO { private final JdbcTemplate template; public ProductDAO(JdbcTemplate template) { this.template = template; } public List findProducts() { return template.query("select * from products;", this::map); } private ProductEntity map(ResultSet resultSet, int i) throws SQLException { return new ProductEntity() .setId(resultSet.getString("id")) .setName(resultSet.getString("name")); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductDTO.java ================================================ package com.phauer.modernunittesting; import java.util.Objects; import java.util.StringJoiner; public class ProductDTO { private String id; private String name; private double price; public double getPrice() { return price; } public ProductDTO setPrice(double price) { this.price = price; return this; } public String getId() { return id; } public ProductDTO setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductDTO setName(String name) { this.name = name; return this; } @Override public String toString() { return new StringJoiner(", ", ProductDTO.class.getSimpleName() + "[", "]") .add("id='" + id + "'") .add("name='" + name + "'") .add("price=" + price) .toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductDTO that = (ProductDTO) o; return Double.compare(that.price, price) == 0 && Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name, price); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductEntity.java ================================================ package com.phauer.modernunittesting; public class ProductEntity { private String id; private String name; private String category; private String description; private int stockAmount; public String getDescription() { return description; } public ProductEntity setDescription(String description) { this.description = description; return this; } public int getStockAmount() { return stockAmount; } public ProductEntity setStockAmount(int stockAmount) { this.stockAmount = stockAmount; return this; } public String getCategory() { return category; } public ProductEntity setCategory(String category) { this.category = category; return this; } public String getId() { return id; } public ProductEntity setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductEntity setName(String name) { this.name = name; return this; } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductModel.java ================================================ package com.phauer.modernunittesting; import java.util.Objects; import java.util.StringJoiner; public class ProductModel { private String id; private String name; public String getId() { return id; } public ProductModel setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductModel setName(String name) { this.name = name; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductModel that = (ProductModel) o; return Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name); } @Override public String toString() { return new StringJoiner(", ", ProductModel.class.getSimpleName() + "[", "]") .add("id='" + id + "'") .add("name='" + name + "'") .toString(); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/ProductView.java ================================================ package com.phauer.modernunittesting; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import java.util.List; import java.util.stream.Collectors; @Route(value = "", layout = MainLayout.class) @PageTitle("Hello Vaadin") public class ProductView extends VerticalLayout { private ProductDAO dao; private Grid grid; public ProductView(ProductDAO dao) { this.dao = dao; initView(); } private void initView() { Button button = new Button("Load Products"); button.addClickListener(this::loadButtonHandler); this.grid = new Grid<>(ProductModel.class); add(button, grid); } private void loadButtonHandler(ClickEvent event) { var products = dao.findProducts(); grid.setItems(toModel(products)); } private List toModel(List products) { return products.stream() .map(this::toModel) .collect(Collectors.toList()); } private ProductModel toModel(ProductEntity entity) { return new ProductModel() .setId(entity.getId()) .setName(entity.getName()); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/SchemaCreator.java ================================================ package com.phauer.modernunittesting; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component public class SchemaCreator implements ApplicationRunner { private final JdbcTemplate jdbcTemplate; public SchemaCreator(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void run(ApplicationArguments args) throws Exception { createSchema(jdbcTemplate); jdbcTemplate.execute("insert into products(id, name) values " + "('1', 'notebook'), " + "('2', 'smartphone'), " + "('3', 'cup') " + "ON CONFLICT(id) DO NOTHING;"); } public static void createSchema(JdbcTemplate jdbcTemplate) { jdbcTemplate.execute("Create Table IF NOT EXISTS products (id varchar(40) primary key, name varchar(40));"); } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/TaxServiceClient.java ================================================ package com.phauer.modernunittesting; import org.springframework.stereotype.Component; @Component public class TaxServiceClient { public TaxServiceClient() { } public TaxServiceClient(String url) { } } ================================================ FILE: modern-best-practices-testing-java/src/main/java/com/phauer/modernunittesting/TaxServiceResponseDTO.java ================================================ package com.phauer.modernunittesting; import java.util.Locale; public class TaxServiceResponseDTO { private String locale; private double rate; public TaxServiceResponseDTO(Locale germany, double rate) { } public String getLocale() { return locale; } public TaxServiceResponseDTO setLocale(String locale) { this.locale = locale; return this; } public double getRate() { return rate; } public TaxServiceResponseDTO setRate(double rate) { this.rate = rate; return this; } } ================================================ FILE: modern-best-practices-testing-java/src/main/resources/application.properties ================================================ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/ spring.datasource.username=postgres spring.datasource.password=password ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/AssertJTest.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; public class AssertJTest { @Test public void bla() { var instant1 = Instant.now(); var instant2 = Instant.now(); var actualProductList = new ArrayList(); actualProductList.add(new Product(1, "Samsung Galaxy", "Smartphone")); var actualProduct = new Product(1, "Samsung", "Smartphone"); var expectedProduct = new Product(2, "iPhone", "Smartphone"); var expectedProduct1 = new Product(); var expectedProduct2 = new Product(); assertThat(actualProductList).containsExactly( createProductDTO("1", "Smartphone", 250.00), createProductDTO("1", "Smartphone", 250.00) ); assertThat(actualProductList) .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2)); assertThat(actualProduct) .isEqualToIgnoringGivenFields(expectedProduct, "id"); assertThat(actualProductList) .usingElementComparatorIgnoringFields("id") .containsExactly(expectedProduct1, expectedProduct2); assertThat(actualProductList) .extracting(Product::getId) .containsExactly(1, 2); assertThat(actualProductList) .filteredOn(product -> product.getCategory().equals("Smartphone")) .extracting(Product::getId) .containsOnly(1, 2); assertThat(actualProductList) .anySatisfy(product -> { assertThat(product.getCategory()).isEqualTo("Smartphone"); assertThat(product.isLiked()).isTrue(); }); assertThat(actualProductList) .filteredOn(product -> product.getCategory().equals("Smartphone")) .allSatisfy(product -> assertThat(product.isLiked()).isTrue()); actualProductList.get(0).setDateCreated(Instant.now()); assertThat(actualProductList.get(0).getDateCreated()).isBetween(instant1, instant2); // Don't assertTrue(actualProductList.contains(expectedProduct)); assertTrue(actualProductList.size() == 5); assertTrue(actualProduct instanceof Product); // Do assertThat(actualProductList).contains(expectedProduct); assertThat(actualProductList).hasSize(5); assertThat(actualProduct).isInstanceOf(Product.class); } private Product createProductDTO(String s, String smartphone, double v) { return new Product(); } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/AwaitilityTest.java ================================================ package com.phauer.modernunittesting; import org.awaitility.core.ConditionFactory; import org.junit.jupiter.api.Test; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; public class AwaitilityTest { private static final ConditionFactory WAIT = await() .atMost(Duration.ofSeconds(6)) .pollInterval(Duration.ofSeconds(1)) .pollDelay(Duration.ofSeconds(1)); @Test public void waitAndPoll() { triggerAsyncEvent(); WAIT.untilAsserted(() -> { assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS); }); } private void triggerAsyncEvent() { } private Thread findInDatabase(int i) { return null; } enum State { SUCCESS } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/DesignControllerTest.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class DesignControllerTest { @Nested class GetDesigns { @Test public void allFieldsAreIncluded() { } @Test public void limitParameter() { } @Test public void filterParameter() { } } @Nested class DeleteDesign { @Test public void designIsRemovedFromDb() { } @Test public void return404OnInvalidIdParameter() { } @Test public void return401IfNotAuthorized() { } } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/DisplayNameTest.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class DisplayNameTest { @Test @DisplayName("Design is removed from database") void designIsRemoved() { } @Test @DisplayName("Return 404 in case of an invalid parameter") void return404() { } @Test @DisplayName("Return 401 if the request is not authorized") void return401() { } } //class DesignControllerTest { // @Test // fun `design is removed from db`() { // } // @Test // fun `return 404 on invalid id parameter`() { // } // // @Test // fun `return 401 if not authorized`() { // } //} ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/HelperFunctions.java ================================================ package com.phauer.modernunittesting; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.testcontainers.containers.PostgreSQLContainer; import javax.sql.DataSource; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class HelperFunctions { private MockWebServer taxService; private JdbcTemplate template; private MockMvc client; @BeforeAll public void setup() throws IOException { // DataSource dataSource = createDataSourceAndStartDatabaseIfNecessary(); // ProductDAO PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); DataSource dataSource = DataSourceBuilder.create() .driverClassName("org.postgresql.Driver") .username(db.getUsername()) .password(db.getPassword()) .url(db.getJdbcUrl()) .build(); this.template = new JdbcTemplate(dataSource); SchemaCreator.createSchema(template); ProductDAO dao = new ProductDAO(template); // TaxServiceClient this.taxService = new MockWebServer(); taxService.start(); TaxServiceClient client = new TaxServiceClient(taxService.url("").toString()); // PriceCalculator PriceCalculator calculator = new PriceCalculator(); // ProductController ProductController controller = new ProductController(dao, client, calculator); this.client = MockMvcBuilders.standaloneSetup(controller).setViewResolvers(new InternalResourceViewResolver()).build(); } // Don't @Test public void categoryQueryParameter() throws Exception { List products = List.of( new ProductEntity().setId("11").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1), new ProductEntity().setId("22").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1), new ProductEntity().setId("33").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2) ); for (ProductEntity product : products) { template.execute(createSqlInsertStatement(product)); } String responseJson = client.perform(get("/products?category=Office")) .andExpect(status().is(200)) .andReturn().getResponse().getContentAsString(); assertThat(toDTOs(responseJson)) .extracting(ProductDTO::getId) .containsOnly("11", "22"); } // Do @Test public void categoryQueryParameter2() throws Exception { insertIntoDatabase( createProductWithCategory("11", "Office"), createProductWithCategory("22", "Office"), createProductWithCategory("33", "Hardware") ); String responseJson = requestProductsByCategory("Office"); assertThat(toDTOs(responseJson)) .extracting(ProductDTO::getId) .containsOnly("11", "22"); } private String createSqlInsertStatement(ProductEntity product) { return "insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"; } private ProductEntity createProductWithCategory(String id, String category) { return new ProductEntity().setId(id).setName("Envelope").setCategory(category).setDescription("An Envelope").setStockAmount(1); } private String requestProductsByCategory(String category) throws Exception { return client.perform(get("/products?category=" + category)) .andExpect(status().is(200)) .andReturn().getResponse().getContentAsString(); } private List toDTOs(String string) throws IOException { TypeReference dtoType = new TypeReference>() { }; return new ObjectMapper().readValue(string, dtoType); } private void insertIntoDatabase(ProductEntity... products) { for (ProductEntity product : products) { template.execute("insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"); } } private String toJson(TaxServiceResponseDTO taxServiceResponseDTO) throws JsonProcessingException { return new ObjectMapper().writeValueAsString(taxServiceResponseDTO); } private DataSource createDataSourceAndStartDatabaseIfNecessary() { DataSourceBuilder builder = DataSourceBuilder.create().driverClassName("org.postgresql.Driver"); try { // e.g. if started once via `docker-compose up`. see docker-compose.yml. Socket socket = new Socket(); socket.connect(new InetSocketAddress("localhost", 5432), 100); socket.close(); return builder.username("postgres").password("password") .url("jdbc:postgresql://localhost:5432/") .build(); } catch (Exception ex) { PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); return builder.username(db.getUsername()).password(db.getPassword()) .url(db.getJdbcUrl()) .build(); } } @BeforeEach public void beforeEach() { template.execute("truncate table products"); } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/ParameterTest.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; public class ParameterTest { private Calculator calculator = new Calculator(); @ParameterizedTest @CsvSource({ "1, 1, 2", "5, 3, 8", "10, -20, -10" }) public void add(int summand1, int summand2, int expectedSum) { assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum); } } class Calculator { public int add(int summand1, int summand2) { return summand1 + summand2; } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/Product.java ================================================ package com.phauer.modernunittesting; import java.time.Instant; import java.util.Objects; import java.util.StringJoiner; public class Product { private int id; private String name; private String category; private Instant dateCreated; private boolean liked; public Product() { } public Product(int id, String name, String category) { this.id = id; this.name = name; this.category = category; } public int getId() { return id; } public Product setId(int id) { this.id = id; return this; } public String getName() { return name; } public Product setName(String name) { this.name = name; return this; } public String getCategory() { return category; } public Product setCategory(String category) { this.category = category; return this; } public Instant getDateCreated() { return dateCreated; } public Product setDateCreated(Instant dateCreated) { this.dateCreated = dateCreated; return this; } public boolean isLiked() { return liked; } public Product setLiked(boolean liked) { this.liked = liked; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Product product = (Product) o; return Objects.equals(id, product.id) && Objects.equals(dateCreated, product.dateCreated); } @Override public int hashCode() { return Objects.hash(id, dateCreated); } @Override public String toString() { return new StringJoiner(", ", Product.class.getSimpleName() + "[", "]") .add("id=" + id) .add("name='" + name + "'") .toString(); } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/ProductControllerITest.java ================================================ package com.phauer.modernunittesting; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.testcontainers.containers.PostgreSQLContainer; import javax.sql.DataSource; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ProductControllerITest { private MockWebServer taxService; private JdbcTemplate template; private MockMvc client; @BeforeAll public void setup() throws IOException { // DataSource dataSource = createDataSourceAndStartDatabaseIfNecessary(); // ProductDAO PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); DataSource dataSource = DataSourceBuilder.create() .driverClassName("org.postgresql.Driver") .username(db.getUsername()) .password(db.getPassword()) .url(db.getJdbcUrl()) .build(); this.template = new JdbcTemplate(dataSource); SchemaCreator.createSchema(template); ProductDAO dao = new ProductDAO(template); // TaxServiceClient this.taxService = new MockWebServer(); taxService.start(); TaxServiceClient client = new TaxServiceClient(taxService.url("").toString()); // PriceCalculator PriceCalculator calculator = new PriceCalculator(); // ProductController ProductController controller = new ProductController(dao, client, calculator); this.client = MockMvcBuilders.standaloneSetup(controller).setViewResolvers(new InternalResourceViewResolver()).build(); } @Test public void databaseDataIsCorrectlyReturned() throws Exception { insertIntoDatabase( new ProductEntity().setId("90").setName("Envelope"), new ProductEntity().setId("50").setName("Pen") ); taxService.enqueue(new MockResponse() .setResponseCode(200) .setBody(toJson(new TaxServiceResponseDTO(Locale.GERMANY, 0.19))) ); String responseJson = client.perform(get("/products").param("token", "asdf")) .andExpect(status().is(200)) .andReturn().getResponse().getContentAsString(); assertThat(toDTOs(responseJson)).containsOnly( new ProductDTO().setId("90").setName("Envelope").setPrice(0.5), new ProductDTO().setId("50").setName("Pen").setPrice(0.5) ); // or assert the data in the database if the request should change something. } private List toDTOs(String string) throws IOException { TypeReference dtoType = new TypeReference>() { }; return new ObjectMapper().readValue(string, dtoType); } private void insertIntoDatabase(ProductEntity... products) { List products2 = List.of(new ProductEntity()); for (ProductEntity product : products) { template.execute("insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"); } } private String toJson(TaxServiceResponseDTO taxServiceResponseDTO) throws JsonProcessingException { return new ObjectMapper().writeValueAsString(taxServiceResponseDTO); } private DataSource createDataSourceAndStartDatabaseIfNecessary() { DataSourceBuilder builder = DataSourceBuilder.create().driverClassName("org.postgresql.Driver"); try { // e.g. if started once via `docker-compose up`. see docker-compose.yml. Socket socket = new Socket(); socket.connect(new InetSocketAddress("localhost", 5432), 100); socket.close(); return builder.username("postgres").password("password") .url("jdbc:postgresql://localhost:5432/") .build(); } catch (Exception ex) { PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); return builder.username(db.getUsername()).password(db.getPassword()) .url(db.getJdbcUrl()) .build(); } } @BeforeEach public void beforeEach() { template.execute("truncate table products"); } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/ProductControllerITest2.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @WebMvcTest(ProductController.class) class ProductControllerITest2 { @Test public void foo() { } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/ProductViewITest.java ================================================ package com.phauer.modernunittesting; import com.github.mvysny.kaributesting.v10.GridKt; import com.github.mvysny.kaributesting.v10.MockVaadin; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.testcontainers.containers.PostgreSQLContainer; import javax.sql.DataSource; import static com.github.mvysny.kaributesting.v10.LocatorJ._click; import static com.github.mvysny.kaributesting.v10.LocatorJ._get; import static org.assertj.core.api.Assertions.assertThat; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ProductViewITest { private JdbcTemplate template; private ProductView view; @BeforeAll public void beforeAll() { MockVaadin.setup(); PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); DataSource dataSource = DataSourceBuilder.create() .driverClassName("org.postgresql.Driver") .username(db.getUsername()) .password(db.getPassword()) .url(db.getJdbcUrl()) .build(); template = new JdbcTemplate(dataSource); SchemaCreator.createSchema(template); } @BeforeEach public void beforeEach() { ProductDAO dao = new ProductDAO(template); view = new ProductView(dao); } @Test public void prodcutsAreCorrectlyDisplayedInTable() { insertIntoDatabase( new ProductEntity().setId("90").setName("Envelope"), new ProductEntity().setId("50").setName("Pen") ); Button button = _get(view, Button.class, spec -> spec.withText("Load Products")); _click(button); Grid grid = _get(view, Grid.class); assertThat(GridKt._size(grid)).isEqualTo(2); assertThat(GridKt._get(grid, 0)) .isEqualTo(new ProductModel().setId("90").setName("Envelope")); } private void insertIntoDatabase(ProductEntity... products) { for (ProductEntity product : products) { template.execute("insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"); } } } ================================================ FILE: modern-best-practices-testing-java/src/test/java/com/phauer/modernunittesting/RandomizedValues.java ================================================ package com.phauer.modernunittesting; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.Random; import java.util.UUID; public class RandomizedValues { @Test public void randomized() { // Don't Instant ts1 = Instant.now(); // 1557582788 Instant ts2 = ts1.plusSeconds(1); // 1557582789 ObjectId generatedId = new ObjectId(); // 5cd6d3c469974112f4be30d2 int randomAmount = new Random().nextInt(500); // 232 UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad } @Test public void fixed() { // Do Instant ts1 = Instant.ofEpochSecond(1550000001); Instant ts2 = Instant.ofEpochSecond(1550000002); ObjectId id1 = new ObjectId("000000000000000000000001"); int amount = 50; UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000000"); } } ================================================ FILE: modern-integration-testing/.gitignore ================================================ HELP.md /target/ !.mvn/wrapper/maven-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ /build/ ================================================ FILE: modern-integration-testing/docker-compose.yml ================================================ version: '3.1' services: db: image: "postgres:11.2-alpine" environment: POSTGRES_PASSWORD: password # user: postgres # pw: password ports: - "5432:5432" adminer: image: adminer restart: always ports: - "90:8080" ================================================ FILE: modern-integration-testing/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE com.phauer modern-integration-testing 0.0.1-SNAPSHOT Demo project for Spring Boot UTF-8 UTF-8 11 13.0.0 1.10.7 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jdbc com.vaadin vaadin-spring-boot-starter org.postgresql postgresql 42.2.5 org.springframework.boot spring-boot-starter-test test junit junit org.assertj assertj-core 3.11.1 test org.junit.jupiter junit-jupiter 5.4.0 test org.testcontainers testcontainers ${testcontainers.version} test org.testcontainers postgresql ${testcontainers.version} test com.squareup.okhttp3 mockwebserver 3.13.1 test com.github.mvysny.kaributesting karibu-testing-v10 1.1.4 test com.vaadin vaadin-bom ${vaadin.version} pom import org.springframework.boot spring-boot-maven-plugin ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/MainLayout.java ================================================ package com.phauer.modernunittesting; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.page.Push; import com.vaadin.flow.component.page.Viewport; import com.vaadin.flow.router.RouterLayout; @Push @Viewport("width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes") public class MainLayout extends Div implements RouterLayout { } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ModernUnitTestingApplication.java ================================================ package com.phauer.modernunittesting; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ModernUnitTestingApplication { public static void main(String[] args) { SpringApplication.run(ModernUnitTestingApplication.class, args); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/PriceCalculator.java ================================================ package com.phauer.modernunittesting; import org.springframework.stereotype.Component; @Component public class PriceCalculator { } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductController.java ================================================ package com.phauer.modernunittesting; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; @RestController public class ProductController { private final ProductDAO dao; public ProductController(ProductDAO dao, TaxServiceClient client, PriceCalculator calculator) { this.dao = dao; } @GetMapping("/products") public List getProducts() { List products = dao.findProducts(); return toDto(products); } private List toDto(List products) { return products.stream() .map(this::toDto) .collect(Collectors.toList()); } private ProductDTO toDto(ProductEntity entity) { return new ProductDTO() .setId(entity.getId()) .setName(entity.getName()) .setPrice(0.5); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductDAO.java ================================================ package com.phauer.modernunittesting; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @Component public class ProductDAO { private final JdbcTemplate template; public ProductDAO(JdbcTemplate template) { this.template = template; } public List findProducts() { return template.query("select * from products;", this::map); } private ProductEntity map(ResultSet resultSet, int i) throws SQLException { return new ProductEntity() .setId(resultSet.getString("id")) .setName(resultSet.getString("name")); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductDTO.java ================================================ package com.phauer.modernunittesting; import java.util.Objects; import java.util.StringJoiner; public class ProductDTO { private String id; private String name; private double price; public double getPrice() { return price; } public ProductDTO setPrice(double price) { this.price = price; return this; } public String getId() { return id; } public ProductDTO setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductDTO setName(String name) { this.name = name; return this; } @Override public String toString() { return new StringJoiner(", ", ProductDTO.class.getSimpleName() + "[", "]") .add("id='" + id + "'") .add("name='" + name + "'") .add("price=" + price) .toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductDTO that = (ProductDTO) o; return Double.compare(that.price, price) == 0 && Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name, price); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductEntity.java ================================================ package com.phauer.modernunittesting; import java.util.Objects; import java.util.StringJoiner; public class ProductEntity { private String id; private String name; public String getId() { return id; } public ProductEntity setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductEntity setName(String name) { this.name = name; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductEntity that = (ProductEntity) o; return Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name); } @Override public String toString() { return new StringJoiner(", ", ProductEntity.class.getSimpleName() + "[", "]") .add("id='" + id + "'") .add("name='" + name + "'") .toString(); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductModel.java ================================================ package com.phauer.modernunittesting; import java.util.Objects; import java.util.StringJoiner; public class ProductModel { private String id; private String name; public String getId() { return id; } public ProductModel setId(String id) { this.id = id; return this; } public String getName() { return name; } public ProductModel setName(String name) { this.name = name; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductModel that = (ProductModel) o; return Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name); } @Override public String toString() { return new StringJoiner(", ", ProductModel.class.getSimpleName() + "[", "]") .add("id='" + id + "'") .add("name='" + name + "'") .toString(); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/ProductView.java ================================================ package com.phauer.modernunittesting; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import java.util.List; import java.util.stream.Collectors; @Route(value = "", layout = MainLayout.class) @PageTitle("Hello Vaadin") public class ProductView extends VerticalLayout { private ProductDAO dao; private Grid grid; public ProductView(ProductDAO dao) { this.dao = dao; initView(); } private void initView() { Button button = new Button("Load Products"); button.addClickListener(this::loadButtonHandler); this.grid = new Grid<>(ProductModel.class); add(button, grid); } private void loadButtonHandler(ClickEvent event) { var products = dao.findProducts(); grid.setItems(toModel(products)); } private List toModel(List products) { return products.stream() .map(this::toModel) .collect(Collectors.toList()); } private ProductModel toModel(ProductEntity entity) { return new ProductModel() .setId(entity.getId()) .setName(entity.getName()); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/SchemaCreator.java ================================================ package com.phauer.modernunittesting; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component public class SchemaCreator implements ApplicationRunner { private final JdbcTemplate jdbcTemplate; public SchemaCreator(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void run(ApplicationArguments args) throws Exception { createSchema(jdbcTemplate); jdbcTemplate.execute("insert into products(id, name) values " + "('1', 'notebook'), " + "('2', 'smartphone'), " + "('3', 'cup') " + "ON CONFLICT(id) DO NOTHING;"); } public static void createSchema(JdbcTemplate jdbcTemplate) { jdbcTemplate.execute("Create Table IF NOT EXISTS products (id varchar(40) primary key, name varchar(40));"); } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/TaxServiceClient.java ================================================ package com.phauer.modernunittesting; import org.springframework.stereotype.Component; @Component public class TaxServiceClient { public TaxServiceClient() { } public TaxServiceClient(String url) { } } ================================================ FILE: modern-integration-testing/src/main/java/com/phauer/modernunittesting/TaxServiceResponseDTO.java ================================================ package com.phauer.modernunittesting; import java.util.Locale; public class TaxServiceResponseDTO { private String locale; private double rate; public TaxServiceResponseDTO(Locale germany, double rate) { } public String getLocale() { return locale; } public TaxServiceResponseDTO setLocale(String locale) { this.locale = locale; return this; } public double getRate() { return rate; } public TaxServiceResponseDTO setRate(double rate) { this.rate = rate; return this; } } ================================================ FILE: modern-integration-testing/src/main/resources/application.properties ================================================ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/ spring.datasource.username=postgres spring.datasource.password=password ================================================ FILE: modern-integration-testing/src/test/java/com/phauer/modernunittesting/AssertJTest.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; public class AssertJTest { @Test public void bla(){ var instant1 = Instant.now(); var instant2 = Instant.now(); var actualProductList = new ArrayList(); var actualProduct = new Product(); var expectedProduct = new Product(); var expectedProduct1 = new Product(); var expectedProduct2 = new Product(); assertThat(actualProductList).containsExactly( createProductDTO("1", "Smartphone", 250.00), createProductDTO("1", "Smartphone", 250.00) ); assertThat(actualProductList).anySatisfy(product -> { assertThat(product.getDateCreated()).isBetween(instant1, instant2); }); assertThat(actualProduct) .isEqualToIgnoringGivenFields(expectedProduct, "id"); assertThat(actualProductList) .usingElementComparatorIgnoringFields("id") .containsExactly(expectedProduct1, expectedProduct2); assertThat(actualProductList) .extracting(Product::getId) .containsExactly("1", "2"); } private Product createProductDTO(String s, String smartphone, double v) { return new Product(); } } ================================================ FILE: modern-integration-testing/src/test/java/com/phauer/modernunittesting/Product.java ================================================ package com.phauer.modernunittesting; import java.time.Instant; import java.util.Objects; public class Product { private String id; private Instant dateCreated; public String getId() { return id; } public Product setId(String id) { this.id = id; return this; } public Instant getDateCreated() { return dateCreated; } public Product setDateCreated(Instant dateCreated) { this.dateCreated = dateCreated; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Product product = (Product) o; return Objects.equals(id, product.id) && Objects.equals(dateCreated, product.dateCreated); } @Override public int hashCode() { return Objects.hash(id, dateCreated); } } ================================================ FILE: modern-integration-testing/src/test/java/com/phauer/modernunittesting/ProductControllerITest.java ================================================ package com.phauer.modernunittesting; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.testcontainers.containers.PostgreSQLContainer; import javax.sql.DataSource; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ProductControllerITest { private MockWebServer taxService; private JdbcTemplate template; private MockMvc client; @BeforeAll public void setup() throws IOException { // DataSource dataSource = createDataSourceAndStartDatabaseIfNecessary(); // ProductDAO PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); DataSource dataSource = DataSourceBuilder.create() .driverClassName("org.postgresql.Driver") .username(db.getUsername()) .password(db.getPassword()) .url(db.getJdbcUrl()) .build(); this.template = new JdbcTemplate(dataSource); SchemaCreator.createSchema(template); ProductDAO dao = new ProductDAO(template); // TaxServiceClient this.taxService = new MockWebServer(); taxService.start(); TaxServiceClient client = new TaxServiceClient(taxService.url("").toString()); // PriceCalculator PriceCalculator calculator = new PriceCalculator(); // ProductController ProductController controller = new ProductController(dao, client, calculator); this.client = MockMvcBuilders.standaloneSetup(controller).setViewResolvers(new InternalResourceViewResolver()).build(); } @Test public void databaseDataIsCorrectlyReturned() throws Exception { insertIntoDatabase( new ProductEntity().setId("90").setName("Envelope"), new ProductEntity().setId("50").setName("Pen") ); taxService.enqueue(new MockResponse() .setResponseCode(200) .setBody(toJson(new TaxServiceResponseDTO(Locale.GERMANY, 0.19))) ); String responseJson = client.perform(get("/products")) .andExpect(status().is(200)) .andReturn().getResponse().getContentAsString(); assertThat(toDTOs(responseJson)).containsOnly( new ProductDTO().setId("90").setName("Envelope").setPrice(0.5), new ProductDTO().setId("50").setName("Pen").setPrice(0.5) ); // or assert the data in the database if the request should change something. } private List toDTOs(String string) throws IOException { TypeReference dtoType = new TypeReference>() { }; return new ObjectMapper().readValue(string, dtoType); } private void insertIntoDatabase(ProductEntity... products) { for (ProductEntity product : products) { template.execute("insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"); } } private String toJson(TaxServiceResponseDTO taxServiceResponseDTO) throws JsonProcessingException { return new ObjectMapper().writeValueAsString(taxServiceResponseDTO); } private DataSource createDataSourceAndStartDatabaseIfNecessary() { DataSourceBuilder builder = DataSourceBuilder.create().driverClassName("org.postgresql.Driver"); try { // e.g. if started once via `docker-compose up`. see docker-compose.yml. Socket socket = new Socket(); socket.connect(new InetSocketAddress("localhost", 5432), 100); socket.close(); return builder.username("postgres").password("password") .url("jdbc:postgresql://localhost:5432/") .build(); } catch (Exception ex) { PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); return builder.username(db.getUsername()).password(db.getPassword()) .url(db.getJdbcUrl()) .build(); } } @BeforeEach public void beforeEach() { template.execute("truncate table products"); } } ================================================ FILE: modern-integration-testing/src/test/java/com/phauer/modernunittesting/ProductControllerITest2.java ================================================ package com.phauer.modernunittesting; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @WebMvcTest(ProductController.class) class ProductControllerITest2 { @Test public void foo() { } } ================================================ FILE: modern-integration-testing/src/test/java/com/phauer/modernunittesting/ProductViewITest.java ================================================ package com.phauer.modernunittesting; import com.github.mvysny.kaributesting.v10.GridKt; import com.github.mvysny.kaributesting.v10.MockVaadin; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.testcontainers.containers.PostgreSQLContainer; import javax.sql.DataSource; import static com.github.mvysny.kaributesting.v10.LocatorJ._click; import static com.github.mvysny.kaributesting.v10.LocatorJ._get; import static org.assertj.core.api.Assertions.assertThat; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ProductViewITest { private JdbcTemplate template; private ProductView view; @BeforeAll public void beforeAll() { MockVaadin.setup(); PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine"); db.start(); DataSource dataSource = DataSourceBuilder.create() .driverClassName("org.postgresql.Driver") .username(db.getUsername()) .password(db.getPassword()) .url(db.getJdbcUrl()) .build(); template = new JdbcTemplate(dataSource); SchemaCreator.createSchema(template); } @BeforeEach public void beforeEach() { ProductDAO dao = new ProductDAO(template); view = new ProductView(dao); } @Test public void prodcutsAreCorrectlyDisplayedInTable() { insertIntoDatabase( new ProductEntity().setId("90").setName("Envelope"), new ProductEntity().setId("50").setName("Pen") ); Button button = _get(view, Button.class, spec -> spec.withText("Load Products")); _click(button); Grid grid = _get(view, Grid.class); assertThat(GridKt._size(grid)).isEqualTo(2); assertThat(GridKt._get(grid, 0)) .isEqualTo(new ProductModel().setId("90").setName("Envelope")); } private void insertIntoDatabase(ProductEntity... products) { for (ProductEntity product : products) { template.execute("insert into products(id, name) values ('" + product.getId() + "', '" + product.getName() + "');"); } } } ================================================ FILE: mongodb-practice/connection-strings.sh ================================================ mongo --host db1.domain.com --port 27017 -u writeUser -p CPy8f82cb productService mongo --host db1.domain.com --port 27017 -u readUser -p qBr7bEJSm productService mongo --host db2.domain.com --port 27018 -u writeUser -p oFfQdzkKR checkout mongo --host db2.domain.com --port 27018 -u readUser -p ehhhTbmeT checkout mongo --host db3.domain.com --port 27019 -u writeUser -p LkFnSmNYb shop mongo --host db3.domain.com --port 27019 -u readUser -p 4aYLLcgn8 shop mongo --host localhost --port 27017 ================================================ FILE: mongodb-practice/docker-compose.yml ================================================ version: '3' services: mongo: image: "mongo:4.0.2" ports: - "27017:27017" command: --profile=1 --slowms=0 mongo-seeding: build: ./local-dev/mongo-seeding/ depends_on: - mongo mongo-express: image: "mongo-express:0.49.0" ports: - "8081:8081" depends_on: - mongo ================================================ FILE: mongodb-practice/example.json ================================================ db.products.insert({name: "Smartphone", amount: 5, dateCreated: new Date(), isActive: true, tags: ["a", "b"]}) db.products.insert({name: "Notebook", amount: 2, dateCreated: new Date(), isActive: false, tags: []}) ================================================ FILE: mongodb-practice/local-dev/mongo-seeding/Dockerfile ================================================ FROM python:3.7.2-alpine3.8 RUN pip install --ignore-installed "pymongo==3.7.2" "Faker==1.0.2" COPY seedMongo.py / CMD python3 /seedMongo.py ================================================ FILE: mongodb-practice/local-dev/mongo-seeding/seedMongo.py ================================================ #!/usr/bin/env python3 import random from datetime import datetime, timedelta from bson import ObjectId from faker import Faker from pymongo import MongoClient faker = Faker("en") def seed(): print("Start seeding...") client = MongoClient('mongodb://mongo:27017/test') db = client.test new_products = [generate_product() for _ in range(500)] db.products.delete_many({}) db.products.insert_many(new_products) print("Finished seeding.") def generate_product(): return { "_id": ObjectId(), "name": faker.company(), "amount": random.randrange(100), "isActive": faker.boolean(chance_of_getting_true=80), "dateCreated": faker.date_time_between(start_date="-10y", end_date="now"), "tags": [faker.word(), faker.word()] } if __name__ == '__main__': seed() ================================================ FILE: python-demo/.gitignore ================================================ .idea *.iml ================================================ FILE: python-demo/1concise-powerful.py ================================================ # You don’t need {} for method bodies (use indent instead), no semicolon, no () in if conditions. def divide(a, b): if b == 0: raise ValueError("Dude, you can't divide {} by {}".format(a, b)) return a / b # more intuitive than Java's ternary operator parameter = "Peter" name = "Unknown" if parameter is None else parameter # multi-line strings. praise the lord! description = """This is my multi-line string. """ # chained comparison if 200 <= status_code < 300: print("success") ================================================ FILE: python-demo/2collections.py ================================================ # Python is just awesome when it comes to collections # nice literals for list, tuple, sets, dicts my_list = [1, 2, 3] my_tuple = (1, 2) my_set = {1, 2, 2} my_dict = {"a": 1, "b": 2} # this collection literals makes dealing with JSON a pleasure. # this is also the reason why the Python driver for MongoDB makes definitely more fun. # contains contains_element = 1 in my_list # True contains_key = "a" not in my_dict # False # access value = my_list[0] value2 = my_dict["a"] # iteration for element in my_list: print(element) for index, element in enumerate(my_list): # foreach with index! print(index, element) for key, value in my_dict.items(): # how cool is that?! print(key, value) # list can be used as stacks... print(my_list.pop()) # 3 (remove and return element) # ...and as queues print(my_list.pop(0)) # 1 (remove and return first element) # == List Comprehension == # That's my personal kick-ass feature of Python # It's like map() and filter() of the Java 8 Stream API, but much better. # syntax: [ for in if ] # example: names = ["peter", "paul", "florian", "albert"] # filter for names starting with "p" (filter()) and upper case them (map()) result = [name.upper() for name in names if name.startswith("p")] print(result) # ["PETER", "PAUL"] # and it returns... a new list! no annoying collect(Collectors.toList()) boilerplate. # i really love this powerful and concise syntax. # slice syntax for extracting parts of a collection # syntax: collection[startIndex:stopIndex(:step)] # startIndex defaults to 0; stopIndex defaults to len(col); step defaults to 1, negative step reverses direction of iteration print(names[1:2]) # paul print(names[1:]) # all except the first: paul, florian, albert print(names[:1]) # get first element: peter print(names[:]) # copy whole collection print(names[::-1]) # reverse list: ['albert', 'florian', 'paul', 'peter'] # works also for strings print("hello nice world"[6:10]) # nice ================================================ FILE: python-demo/3functions.py ================================================ def copy(source_file, target_file, override): pass # imagine some code here... # unclear what the parameters mean: copy("~/file.txt", "~/file2.txt", True) # named parameters make method calls readable! copy(source_file="~/file.txt", target_file="~/file2.txt", override=True) # it's also more secure, because an error is thrown, if the argument name doesn't exist # default parameters! no silly constructor chaining. def copy2(source_file, target_file, override = True): pass # imagine some code here... copy2(source_file="~/file.txt", target_file="~/file2.txt") # functions are first class citizens. You can assign them to variables, return and pass them around my_copy = copy2 # lambdas get_year = lambda date: date.year import datetime today = datetime.date.today() some_day = datetime.date(2010, 9, 15) for date in [today, some_day]: print(get_year(date)) # 2016, 2010 # multiple return values via automatic tuple packing and unpacking def multiple_return(): return 1, 2, True # will be packed to a tuple print(multiple_return()) # (1, 2, True) first, second, third = multiple_return() # unpacking print(first, second, third) # 1 2 True ================================================ FILE: python-demo/4classes.py ================================================ # let's define a User class with the properties name and age class User: def __init__(self, name, age): self.name = name self.age = age def introduce_yourself(self): print("Hi, I'm {}, {} years old".format(self.name, self.age)) # create a User user = User("Hauke", 28) print(user.name) # Hauke user.introduce_yourself() # Hi, I'm Hauke, 28 years old # by the way there is also Inheritance and Polymorphism ================================================ FILE: python-demo/5operator-overloading.py ================================================ import datetime today = datetime.date.today() some_day = datetime.date(2016, 9, 15) print(today - some_day) # "17 days, 0:00:00". difference between dates. set1 = {1, 2, 3} set2 = {3, 4, 5} print(set2 - set1) # {4, 5}. difference between sets # Let's define our own overloading # Therefore, we use Protocols. Protocols define method signatures. If you implement these methods, you can use certain operators. # e.g. for "==" we need to implement "__eq__" class User: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): return self.name is other.name and self.age is other.age print(User("Hauke", 28) == User("Peter", 30)) # False # e.g. for "list[element]" we need to implement "__getitem__" class UserAgeFinder: def __init__(self): self.user_list = [User("Hauke", 28), User("Peter", 30), User("Hugo", 55)] def __getitem__(self, name): return [user.age for user in self.user_list if user.name is name] finder = UserAgeFinder() print(finder["Hauke"]) # [28] ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/.gitignore ================================================ .idea/ *.iml dependency-reduced-pom.xml target/ ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/README.md ================================================ # rest-api-doc-jaxrs-swagger-asciidoc ## Build and Start ```bash mvn package java -jar target/rest-api-doc-jaxrs-swagger-asciidoc-1.0-SNAPSHOT.jar server config.yml ``` ## Resources ``` # API: GET http://localhost:8080/bands GET http://localhost:8080/bands/ POST http://localhost:8080/bands # Swagger Specification: GET http://localhost:8080/swagger.json GET http://localhost:8080/swagger.yaml # API HTML Documentation: GET http://localhost:8080/application-doc.html ``` ## Optional: Swagger-UI ```bash # start application cd swagger-ui docker-compose up # open in browser: http://localhost:8090/?url=http://localhost:8080/swagger.json ``` ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/config.yml ================================================ logging: level: INFO loggers: de.philipphauer.blog: DEBUG ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/generate-documentation.sh ================================================ #!/usr/bin/env bash # Convenience script for development to generate only the HTML Documentation without executing the whole Maven lifecycle. set -e case "$1" in '') cat < 4.0.0 3.0.0 de.philipphauer.blog rest-api-doc-jaxrs-swagger-asciidoc 1.0-SNAPSHOT jar rest-api-doc-jaxrs-swagger-asciidoc UTF-8 UTF-8 1.0.6 de.philipphauer.blog.RestApiDocApplication ${project.basedir}/src/docs/asciidoc ${project.build.directory}/asciidoc/generated ${project.build.outputDirectory} io.dropwizard dropwizard-bom ${dropwizard.version} pom import io.dropwizard dropwizard-core io.swagger swagger-jersey2-jaxrs 1.5.10 io.github.swagger2markup swagger2markup 1.2.0 org.codehaus.mojo exec-maven-plugin 1.5.0 de.philipphauer.blog.apiDocGen.SwaggerAndAsciiDocGenerator ${project.build.outputDirectory} ${generated.asciidoc.directory} generate-swagger-and-asciidoc process-classes java org.asciidoctor asciidoctor-maven-plugin 1.5.3 ${asciidoctor.input.directory} index.adoc font book left 3 ${generated.asciidoc.directory} custom.css output-html test process-asciidoc html5 highlightjs ${asciidoctor.html.output.directory} maven-shade-plugin 2.4.1 true ${mainClass} *:* META-INF/*.SF META-INF/*.DSA META-INF/*.RSA package shade maven-jar-plugin 2.6 true ${mainClass} maven-compiler-plugin 3.3 1.8 1.8 maven-source-plugin 2.4 attach-sources jar maven-javadoc-plugin 2.10.3 attach-javadocs jar false jcenter-releases jcenter http://jcenter.bintray.com ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/docs/asciidoc/custom.css ================================================ /* see below for custom styling */ /* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} audio,canvas,video{display:inline-block} audio:not([controls]){display:none;height:0} [hidden],template{display:none} script{display:none!important} html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} body{margin:0} a{background:transparent} a:focus{outline:thin dotted} a:active,a:hover{outline:0} h1{font-size:2em;margin:.67em 0} abbr[title]{border-bottom:1px dotted} b,strong{font-weight:bold} dfn{font-style:italic} hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} mark{background:#ff0;color:#000} code,kbd,pre,samp{font-family:monospace;font-size:1em} pre{white-space:pre-wrap} q{quotes:"\201C" "\201D" "\2018" "\2019"} small{font-size:80%} sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} sup{top:-.5em} sub{bottom:-.25em} img{border:0} svg:not(:root){overflow:hidden} figure{margin:0} fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} legend{border:0;padding:0} button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} button,input{line-height:normal} button,select{text-transform:none} button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} button[disabled],html input[disabled]{cursor:default} input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} textarea{overflow:auto;vertical-align:top} table{border-collapse:collapse;border-spacing:0} *,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} html,body{font-size:100%} body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto} a:hover{cursor:pointer} img,object,embed{max-width:100%;height:auto} object,embed{height:100%} img{-ms-interpolation-mode:bicubic} .left{float:left!important} .right{float:right!important} .text-left{text-align:left!important} .text-right{text-align:right!important} .text-center{text-align:center!important} .text-justify{text-align:justify!important} .hide{display:none} body{-webkit-font-smoothing:antialiased} img,object,svg{display:inline-block;vertical-align:middle} textarea{height:auto;min-height:50px} select{width:100%} .center{margin-left:auto;margin-right:auto} .spread{width:100%} p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6} .subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} a{color:#2156a5;text-decoration:underline;line-height:inherit} a:hover,a:focus{color:#1d4b8f} a img{border:none} p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} p aside{font-size:.875em;line-height:1.35;font-style:italic} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} h1{font-size:2.125em} h2{font-size:1.6875em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} h4,h5{font-size:1.125em} h6{font-size:1em} hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} em,i{font-style:italic;line-height:inherit} strong,b{font-weight:bold;line-height:inherit} small{font-size:60%;line-height:inherit} code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} ul.no-bullet{list-style:none} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} abbr{text-transform:none} blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} blockquote cite:before{content:"\2014 \0020"} blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} @media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} h1{font-size:2.75em} h2{font-size:2.3125em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} h4{font-size:1.4375em}} table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} table thead,table tfoot{background:#f7f8f7;font-weight:bold} table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} body{tab-size:4} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} .clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} .clearfix:after,.float-group:after{clear:both} *:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} .keyseq{color:rgba(51,51,51,.8)} kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} .keyseq kbd:first-child{margin-left:0} .keyseq kbd:last-child{margin-right:0} .menuseq,.menu{color:rgba(0,0,0,.8)} b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} b.button:before{content:"[";padding:0 3px 0 2px} b.button:after{content:"]";padding:0 2px 0 3px} p a>code:hover{color:rgba(0,0,0,.9)} #header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} #header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} #header:after,#content:after,#footnotes:after,#footer:after{clear:both} #content{margin-top:1.25em} #content:before{content:none} #header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} #header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8} #header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px} #header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} #header .details span:first-child{margin-left:-.125em} #header .details span.email a{color:rgba(0,0,0,.85)} #header .details br{display:none} #header .details br+span:before{content:"\00a0\2013\00a0"} #header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} #header .details br+span#revremark:before{content:"\00a0|\00a0"} #header #revnumber{text-transform:capitalize} #header #revnumber:after{content:"\00a0"} #content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} #toc{border-bottom:1px solid #efefed;padding-bottom:.5em} #toc>ul{margin-left:.125em} #toc ul.sectlevel0>li>a{font-style:italic} #toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} #toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} #toc li{line-height:1.3334;margin-top:.3334em} #toc a{text-decoration:none} #toc a:active{text-decoration:underline} #toctitle{color:#7a2518;font-size:1.2em} @media only screen and (min-width:768px){#toctitle{font-size:1.375em} body.toc2{padding-left:15em;padding-right:0} #toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} #toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} #toc.toc2>ul{font-size:.9em;margin-bottom:0} #toc.toc2 ul ul{margin-left:0;padding-left:1em} #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} body.toc2.toc-right{padding-left:0;padding-right:15em} body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}} @media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} #toc.toc2{width:20em} #toc.toc2 #toctitle{font-size:1.375em} #toc.toc2>ul{font-size:.95em} #toc.toc2 ul ul{padding-left:1.25em} body.toc2.toc-right{padding-left:0;padding-right:20em}} #content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} #content #toc>:first-child{margin-top:0} #content #toc>:last-child{margin-bottom:0} #footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} #footer-text{color:rgba(255,255,255,.8);line-height:1.44} .sect1{padding-bottom:.625em} @media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}} .sect1+.sect1{border-top:1px solid #efefed} #content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} #content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} #content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} #content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} #content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} .audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} .admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0} .paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)} table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} .exampleblock>.content>:first-child{margin-top:0} .exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} .sidebarblock>:first-child{margin-top:0} .sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} .exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} .sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} .literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em} .literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal} @media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}} @media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}} .literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} .listingblock pre.highlightjs{padding:0} .listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} .listingblock pre.prettyprint{border-width:0} .listingblock>.content{position:relative} .listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} .listingblock:hover code[data-lang]:before{display:block} .listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} .listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45} table.pyhltable td.code{padding-left:.75em;padding-right:0} pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} pre.pygments .lineno{display:inline-block;margin-right:.25em} table.pyhltable .linenodiv{background:none!important;padding-right:0!important} .quoteblock{margin:0 1em 1.25em 1.5em;display:table} .quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} .quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} .quoteblock blockquote{margin:0;padding:0;border:0} .quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} .quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} .quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right} .quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} .quoteblock .quoteblock blockquote{padding:0 0 0 .75em} .quoteblock .quoteblock blockquote:before{display:none} .verseblock{margin:0 1em 1.25em 1em} .verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} .verseblock pre strong{font-weight:400} .verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} .quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} .quoteblock .attribution br,.verseblock .attribution br{display:none} .quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} .quoteblock.abstract{margin:0 0 1.25em 0;display:block} .quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} .quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} table.tableblock{max-width:100%;border-collapse:separate} table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0} table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0} table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0} table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0} table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0} table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0} table.frame-all{border-width:1px} table.frame-sides{border-width:0 1px} table.frame-topbot{border-width:1px 0} th.halign-left,td.halign-left{text-align:left} th.halign-right,td.halign-right{text-align:right} th.halign-center,td.halign-center{text-align:center} th.valign-top,td.valign-top{vertical-align:top} th.valign-bottom,td.valign-bottom{vertical-align:bottom} th.valign-middle,td.valign-middle{vertical-align:middle} table thead th,table tfoot th{font-weight:bold} tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} p.tableblock>code:only-child{background:none;padding:0} p.tableblock{font-size:1em} td>div.verse{white-space:pre} ol{margin-left:1.75em} ul li ol{margin-left:1.5em} dl dd{margin-left:1.125em} dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em} ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px} ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block} ul.inline>li>*{display:block} .unstyled dl dt{font-weight:400;font-style:normal} ol.arabic{list-style-type:decimal} ol.decimal{list-style-type:decimal-leading-zero} ol.loweralpha{list-style-type:lower-alpha} ol.upperalpha{list-style-type:upper-alpha} ol.lowerroman{list-style-type:lower-roman} ol.upperroman{list-style-type:upper-roman} ol.lowergreek{list-style-type:lower-greek} .hdlist>table,.colist>table{border:0;background:none} .hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} td.hdlist1{font-weight:bold;padding-bottom:1.25em} .literalblock+.colist,.listingblock+.colist{margin-top:-.5em} .colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} .colist>table tr>td:last-of-type{padding:.25em 0} .thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} .imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} .imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} .imageblock>.title{margin-bottom:0} .imageblock.thumb,.imageblock.th{border-width:6px} .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} .image.left{margin-right:.625em} .image.right{margin-left:.625em} a.image{text-decoration:none;display:inline-block} a.image object{pointer-events:none} sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} sup.footnote a,sup.footnoteref a{text-decoration:none} sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} #footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} #footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0} #footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;text-indent:-1.05em;margin-bottom:.2em} #footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none} #footnotes .footnote:last-of-type{margin-bottom:0} #content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} .gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} .gist .file-data>table td.line-data{width:99%} div.unbreakable{page-break-inside:avoid} .big{font-size:larger} .small{font-size:smaller} .underline{text-decoration:underline} .overline{text-decoration:overline} .line-through{text-decoration:line-through} .aqua{color:#00bfbf} .aqua-background{background-color:#00fafa} .black{color:#000} .black-background{background-color:#000} .blue{color:#0000bf} .blue-background{background-color:#0000fa} .fuchsia{color:#bf00bf} .fuchsia-background{background-color:#fa00fa} .gray{color:#606060} .gray-background{background-color:#7d7d7d} .green{color:#006000} .green-background{background-color:#007d00} .lime{color:#00bf00} .lime-background{background-color:#00fa00} .maroon{color:#600000} .maroon-background{background-color:#7d0000} .navy{color:#000060} .navy-background{background-color:#00007d} .olive{color:#606000} .olive-background{background-color:#7d7d00} .purple{color:#600060} .purple-background{background-color:#7d007d} .red{color:#bf0000} .red-background{background-color:#fa0000} .silver{color:#909090} .silver-background{background-color:#bcbcbc} .teal{color:#006060} .teal-background{background-color:#007d7d} .white{color:#bfbfbf} .white-background{background-color:#fafafa} .yellow{color:#bfbf00} .yellow-background{background-color:#fafa00} span.icon>.fa{cursor:default} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]:after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} p,blockquote,dt,td.content,span.alt{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} .print-only{display:none!important} @media print{@page{margin:1.25cm .75cm} *{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} a{color:inherit!important;text-decoration:underline!important} a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} abbr[title]:after{content:" (" attr(title) ")"} pre,blockquote,tr,img,object,svg{page-break-inside:avoid} thead{display:table-header-group} svg{max-width:100%} p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} #toc,.sidebarblock,.exampleblock>.content{background:none!important} #toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important} .sect1{padding-bottom:0!important} .sect1+.sect1{border:0!important} #header>h1:first-child{margin-top:1.25rem} body.book #header{text-align:center} body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0} body.book #header .details{border:0!important;display:block;padding:0!important} body.book #header .details span:first-child{margin-left:0!important} body.book #header .details br{display:block} body.book #header .details br+span:before{content:none!important} body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} .listingblock code[data-lang]:before{display:block} #footer{background:none!important;padding:0 .9375em} #footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em} .hide-on-print{display:none!important} .print-only{display:block!important} .hide-for-print{display:none!important} .show-for-print{display:inherit!important}} /* custom styling */ h1,h2,h3,h4,h5,div#toctitle{ color: #428bca; } div#header{ background: url("https://blog.philipphauer.de/img/philipphauer-logo.png") no-repeat; background-size: 15px; background-position-x: 15px; background-position-y: 35px; padding-left: 47px; border-bottom: 1px solid #ddddd8; } div#header h1{ border-bottom: none !important; } a { color: #428bca; } a:hover, a:active{ color: #295d83; } div.admonitionblock>table td.content { color: rgba(0,0,0,.8); } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/docs/asciidoc/general-remarks.adoc ================================================ == General Remarks Placeholder for your manually written documenation... WARNING: AsciiDoc Warning! ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/docs/asciidoc/index.adoc ================================================ include::{generated}/overview.adoc[] include::general-remarks.adoc[] include::usage.adoc[] include::{generated}/paths.adoc[] include::{generated}/security.adoc[] include::{generated}/definitions.adoc[] ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/docs/asciidoc/usage.adoc ================================================ == Usage Even more manually written documentation. For instance, you can describe common use cases or workflows for your API here. === Retrieve all Bands Just call `GET` on the `link:#_getbands[/bands]` resource. It will return link:#_payload_for_band_retrieval[band objects]. [source,json] ---- [ { "foundation": 1997, "id": "faf86775-ca8e-414b-bfe3-fbe2cd378a05", "name": "Flogging Molly" } ] ---- As you can see, you can also link to the generated documentation. ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/RestApiDocApplication.java ================================================ package de.philipphauer.blog; import de.philipphauer.blog.resources.BandResource; import de.philipphauer.blog.resources.CORSFilter; import de.philipphauer.blog.resources.DocumentationResource; import io.dropwizard.Application; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; public class RestApiDocApplication extends Application { public static void main(final String[] args) throws Exception { new RestApiDocApplication().run(args); } @Override public String getName() { return "RestApiDocApplication"; } @Override public void initialize(final Bootstrap bootstrap) { } @Override public void run(final RestApiDocConfiguration configuration, final Environment environment) { environment.jersey().register(BandResource.class); environment.jersey().register(DocumentationResource.class); environment.jersey().register(CORSFilter.class); } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/RestApiDocConfiguration.java ================================================ package de.philipphauer.blog; import io.dropwizard.Configuration; public class RestApiDocConfiguration extends Configuration { } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/apiDocGen/SwaggerAndAsciiDocGenerator.java ================================================ package de.philipphauer.blog.apiDocGen; import io.github.swagger2markup.Swagger2MarkupConverter; import io.swagger.jaxrs.config.BeanConfig; import io.swagger.models.Swagger; import io.swagger.util.Json; import io.swagger.util.Yaml; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** * a) parses the resource classes and generates the swagger spec, * b) takes the swagger spec and generates the asciidoc. *
* Arguments: * arg[0]: target folder for swagger.json and swagger.yaml. e.g. "target/classes" * arg[1]: target folder for the generated asciidoc. e.g. "target/asciidoc/generated" */ public class SwaggerAndAsciiDocGenerator { public static void main(String[] args) throws IOException { Path swaggerTargetFolder = Paths.get(args[0]); Path asciiDocTargetFolder = Paths.get(args[1]); System.out.println("Creating Swagger Spec in " + swaggerTargetFolder); createSwagger(swaggerTargetFolder); System.out.println("Generating AsciiDoc in " + asciiDocTargetFolder); convertSwaggerToAsciiDoc(swaggerTargetFolder, asciiDocTargetFolder); System.out.println("Done."); } private static void createSwagger(Path swaggerTargetFolder) throws IOException { BeanConfig config = new BeanConfig(); config.setVersion("v1"); config.setTitle("Band API"); config.setDescription("An API to retrieve and create bands."); config.setSchemes(new String[]{"http"}); config.setHost("localhost:8080"); config.setBasePath(""); config.setResourcePackage("de.philipphauer.blog.resources"); config.setScan();//this "setter" triggers the scanning... nice naming... Swagger swagger = config.getSwagger(); String json = Json.pretty().writeValueAsString(swagger); Path jsonFile = swaggerTargetFolder.resolve("swagger.json"); Files.write(jsonFile, json.getBytes(StandardCharsets.UTF_8)); String yaml = Yaml.mapper().writeValueAsString(swagger); Path yamlFile = swaggerTargetFolder.resolve("swagger.yaml"); Files.write(yamlFile, yaml.getBytes(StandardCharsets.UTF_8)); } private static void convertSwaggerToAsciiDoc(Path swaggerTargetFolder, Path asciiDocTargetFolder){ Path swaggerFile = swaggerTargetFolder.resolve("swagger.json"); Swagger2MarkupConverter.from(swaggerFile) .build() .toFolder(asciiDocTargetFolder); } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/resources/BandResource.java ================================================ package de.philipphauer.blog.resources; import com.google.common.collect.ImmutableList; import de.philipphauer.blog.resources.dto.BandCreationDTO; import de.philipphauer.blog.resources.dto.BandRetrievalDTO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.ResponseHeader; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; import java.util.UUID; @Path("/") @Produces(MediaType.APPLICATION_JSON) @Api public class BandResource { private static final BandRetrievalDTO A_BAND = new BandRetrievalDTO() .setId(UUID.randomUUID()) .setName("Flogging Molly") .setFoundation(1997); @GET @Path("/bands") @ApiOperation(value = "Retrieve all Bands", notes = "See [usage](#_retrieve_all_bands).") public List getBands(@QueryParam("offset") @DefaultValue("0") int offset, @QueryParam("limit") @DefaultValue("100") int limit) { return ImmutableList.of(A_BAND); } @GET @Path("/bands/{bandId}") @ApiOperation(value = "Retrieve a single Band", notes = "Description") public BandRetrievalDTO getBand(@PathParam("bandId") String bandId) { return A_BAND; } @POST @Path("/bands/") @Consumes(MediaType.APPLICATION_JSON) @ApiOperation(value = "Create a new Band", notes = "Description") @ApiResponses(value = @ApiResponse(code = 201, message = "Successfully created band", responseHeaders = @ResponseHeader(name = "Location", description = "URL of the created band. e.g. `/band/`") )) public Response createBand(@ApiParam(required = true) BandCreationDTO newBand) { System.out.println(newBand); return null; } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/resources/CORSFilter.java ================================================ package de.philipphauer.blog.resources; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.ext.Provider; import java.io.IOException; @Provider public class CORSFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/resources/DocumentationResource.java ================================================ package de.philipphauer.blog.resources; import com.google.common.base.Throwables; import com.google.common.io.Resources; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; @Path("/") public class DocumentationResource { @GET @Path("swagger.json") @Produces(MediaType.APPLICATION_JSON) public String swaggerJson() { return getFileContent("swagger.json"); } @GET @Path("swagger.yaml") @Produces("application/yaml") public String swaggerYaml(){ return getFileContent("swagger.yaml"); } @GET @Path("application-doc.html") @Produces(MediaType.TEXT_HTML) public String doc(){ return getFileContent("index.html"); } private String getFileContent(String fileName) { try { URL url = Resources.getResource(fileName); return Resources.toString(url, StandardCharsets.UTF_8); } catch (IOException e) { throw Throwables.propagate(e); } } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/resources/dto/BandCreationDTO.java ================================================ package de.philipphauer.blog.resources.dto; import com.google.common.base.MoreObjects; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @ApiModel("Payload for band creation") public class BandCreationDTO { @ApiModelProperty(value = "Name of the band", required = true) private String name; @ApiModelProperty(value = "Year of the foundation", required = true) private int foundation; public String getName() { return name; } public BandCreationDTO setName(String name) { this.name = name; return this; } public int getFoundation() { return foundation; } public BandCreationDTO setFoundation(int foundation) { this.foundation = foundation; return this; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("name", name) .add("foundation", foundation) .toString(); } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/java/de/philipphauer/blog/resources/dto/BandRetrievalDTO.java ================================================ package de.philipphauer.blog.resources.dto; import com.google.common.base.MoreObjects; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.util.UUID; @ApiModel("Payload for band retrieval") public class BandRetrievalDTO { @ApiModelProperty(value = "UUID", required = true) private UUID id; @ApiModelProperty(value = "Name of the band", required = true) private String name; @ApiModelProperty(value = "Year of the foundation", required = true) private int foundation; public BandRetrievalDTO setId(UUID id) { this.id = id; return this; } public UUID getId() { return id; } public String getName() { return name; } public BandRetrievalDTO setName(String name) { this.name = name; return this; } public int getFoundation() { return foundation; } public BandRetrievalDTO setFoundation(int foundation) { this.foundation = foundation; return this; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("id", id) .add("name", name) .add("foundation", foundation) .toString(); } } ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/src/main/resources/banner.txt ================================================ ================================================================================ rest-api-doc-jaxrs-swagger-asciidoc ================================================================================ ================================================ FILE: rest-api-doc-jaxrs-swagger-asciidoc/swagger-ui/docker-compose.yml ================================================ version: '2' services: swagger_ui: image: swaggerapi/swagger-ui ports: - "8090:8080" ================================================ FILE: sealedclasses/.gitignore ================================================ .idea/ *.iml target ================================================ FILE: sealedclasses/docker-compose.yml ================================================ version: '3' services: service-stub: build: ./service-stub/ ports: - "5000:5000" ================================================ FILE: sealedclasses/pom.xml ================================================ 4.0.0 com.phauer sealedclasses 1.0-SNAPSHOT jar com.phauer sealedclasses UTF-8 1.3.20 official 4.12 org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.slf4j slf4j-api 1.7.25 org.springframework spring-web 5.0.10.RELEASE com.fasterxml.jackson.module jackson-module-kotlin 2.9.5 org.jetbrains.kotlin kotlin-test-junit ${kotlin.version} test junit junit ${junit.version} test src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} 1.8 compile compile compile test-compile test-compile test-compile ================================================ FILE: sealedclasses/service-stub/Dockerfile ================================================ FROM python:3.6.4-alpine3.7 RUN pip install --ignore-installed "flask==1.0.1" COPY service-stub.py / CMD python3 /service-stub.py ================================================ FILE: sealedclasses/service-stub/service-stub.py ================================================ #!/usr/bin/env python3 import json from flask import Flask, Response app = Flask(__name__) @app.route('/userProfiles/') def get_design_data(user_id): response = { "id": user_id, "name": "Peter", "avatarUrl": "http://localhost/peter.jpg" } return Response(json.dumps(response), mimetype='application/json') if __name__ == '__main__': # nice: in debug mode, flask detects changes in this file. no need to restart the process! # 0.0.0.0 -> make server accessible externally (important for docker) app.run(debug=True, port=5000, host='0.0.0.0') ================================================ FILE: sealedclasses/src/main/kotlin/com/phauer/HttpUserProfileClient.kt ================================================ package com.phauer import com.phauer.common.Outcome import com.phauer.common.exhaustive import com.phauer.common.restTemplate import org.springframework.web.client.RestClientException import org.springframework.web.client.RestClientResponseException import org.springframework.web.client.RestTemplate import org.springframework.web.client.getForObject import java.io.IOException /** */ class HttpUserProfileClient( private val restTemplate: RestTemplate ) { /** * Version 1: With Exceptions */ @Throws(UserProfileClientException::class) // this (or javadoc) may help to document the exception... fun requestUserProfile1(userId: String): UserProfileDTO = try { restTemplate.getForObject("http://localhost:5000/userProfiles/$userId")!! } catch (ex: IOException) { throw UserProfileClientException( message = "Server request failed due to an IO exception. Id: $userId, Message: ${ex.message}", cause = ex ) } catch (ex: RestClientException) { throw UserProfileClientException( message = "Server request failed. Id: $userId. status code: ${(ex as? RestClientResponseException)?.rawStatusCode}. body: ${(ex as? RestClientResponseException)?.responseBodyAsString}", cause = ex ) } /** * Version 2: With Sealed Classes (domain-specific result class) */ fun requestUserProfile2(userId: String): UserProfileResult = try { val userProfile = restTemplate.getForObject("http://localhost:5000/userProfiles/$userId")!! UserProfileResult.Success(userProfile = userProfile) } catch (ex: IOException) { UserProfileResult.Error( message = "Server request failed due to an IO exception. Id: $userId, Message: ${ex.message}", cause = ex ) } catch (ex: RestClientException) { UserProfileResult.Error( message = "Server request failed. Id: $userId. status code: ${(ex as? RestClientResponseException)?.rawStatusCode}. body: ${(ex as? RestClientResponseException)?.responseBodyAsString}", cause = ex ) } /** * Version 3: With Sealed Classes (generic Result classes) */ fun requestUserProfile3(userId: String): Outcome = try { val userProfile = restTemplate.getForObject("http://localhost:5000/userProfiles/$userId")!! Outcome.Success(value = userProfile) } catch (ex: IOException) { Outcome.Error( message = "Server request failed due to an IO exception. Id: $userId, Message: ${ex.message}", cause = ex ) } catch (ex: RestClientException) { Outcome.Error( message = "Server request failed. Id: $userId. status code: ${(ex as? RestClientResponseException)?.rawStatusCode}. body: ${(ex as? RestClientResponseException)?.responseBodyAsString}", cause = ex ) } fun downloadImage1(avatarUrl: String): ByteArray { return ByteArray(0) } fun downloadImage2(avatarUrl: String): ImageDownloadResult { return ImageDownloadResult.Success(ByteArray(0)) } } class UserProfileClientException(message: String, cause: Exception? = null) : RuntimeException(message, cause) class ImageDownloadException(message: String, cause: Exception? = null) : RuntimeException(message, cause) class SuspiciousException(message: String, cause: Exception? = null) : RuntimeException(message, cause) sealed class UserProfileResult { data class Success(val userProfile: UserProfileDTO) : UserProfileResult() data class Error(val message: String, val cause: Exception? = null) : UserProfileResult() } sealed class ImageDownloadResult { data class Success(val image: ByteArray) : ImageDownloadResult() data class Error(val message: String, val cause: Exception? = null) : ImageDownloadResult() } data class UserProfileDTO( val id: String, val name: String, val avatarUrl: String ) fun main() { val client = HttpUserProfileClient(restTemplate()) val userId = "1" /* * Version 1 */ val avatarUrl = try { client.requestUserProfile1(userId).avatarUrl } catch (ex: UserProfileClientException) { "http://localhost/defaultAvatar.png" } try { val result = client.requestUserProfile1(userId) processUserProfile(result) } catch (ex: UserProfileClientException) { queueDesignForRetry(userId, ex.message!!) } /* * Version 2 */ val avatarUrl2 = when (val result = client.requestUserProfile2(userId)) { is UserProfileResult.Success -> result.userProfile.avatarUrl is UserProfileResult.Error -> "http://localhost/defaultAvatar.png" } when (val result = client.requestUserProfile2(userId)) { is UserProfileResult.Success -> processUserProfile(result.userProfile) is UserProfileResult.Error -> queueDesignForRetry(userId, result.message) }.exhaustive /* * Version 3 */ val avatarUrl3 = when (val result = client.requestUserProfile3(userId)) { is Outcome.Success -> result.value.avatarUrl is Outcome.Error -> "http://localhost/defaultAvatar.png" } when (val result = client.requestUserProfile3(userId)) { is Outcome.Success -> processUserProfile(result.value) is Outcome.Error -> queueDesignForRetry(userId, result.message) }.exhaustive /* Readability Safety (force to handle error case. adding new error type) Easy to Use (compiler guides us) less-error prone (no uncaught runtime exceptions, what about adding new exception types?) FP-compatibility (no side-effects) */ } fun aMoreComplicatedExample() { val client = HttpUserProfileClient(restTemplate()) val userId = "1" try { val profile = client.requestUserProfile1(userId) try { val image = client.downloadImage1(profile.avatarUrl) processImage(image) } catch (ex: ImageDownloadException) { queueForRetry(userId, ex.message) } } catch (ex: UserProfileClientException) { showMessageToUser(userId, ex.message) } catch (ex: SuspiciousException) { // which method throws this exception? // requestUserProfile1()? downloadImage1()? processImage()? queueForRetry()? } // have we forgot to catch an exception? Who knows. // vs when (val profileResult = client.requestUserProfile2(userId)) { is UserProfileResult.Success -> { when (val imageResult = client.downloadImage2(profileResult.userProfile.avatarUrl)) { is ImageDownloadResult.Success -> processImage(imageResult.image) is ImageDownloadResult.Error -> queueForRetry(userId, imageResult.message) } } is UserProfileResult.Error -> showMessageToUser(userId, profileResult.message) } } fun showMessageToUser(userId: String, message: String?) { } fun queueForRetry(userId: String, message: String?) { } fun processImage(image: ByteArray) { } fun queueDesignForRetry(designId: String, errorMessage: String) { println("queueJobForRetry $designId") println(errorMessage) } fun processUserProfile(value: UserProfileDTO) { println("processUserProfile $value") } ================================================ FILE: sealedclasses/src/main/kotlin/com/phauer/ImageAvailabilityClient.kt ================================================ package com.phauer import com.phauer.common.restTemplate import org.springframework.web.client.HttpClientErrorException import org.springframework.web.client.ResourceAccessException import org.springframework.web.client.RestClientException import org.springframework.web.client.RestTemplate /** * enum as a result can also be fine. */ class ImageAvailabilityClient( private val restTemplate: RestTemplate ) { fun checkAvailabilityState(imageUrl: String): ImageAvailabilityState = try { restTemplate.headForHeaders(imageUrl) ImageAvailabilityState.OK } catch (exception: HttpClientErrorException) { ImageAvailabilityState.UNAVAILABLE } catch (exception: ResourceAccessException) { ImageAvailabilityState.TEMPORARY_UNAVAILABLE } catch (exception: RestClientException) { ImageAvailabilityState.UNAVAILABLE } } enum class ImageAvailabilityState { OK, TEMPORARY_UNAVAILABLE, UNAVAILABLE } fun main() { val client = ImageAvailabilityClient(restTemplate()) val imageUrl = "http://localhost/dog.jpg" when (client.checkAvailabilityState(imageUrl)) { ImageAvailabilityState.OK -> markImageAsAvailable(imageUrl) ImageAvailabilityState.UNAVAILABLE -> markImageAsUnavailable(imageUrl) ImageAvailabilityState.TEMPORARY_UNAVAILABLE -> { // do nothing right now and just wait for next execution cycle } } } fun markImageAsUnavailable(imageUrl: Any) { } fun markImageAsAvailable(imageUrl: Any) { } ================================================ FILE: sealedclasses/src/main/kotlin/com/phauer/LdapDAO.kt ================================================ package com.phauer import com.phauer.AuthenticationResult.LdapGroup import com.phauer.AuthenticationResult.Success /** * create tailored result object */ class LdapDAO( ) { fun authenticate(username: String, password: String): AuthenticationResult { return Success(LdapGroup.READONLY) } } sealed class AuthenticationResult { data class Success(val group: LdapGroup) : AuthenticationResult() data class Failure(val reason: FailureReason) : AuthenticationResult() enum class FailureReason { BLANK_USER_OR_PW, INVALID_USER_OR_PW, USER_IS_NOT_IN_GROUP, CONNECTION_ISSUES } enum class LdapGroup { READONLY, NORMAL, ADMIN } } ================================================ FILE: sealedclasses/src/main/kotlin/com/phauer/common/Common.kt ================================================ package com.phauer.common import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import org.springframework.http.converter.BufferedImageHttpMessageConverter import org.springframework.http.converter.ByteArrayHttpMessageConverter import org.springframework.http.converter.StringHttpMessageConverter import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.web.client.RestTemplate import java.nio.charset.StandardCharsets /* The name "Result" conflicts with a class from the Kotlin std lib. */ sealed class Outcome { data class Success(val value: T) : Outcome() data class Error(val message: String, val cause: Exception? = null) : Outcome() } val T.exhaustive: T get() = this fun restTemplate(): RestTemplate { val mapper = ObjectMapper().registerKotlinModule() mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) return RestTemplate().apply { messageConverters = listOf( MappingJackson2HttpMessageConverter(mapper), StringHttpMessageConverter(StandardCharsets.UTF_8), BufferedImageHttpMessageConverter(), ByteArrayHttpMessageConverter() ) } } ================================================ FILE: sealedclasses/src/test/kotlin/com/phauer/HelloTest.kt ================================================ package com.phauer import org.junit.Test import kotlin.test.assertEquals class HelloTest { } ================================================ FILE: smooth-local-dev-docker/.gitignore ================================================ .idea/ target/ *.iml ================================================ FILE: smooth-local-dev-docker/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] flask = "==0.12.2" Faker = "==0.8.10" pymongo = "==3.6.1" mysql-connector = "==2.1.6" [dev-packages] [requires] python_version = "3.6" ================================================ FILE: smooth-local-dev-docker/bla.conf ================================================ ktor { deployment { port = 8080 watch = [ solutions/exercise4 ] } application { modules = [ de.philipphauer.blog.MainKt.module ] } } ================================================ FILE: smooth-local-dev-docker/docker-compose.yml ================================================ version: '3' services: mongo: image: mongo:3.4.3 ports: - "27017:27017" command: --profile=1 --slowms=0 mongo_seeding: build: ./local-dev/mongo-seeding depends_on: - mongo mysql: image: mysql:5.6.33 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "database" mysql_seeding: build: ./local-dev/mysql-seeding depends_on: - mysql external_service_stub: build: ./local-dev/external-service-stub ports: - "5000:5000" # external_service_mongo: # image: mongo:3.4.3 # ports: # - "27018:27017" # command: --profile=1 --slowms=0 # external_service_wrapped: # build: # context: local-dev/external-service-wrapped/ # args: # SERVICE_VERSION: 2.13.13 # ports: # - "8080:8080" # depends_on: # - external_service_mongo ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-stub/Dockerfile ================================================ FROM python:3.6.4-alpine3.7 RUN pip install pipenv COPY Pipfile* / RUN pipenv install --deploy --system COPY external-service-stub.py / COPY static-user-response.json /static-user-response.json CMD python3 /external-service-stub.py ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-stub/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] flask = "==0.12.2" Faker = "==0.8.10" [dev-packages] [requires] python_version = "3.6" ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-stub/external-service-stub.py ================================================ #!/usr/bin/env python3 import json from faker import Faker from flask import Flask, Response app = Flask(__name__) faker = Faker('en') # A: Generate the payload with faker @app.route('/users', methods=['GET']) def get_users_faker(): response_users = [generate_user(user_id) for user_id in range(50)] payload = { 'users': response_users, 'size': len(response_users) } return Response(json.dumps(payload), mimetype='application/json') def generate_user(user_id): return { 'id': user_id, 'email': faker.email(), 'name': faker.name(), 'address': faker.address(), 'company': faker.company(), 'keyAccountInfo': faker.sentence(nb_words=6) if faker.boolean(chance_of_getting_true=50) else None } # B: Return a static payload @app.route('/users2', methods=['GET']) def get_users_static(): with open('static-user-response.json', 'r') as payload_file: return Response(payload_file.read(), mimetype='application/json') app.run(debug=False, port=5000, host='0.0.0.0') ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-stub/static-user-response.json ================================================ { "size": 50, "users": [ { "company": "Powers Inc", "email": "ihouston@garcia.com", "id": 0, "key_account_info": null, "name": "Shannon Moore" }, { "company": "Stark-Sullivan", "email": "katie55@wood.com", "id": 1, "key_account_info": null, "name": "Jeffery Sloan" }, { "company": "Garza, Gonzalez and Phillips", "email": "wheath@roberts-owens.com", "id": 2, "key_account_info": "This is an important account!", "name": "Nicole Collins" } ] } ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-wrapped/Dockerfile ================================================ FROM openjdk:8u151-jre-alpine3.7 RUN apk add --no-cache curl # default build argument. overwritten in docker-compose.yml ARG SERVICE_VERSION=2.13.13 RUN curl --user nexusUser:nexusPassword --output external-service.jar https://my-nexus-repo.com/repository/maven-public/de/philipphauer/blog/external-service/$SERVICE_VERSION/external-service-$SERVICE_VERSION.jar COPY config.yaml / CMD java -jar external-service.jar --spring.config.location config.yaml ================================================ FILE: smooth-local-dev-docker/local-dev/external-service-wrapped/config.yaml ================================================ server: port: 8080 data: mongodb: # the host is the service name in the docker-compose.yml. uri: "mongodb://user:password@external_service_mongo:27018/test" ================================================ FILE: smooth-local-dev-docker/local-dev/mongo-seeding/Dockerfile ================================================ FROM python:3.6.4-alpine3.7 RUN pip install pipenv COPY Pipfile* / RUN pipenv install --deploy --system COPY seed-mongo.py / CMD python3 /seed-mongo.py ================================================ FILE: smooth-local-dev-docker/local-dev/mongo-seeding/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] pymongo = "==3.6.1" Faker = "==0.8.10" [dev-packages] [requires] python_version = "3.6" ================================================ FILE: smooth-local-dev-docker/local-dev/mongo-seeding/seed-mongo.py ================================================ #!/usr/bin/env python3.6 import random from typing import List from bson import ObjectId from faker import Faker from pymongo import MongoClient POSSIBLE_STATES = ['ACTIVE', 'INACTIVE'] POSSIBLE_TAGS = ['vacation', 'business', 'technology', 'mobility', 'apparel'] faker = Faker('en') class MongoSeeder: def __init__(self): host = 'mongo' if script_runs_within_container() else 'localhost' client = MongoClient(f'mongodb://{host}:27017/test') self.db = client.test def seed(self): print('Clearing collection...') self.db.designs.remove({}) print('Inserting new designs...') designs = [generate_design() for _ in range(100)] self.db.designs.insert_many(designs) print('Done.') def generate_design(): data = { '_id': ObjectId() , 'name': faker.word() , 'description': faker.sentence(nb_words=7) , 'date': faker.date_time() , 'tags': choose_max_n_times(possibilities=POSSIBLE_TAGS, max_n=3) , 'state': random.choice(POSSIBLE_STATES) , 'designer': { 'id': random.randint(0, 999999) , 'name': faker.name() , 'address': faker.address() } } if faker.boolean(chance_of_getting_true=50): data['superDesign'] = True return data def script_runs_within_container(): with open('/proc/1/cgroup', 'r') as cgroup_file: return 'docker' in cgroup_file.read() def choose_max_n_times(possibilities: List, max_n: int) -> List: copied_list = list(possibilities) random.shuffle(copied_list) n = random.randint(0, max_n) chosen = [copied_list.pop() for _ in range(n)] return chosen MongoSeeder().seed() ================================================ FILE: smooth-local-dev-docker/local-dev/mysql-seeding/Dockerfile ================================================ FROM python:3.6.4-alpine3.7 RUN pip install pipenv COPY Pipfile* / RUN pipenv install --deploy --system COPY seed-mysql.py / CMD python3 /seed-mysql.py ================================================ FILE: smooth-local-dev-docker/local-dev/mysql-seeding/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] mysql-connector = "==2.1.6" Faker = "==0.8.10" [dev-packages] [requires] python_version = "3.6" ================================================ FILE: smooth-local-dev-docker/local-dev/mysql-seeding/seed-mysql.py ================================================ #!/usr/bin/env python3.6 import random import time import mysql.connector from faker import Faker from mysql.connector import InterfaceError POSSIBLE_STATES = ['ACTIVE', 'INACTIVE'] faker = Faker('en') class MySqlSeeder: def __init__(self): config = { 'user': 'root', 'password': 'root', 'host': 'mysql' if script_runs_within_container() else 'localhost', 'port': '3306', 'database': 'database' } while not hasattr(self, 'connection'): try: self.connection = mysql.connector.connect(**config) self.cursor = self.connection.cursor() except InterfaceError: print("MySQL Container has not started yet. Sleep and retry...") time.sleep(1) def seed(self): print("Clearing old data...") self.drop_user_table() print("Start seeding...") self.create_user_table() self.insert_users() self.connection.commit() self.cursor.close() self.connection.close() print("Done") def create_user_table(self): sql = ''' CREATE TABLE users( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), state VARCHAR(50), birthday TIMESTAMP, notes VARCHAR(150), is_adult TINYINT(1) ); ''' self.cursor.execute(sql) def insert_users(self): for _ in range(300): sql = ''' INSERT INTO users (name, state, birthday, notes, is_adult) VALUES (%(name)s, %(state)s, %(birthday)s, %(notes)s, %(is_adult)s); ''' user_data = { 'name': faker.name(), 'state': random.choice(POSSIBLE_STATES), 'birthday': faker.date_time(), 'notes': faker.sentence(nb_words=5), 'is_adult': faker.boolean(chance_of_getting_true=80) } self.cursor.execute(sql, user_data) def drop_user_table(self): self.cursor.execute('DROP TABLE IF EXISTS users;') def script_runs_within_container(): with open('/proc/1/cgroup', 'r') as cgroup_file: return 'docker' in cgroup_file.read() MySqlSeeder().seed() ================================================ FILE: smooth-local-dev-docker/pom.xml ================================================ 4.0.0 de.philipphauer.blog smooth-local-dev-docker 1.0-SNAPSHOT jar UTF-8 1.2.30 4.12 0.9.1 1.8 false io.ktor ktor-server-netty ${ktor.version} io.ktor ktor-server-core ${ktor.version} io.ktor ktor-client-cio ${ktor.version} org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.slf4j slf4j-simple 1.7.25 org.springframework.data spring-data-mongodb 2.0.6.RELEASE com.fasterxml.jackson.module jackson-module-kotlin 2.9.5 org.jetbrains.kotlin kotlin-test-junit ${kotlin.version} test junit junit ${junit.version} test org.springframework.boot spring-boot-starter-jdbc 2.0.1.RELEASE mysql mysql-connector-java 6.0.6 src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} 1.8 -Xcoroutines=enable compile compile compile test-compile test-compile test-compile ktor http://dl.bintray.com/kotlin/ktor kotlinx http://dl.bintray.com/kotlin/kotlinx jcenter http://jcenter.bintray.com ================================================ FILE: smooth-local-dev-docker/src/main/kotlin/de/philipphauer/blog/ExternalServiceUserClient.kt ================================================ package de.philipphauer.blog import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.request.get class ExternalServiceUserClient { private val client = HttpClient(CIO) private val mapper = ObjectMapper().registerKotlinModule() suspend fun findUsers(): List { val json = client.get("http://localhost:5000/users") val userResponse = mapper.readValue(json, UserResponseDTO::class.java) return userResponse.users } } data class UserResponseDTO( val size: Int, val users: List ) data class UserDTO( val address: String, val company: String, val email: String, val id: Int, val keyAccountInfo: String?, val name: String ) ================================================ FILE: smooth-local-dev-docker/src/main/kotlin/de/philipphauer/blog/Main.kt ================================================ package de.philipphauer.blog import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install import io.ktor.features.CallLogging import io.ktor.features.DefaultHeaders import io.ktor.http.ContentType import io.ktor.response.respondText import io.ktor.routing.Routing import io.ktor.routing.get import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty fun Application.module() { val designDao = MongoDesignDAO() val userDao = MySqlUserDAO() val userClient = ExternalServiceUserClient() install(DefaultHeaders) install(CallLogging) install(Routing) { get("/designs") { val designs = designDao.findDesigns() call.respondText(text = designs.toJson(), contentType = ContentType.Application.Json) } get("/users") { val users = userDao.findUsers() call.respondText(text = users.toJson(), contentType = ContentType.Application.Json) } get("/users_ext") { val users = userClient.findUsers() call.respondText(text = users.toJson(), contentType = ContentType.Application.Json) } } } fun main(args: Array) { embeddedServer( Netty, port = 8080 , watchPaths = listOf("target") , module = Application::module ).start(wait = true) } val mapper = ObjectMapper().registerKotlinModule() private fun Any.toJson() = mapper.writeValueAsString(this) ================================================ FILE: smooth-local-dev-docker/src/main/kotlin/de/philipphauer/blog/MongoDesignDAO.kt ================================================ package de.philipphauer.blog import com.mongodb.MongoClientURI import org.bson.types.ObjectId import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.SimpleMongoDbFactory import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Field import java.time.Instant class MongoDesignDAO { private val dbFactory = SimpleMongoDbFactory(MongoClientURI("mongodb://localhost:27017/test")) private val template = MongoTemplate(dbFactory) fun findDesigns(): List = template.findAll(Design::class.java) } @Document(collection = "designs") data class Design( val id: ObjectId, val name: String, val description: String, val date: Instant, val tags: List, val state: String, val designer: Designer ) data class Designer( @field:Field("id") val id: Int, val name: String, val address: String ) ================================================ FILE: smooth-local-dev-docker/src/main/kotlin/de/philipphauer/blog/MySqlUserDAO.kt ================================================ package de.philipphauer.blog import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.jdbc.core.JdbcTemplate import java.sql.ResultSet import java.time.Instant class MySqlUserDAO { private val dataSource = DataSourceBuilder.create() .username("root") .password("root") .url("jdbc:mysql://localhost:3306/database") .driverClassName("com.mysql.cj.jdbc.Driver") .build() private val template = JdbcTemplate(dataSource) fun findUsers() = template.query("SELECT * FROM users;", this::mapToUser) private fun mapToUser(rs: ResultSet, rowNum: Int) = User( id = rs.getInt("id") , name = rs.getString("name") , state = State.valueOf(rs.getString("state")) , birthday = rs.getTimestamp("birthday").toInstant() , notes = rs.getString("notes") , adult = rs.getBoolean("is_adult") ) } data class User( val id: Int, val name: String, val state: State, val birthday: Instant, val notes: String, val adult: Boolean ) enum class State { ACTIVE, INACTIVE } ================================================ FILE: smooth-local-dev-docker/src/test/kotlin/de/philipphauer/blog/HelloTest.kt ================================================ package de.philipphauer.blog import org.junit.Test import kotlin.test.assertEquals class HelloTest { } ================================================ FILE: testingrestservice/integration-tests/.gitignore ================================================ .idea *.iml target ================================================ FILE: testingrestservice/integration-tests/pom.xml ================================================ 4.0.0 de.philipphauer.blog.testingrestservice integration-tests 1.0-SNAPSHOT 1.8 1.0.5-2 org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} compile compile compile test-compile test-compile test-compile org.apache.maven.plugins maven-compiler-plugin 3.5 compile compile compile testCompile test-compile testCompile ${java.version} ${java.version} junit junit 4.12 test org.assertj assertj-core 1.7.1 test com.jayway.restassured rest-assured 2.8.0 test com.jayway.awaitility awaitility 1.7.0 test com.fasterxml.jackson.core jackson-databind 2.9.10.1 test org.hamcrest hamcrest-core 1.3 test org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.jetbrains.kotlin kotlin-test ${kotlin.version} test com.fasterxml.jackson.module jackson-module-kotlin 2.8.4 ================================================ FILE: testingrestservice/integration-tests/src/test/java/de/philipphauer/blog/testingrestservice/integrationtests/BlogsTest.java ================================================ package de.philipphauer.blog.testingrestservice.integrationtests; import com.jayway.awaitility.Duration; import com.jayway.awaitility.core.ConditionFactory; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.filter.log.RequestLoggingFilter; import com.jayway.restassured.filter.log.ResponseLoggingFilter; import com.jayway.restassured.http.ContentType; import com.jayway.restassured.path.json.JsonPath; import com.jayway.restassured.specification.RequestSpecification; import de.philipphauer.blog.testingrestservice.integrationtests.dto.BlogDTO; import de.philipphauer.blog.testingrestservice.integrationtests.dto.BlogListDTO; import org.junit.BeforeClass; import org.junit.Test; import java.util.concurrent.TimeUnit; import static com.jayway.awaitility.Awaitility.await; import static com.jayway.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; public class BlogsTest { //=> create test code that is readable and easy write. this way keep tests maintainable. //use rest-assured (nice readable fluent api) @Test public void collectionResourceOK(){ given() .param("limit", 20) .when() .get("blogs") .then() .statusCode(200); //(import static com.jayway.restassured.RestAssured.given;) } String host = System.getProperty("host");//better remove this lines for clearity String port = System.getProperty("port"); //use spec to reuse common request parameter private static RequestSpecification spec; @BeforeClass public static void initSpec(){ spec = new RequestSpecBuilder() .setContentType(ContentType.JSON) .setBaseUri("http://localhost:8080/") .addFilter(new ResponseLoggingFilter())//log request and response for better debugging. You can also only log if a requests fails. .addFilter(new RequestLoggingFilter()) .build(); } @Test public void useSpec(){ given() .spec(spec) .param("limit", 20) .when() .get("blogs") .then() .statusCode(200); } @Test public void createBlogAndCheckExistence(){ //use POJOs/DTO to create dummy data and use serialization to get JSON. don't construct json manually. //construction your json payload with javax.json.JsonObject doesn't make any fun at all. verbose, cumbersome, not type-safe and error-prone. instead use POJOs. //POJOs: //DON'T use ordinary setters -> verbose to write. // //DON'T use huge constructor with every possible field as an argument -> hard to read BlogDTO blogDTO = new BlogDTO("Example", "Example", "www.blogdomain.de");//which parameter means what? hard to read. //better: use fluent setter in POJO -> readable! see meaning of every argument. //use intelliJ's generator for setter (Alt+Insert > Getter and Setter) can be performed using only the keyboard. // BlogDTO newBlog = new BlogDTO() .setName("Example") .setDescription("Example") .setUrl("www.blogdomain.de"); //object mapping is built-in into rest-assured. only pass pojo to post. automatically serialized to json and put into the http body //just add jackson as a project dependency -> rest-assured will automatically use it. String locationHeader = given() .spec(spec) .body(newBlog) .when() .post("blogs") .then() .statusCode(201) .extract().header("location"); //extract location header and check existence //use POJO and deserialize respone json to your POJO again. use @JsonIgnoreProperties(ignoreUnknown = true) in pojo if necessary. BlogDTO retrievedBlog = given() .spec(spec) .when() .get(locationHeader) .then() .statusCode(200) .extract().as(BlogDTO.class); //using you POJOs (instead of JSONObject or Strings) makes equality check easy, typesafe and readeble. //use assertj (nice fluent, powerful and typesafe testing api; produces readable failure messages). I like it much more than hamcrest (problem to find matcher that can be used with type XY). assertThat(retrievedBlog.getName()).isEqualTo(newBlog.getName()); assertThat(retrievedBlog.getDescription()).isEqualTo(newBlog.getDescription()); assertThat(retrievedBlog.getUrl()).isEqualTo(newBlog.getUrl()); //import static org.assertj.core.api.Assertions.assertThat; //even better: assertThat(retrievedBlog).isEqualToIgnoringGivenFields(newBlog, "id"); } //write clean test code: keep your test readable and maintainable! keep test methods short; use sub methods with nice descriptive names to make your test readable //every time you start building block and override it with a comment -> extract the block to a method instead and use the comment as a method name. (Strg+Alt+M in IntelliJ) @Test public void createBlogAndCheckExistenceReadable(){ BlogDTO newBlog = createDummyBlog(); String blogResourceLocation = createResource("blogs", newBlog); BlogDTO retrievedBlog = getResource(blogResourceLocation, BlogDTO.class); assertEqualBlog(newBlog, retrievedBlog); } private BlogDTO createDummyBlog() { return new BlogDTO() .setName("Example Name") .setDescription("Example Description") .setUrl("www.blogdomain.de"); } //nice reusable method private String createResource(String path, Object bodyPayload) { return given() .spec(spec) .body(bodyPayload) .when() .post(path) .then() .statusCode(201) .extract().header("location"); } //nice reusable method private T getResource(String locationHeader, Class responseClass) { return given() .spec(spec) .when() .get(locationHeader) .then() .statusCode(200) .extract().as(responseClass); } private void assertEqualBlog(BlogDTO newBlog, BlogDTO retrievedBlog) { assertThat(retrievedBlog.getName()).isEqualTo(newBlog.getName()); assertThat(retrievedBlog.getDescription()).isEqualTo(newBlog.getDescription()); assertThat(retrievedBlog.getUrl()).isEqualTo(newBlog.getUrl()); } //use abstract test class (spec, generic methods (createResource() and getResource()) //use asserj's as() to add domain information to your assertion failure messages private void assertEqualBlog2(BlogDTO newBlog, BlogDTO retrievedBlog) { assertThat(retrievedBlog.getName()).as("Blog Name").isEqualTo(newBlog.getName()); assertThat(retrievedBlog.getDescription()).as("Blog Description").isEqualTo(newBlog.getDescription()); assertThat(retrievedBlog.getUrl()).as("Blog URL").isEqualTo(newBlog.getUrl()); } //I always create a POJO and map json to object. even when used just once for a request response. //to simplify the creation of the POJO class, a) consider public fields (OMG!) b) only add the fields you are interested in (+@JsonIgnoreProperties(ignoreUnknown = true)) // vgl with json-response (with all fields) //but: when you use class to create a dummy obj (e.g. to post it to the service) --> easier with fluent setter and private fields. @Test public void getAllBlogsWithMapping(){ BlogListDTO retrievedBlogs = given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .extract().as(BlogListDTO.class); assertThat(retrievedBlogs.count).isGreaterThan(7); assertThat(retrievedBlogs.blogs).isNotEmpty(); } //alternative to separate POJO class/Mapping: // to extract simple things from response -> jsonpath // (strings -> but not type-safe and error-prone). only for simple things. @Test public void getAllBlogsWithJsonPath(){ //A) using assertj (i prefer because easy to debug and readable; and typesafe, finding matcher) JsonPath retrievedBlogs = given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .extract().jsonPath(); assertThat(retrievedBlogs.getInt("count")).isGreaterThan(5); assertThat(retrievedBlogs.getList("blogs.id")).isNotEmpty(); //B) using directly in rest-assured statement with hamcrest matcher. built-in hamcrest support in rest-assured. shorter&concise, you have to use hamcrest. ;-) //not type-safe, error-prone, harder to debug //import static org.hamcrest.Matchers.*; given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .content("count", greaterThan(5)) .content("blogs", is(not(empty()))); } //more sophisticated assertions @Test public void createBlogAndCheckInList(){ BlogDTO newBlog = createDummyBlog(); String blogResourceLocation = createResource("blogs", newBlog); int createdBlogId = extractId(blogResourceLocation); //use assertj to make powerful assertions about the responded data (e.g. list containsId) //a) object mapping + assertj. typesafe and readable, but more verbose. easier to debug. BlogListDTO retrievedBlogList = given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .extract().as(BlogListDTO.class); assertThat(retrievedBlogList.blogs) .extracting(blogEntry -> blogEntry.id) .contains(createdBlogId); //nice extracting() (like map() from java 8 stream api) //b) jsonpath + hamcrest. jsonpath is also powerful // (recognizes that "blogs" is a field. "blogs.id" returns list of ids. // less robust, harder to debug, but more concise. but trouble with types. given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .content("blogs.id", hasItem(createdBlogId)); //however, jsonpath can be useful -> see docs for more details //c) jsonpath + assertj JsonPath retrievedBlogList2 = given() .spec(spec) .when() .get("blogs") .then() .statusCode(200) .extract().jsonPath(); assertThat(retrievedBlogList2.getList("blogs.id")) .contains(createdBlogId); } @Test public void jsonPath(){ JsonPath jsonPath = new JsonPath("{\"blogs\":[\"posts\":[{\"author\":{\"name\":\"Paul\"}}]]}"); //jsonpath useful when accessing one element in a deeply nested document. don't write a pojo class if you only interested in one element jsonPath.getString("blogs[0].posts[0].author.name"); } private int extractId(String resourceLocation) { int slashIndex = resourceLocation.lastIndexOf("/"); String idString = resourceLocation.substring(slashIndex + 1); return Integer.parseInt(idString); } //Wait and Poll: Dealing with asynchronous behavior (like events) //use awaitility to wait and poll until a certain assertion becomes true or a timeout exceeds. //import static com.jayway.awaitility.Awaitility.await; @Test public void waitAndPoll(){ sendAsyncEventThatChangesABlog(123); await().atMost(Duration.TWO_SECONDS).until(() -> { given() .when() .get("blogs/123") .then() .statusCode(200); }); } private void sendAsyncEventThatChangesABlog(int i) { } //await(), atMost() etc returns immutable ConditionFactory. -> configure once behavior for polling and waiting and reuse it public static final ConditionFactory WAIT = await() .atMost(new Duration(15, TimeUnit.SECONDS)) .pollInterval(Duration.ONE_SECOND) .pollDelay(Duration.ONE_SECOND); @Test public void waitAndPoll2(){ WAIT.until(() -> { //... }); } // - Given-When-Then pattern // - keep this parts short. best: only one or a few parameterized method invocation. use submethods. @Test public void test(){ //Given: set up the input for the action under test (test data, mocks, stubs) //When: execute the action you want to test. //Then: check the output with assertions } } ================================================ FILE: testingrestservice/integration-tests/src/test/java/de/philipphauer/blog/testingrestservice/integrationtests/dto/BlogDTO.java ================================================ package de.philipphauer.blog.testingrestservice.integrationtests.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public class BlogDTO { private String name; private String description; private String url; public BlogDTO() {} //don't: public BlogDTO(String name, String description, String url) { this.name = name; this.description = description; this.url = url; } public String getName() { return name; } public BlogDTO setName(String name) { this.name = name; return this; } public String getDescription() { return description; } public BlogDTO setDescription(String description) { this.description = description; return this; } public String getUrl() { return url; } public BlogDTO setUrl(String url) { this.url = url; return this; } } ================================================ FILE: testingrestservice/integration-tests/src/test/java/de/philipphauer/blog/testingrestservice/integrationtests/dto/BlogListDTO.java ================================================ package de.philipphauer.blog.testingrestservice.integrationtests.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class BlogListDTO { //we leave out offset and limit, because we are not interested in this fields. public int count; public List blogs; public static class BlogReference{ public int id; public String name; public String href; } } ================================================ FILE: testingrestservice/integration-tests/src/test/java/de/philipphauer/blog/testingrestservice/integrationtests/dtoKotlin/BlogDTOKotlin.kt ================================================ package de.philipphauer.blog.testingrestservice.integrationtests.dtoKotlin import com.fasterxml.jackson.annotation.JsonIgnoreProperties //definition: data class BlogDTO (val name: String, val description: String, val url: String) //getters, constructor, hashCode(), equals(), toString(), copy() are included! //usage: val newBlog = BlogDTO( name = "Example", description = "Example", url = "www.blogdomain.de") //readable due to named arguments //let's use different DTOs for creation and retrieval, because they differ (id!) @JsonIgnoreProperties(ignoreUnknown = true) data class BlogRetrievalDTO (val name: String, val description: String, val url: String) ================================================ FILE: testingrestservice/integration-tests/src/test/java/de/philipphauer/blog/testingrestservice/integrationtests/dtoKotlin/BlogsKotlinTest.kt ================================================ package de.philipphauer.blog.testingrestservice.integrationtests.dtoKotlin import com.jayway.restassured.RestAssured.given import com.jayway.restassured.builder.RequestSpecBuilder import com.jayway.restassured.filter.log.RequestLoggingFilter import com.jayway.restassured.filter.log.ResponseLoggingFilter import com.jayway.restassured.http.ContentType import com.jayway.restassured.specification.RequestSpecification import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass import org.junit.Test class BlogsKotlinTest { companion object{ private var spec: RequestSpecification? = null @JvmStatic @BeforeClass fun initSpec() { spec = RequestSpecBuilder() .setContentType(ContentType.JSON) .setBaseUri("http://localhost:8080/") .addFilter(ResponseLoggingFilter()) .addFilter(RequestLoggingFilter()) .build() } } @Test fun kotlinPower(){ //it's important to add the jackson-module-kotlin to your project dependencies //in order to make the deserialization to the Kotlin object work. val retrievedBlog = given() .spec(spec) .`when`() .get("blogs/1") .then() .statusCode(200) .extract().`as`(BlogRetrievalDTO::class.java) assertThat(retrievedBlog.name).isNotEmpty() } } ================================================ FILE: testingrestservice/service/.gitignore ================================================ *.iml .idea target db ================================================ FILE: testingrestservice/service/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 1.3.2.RELEASE de.philipphauer.blog.testingrestservice service 1.0-SNAPSHOT 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-test com.h2database h2 org.springframework.boot spring-boot-devtools true de.sven-jacobs loremipsum 1.0 com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.7.0 junit junit 4.12 test com.squareup.okhttp3 mockwebserver 3.2.0 test org.assertj assertj-core 1.7.1 test org.springframework.boot spring-boot-maven-plugin ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/BlogApplication.java ================================================ package de.philipphauer.blog.testingrestservice.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BlogApplication { private static final Logger log = LoggerFactory.getLogger(BlogApplication.class); public static void main(String[] args) { SpringApplication.run(BlogApplication.class); log.info("/h2-console with url='jdbc:h2:mem:testdb;', user='sa', pw=''"); } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/BlogRepository.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.BlogEntity; import org.springframework.data.repository.CrudRepository; public interface BlogRepository extends CrudRepository { } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/DatabaseInitializer.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.BlogEntity; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.CommentEntity; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.PostEntity; import de.svenjacobs.loremipsum.LoremIpsum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.Stream; @Component public class DatabaseInitializer implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class); LoremIpsum loremIpsum = new LoremIpsum(); @Autowired private BlogRepository repo; @Override public void run(String... strings) throws Exception { log.info("Creating dummy data."); List blogs = createBlogs(6); repo.save(blogs); } private List createComments(int amount) { return Stream.generate(() -> new CommentEntity() .setAuthor(createRandomName()) .setCreatedDateTime(LocalDateTime.now()) .setContent(createRandomCommentText()) ) .limit(amount) .collect(Collectors.toList()); } private List createPosts(int amount) { return Stream.generate(() -> { String title = createRandomPostTitle(); String slug = toSlug(title); return new PostEntity() .setAuthor(createRandomName()) .setCreatedDateTime(LocalDateTime.now()) .setTeaser(loremIpsum.getParagraphs(1)) .setContent(loremIpsum.getParagraphs(7)) .setComments(createComments(5)) .setTitle(title) .setSlug(slug) .setViewCount(random.nextInt(1000)) .setTags(createRandomTags()) .setFeaturedImage(slug+".png"); } ) .limit(amount) .collect(Collectors.toList()); } private List createBlogs(int amount) { return Stream.generate(() -> { String title = createRandomBlogTitle(); return new BlogEntity() .setName(title) .setDescription(loremIpsum.getParagraphs(1)) .setPosts(createPosts(9)) .setUrl("http://www." +toSlug(title) + ".com"); } ) .limit(amount) .collect(Collectors.toList()); } private Random random = new Random(); private List firstNames = Arrays.asList("Max", "Paul", "Tim", "Nils", "Angela", "Maria", "Lea", "Sven", "Helena"); private List lastNames = Arrays.asList("Müller", "Schmidt", "Merkel", "Henkel", "Lange", "Marx", "Heine", "Fischer", "Bauer"); private String createRandomName() { String firstName = getRandomElement(firstNames); String lastName = getRandomElement(lastNames); return firstName + " " + lastName; } private List comments = Arrays.asList("Cool!", "Awesome!", "Thanks!", "Well done!", "That's terrible.", "I like nuts."); private String createRandomCommentText() { return getRandomElement(comments); } private List parts1 = Arrays.asList("Creating", "Analysing", "Designing", "Implementing", "Investigating", "Ignoring"); private List parts2 = Arrays.asList("Performance", "Footprint", "Code", "Quality", "Architecture", "Speed", "Test Code", "Communication"); private List parts3 = Arrays.asList("of Microservices", "of Docker", "of Spring Boot", "of a Vaadin application", "of RESTful Services", "of SOAP Services", "of angular.js", "of react.js"); private String createRandomPostTitle() { String part1 = getRandomElement(parts1); String part2 = getRandomElement(parts2); String part3 = getRandomElement(parts3); return part1 + " " + part2 + " " + part3; } private List blogTitles = Arrays.asList("Java Ecosystem", "Web Development", "Web Architecture", "Software Architecture", "Software Archaeology", "Test Driven Development", "Model Driven Development", "Software Craftsmanship", "Build and Delivery"); private String createRandomBlogTitle() { return getRandomElement(blogTitles); } private String getRandomElement(List list) { return list.get(random.nextInt(list.size())); } private List tags = Arrays.asList("Java", "Web", "Build", "Architecture", "Development", "Integration", "Modelling", "REST", "Scalability", "Cloud", "SOAP", "HTTP", "Swagger", "Best Practice", "Test", "TDD", "Vaadin", "Clean Code", "HATEOAS", "Spring Boot"); private String[] createRandomTags() { return new String[]{getRandomElement(tags), getRandomElement(tags), getRandomElement(tags)}; // return Arrays.asList(getRandomElement(tags), getRandomElement(tags), getRandomElement(tags)); } public String toSlug(String blogTitle){ return blogTitle.replaceAll(" ", "_").toLowerCase(); } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/PostRepository.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.PostEntity; import org.springframework.data.repository.CrudRepository; public interface PostRepository extends CrudRepository { } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/entities/BlogEntity.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess.entities; import javax.persistence.*; import javax.validation.constraints.Size; import java.util.List; @Entity public class BlogEntity { @Id @GeneratedValue(strategy= GenerationType.AUTO) private long id; private String name; @Size(max=300) private String description; private String url; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List posts; public long getId() { return id; } public BlogEntity setId(long id) { this.id = id; return this; } public String getName() { return name; } public BlogEntity setName(String name) { this.name = name; return this; } public String getDescription() { return description; } public BlogEntity setDescription(String description) { this.description = description; return this; } public List getPosts() { return posts; } public BlogEntity setPosts(List posts) { this.posts = posts; return this; } public String getUrl() { return url; } public BlogEntity setUrl(String url) { this.url = url; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/entities/CommentEntity.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess.entities; import javax.persistence.*; import java.time.LocalDateTime; @Entity public class CommentEntity { @Id @GeneratedValue(strategy= GenerationType.AUTO) private long id; private LocalDateTime createdDateTime; private String author; @Lob private String content; public long getId() { return id; } public CommentEntity setId(long id) { this.id = id; return this; } public LocalDateTime getCreatedDateTime() { return createdDateTime; } public CommentEntity setCreatedDateTime(LocalDateTime createdDateTime) { this.createdDateTime = createdDateTime; return this; } public String getAuthor() { return author; } public CommentEntity setAuthor(String author) { this.author = author; return this; } public String getContent() { return content; } public CommentEntity setContent(String content) { this.content = content; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/dataaccess/entities/PostEntity.java ================================================ package de.philipphauer.blog.testingrestservice.service.dataaccess.entities; import javax.persistence.*; import javax.validation.constraints.Size; import java.time.LocalDateTime; import java.util.List; @Entity public class PostEntity { @Id @GeneratedValue(strategy= GenerationType.AUTO) private long id; private LocalDateTime createdDateTime; private String title; private String author; private String slug; private int viewCount; private String featuredImage; private String[] tags; @Size(max=300) private String teaser; @Lob private String content; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List comments; public String[] getTags() { return tags; } public PostEntity setTags(String[] tags) { this.tags = tags; return this; } public String getFeaturedImage() { return featuredImage; } public PostEntity setFeaturedImage(String featuredImage) { this.featuredImage = featuredImage; return this; } public int getViewCount() { return viewCount; } public PostEntity setViewCount(int viewCount) { this.viewCount = viewCount; return this; } public String getSlug() { return slug; } public PostEntity setSlug(String slug) { this.slug = slug; return this; } public long getId() { return id; } public PostEntity setId(long id) { this.id = id; return this; } public LocalDateTime getCreatedDateTime() { return createdDateTime; } public PostEntity setCreatedDateTime(LocalDateTime createdDateTime) { this.createdDateTime = createdDateTime; return this; } public String getTitle() { return title; } public PostEntity setTitle(String title) { this.title = title; return this; } public String getTeaser() { return teaser; } public PostEntity setTeaser(String teaser) { this.teaser = teaser; return this; } public String getContent() { return content; } public PostEntity setContent(String content) { this.content = content; return this; } public List getComments() { return comments; } public PostEntity setComments(List comments) { this.comments = comments; return this; } public String getAuthor() { return author; } public PostEntity setAuthor(String author) { this.author = author; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/rest/BlogsResource.java ================================================ package de.philipphauer.blog.testingrestservice.service.rest; import de.philipphauer.blog.testingrestservice.service.dataaccess.BlogRepository; import de.philipphauer.blog.testingrestservice.service.dataaccess.PostRepository; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.BlogEntity; import de.philipphauer.blog.testingrestservice.service.dataaccess.entities.PostEntity; import de.philipphauer.blog.testingrestservice.service.rest.dto.BlogDTO; import de.philipphauer.blog.testingrestservice.service.rest.dto.BlogsDTO; import de.philipphauer.blog.testingrestservice.service.rest.dto.ReferenceDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @RestController @RequestMapping(value = "/blogs", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public class BlogsResource { @Autowired private BlogRepository blogRepo; @Autowired private PostRepository postRepo; @RequestMapping(value = "", method = RequestMethod.POST) public ResponseEntity createBlog(UriComponentsBuilder uriBuilder, @RequestBody BlogDTO blogDto) { BlogEntity blog = mapToEntry(blogDto); blogRepo.save(blog); UriComponents uriComponents = uriBuilder.path("blogs/{id}").buildAndExpand(blog.getId()); HttpHeaders headers = new HttpHeaders(); headers.setLocation(uriComponents.toUri()); return new ResponseEntity(headers, HttpStatus.CREATED); } private BlogEntity mapToEntry(BlogDTO blogDto) { return new BlogEntity() .setUrl(blogDto.getUrl()) .setDescription(blogDto.getDescription()) .setName(blogDto.getName()); } @RequestMapping(value = "", method = RequestMethod.GET) public BlogsDTO getAll() { Collection blogs = (Collection) blogRepo.findAll(); BlogsDTO blogList = mapToDTO(blogs); return blogList; } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public BlogDTO getBlog(@PathVariable("id") Long id) { BlogEntity blog = blogRepo.findOne(id); BlogDTO blogDTO = mapToDTO(blog); return blogDTO; } @RequestMapping(value = "/{blogId}/posts", method = RequestMethod.GET) public List getAllBlogPosts(@PathVariable("blogId") Long blogId) { BlogEntity blog = blogRepo.findOne(blogId); List posts = blog.getPosts(); return mapToDTO(blog.getId(), posts); } //let's ignore the blogId for the sake of simplicity, because the postId is unique and and not context is necessary @RequestMapping(value = "/{blogId}/posts/{postId}", method = RequestMethod.GET) public PostEntity getBlogPost(@PathVariable("blogId") Long blogId, @PathVariable("postId") Long postId) { PostEntity post = postRepo.findOne(postId); return post; } private BlogDTO mapToDTO(BlogEntity blog) { List posts = blog.getPosts(); List postsDTO = mapToDTO(blog.getId(), posts); BlogDTO blogDTO = new BlogDTO() .setName(blog.getName()) .setDescription(blog.getDescription()) .setPosts(postsDTO) .setUrl(blog.getUrl()); return blogDTO; } private List mapToDTO(long blogId, List posts) { return posts.stream() .map(post -> mapToDTO(blogId, post)) .collect(Collectors.toList()); } private ReferenceDTO mapToDTO(long blogId, PostEntity post){ long id = post.getId(); String title = post.getTitle(); String href = "/blogs/" + blogId + "/posts/" + id; ReferenceDTO ref = new ReferenceDTO().setId(id).setName(title).setHref(href); return ref; } private BlogsDTO mapToDTO(Collection blogs) { List blogRefs = blogs.stream() .map(blogEntry -> { String name = blogEntry.getName(); long id = blogEntry.getId(); String href = "/blogs/" + id; ReferenceDTO ref = new ReferenceDTO().setId(id).setName(name).setHref(href); return ref; }) .collect(Collectors.toList()); return new BlogsDTO() .setBlogs(blogRefs) .setOffset(0) .setLimit(50) .setCount(blogs.size()); } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/rest/dto/BlogDTO.java ================================================ package de.philipphauer.blog.testingrestservice.service.rest.dto; import java.util.List; public class BlogDTO { private String name; private String description; private String url; private List posts; public String getUrl() { return url; } public BlogDTO setUrl(String url) { this.url = url; return this; } public String getName() { return name; } public BlogDTO setName(String name) { this.name = name; return this; } public String getDescription() { return description; } public BlogDTO setDescription(String description) { this.description = description; return this; } public List getPosts() { return posts; } public BlogDTO setPosts(List posts) { this.posts = posts; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/rest/dto/BlogsDTO.java ================================================ package de.philipphauer.blog.testingrestservice.service.rest.dto; import java.util.List; public class BlogsDTO { private int count; private int offset; private int limit; private List blogs; public List getBlogs() { return blogs; } public int getCount() { return count; } public int getOffset() { return offset; } public int getLimit() { return limit; } public BlogsDTO setBlogs(List blogs) { this.blogs = blogs; return this; } public BlogsDTO setCount(int count) { this.count = count; return this; } public BlogsDTO setOffset(int offset) { this.offset = offset; return this; } public BlogsDTO setLimit(int limit) { this.limit = limit; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/rest/dto/ReferenceDTO.java ================================================ package de.philipphauer.blog.testingrestservice.service.rest.dto; public class ReferenceDTO { private long id; private String name; private String href; public String getName() { return name; } public ReferenceDTO setName(String name) { this.name = name; return this; } public long getId() { return id; } public ReferenceDTO setId(long id) { this.id = id; return this; } public String getHref() { return href; } public ReferenceDTO setHref(String href) { this.href = href; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/servicecall/ImageReference.java ================================================ package de.philipphauer.blog.testingrestservice.service.servicecall; public class ImageReference { private String id; private String href; public String getId() { return id; } public ImageReference setId(String id) { this.id = id; return this; } public String getHref() { return href; } public ImageReference setHref(String href) { this.href = href; return this; } } ================================================ FILE: testingrestservice/service/src/main/java/de/philipphauer/blog/testingrestservice/service/servicecall/ImageServiceClient.java ================================================ package de.philipphauer.blog.testingrestservice.service.servicecall; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; import java.util.Arrays; public class ImageServiceClient { private final String imageServiceHost; private final int imageServicePort; public ImageServiceClient(String imageServiceHost, int imageServicePort) { this.imageServiceHost = imageServiceHost; this.imageServicePort = imageServicePort; } public ImageReference requestImage(String id){ RestTemplate restTemplate = new RestTemplate(Arrays.asList(new MappingJackson2HttpMessageConverter())); ImageReference imageReference = restTemplate.getForObject("http://{host}:{port}/images/{id}", ImageReference.class, imageServiceHost, imageServicePort, id); return imageReference; } } ================================================ FILE: testingrestservice/service/src/main/resources/application.properties ================================================ spring.jackson.serialization.write_dates_as_timestamps=false logging.level.org.springframework.web=INFO logging.level.org.hibernate=INFO ================================================ FILE: testingrestservice/service/src/test/java/de/philipphauer/blog/testingrestservice/service/servicecall/ImageReferenceServiceClientTest.java ================================================ package de.philipphauer.blog.testingrestservice.service.servicecall; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; import org.junit.Test; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; public class ImageReferenceServiceClientTest { private MockWebServer imageService; private ImageServiceClient imageClient; @Before public void init() throws IOException { imageService = new MockWebServer(); imageService.start(); //search for an available port HttpUrl baseUrl = imageService.url("/images/"); imageClient = new ImageServiceClient(baseUrl.host(), baseUrl.port()); } @Test public void requestImage() throws JsonProcessingException { ImageReference expectedImageRef = new ImageReference().setId("123").setHref("http://images.company.org/123"); String json = new ObjectMapper().writeValueAsString(expectedImageRef); imageService.enqueue(new MockResponse() .addHeader("Content-Type", "application/json") .setBody(json)); ImageReference retrievedImageRef = imageClient.requestImage("123"); assertThat(retrievedImageRef.getId()).isEqualTo(expectedImageRef.getId()); assertThat(retrievedImageRef.getHref()).isEqualTo(expectedImageRef.getHref()); } } ================================================ FILE: ti-continuation-token/.gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar .idea/ *.iml dependency-reduced-pom.xml ================================================ FILE: ti-continuation-token/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip ================================================ FILE: ti-continuation-token/README.md ================================================ # Example Implementation for the `Timestamp_ID` Continuation Token ```bash ./mvnw package && java -jar target/demo-kotlin*.jar ``` Open `http://localhost:8000/designs?pageSize=3` in your browser and click on the URL in the `nextPage` field in the json payload. You can also submit a certain start date with the query parameter `modifiedSince`: `http://localhost:8000/designs?pageSize=3&modifiedSince=1512757072` The demo application is a lightweight HTTP service written in Kotlin and powered by [HTTP4K](https://www.http4k.org/). It starts within 600 ms 🏇 ================================================ FILE: ti-continuation-token/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # 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 # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # 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 # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home 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 saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME 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"` 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? 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 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" fi else JAVACMD="`which java`" 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." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} echo $MAVEN_PROJECTBASEDIR MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # 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"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: ti-continuation-token/mvnw.cmd ================================================ @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 @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="%MAVEN_PROJECTBASEDIR%\.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_CONFIG% %* 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% ================================================ FILE: ti-continuation-token/pom.xml ================================================ 4.0.0 de.philipphauer ti-continuation-token 1.0-SNAPSHOT jar UTF-8 1.2.21 5.0.2 3.0.1 1.8 org.jetbrains.kotlin kotlin-stdlib-jdk8 ${kotlin.version} org.http4k http4k-core ${http4k.version} org.http4k http4k-server-jetty ${http4k.version} org.http4k http4k-format-jackson ${http4k.version} org.springframework spring-jdbc 5.0.1.RELEASE com.h2database h2 1.4.196 org.jetbrains.kotlin kotlin-reflect ${kotlin.version} org.junit.jupiter junit-jupiter-api ${junit5.version} test org.assertj assertj-core 3.8.0 test org.junit.jupiter junit-jupiter-params 5.0.2 test com.nhaarman mockito-kotlin 1.5.0 test src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} compile compile compile test-compile test-compile test-compile maven-surefire-plugin 2.19 org.junit.platform junit-platform-surefire-provider 1.0.2 org.junit.jupiter junit-jupiter-engine ${junit5.version} org.apache.maven.plugins maven-shade-plugin 3.1.0 de.philipphauer.blog.pagination.MainKt package shade ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/DesignDAO.kt ================================================ package de.philipphauer.blog.pagination import de.philipphauer.blog.pagination.token.ContinuationToken import de.philipphauer.blog.pagination.token.Page import de.philipphauer.blog.pagination.token.createPage import org.springframework.jdbc.core.JdbcTemplate import java.sql.ResultSet import java.time.Clock import java.time.Instant import javax.sql.DataSource class DesignDAO( dataSource: DataSource, /** using the timestamp of the application (instead of the database) is only appropriate * if the application also exclusively sets and updates the timestamp */ private val clock: Clock ) { private val template = JdbcTemplate(dataSource) fun getDesignsSince(modifiedSince: Instant, pageSize: Int): Page { val sql = """SELECT * FROM designs WHERE dateModified >= FROM_UNIXTIME(${modifiedSince.epochSecond}) AND dateModified < FROM_UNIXTIME(${clock.instant().epochSecond}) ORDER BY dateModified asc, id asc LIMIT $pageSize;""" val designs = template.query(sql, this::mapToDesign) return createPage(entities = designs, previousToken = null, pageSize = pageSize) } fun getNextDesignPage(token: ContinuationToken, pageSize: Int): Page { val sql = """SELECT * FROM designs WHERE ( dateModified > FROM_UNIXTIME(${token.timestamp}) OR (dateModified = FROM_UNIXTIME(${token.timestamp}) AND id > ${token.id}) ) AND dateModified < FROM_UNIXTIME(${clock.instant().epochSecond}) ORDER BY dateModified asc, id asc LIMIT $pageSize;""" val designs = template.query(sql, this::mapToDesign) return createPage(entities = designs, previousToken = token, pageSize = pageSize) } private fun mapToDesign(rs: ResultSet, rowNum: Int) = DesignEntity( id = rs.getString("id"), title = rs.getString("title"), imageUrl = rs.getString("imageUrl"), dateModified = rs.getTimestamp("dateModified").toInstant() ) } ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/DesignEntity.kt ================================================ package de.philipphauer.blog.pagination import de.philipphauer.blog.pagination.token.Pageable import java.time.Instant data class DesignEntity( override val id: String, val title: String, val imageUrl: String, private val dateModified: Instant ) : Pageable { override val timestamp = dateModified.epochSecond } ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/DesignResource.kt ================================================ package de.philipphauer.blog.pagination import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import de.philipphauer.blog.pagination.token.toContinuationToken import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status import java.time.Instant class DesignResource(private val dao: DesignDAO) { fun getDesigns(request: Request): Response { val modifiedSince = request.query("modifiedSince").toInstantOrNull() val token = request.query("continuationToken")?.toContinuationToken() val pageSize = request.query("pageSize")?.toInt() ?: 3 val page = when { modifiedSince == null && token == null -> dao.getDesignsSince(Instant.ofEpochSecond(0), pageSize) modifiedSince != null && token == null -> dao.getDesignsSince(modifiedSince, pageSize) modifiedSince == null && token != null -> dao.getNextDesignPage(token, pageSize) else -> return Response(Status.BAD_REQUEST) } val dto = PageDTO( designs = page.entities.map(::mapToDTO), continuationToken = page.token?.toString(), hasNext = page.hasNext, nextPage = page.token?.let { "http://localhost:8000/designs?pageSize=$pageSize&continuationToken=${page.token}" } ) return Response(Status.OK) .header("Content-Type", "application/json;charset=UTF-8") .body(dto.toJson()) } } private fun String?.toInstantOrNull() = when (this) { null -> null else -> Instant.ofEpochSecond(this.toLong()) } private fun mapToDTO(entity: DesignEntity) = DesignDTO( id = entity.id, title = entity.title, imageUrl = entity.imageUrl, dateModified = entity.timestamp ) data class DesignDTO( val id: String, val title: String, val imageUrl: String, val dateModified: Long ) data class PageDTO( val designs: List, val continuationToken: String?, val nextPage: String?, val hasNext: Boolean ) private val mapper = jacksonObjectMapper() private fun PageDTO.toJson() = mapper.writeValueAsString(this) ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/Main.kt ================================================ package de.philipphauer.blog.pagination import de.philipphauer.blog.pagination.util.DesignDatabaseUtil import de.philipphauer.blog.pagination.util.FunctionsMySQL import org.eclipse.jetty.server.NCSARequestLog import org.eclipse.jetty.server.Server import org.h2.jdbcx.JdbcDataSource import org.http4k.core.Method import org.http4k.routing.bind import org.http4k.routing.routes import org.http4k.server.Jetty import org.http4k.server.asServer import org.springframework.core.io.ClassPathResource import org.springframework.jdbc.datasource.init.ScriptUtils import java.time.Clock import java.time.Instant fun main(args: Array) { val resource = bootstrapDesignResource() val routingHandler = routes( "/designs" bind Method.GET to resource::getDesigns ) val jetty = Server(8000).apply { requestLog = NCSARequestLog() } val server = routingHandler.asServer(Jetty(jetty)).start() println("Try http://localhost:8000/designs?pageSize=3 and click on the nextPage URL") println("or http://localhost:8000/designs?pageSize=3&modifiedSince=1512757072") server.block() } private fun bootstrapDesignResource(): DesignResource { val dataSource = JdbcDataSource().apply { user = "sa" password = "" setURL("jdbc:h2:mem:access;MODE=MySQL;DB_CLOSE_DELAY=-1") } FunctionsMySQL.register(dataSource.connection) ScriptUtils.executeSqlScript(dataSource.connection, ClassPathResource("create-designs-table.sql")) DesignDatabaseUtil(dataSource).createDesigns(amount = 7, startDate = Instant.ofEpochSecond(1512757070)) val dao = DesignDAO(dataSource, Clock.systemUTC()) return DesignResource(dao) } ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/token/Model.kt ================================================ package de.philipphauer.blog.pagination.token interface Pageable { val id: String val timestamp: Long } data class ContinuationToken( val timestamp: Long, val id: String ) { override fun toString() = "${timestamp}_$id" } data class Page( val entities: List, val token: ContinuationToken?, val hasNext: Boolean ) ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/token/Pagination.kt ================================================ package de.philipphauer.blog.pagination.token @Throws(InvalidContinuationTokenException::class) fun String?.toContinuationToken(): ContinuationToken? { this ?: return null val parts = this.split("_") if (parts.size != 2) { throw createException(this, null) } try { val timestamp = java.lang.Long.parseUnsignedLong(parts[0]) val id = parts[1] return ContinuationToken(timestamp, id) } catch (ex: Exception) { throw createException(this, ex) } } fun createPage( entities: List, previousToken: ContinuationToken?, pageSize: Int ): Page = Page( entities = entities, token = if (entities.isEmpty()) previousToken else createToken(entities), hasNext = entities.size >= pageSize ) private fun createToken(entities: List): ContinuationToken { val lastEntity = entities.last() return ContinuationToken(lastEntity.timestamp, lastEntity.id) } private fun createException(token: String, ex: Exception?): InvalidContinuationTokenException { return InvalidContinuationTokenException("Invalid token '$token'", ex) } class InvalidContinuationTokenException(msg: String, cause: Exception?) : RuntimeException(msg, cause) ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/util/DesignDatabaseUtil.kt ================================================ package de.philipphauer.blog.pagination.util import org.springframework.jdbc.core.JdbcTemplate import java.time.Instant import javax.sql.DataSource class DesignDatabaseUtil(dataSource: DataSource) { private val utilTemplate = JdbcTemplate(dataSource) fun createDesigns(amount: Int, startDate: Instant = Instant.now()) { val values = (1..amount).mapIndexed { i, _ -> arrayOf(i, "Cat $i", "http://domain.de/cat$i.jpg", startDate.plusSeconds(i.toLong()).epochSecond) } utilTemplate.batchUpdate( "INSERT INTO designs (id, title, imageUrl, dateModified) VALUES (?, ?, ?, FROM_UNIXTIME(?))", values ) } fun insertDesigns(designData: List>) { val values = designData.map { (id, timestamp) -> arrayOf(id, "Cat $id", "http://domain.de/cat$id.jpg", timestamp) } utilTemplate.batchUpdate( "INSERT INTO designs (id, title, imageUrl, dateModified) VALUES (?, ?, ?, FROM_UNIXTIME(?))", values ) } fun removeAllDesigns() { utilTemplate.execute("TRUNCATE TABLE designs;") } fun update(id: String, now: Instant) { val newTitle = "Cat $id (UPDATED)" utilTemplate.update("""UPDATE designs SET dateModified = FROM_UNIXTIME(?), title = ? WHERE id = ?;""", now.epochSecond, newTitle, id) } } ================================================ FILE: ti-continuation-token/src/main/kotlin/de/philipphauer/blog/pagination/util/FunctionsMySQL.kt ================================================ package de.philipphauer.blog.pagination.util import org.h2.util.StringUtils import java.sql.Connection import java.sql.SQLException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale /** * https://github.com/h2database/h2database/blob/master/h2/src/main/org/h2/mode/FunctionsMySQL.java * This class implements some MySQL-specific functions. * * @author Jason Brittain * @author Thomas Mueller */ object FunctionsMySQL { /** * The date format of a MySQL formatted date/time. * Example: 2008-09-25 08:40:59 */ private val DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" /** * Format replacements for MySQL date formats. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-format */ private val FORMAT_REPLACE = arrayOf( "%a", "EEE", "%b", "MMM", "%c", "MM", "%d", "dd", "%e", "d", "%H", "HH", "%h", "hh", "%I", "hh", "%i", "mm", "%j", "DDD", "%k", "H", "%l", "h", "%M", "MMMM", "%m", "MM", "%p", "a", "%r", "hh:mm:ss a", "%S", "ss", "%s", "ss", "%T", "HH:mm:ss", "%W", "EEEE", "%w", "F", "%Y", "yyyy", "%y", "yy", "%%", "%" ) /** * Register the functionality in the database. * Nothing happens if the functions are already registered. * * @param conn the connection */ @Throws(SQLException::class) fun register(conn: Connection) { val init = arrayOf("UNIX_TIMESTAMP", "unixTimestamp", "FROM_UNIXTIME", "fromUnixTime", "DATE", "date") val stat = conn.createStatement() var i = 0 while (i < init.size) { val alias = init[i] val method = init[i + 1] stat.execute( "CREATE ALIAS IF NOT EXISTS " + alias + " FOR \"" + FunctionsMySQL::class.java!!.name + "." + method + "\"" ) i += 2 } } /** * Get the seconds since 1970-01-01 00:00:00 UTC. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_unix-timestamp * * @return the current timestamp in seconds (not milliseconds). */ @JvmStatic fun unixTimestamp(): Int { return (System.currentTimeMillis() / 1000L).toInt() } /** * Get the seconds since 1970-01-01 00:00:00 UTC of the given timestamp. * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_unix-timestamp * * @param timestamp the timestamp * @return the current timestamp in seconds (not milliseconds). */ @JvmStatic fun unixTimestamp(timestamp: java.sql.Timestamp): Int { return (timestamp.time / 1000L).toInt() } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_from-unixtime * * @param seconds The current timestamp in seconds. * @return a formatted date/time String in the format "yyyy-MM-dd HH:mm:ss". */ @JvmStatic fun fromUnixTime(seconds: Int): String { val formatter = SimpleDateFormat( DATE_TIME_FORMAT, Locale.ENGLISH ) return formatter.format(Date(seconds * 1000L)) } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_from-unixtime * * @param seconds The current timestamp in seconds. * @param format The format of the date/time String to return. * @return a formatted date/time String in the given format. */ @JvmStatic fun fromUnixTime(seconds: Int, format: String): String { var format = format format = convertToSimpleDateFormat(format) val formatter = SimpleDateFormat(format, Locale.ENGLISH) return formatter.format(Date(seconds * 1000L)) } private fun convertToSimpleDateFormat(format: String): String { var format = format val replace = FORMAT_REPLACE var i = 0 while (i < replace.size) { format = StringUtils.replaceAll(format, replace[i], replace[i + 1]) i += 2 } return format } /** * See * http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date * This function is dependent on the exact formatting of the MySQL date/time * string. * * @param dateTime The date/time String from which to extract just the date * part. * @return the date part of the given date/time String argument. */ @JvmStatic fun date(dateTime: String?): String? { if (dateTime == null) { return null } val index = dateTime!!.indexOf(' ') return if (index != -1) { dateTime!!.substring(0, index) } else dateTime } } ================================================ FILE: ti-continuation-token/src/main/resources/create-designs-table.sql ================================================ CREATE TABLE designs ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, imageUrl VARCHAR(100) NOT NULL, dateModified TIMESTAMP(0) NOT NULL ); ================================================ FILE: ti-continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/Common.kt ================================================ package de.philipphauer.blog.pagination import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.http4k.core.Response val mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } fun Response.toPageDTO(): PageDTO = mapper.readValue(bodyString(), PageDTO::class.java) ================================================ FILE: ti-continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/DesignResourceTest.kt ================================================ package de.philipphauer.blog.pagination import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever import de.philipphauer.blog.pagination.util.DesignDatabaseUtil import de.philipphauer.blog.pagination.util.FunctionsMySQL import org.assertj.core.api.Assertions.assertThat import org.h2.jdbcx.JdbcDataSource import org.http4k.core.Method import org.http4k.core.Request import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.springframework.core.io.ClassPathResource import org.springframework.jdbc.datasource.init.ScriptUtils import java.time.Clock import java.time.Instant @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DesignResourceTest { private val util = DesignDatabaseUtil(dataSource) init { FunctionsMySQL.register(dataSource.connection) ScriptUtils.executeSqlScript(dataSource.connection, ClassPathResource("create-designs-table.sql")) } @Test fun `page through two pages`() { val resource = createDesignResource() val startDate = Instant.ofEpochSecond(1512757070) util.createDesigns(amount = 5, startDate = startDate) val firstPageResponse = resource.getDesigns(Request(Method.GET, "/designs?pageSize=3")) val firstPage = firstPageResponse.toPageDTO() assertThat(firstPage.continuationToken).isNotNull() assertThat(firstPage.hasNext).isTrue() assertThat(firstPage.nextPage).isNotNull() assertThat(firstPage.designs).containsExactly( DesignDTO(id = "0", title = "Cat 0", imageUrl = "http://domain.de/cat0.jpg", dateModified = 1512757070), DesignDTO(id = "1", title = "Cat 1", imageUrl = "http://domain.de/cat1.jpg", dateModified = 1512757071), DesignDTO(id = "2", title = "Cat 2", imageUrl = "http://domain.de/cat2.jpg", dateModified = 1512757072) ) val secondPageResponse = resource.getDesigns(Request(Method.GET, firstPage.nextPage!!)) val secondPage = secondPageResponse.toPageDTO() assertThat(secondPage.continuationToken).isNotNull() assertThat(secondPage.hasNext).isFalse() assertThat(secondPage.nextPage).isNotNull() assertThat(secondPage.designs).containsExactly( DesignDTO(id = "3", title = "Cat 3", imageUrl = "http://domain.de/cat3.jpg", dateModified = 1512757073), DesignDTO(id = "4", title = "Cat 4", imageUrl = "http://domain.de/cat4.jpg", dateModified = 1512757074) ) } @Test fun `start with a certain modifiedSince query parameter and page through two pages`() { val resource = createDesignResource() val modifiedSince: Long = 1512757072 val designData = listOf>( "0" to 1512757070 , "1" to 1512757071 , "2" to modifiedSince , "3" to 1512757073 , "4" to 1512757074 , "5" to 1512757075 ) util.insertDesigns(designData) val firstPageResponse = resource.getDesigns(Request(Method.GET, "/designs?modifiedSince=$modifiedSince&pageSize=3")) val firstPage = firstPageResponse.toPageDTO() assertThat(firstPage.continuationToken).isNotNull() assertThat(firstPage.hasNext).isTrue() assertThat(firstPage.nextPage).isNotNull() assertThat(firstPage.designs).containsExactly( DesignDTO(id = "2", title = "Cat 2", imageUrl = "http://domain.de/cat2.jpg", dateModified = 1512757072), DesignDTO(id = "3", title = "Cat 3", imageUrl = "http://domain.de/cat3.jpg", dateModified = 1512757073), DesignDTO(id = "4", title = "Cat 4", imageUrl = "http://domain.de/cat4.jpg", dateModified = 1512757074) ) val secondPageResponse = resource.getDesigns(Request(Method.GET, firstPage.nextPage!!)) val secondPage = secondPageResponse.toPageDTO() assertThat(secondPage.continuationToken).isNotNull() assertThat(secondPage.hasNext).isFalse() assertThat(secondPage.nextPage).isNotNull() assertThat(secondPage.designs).containsExactly( DesignDTO(id = "5", title = "Cat 5", imageUrl = "http://domain.de/cat5.jpg", dateModified = 1512757075) ) } /** * we miss an element when the following three things are happening within a single second (and given that this is the current second): a) element 3's timestamp is set to now (99), b) the last page with element 3 is returned (token `3_99`) and c) element 2's timestamp is also set to now (99). (given a timestamp column with second precision. with ms precision, this three things have to happen within one ms) solution: add condition `AND timestamp > now()` to the where clause. */ @Test fun `dont miss elements when updates are happening before the last page`() { val serverClock = mock() val resource = createDesignResource(serverClock) val designData = listOf>( "1" to 10 , "2" to 20 , "3" to 30 ) util.insertDesigns(designData) val client = PaginationClient(resource = resource, pageSize = 3) val sameSecond = Instant.ofEpochSecond(99) util.update(id = "3", now = sameSecond) whenever(serverClock.instant()).doReturn(sameSecond) //to override the server's "now()". client.retrieveNextPageAndRememberResult() util.update(id = "2", now = sameSecond) //this must not be missed in the next request //simulate a passed second (no Thread.sleep() in test required). in reality, this would be a new pagination run whenever(serverClock.instant()).doReturn(sameSecond.plusSeconds(1)) client.retrieveNextPageAndRememberResult() val allDesigns = client.getAllRetrievedDesigns() assertThat(allDesigns).containsOnly( DesignDTO(id = "1", title = "Cat 1", imageUrl = "http://domain.de/cat1.jpg", dateModified = 10) , DesignDTO(id = "2", title = "Cat 2", imageUrl = "http://domain.de/cat2.jpg", dateModified = 20) , DesignDTO(id = "3", title = "Cat 3 (UPDATED)", imageUrl = "http://domain.de/cat3.jpg", dateModified = sameSecond.epochSecond) //this must not be missed: , DesignDTO(id = "2", title = "Cat 2 (UPDATED)", imageUrl = "http://domain.de/cat2.jpg", dateModified = sameSecond.epochSecond) ) } @Test fun `dont return elements with current timestamp - request with touched since`() { val serverClock = mock() val resource = createDesignResource(serverClock) val sameSecond = Instant.ofEpochSecond(99) val designData = listOf>( "1" to 10 , "2" to 20 , "3" to sameSecond.epochSecond ) util.insertDesigns(designData) whenever(serverClock.instant()).doReturn(sameSecond) val response = resource.getDesigns(Request(Method.GET, "/designs?pageSize=3&touchedSince=0")).toPageDTO() assertThat(response.continuationToken).isEqualTo("20_2") assertThat(response.designs).containsOnly( DesignDTO(id = "1", title = "Cat 1", imageUrl = "http://domain.de/cat1.jpg", dateModified = 10) , DesignDTO(id = "2", title = "Cat 2", imageUrl = "http://domain.de/cat2.jpg", dateModified = 20) ) } @Test fun `dont return elements with current timestamp - request with token`() { val serverClock = mock() val resource = createDesignResource(serverClock) val sameSecond = Instant.ofEpochSecond(99) val designData = listOf>( "1" to 10 , "2" to 20 , "3" to sameSecond.epochSecond ) util.insertDesigns(designData) whenever(serverClock.instant()).doReturn(sameSecond) val response = resource.getDesigns(Request(Method.GET, "/designs?pageSize=3&continuationToken=1_10")).toPageDTO() assertThat(response.continuationToken).isEqualTo("20_2") assertThat(response.designs).containsOnly( DesignDTO(id = "1", title = "Cat 1", imageUrl = "http://domain.de/cat1.jpg", dateModified = 10) , DesignDTO(id = "2", title = "Cat 2", imageUrl = "http://domain.de/cat2.jpg", dateModified = 20) ) } @BeforeEach fun cleanup() { util.removeAllDesigns() } private fun createDesignResource(clock: Clock = Clock.systemUTC()): DesignResource { val dao = DesignDAO(dataSource, clock) return DesignResource(dao) } } private val dataSource = JdbcDataSource().apply { user = "sa" password = "" setURL("jdbc:h2:mem:access;MODE=MySQL;DB_CLOSE_DELAY=-1") } ================================================ FILE: ti-continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/PaginationClient.kt ================================================ package de.philipphauer.blog.pagination import org.http4k.core.Method import org.http4k.core.Request class PaginationClient( private val pageSize: Int, private val resource: DesignResource ) { private val retrievedDesigns = mutableListOf() private var nextContinuationToken: String? = null private var requestCounter = 0 fun retrieveNextPageAndRememberResult() { val page = if (nextContinuationToken == null) { println("Request ${requestCounter++}: Without token") resource.getDesigns(Request(Method.GET, "/designs?pageSize=$pageSize")).toPageDTO() } else { println("Request ${requestCounter++}: With token $nextContinuationToken") resource.getDesigns(Request(Method.GET, "/designs?pageSize=$pageSize&continuationToken=$nextContinuationToken")).toPageDTO() } println(" Retrieved ${page.designs.size} designs: ${page.designs.map(DesignDTO::id)}. Next token will be ${page.continuationToken}") retrievedDesigns.addAll(page.designs) nextContinuationToken = page.continuationToken } fun getAllRetrievedDesigns() = retrievedDesigns.toList() } ================================================ FILE: ti-continuation-token/src/test/kotlin/de/philipphauer/blog/pagination/token/PaginationTest.kt ================================================ package de.philipphauer.blog.pagination.token import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.ValueSource import java.util.stream.Stream @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class PaginationTest { @ParameterizedTest @MethodSource("validTokenProvider") fun parse_valid(data: ValidTokenTestData) { assertThat(data.token.toContinuationToken()).isEqualTo(data.continuationToken) } private fun validTokenProvider() = Stream.of( ValidTokenTestData("1511443755_2", ContinuationToken(1511443755, "2")) , ValidTokenTestData("1463997600_10273521", ContinuationToken(1463997600, "10273521")) , ValidTokenTestData("151144375_1", ContinuationToken(151144375, "1")) // id can also be strings , ValidTokenTestData("151144375_id", ContinuationToken(151144375, "id")) // timestamp can also have millisecond precision , ValidTokenTestData("1511443755999_1", ContinuationToken(1511443755999, "1")) , ValidTokenTestData(null, null) ) @ParameterizedTest @ValueSource( strings = [ "asdf_1_1842521611" , "asdf_1" , "1511443755_sadfasd_1842521611" , "1511443755_1_sadfasd" , "" , "__" , "12__" , "12__213" , "_1231_213" , "-1231_213" , "-2_23" ] ) fun parse_invalid(invalidToken: String) { assertThatThrownBy { invalidToken.toContinuationToken() } .isInstanceOf(InvalidContinuationTokenException::class.java) .hasMessageStartingWith("Invalid token '$invalidToken'") } @Test fun onlyOnePage() { val entities = listOf( TestPageable("1", 10), TestPageable("2", 20), TestPageable("3", 30) ) val actualPage = createPage(entities, null, 10) val expectedPage = Page( entities = entities, token = ContinuationToken(30, "3"), hasNext = false ) assertThat(actualPage).isEqualTo(expectedPage) } @Test fun hasNextPage() { val entities = listOf( TestPageable("1", 10), TestPageable("2", 20), TestPageable("3", 30) ) val actualPage = createPage(entities, null, 3) val expectedPage = Page( entities = entities, token = ContinuationToken(30, "3"), hasNext = true ) assertThat(actualPage).isEqualTo(expectedPage) } @Test fun onlyOneElement() { val entities = listOf(TestPageable("1", 10)) val actualPage = createPage(entities, null, 3) val expectedPage = Page( entities = entities, token = ContinuationToken(10, "1"), hasNext = false ) assertThat(actualPage).isEqualTo(expectedPage) } @Test fun emptyPage() { val entities = listOf() val actualPage = createPage(entities, null, 3) val expectedPage = Page(entities, null, false) assertThat(actualPage).isEqualTo(expectedPage) } @Test fun emptyPage_returnReceivedTokenInCaseOfEmptyPage() { val entities = listOf() val token = ContinuationToken(30, "3") val actualPage = createPage(entities, token, 3) val expectedPage = Page(entities, token, false) assertThat(actualPage).isEqualTo(expectedPage) } @Test fun returnANewTokenEvenIfAnTokenIsPassed() { val entities = listOf( TestPageable("4", 40), TestPageable("5", 50), TestPageable("6", 60) ) val actualPage = createPage( entities, ContinuationToken(30, "3"), 10 ) val expectedPage = Page( entities = entities, token = ContinuationToken(60, "6"), hasNext = false ) assertThat(actualPage).isEqualTo(expectedPage) } } data class TestPageable( override val id: String, override val timestamp: Long ) : Pageable data class ValidTokenTestData( val token: String?, val continuationToken: ContinuationToken? ) ================================================ FILE: unit-tests-kotlin/.gitignore ================================================ target .idea *.iml ================================================ FILE: unit-tests-kotlin/pom.xml ================================================ 4.0.0 de.philipphauer.blog unit-tests-kotlin 1.0-SNAPSHOT UTF-8 UTF-8 1.8 11 1.4.0 5.0.3 8.3.0 4.2.5 com.vaadin vaadin-server ${vaadin.version} org.jetbrains.kotlin kotlin-reflect ${kotlin.version} org.assertj assertj-core 3.11.1 test org.junit.jupiter junit-jupiter 5.5.2 test junit junit 4.12 test org.testcontainers testcontainers 1.5.0 test io.mockk mockk 1.9.3 test io.kotest kotest-runner-junit5-jvm ${kotest.version} test io.kotest kotest-assertions-core-jvm ${kotest.version} test io.kotest kotest-property-jvm ${kotest.version} test com.vaadin vaadin-bom ${vaadin.version} pom import src/main/kotlin src/test/kotlin kotlin-maven-plugin org.jetbrains.kotlin -Xjsr305=strict org.apache.maven.plugins maven-surefire-plugin 2.22.2 ================================================ FILE: unit-tests-kotlin/src/main/kotlin/com/phauer/unittestkotlin/MongoDAO.kt ================================================ package com.phauer.unittestkotlin //class under test class MongoDAO(host: String, port: Int) { } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/BackticksAndNestedClasses.kt ================================================ package com.phauer.unittestkotlin import org.junit.jupiter.api.Test class DesignControllerTest { @Test fun `design is removed from db`() { } @Test fun `return 404 on invalid id parameter`() { } @Test fun `return 401 if not authorized`() { } } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/DataClassAssertions.kt ================================================ package com.phauer.unittestkotlin import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.equality.shouldBeEqualToIgnoringFields import io.kotest.matchers.equality.shouldBeEqualToUsingFields import io.kotest.matchers.shouldBe import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class DataClassAssertions { //Don't @Test fun test() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) assertThat(actualDesign.id).isEqualTo(2) assertThat(actualDesign.userId).isEqualTo(9) assertThat(actualDesign.name).isEqualTo("Cat") /* org.junit.ComparisonFailure: expected:<[2]> but was:<[1]> Expected :2 Actual :1 */ } @Test fun test_kotest() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) actualDesign.id shouldBe 2 // ComparisonFailure actualDesign.userId shouldBe 9 actualDesign.name shouldBe "Cat" /* org.opentest4j.AssertionFailedError: expected:<2> but was:<1> Expected :2 Actual :1 */ } //Do @Test fun test2() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) val expectedDesign = Design( id = 2, userId = 9, name = "Cat" ) assertThat(actualDesign).isEqualTo(expectedDesign) /* org.junit.ComparisonFailure: expected: but was: Expected :Design(id=2, userId=9, name=Cat) Actual :Design(id=1, userId=9, name=Cat) */ } @Test fun test2_kotest() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) val expectedDesign = Design( id = 2, userId = 9, name = "Cat" ) actualDesign shouldBe expectedDesign /* org.opentest4j.AssertionFailedError: data class diff for de.philipphauer.blog.unittestkotlin.Design └ id: expected:<2> but was:<1> expected: but was: Expected :Design(id=2, userId=9, name=Cat) Actual :Design(id=1, userId=9, name=Cat) */ } //Do @Test fun lists() { val client = DesignClient() val actualDesigns = client.getAllDesigns() assertThat(actualDesigns).containsExactly( Design( id = 1, userId = 9, name = "Cat" ), Design( id = 2, userId = 4, name = "Dog" ) ) /* java.lang.AssertionError: Expecting: <[Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dogggg)]> to contain exactly (and in same order): <[Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dog)]> but some elements were not found: <[Design(id=2, userId=4, name=Dog)]> and others were not expected: <[Design(id=2, userId=4, name=Dogggg)]> */ } @Test fun lists_kotest() { val client = DesignClient() val actualDesigns = client.getAllDesigns() actualDesigns.shouldContainExactly( Design( id = 1, userId = 9, name = "Cat" ), Design( id = 2, userId = 4, name = "Dog" ) ) /* java.lang.AssertionError: Expecting: [ Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dog) ] but was: [ Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dogggg) ] Some elements were missing: [ Design(id=2, userId=4, name=Dog) ] and some elements were unexpected: [ Design(id=2, userId=4, name=Dogggg) ] */ } @Test fun sophisticatedAssertions_single() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) val expectedDesign = Design( id = 2, userId = 9, name = "Cat" ) assertThat(actualDesign).isEqualToIgnoringGivenFields(expectedDesign, "id") assertThat(actualDesign).isEqualToComparingOnlyGivenFields(expectedDesign, "userId", "name") } @Test fun sophisticatedAssertions_single_kotest() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) val expectedDesign = Design( id = 2, userId = 9, name = "Cat" ) actualDesign.shouldBeEqualToIgnoringFields(expectedDesign, Design::id) actualDesign.shouldBeEqualToUsingFields(expectedDesign, Design::userId, Design::name) } @Test fun sophisticatedAssertions_lists() { val client = DesignClient() val actualDesigns = client.getAllDesigns() assertThat(actualDesigns).usingElementComparatorIgnoringFields("dateCreated").containsExactly( Design( id = 1, userId = 9, name = "Cat" ), Design( id = 2, userId = 4, name = "Dog" ) ) assertThat(actualDesigns).usingElementComparatorOnFields("userId", "name").containsExactly( Design( id = 1, userId = 9, name = "Cat" ), Design( id = 2, userId = 4, name = "Dog" ) ) } @Test fun sophisticatedAssertions_lists_kotest() { val client = DesignClient() val actualDesigns = client.getAllDesigns() // TODO doesn't seem to exist in kotest yet // assertThat(actualDesigns).usingElementComparatorIgnoringFields("dateCreated").containsExactly( // Design(id = 1, userId = 9, name = "Cat", dateCreated = Instant.ofEpochSecond(1518278198)), // Design(id = 2, userId = 4, name = "Dogggg", dateCreated = Instant.ofEpochSecond(1518279000)) // ) // assertThat(actualDesigns).usingElementComparatorOnFields("id", "name").containsExactly( // Design(id = 1, userId = 9, name = "Cat", dateCreated = Instant.ofEpochSecond(1518278198)), // Design(id = 2, userId = 4, name = "Dogggg", dateCreated = Instant.ofEpochSecond(1518279000)) // ) } @Test fun grouping() { val client = DesignClient() val actualDesign = client.requestDesign(id = 1) actualDesign.asClue { it.id shouldBe 2 it.userId shouldBe 9 it.name shouldBe "Cat" } /** * org.opentest4j.AssertionFailedError: Design(id=1, userId=9, name=Cat, dateCreated=2018-02-10T15:56:38Z) expected:<2> but was:<1> Expected :2 Actual :1 */ } } data class Design( val id: Int, val userId: Int, val name: String ) class DesignClient { fun requestDesign(id: Int) = Design( id = 1, userId = 9, name = "Cat" ) fun getAllDesigns() = listOf( Design( id = 1, userId = 9, name = "Cat" ), Design( id = 2, userId = 4, name = "Dogggg" ) ) } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/HandlingState.kt ================================================ package com.phauer.unittestkotlin import com.vaadin.navigator.View import com.vaadin.ui.Button import com.vaadin.ui.Panel import io.kotest.matchers.shouldBe import io.mockk.clearAllMocks import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DesignViewTest { private val dao: DesignDAO = mockk() // the class under test has state private lateinit var view: DesignView @BeforeEach fun init() { clearAllMocks() view = DesignView(dao) } @Test fun changeButton() { view.button.caption shouldBe "Hi" view.changeButton() view.button.caption shouldBe "Hallo" } } // class with state class DesignView(val dao: DesignDAO) : Panel(), View { val button = Button("Hi") init { content = button } fun changeButton() { button.caption = "Hallo" } } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/IntroductionExample.kt ================================================ package com.phauer.unittestkotlin //import com.nhaarman.mockito_kotlin.mock //import com.nhaarman.mockito_kotlin.reset //import com.nhaarman.mockito_kotlin.whenever //import org.junit.Assert.assertEquals //import org.junit.Before //import org.junit.BeforeClass //import org.junit.Test // //class UserControllerTest { // companion object { // @JvmStatic private lateinit var controller: UserController // @JvmStatic private lateinit var repo: UserRepository // @BeforeClass @JvmStatic fun initialize() { // repo = mock() // controller = UserController(repo) // } // } // @Test // fun findUser_UserFoundAndHasCorrectValues() { // `when`((repo.findUser(1))).thenReturn(User(1, "Peter")) // val user = controller.getUser(1) // assertEquals(user?.name, "Peter") // } // @Before // fun clear(){ // reset(repo) // } //} open class UserRepository { open fun findUser(id: Int): User? { return User(id, "Peter") } } class UserController( val repo: UserRepository ) { fun getUser(id: Int): User? { return repo.findUser(id) } } data class User( val id: Int, val name: String ) ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/KGenericContainer.kt ================================================ package com.phauer.unittestkotlin import org.testcontainers.containers.GenericContainer class KGenericContainer(imageName: String) : GenericContainer(imageName) ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/MockHandling.kt ================================================ package com.phauer.unittestkotlin import io.mockk.clearAllMocks import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.TestInstance // Do: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DesignControllerTest_Mock { private val dao: DesignDAO = mockk() private val mapper: DesignMapper = mockk() private val controller = DesignController(dao, mapper) @BeforeEach fun init() { clearAllMocks() } // takes 210 ms @RepeatedTest(300) fun foo() { controller.doSomething() } } //Don't @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DesignControllerTest_RecreatingMocks { private lateinit var dao: DesignDAO private lateinit var mapper: DesignMapper private lateinit var controller: DesignController @BeforeEach fun init() { dao = mockk() mapper = mockk() controller = DesignController(dao, mapper) } // takes 1,5 s! @RepeatedTest(300) fun foo() { controller.doSomething() } } open class DesignDAO open class DesignMapper class DesignController(val dao: DesignDAO, val mapper: DesignMapper) { fun doSomething() { } } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/MongoDAOTestJUnit4.kt ================================================ package com.phauer.unittestkotlin import org.junit.BeforeClass import org.junit.Test //JUnit4. Don't: class MongoDAOTestJUnit4 { companion object { @JvmStatic private lateinit var mongo: KGenericContainer @JvmStatic private lateinit var mongoDAO: MongoDAO @BeforeClass @JvmStatic fun initialize() { mongo = KGenericContainer("mongo:3.4.3").apply { withExposedPorts(27017) start() } mongoDAO = MongoDAO(host = mongo.containerIpAddress, port = mongo.getMappedPort(27017)) } } @Test fun foo() { // test mongoDAO } } //JUnit4 -> java.lang.Exception: Method init() should be static // @BeforeClass // fun init(){ // } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/MongoDAOTestJUnit5.kt ================================================ package com.phauer.unittestkotlin import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance //Do: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MongoDAOTestJUnit5 { private val mongo = KGenericContainer("mongo:3.4.3").apply { withExposedPorts(27017) start() } private val mongoDAO = MongoDAO(host = mongo.containerIpAddress, port = mongo.getMappedPort(27017)) @Test fun foo() { // test mongoDAO } } //Do: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MongoDAOTestJUnit5Constructor { private val mongo: KGenericContainer private val mongoDAO: MongoDAO init { mongo = KGenericContainer("mongo:3.4.3").apply { withExposedPorts(27017) start() } mongoDAO = MongoDAO(host = mongo.containerIpAddress, port = mongo.getMappedPort(27017)) } @Test fun foo() { // test mongoDAO } } //Don't: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MongoDAOTestJUnit5BeforeAll { private val mongo: KGenericContainer private val mongoDAO: MongoDAO init { mongo = KGenericContainer("mongo:3.4.3").apply { withExposedPorts(27017) start() } mongoDAO = MongoDAO(host = mongo.containerIpAddress, port = mongo.getMappedPort(27017)) } @Test fun foo() { // test mongoDAO } } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/ParseTest.kt ================================================ package com.phauer.unittestkotlin import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ParseTest { @Test fun `parse valid tokens 1`() { assertThat(parse("1511443755_2")).isEqualTo(Token(1511443755, "2")) assertThat(parse("151175_13521")).isEqualTo(Token(151175, "13521")) assertThat(parse("151144375_id")).isEqualTo(Token(151144375, "id")) assertThat(parse("15114437599_1")).isEqualTo(Token(15114437599, "1")) assertThat(parse(null)).isEqualTo(null) } @Test fun `parse valid tokens 2`() { assertThat(parse("1511443755_2")).isEqualTo(Token(1511443755, "2")) assertThat(parse("151175_13521")).isEqualTo(Token(151175, "13521")) assertThat(parse("151144375_id")).isEqualTo(Token(151144375, "id")) assertThat(parse("15114437599_1")).isEqualTo(Token(15114437599, "1")) assertThat(parse(null)).isEqualTo(null) } @ParameterizedTest @MethodSource("validTokenProvider") fun `parse valid tokens`(data: TestData) { assertThat(parse(data.input)).isEqualTo(data.expected) } private fun validTokenProvider() = Stream.of( TestData(input = "1511443755_2", expected = Token(1511443755, "2")), TestData(input = "151175_13521", expected = Token(151175, "13521")), TestData(input = "151144375_id", expected = Token(151144375, "id")), TestData(input = "15114437599_1", expected = Token(15114437599, "1")), TestData(input = null, expected = null) ) } data class TestData( val input: String?, val expected: Token? ) fun parse(value: String?): Token? { value ?: return null val parts = value.split("_") if (parts.size != 2) { throw IllegalArgumentException(value, null) } try { val timestamp = java.lang.Long.parseUnsignedLong(parts[0]) val id = parts[1] return Token(timestamp, id) } catch (ex: Exception) { throw IllegalArgumentException(value, ex) } } data class Token( val timestamp: Long, val id: String ) ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/ParseTestKotest.kt ================================================ package com.phauer.unittestkotlin import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ParseTestKotest { @Test fun `parse valid tokens`() { parse("1511443755_2") shouldBe Token(1511443755, "2") parse("151175_13521") shouldBe Token(151175, "13521") parse("151144375_id") shouldBe Token(151144375, "id") parse("15114437599_12") shouldBe Token(15114437599, "1") parse(null) shouldBe null } @ParameterizedTest @MethodSource("validTokenProvider") fun `parse valid tokens`(data: TestData) { parse(data.input) shouldBe data.expected } private fun validTokenProvider() = Stream.of( TestData(input = "1511443755_2", expected = Token(1511443755, "2")), TestData(input = "151175_13521", expected = Token(151175, "13521")), TestData(input = "151144375_id", expected = Token(151144375, "id")), TestData(input = "15114437599_1", expected = Token(15114437599, "1")), TestData(input = null, expected = null) ) } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/TestSpecificExtFunctions.kt ================================================ package com.phauer.unittestkotlin import io.kotest.matchers.floats.plusOrMinus import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test class TestSpecificExtFunctions { // Don't @Test fun bla() { val taxRate1 = 0.3f val taxRate2 = 0.2f val taxRate3 = 0.5f taxRate1 shouldBe 0.3f.plusOrMinus(0.001f) taxRate2 shouldBe 0.2f.plusOrMinus(0.001f) taxRate3 shouldBe 0.5f.plusOrMinus(0.001f) } // Do @Test fun bla2() { val taxRate1 = 0.3f val taxRate2 = 0.2f val taxRate3 = 0.5f taxRate1 shouldBeCloseTo 0.3f taxRate2 shouldBeCloseTo 0.2f taxRate3 shouldBeCloseTo 0.5f } private infix fun Float.shouldBeCloseTo(expected: Float) = this shouldBe expected.plusOrMinus(0.001f) } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/assertAllOrSomeFields/AssertAllOrSomeFields.kt ================================================ package com.phauer.unittestkotlin.assertAllOrSomeFields import io.kotest.assertions.asClue import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test class AssertAllOrSomeFields { private val dao = DesignDAO() @Test fun `all fields are correctly saved`() { // insert design into database val expectedDesign = Design( name = "cat", userId = 10, tags = listOf("Cat", "Animal") ) dao.findDesign(1) shouldBe expectedDesign } @Test fun `name and tags of a design are changed`() { // change name and tags val expectedDesign = Design( name = "cat", userId = 10, tags = listOf("Cat", "Animal") ) dao.findDesign(1) shouldBe expectedDesign } @Test fun `name and tags of a design are changed 2`() { // change name and tags dao.findDesign(1).asClue { it.name shouldBe "Cat" it.userId shouldBe 10 } } } data class Design( val userId: Int, val name: String, val tags: List ) class DesignDAO { fun findDesign(i: Int): Design { TODO() } } ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/foo/CreationHelper.kt ================================================ package com.phauer.unittestkotlin.foo import java.time.Instant import java.util.Locale data class Design( val id: Int, val userId: Int, val name: String, val fileName: String, val dateCreated: Instant, val dateModified: Instant, val tags: Map> ) data class Tag( val value: String ) fun createDesign( id: Int = 1, name: String = "Cat", date: Instant = Instant.ofEpochSecond(1518278198), tags: Map> = mapOf( Locale.US to listOf(Tag(value = "$name in English")), Locale.GERMANY to listOf(Tag(value = "$name in German")) ) ) = Design( id = id, userId = 9, name = name, fileName = name, dateCreated = date, dateModified = date, tags = tags ) //usage: val testDesign = createDesign() val testDesign2 = createDesign(id = 1, name = "Fox") val testDesign3 = createDesign(id = 1, name = "Fox", tags = mapOf()) ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/foo/MockK.kt ================================================ package com.phauer.unittestkotlin.foo import com.phauer.unittestkotlin.DesignController import com.phauer.unittestkotlin.DesignDAO import com.phauer.unittestkotlin.DesignMapper import io.mockk.clearMocks import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.TestInstance //with MockK, the mocked class can be final! no changes required! // Do: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DesignControllerTest_MockK { private val dao: DesignDAO = mockk() private val mapper: DesignMapper = mockk() private val controller = DesignController(dao, mapper) @BeforeEach fun init() { clearMocks(dao, mapper) } // takes 250 ms @RepeatedTest(300) fun foo() { controller.doSomething() } } //Don't @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DesignControllerTest_RecreatingMocks_MockK { private lateinit var dao: DesignDAO private lateinit var mapper: DesignMapper private lateinit var controller: DesignController @BeforeEach fun init() { dao = mockk() mapper = mockk() controller = DesignController(dao, mapper) } // takes 2 s! (mockk is even slower (0,5 s) than mockito-kotlin) // but this approach is deprecated anyway. @RepeatedTest(300) fun foo() { controller.doSomething() } } //class DesignDAO //class DesignMapper //class DesignController(val dao: DesignDAO, val mapper: DesignMapper) { // fun doSomething() { // // } //} ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/mockk/UserScheduler.kt ================================================ package com.phauer.unittestkotlin.mockk class UserScheduler( val client: UserClient, val dao: UserDAO ) { fun start(id: Int) { val user = client.getUser(id) dao.saveUser(user) } } class UserClient { fun getUser(id: Int): User { println("getClient()") return User(id = 99, name = "Albert", age = 30) } } class UserDAO { fun saveUser(user: User) { println("saveUser()") } } data class User( val id: Int, val name: String, val age: Int ) ================================================ FILE: unit-tests-kotlin/src/test/kotlin/com/phauer/unittestkotlin/mockk/UserSchedulerTest_MockK.kt ================================================ package com.phauer.unittestkotlin.mockk import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import io.mockk.verifySequence import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance // Do: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserSchedulerTest { private val dao: UserDAO = mockk(relaxed = true) private val client: UserClient = mockk(relaxed = true) private val scheduler = UserScheduler(client, dao) @BeforeEach fun init() { clearAllMocks() } @Test fun start() { val daoMock: UserDAO = mockk(relaxed = true) val clientMock: UserClient = mockk() val user = User(id = 1, name = "Ben", age = 29) every { clientMock.getUser(any()) } returns user val scheduler = UserScheduler(clientMock, daoMock) scheduler.start(1) verifySequence { clientMock.getUser(1) daoMock.saveUser(user) } } @Test fun bla() { val clientMock: UserClient = mockk(relaxed = true) println(clientMock.getUser(1).age) // 0 // val clientMock2: UserClient = mockk() // println(clientMock2.getUser(1).age) // exception } } ================================================ FILE: unit-tests-kotlin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMakerXXX ================================================ mock-maker-inline ================================================ FILE: uuid-mysql-hibernate/.gitignore ================================================ .idea *.iml target/ ================================================ FILE: uuid-mysql-hibernate/README.md ================================================ ```bash # install docker, docker-compose and httpie up front docker-compose up #starts mysql mvn package #builds the service java -jar target/uuid-mysql-hibernate-1.jar & #start the service http POST localhost:8080/products name=paul #creates a product. Hibernate generate a UUID http GET localhost:8080/products #get all products to see the UUIDs ``` ================================================ FILE: uuid-mysql-hibernate/docker-compose.yml ================================================ version: '2' services: mysql: image: mysql:5.7.13 ports: - "3306:3306" environment: - MYSQL_ROOT_PASSWORD=root #for user 'root' - MYSQL_DATABASE=testdb ================================================ FILE: uuid-mysql-hibernate/pom.xml ================================================ 4.0.0 de.philipphauer.blog uuid-mysql-hibernate 1 jar uuid-mysql-hibernate org.springframework.boot spring-boot-starter-parent 1.3.6.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ================================================ FILE: uuid-mysql-hibernate/src/main/java/de/philipphauer/blog/ProductsResource.java ================================================ package de.philipphauer.blog; import de.philipphauer.blog.model.Product; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.persistence.EntityManager; import javax.persistence.Query; import java.util.Collection; import java.util.Map; @RestController @RequestMapping(value = "/products") public class ProductsResource { @Autowired private EntityManager entityManager; @Transactional @RequestMapping(value = "", method = RequestMethod.POST) public void createProduct(@RequestBody Map requestBody) { String name = (String) requestBody.get("name"); Product paul = new Product().setName(name); entityManager.persist(paul); } @Transactional @RequestMapping(value = "", method = RequestMethod.GET) public Collection getProducts() { Query query = entityManager.createQuery("SELECT p FROM Product p"); return (Collection) query.getResultList(); } } ================================================ FILE: uuid-mysql-hibernate/src/main/java/de/philipphauer/blog/UuidMysqlHibernateApplication.java ================================================ package de.philipphauer.blog; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class UuidMysqlHibernateApplication { public static void main(String[] args) { SpringApplication.run(UuidMysqlHibernateApplication.class, args); } } ================================================ FILE: uuid-mysql-hibernate/src/main/java/de/philipphauer/blog/model/Product.java ================================================ package de.philipphauer.blog.model; import org.hibernate.annotations.GenericGenerator; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import java.util.UUID; @Entity public class Product { @Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "uuid2") @Column(columnDefinition = "BINARY(16)") private UUID id; private String name; public UUID getId() { return id; } public Product setId(UUID id) { this.id = id; return this; } public String getName() { return name; } public Product setName(String name) { this.name = name; return this; } } ================================================ FILE: uuid-mysql-hibernate/src/main/resources/application.properties ================================================ spring.datasource.url=jdbc:mysql://localhost:3306/testdb spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect spring.jpa.hibernate.ddl-auto = create-drop spring.jpa.show-sql = true ================================================ FILE: vaadin-10-sass-cssrefresh/.gitignore ================================================ HELP.md /target/ !.mvn/wrapper/maven-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ /build/ ### VS Code ### .vscode/ ================================================ FILE: vaadin-10-sass-cssrefresh/README.md ================================================ # Vaadin 10: SASS Integration and CSS-Refresh Development Workflow ```bash # start the app (execute main() in Vaadin10SassCssrefreshApplication) mvn sass:watch # open http://localhost:8080/ # change sass # profit (= css will be updated in the browser without a page reload) ``` If you are not using `sass:watch`, mind to execute at least `mvn sass:update-stylesheets` once up front before starting the app. Otherwise there will be no CSS at all. ================================================ FILE: vaadin-10-sass-cssrefresh/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.4.RELEASE com.phauer vaadin-10-sass-cssrefresh 0.0.1-SNAPSHOT vaadin-10-sass-cssrefresh Demo project for Spring Boot 11 13.0.3 org.springframework.boot spring-boot-starter-web com.vaadin vaadin-spring-boot-starter org.springframework.boot spring-boot-starter-test test com.vaadin vaadin-bom ${vaadin.version} pom import org.springframework.boot spring-boot-maven-plugin nl.geodienstencentrum.maven sass-maven-plugin 3.7.1 ${basedir}/src/main/resources/META-INF/resources/frontend/styles **/*.scss .. ${basedir}/target/classes/META-INF/resources/frontend/styles generate-resources update-stylesheets ================================================ FILE: vaadin-10-sass-cssrefresh/src/main/java/com/phauer/vaadin10sasscssrefresh/CustomVaadinServiceListener.java ================================================ package com.phauer.vaadin10sasscssrefresh; import com.vaadin.flow.server.BootstrapListener; import com.vaadin.flow.server.BootstrapPageResponse; import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.VaadinServiceInitListener; import org.jsoup.nodes.Element; import org.springframework.stereotype.Component; @Component public class CustomVaadinServiceListener implements VaadinServiceInitListener { @Override public void serviceInit(ServiceInitEvent event) { if (!event.getSource().getDeploymentConfiguration().isProductionMode()) { event.addBootstrapListener(new CustomBootstrapListener()); } } static class CustomBootstrapListener implements BootstrapListener { @Override public void modifyBootstrapPage(BootstrapPageResponse response) { Element head = response.getDocument().head(); head.append(""); } } } ================================================ FILE: vaadin-10-sass-cssrefresh/src/main/java/com/phauer/vaadin10sasscssrefresh/ExampleView.java ================================================ package com.phauer.vaadin10sasscssrefresh; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Label; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.Route; @Route(value = "", layout = MainLayout.class) public class ExampleView extends VerticalLayout { public ExampleView(){ addClassName("exampleView"); add( new H1("Example View"), new Button("Do Something", this::addLabelToView) ); } private void addLabelToView(ClickEvent