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