├── .github └── workflows │ ├── build.yml │ ├── docker-publish.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── product.png └── src ├── main ├── kotlin │ └── de │ │ └── paulbrejla │ │ └── holidays │ │ ├── HolidaysApiApplication.kt │ │ ├── application │ │ ├── Assembler.kt │ │ ├── api │ │ │ ├── HolidayService.kt │ │ │ └── RateLimitService.kt │ │ └── impl │ │ │ ├── GlobalRateLimitServiceImpl.kt │ │ │ └── HolidayServiceImpl.kt │ │ ├── config │ │ ├── ConfigurationProperties.kt │ │ ├── SwaggerConfiguration.kt │ │ └── WebMvcConfiguration.kt │ │ ├── domain │ │ ├── AllOpen.kt │ │ ├── Enums.kt │ │ ├── Models.kt │ │ └── NoArg.kt │ │ ├── infrastructure │ │ ├── Assembler.kt │ │ ├── Dto.kt │ │ ├── api │ │ │ └── HolidayRepository.kt │ │ └── loader │ │ │ ├── api │ │ │ └── CalendarLoader.kt │ │ │ └── impl │ │ │ ├── GitCalendarLoaderGateway.kt │ │ │ └── LocalFilesystemCalendarLoaderGateway.kt │ │ ├── mvc │ │ ├── api │ │ │ └── ViewController.kt │ │ └── impl │ │ │ └── ViewControllerImpl.kt │ │ └── rest │ │ ├── Assembler.kt │ │ ├── Dto.kt │ │ ├── api │ │ └── HolidayWsV1.kt │ │ └── impl │ │ ├── HolidayWsV1Impl.kt │ │ └── RateLimitFilter.kt └── resources │ ├── application.yml │ ├── holidays │ └── .gitignore │ └── templates │ └── index.html └── test ├── kotlin └── de │ └── paulbrejla │ └── holidays │ ├── HolidaysApiApplicationTests.kt │ ├── infrastructure │ └── loader │ │ └── impl │ │ └── LocalFilesystemCalendarLoaderGatewayTest.kt │ └── rest │ ├── AssemblerKtTest.kt │ └── impl │ ├── HolidayWsV1ImplITest.kt │ └── HolidayWsV1ImplTest.kt └── resources ├── application.yml └── holidays ├── de ├── ferien_baden-wuerttemberg_2017.ics └── ferien_bremen_2017.ics └── ferien_baden-wuerttemberg_2017.ics /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Get Version 13 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 14 | - name: Show version tag 15 | run: echo Building version ${{ env.TAG }} 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'adopt' 22 | - name: Grant execute permission for gradlew 23 | run: chmod +x gradlew 24 | - name: Build jar 25 | run: ./gradlew bootJar 26 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | schedule: 10 | - cron: '16 21 * * *' 11 | release: 12 | types: [published] 13 | 14 | env: 15 | # Use docker.io for Docker Hub if empty 16 | REGISTRY: ghcr.io 17 | # github.repository as / 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | 21 | jobs: 22 | build: 23 | 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | packages: write 28 | # This is used to complete the identity challenge 29 | # with sigstore/fulcio when running outside of PRs. 30 | id-token: write 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v3 35 | 36 | # Install the cosign tool except on PR 37 | # https://github.com/sigstore/cosign-installer 38 | - name: Install cosign 39 | if: github.event_name != 'pull_request' 40 | uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25 41 | with: 42 | cosign-release: 'v1.9.0' 43 | 44 | 45 | # Workaround: https://github.com/docker/build-push-action/issues/461 46 | - name: Setup Docker buildx 47 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 48 | 49 | # Login against a Docker registry except on PR 50 | # https://github.com/docker/login-action 51 | - name: Log into registry ${{ env.REGISTRY }} 52 | if: github.event_name != 'pull_request' 53 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 54 | with: 55 | registry: ${{ env.REGISTRY }} 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | # Extract metadata (tags, labels) for Docker 60 | # https://github.com/docker/metadata-action 61 | - name: Extract Docker metadata 62 | id: meta 63 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 64 | with: 65 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 66 | 67 | # Build and push Docker image with Buildx (don't push on PR) 68 | # https://github.com/docker/build-push-action 69 | - name: Build and push Docker image 70 | id: build-and-push 71 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 72 | with: 73 | context: . 74 | push: ${{ github.event_name != 'pull_request' }} 75 | tags: ${{ steps.meta.outputs.tags }} 76 | labels: ${{ steps.meta.outputs.labels }} 77 | 78 | # Sign the resulting Docker image digest except on PRs. 79 | # This will only write to the public Rekor transparency log when the Docker 80 | # repository is public to avoid leaking data. If you would like to publish 81 | # transparency data even for private images, pass --force to cosign below. 82 | # https://github.com/sigstore/cosign 83 | - name: Sign the published Docker image 84 | if: ${{ github.event_name != 'pull_request' }} 85 | env: 86 | COSIGN_EXPERIMENTAL: "true" 87 | # This step uses the identity token to provision an ephemeral certificate 88 | # against the sigstore community Fulcio instance. 89 | run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} 90 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Application 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | - name: Run tests 24 | run: ./gradlew check 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | .DS_Store 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | newrelic.jar 14 | newrelic.yml 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | nbproject/private/ 24 | build/ 25 | nbbuild/ 26 | dist/ 27 | nbdist/ 28 | .nb-gradle/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:11 2 | 3 | ENV APP_HOME /usr/src/app 4 | ENV APP_NAME holidays-api 5 | 6 | WORKDIR $APP_HOME 7 | ADD . $APP_HOME 8 | 9 | RUN ./gradlew build 10 | 11 | EXPOSE 80 12 | 13 | CMD ["java","-Xms156m","-Xmx500M","-jar","/usr/src/app/build/libs/app-0.0.1-SNAPSHOT.jar"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paul Brejla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![ferien-api](https://circleci.com/gh/paulbrejla/ferien-api.svg?style=shield)](https://app.circleci.com/pipelines/github/paulbrejla/ferien-api) 2 | 3 | This is where the code for ferien-api.de lives. 4 | 5 | 6 | ## Table of Contents 7 | 8 | * [About the Project](#about-the-project) 9 | * [Built With](#built-with) 10 | * [Getting Started](#getting-started) 11 | * [Prerequisites](#prerequisites) 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Roadmap](#roadmap) 15 | * [Contributing](#contributing) 16 | * [License](#license) 17 | * [Contact](#contact) 18 | * [Acknowledgements](#acknowledgements) 19 | 20 | 21 | 22 | 23 | 24 | 25 | ### Built With 26 | This project is built with Spring Boot and Kotlin, using an embedded h2 database 27 | as its data store. 28 | 29 | 30 | ## Getting Started 31 | 32 | To get a local copy up and running follow these simple steps. 33 | 34 | ### Installation 35 | 36 | 1. Clone the repo 37 | ```sh 38 | git clone https://github.com/paulbrejla/ferien-api.git 39 | ``` 40 | 41 | 2. Add _.ics_ calendar files to _/main/resources/holidays_ 42 | - Filenames need to conform to the following format: 43 | ``` 44 | ferien_{state}.ics 45 | e.g. ferien_Bremen.ics 46 | ``` 47 | 48 | 3. Configure environment variables to load _.ics_ files from the classpath or from a git repo 49 | 50 | | Property | filesystem (classpath) | git | 51 | |-----------|------------------------|-------------------------------------------------------------| 52 | | source | `filesystem` | `git` | 53 | | remoteURL | Not needed | Git URL e.g. https://github.com/paulbrejla/ferien-api.git | 54 | | branch | Not needed | Branch e.g. `master` | 55 | | filePath | Not needed | Path to look up ics files e.g. `src/test/resources/holidays/` | 56 | 57 | 4. Run 58 | ```sh 59 | ./gradlew bootRun 60 | ``` 61 | 62 | ### Build with Docker 63 | 64 | 1. Build Docker Image 65 | ```sh 66 | docker build -t holidays-api . 67 | ``` 68 | 69 | 2. Tag Docker Image 70 | ```sh 71 | docker tag holidays-api:latest remote-repo/holidays-api:latest 72 | ``` 73 | 74 | 3. Push Docker Image to remote repo 75 | ```sh 76 | docker push remote-repo/holidays-api:latest 77 | ``` 78 | 79 | ## FAQ 80 | 81 | ### I am running into rate limits when calling the API - can you fix this? 82 | As the API is heavily abused, aggressive rate limiting is in place. If you need higher rate limits, you need to self-host it. 83 | 84 | ### Some dates are wrong - can you fix them? 85 | The source ics files are here: https://github.com/paulbrejla/ferien-api-data. 86 | To change a holiday date, open the ics file for the state and year and find the holiday entry. Change the entry and create a pull 87 | request against _main_. 88 | 89 | ### Dates are missing - can you add them? 90 | The source ics files are here: https://github.com/paulbrejla/ferien-api-data. 91 | To add a holidays for a given state and year, provide an ics file and create a PR against _main_. 92 | 93 | ### Data is unreliable - can you fix data issues? 94 | The source ics files are here: https://github.com/paulbrejla/ferien-api-data. 95 | To change a holiday date, open the ics file for the state and year and find the holiday entry. Change the entry and create a pull 96 | request against _main_. 97 | 98 | 99 | 100 | ## License 101 | Distributed under the MIT License. See LICENSE for more information. 102 | 103 | 104 | ### Contributing 105 | 106 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 107 | 108 | #### Pull Request Process 109 | 110 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 111 | 2. Update the README.md with details of changes, this includes new environment variables, exposed ports, useful file locations and container parameters. 112 | 113 | ### Contact 114 | Paul Brejla - paul(at)paulbrejla.com 115 | 116 | Project Link: https://github.com/paulbrejla/ferien-api 117 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.7.10' 4 | springBootVersion = '2.7.3' 5 | } 6 | repositories { 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 12 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 13 | classpath "org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}" 14 | } 15 | } 16 | 17 | apply plugin: 'kotlin' 18 | apply plugin: 'kotlin-spring' 19 | apply plugin: 'kotlin-noarg' 20 | apply plugin: 'kotlin-jpa' 21 | apply plugin: "kotlin-allopen" 22 | apply plugin: 'org.springframework.boot' 23 | 24 | group = 'de.paulbrejla' 25 | version = '0.0.1-SNAPSHOT' 26 | sourceCompatibility = 1.8 27 | compileKotlin { 28 | kotlinOptions.jvmTarget = "1.8" 29 | } 30 | compileTestKotlin { 31 | kotlinOptions.jvmTarget = "1.8" 32 | } 33 | 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | 43 | dependencies { 44 | implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") 45 | implementation("org.springframework.boot:spring-boot-starter-jetty:${springBootVersion}") 46 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}") 47 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}") 48 | implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") 49 | implementation 'net.sf.biweekly:biweekly:0.6.6' 50 | implementation group: 'commons-io', name: 'commons-io', version: '2.4' 51 | implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}") 52 | implementation("com.h2database:h2:1.4.200") 53 | implementation group: 'org.hibernate', name: 'hibernate-java8', version: '5.1.0.Final' 54 | implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}") 55 | implementation("io.springfox:springfox-boot-starter:3.0.0") 56 | implementation('io.springfox:springfox-swagger-ui:3.0.0') 57 | implementation("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r") 58 | implementation("com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0") 59 | // Foundation 60 | implementation('org.webjars:foundation:6.4.3') 61 | 62 | testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") 63 | testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' 64 | } 65 | 66 | noArg { 67 | annotation("de.paulbrejla.holidays.domain.NoArg") 68 | } 69 | 70 | allOpen { 71 | annotation("de.paulbrejla.holidays.domain.AllOpen") 72 | } 73 | configurations { 74 | compile.exclude module: "spring-boot-starter-tomcat" 75 | } 76 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulbrejla/ferien-api/a382b5f168ac34fc8c3616aea131dfe35637e6e4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 21 17:28:29 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulbrejla/ferien-api/a382b5f168ac34fc8c3616aea131dfe35637e6e4/product.png -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/HolidaysApiApplication.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays 2 | 3 | import de.paulbrejla.holidays.config.LoaderProperties 4 | import org.springframework.boot.SpringApplication 5 | import org.springframework.boot.autoconfigure.SpringBootApplication 6 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties 8 | import org.springframework.scheduling.annotation.EnableScheduling 9 | import org.springframework.web.servlet.config.annotation.EnableWebMvc 10 | 11 | @SpringBootApplication 12 | @EnableScheduling 13 | @EnableConfigurationProperties(LoaderProperties::class) 14 | class HolidaysApiApplication 15 | 16 | fun main(args: Array) { 17 | SpringApplication.run(HolidaysApiApplication::class.java, *args) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/application/Assembler.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.application 2 | 3 | import biweekly.component.VEvent 4 | import de.paulbrejla.holidays.domain.Holiday 5 | import de.paulbrejla.holidays.domain.State 6 | import java.time.* 7 | import java.util.* 8 | 9 | fun assembleHoliday(event: VEvent, state: String): Holiday = Holiday( 10 | id = 0, stateCode = assembleStateCode(state), 11 | summary = event.summary.value.lowercase(Locale.getDefault()), 12 | start = LocalDateTime.ofInstant(event.dateStart.value.toInstant(), ZoneId.of("CET")).toLocalDate(), 13 | end = LocalDateTime.ofInstant(event.dateEnd.value.toInstant(), ZoneId.of("CET")).toLocalDate(), 14 | year = event.dateStart.value.rawComponents.year, 15 | slug = assembleSlug(event.dateStart.value.rawComponents.year, event.summary.value, assembleStateCode(state)) 16 | ) 17 | 18 | fun assembleSlug(startDate: Int, summary: String, stateCode: State): String = 19 | "${summary.toLowerCase()}-$startDate-$stateCode" 20 | 21 | fun assembleStateCode(state: String): State = when (state) { 22 | "baden-wuerttemberg", "Baden-Wuerttemberg", "baden-württemberg" -> State.BW 23 | "bayern", "Bayern" -> State.BY 24 | "berlin", "Berlin" -> State.BE 25 | "brandenburg", "Brandenburg" -> State.BB 26 | "bremen", "Bremen" -> State.HB 27 | "hamburg", "Hamburg" -> State.HH 28 | "hessen", "Hessen" -> State.HE 29 | "mecklenburg-vorpommern", "Mecklenburg-Vorpommern" -> State.MV 30 | "niedersachsen", "Niedersachsen" -> State.NI 31 | "nordrhein-westfalen", "Nordrhein-Westfalen" -> State.NW 32 | "rheinland-pfalz", "Rheinland-Pfalz" -> State.RP 33 | "saarland", "Saarland" -> State.SL 34 | "sachsen", "Sachsen" -> State.SN 35 | "sachsen-anhalt", "Sachsen-Anhalt" -> State.ST 36 | "schleswig-holstein", "Schleswig-Holstein" -> State.SH 37 | "thueringen", "Thueringen", "thüringen" -> State.TH 38 | else -> throw Exception("Code for state '$state' not found.") 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/application/api/HolidayService.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.application.api 2 | 3 | import de.paulbrejla.holidays.domain.State 4 | import de.paulbrejla.holidays.rest.HolidayDto 5 | 6 | interface HolidayService { 7 | 8 | fun loadHolidays() 9 | fun findHolidays() : List 10 | fun findHolidays(forState: State) : List 11 | fun findHolidays(forState: State, andYear: Int) : List 12 | 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/application/api/RateLimitService.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.application.api 2 | 3 | import io.github.bucket4j.Bucket 4 | 5 | interface RateLimitService { 6 | fun resolveBucket(bucketId: String): Bucket 7 | fun fetchBucket(bucketId: String): Bucket? 8 | fun isWithinQuota(maxRequests: Int, currentRequests: Int): Boolean 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/application/impl/GlobalRateLimitServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.application.impl 2 | 3 | import de.paulbrejla.holidays.application.api.RateLimitService 4 | import io.github.bucket4j.Bandwidth 5 | import io.github.bucket4j.Bucket 6 | import io.github.bucket4j.Refill 7 | import org.springframework.beans.factory.annotation.Value 8 | import org.springframework.stereotype.Component 9 | import java.time.Duration 10 | import java.util.concurrent.ConcurrentHashMap 11 | 12 | @Component("rateLimitService") 13 | class GlobalRateLimitServiceImpl : RateLimitService { 14 | // For now we create one global bucket that all requests consume from. 15 | @Value("\${rateLimit.globalBucket.id}") 16 | var globalBucketId: String = "" 17 | 18 | @Value("\${rateLimit.globalBucket.capacity}") 19 | var globalBucketCapacity: Long = 0 20 | 21 | 22 | private var buckets: MutableMap = ConcurrentHashMap() 23 | override fun resolveBucket(bucketId: String): Bucket { 24 | return if (buckets.containsKey(bucketId)) { 25 | buckets[bucketId]!! 26 | } else { 27 | createBucket().let { 28 | buckets[bucketId] = it 29 | it 30 | } 31 | } 32 | } 33 | 34 | override fun fetchBucket(bucketId: String): Bucket? { 35 | return buckets[bucketId] 36 | } 37 | 38 | override fun isWithinQuota(maxRequests: Int, currentRequests: Int): Boolean { 39 | TODO("Not yet implemented") 40 | } 41 | 42 | /** 43 | * We only allow 1000 requests per hour from now on. These 1000 requests are refilled when the new 44 | * hour starts. 45 | */ 46 | private fun createBucket(): Bucket { 47 | return Bucket.builder() 48 | .addLimit( 49 | Bandwidth.classic( 50 | globalBucketCapacity, 51 | Refill.intervally(globalBucketCapacity, Duration.ofHours(1)) 52 | ) 53 | ) 54 | .build() 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/application/impl/HolidayServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.application.impl 2 | 3 | import biweekly.component.VEvent 4 | import de.paulbrejla.holidays.application.api.HolidayService 5 | import de.paulbrejla.holidays.application.assembleHoliday 6 | import de.paulbrejla.holidays.domain.State 7 | import de.paulbrejla.holidays.infrastructure.loader.api.CalendarLoader 8 | import de.paulbrejla.holidays.infrastructure.api.HolidayRepository 9 | import de.paulbrejla.holidays.rest.HolidayDto 10 | import de.paulbrejla.holidays.rest.assembleHolidayDto 11 | import org.slf4j.Logger 12 | import org.slf4j.LoggerFactory 13 | import org.springframework.scheduling.annotation.Scheduled 14 | import org.springframework.stereotype.Component 15 | 16 | @Component 17 | class HolidayServiceImpl(val calendarLoader: CalendarLoader, 18 | val holidayRepository: HolidayRepository) : HolidayService { 19 | val logger: Logger = LoggerFactory.getLogger(this::class.java) 20 | 21 | override fun findHolidays(forState: State, andYear: Int): List { 22 | 23 | return holidayRepository.findAllByStateCodeAndYear(stateCode = forState, year = andYear).map { 24 | assembleHolidayDto(it) 25 | } 26 | } 27 | 28 | override fun findHolidays(): List { 29 | return holidayRepository.findAll().map { 30 | assembleHolidayDto(it) 31 | } 32 | } 33 | 34 | override fun findHolidays(forState: State): List { 35 | return holidayRepository.findAllByStateCode(forState).map { assembleHolidayDto(it) } 36 | } 37 | 38 | @Scheduled(fixedRate = 1500000, initialDelay = 5000) 39 | override fun loadHolidays() { 40 | calendarLoader.loadCalendarFiles().forEach { (state, calendars) -> 41 | calendars.forEach { 42 | it.events.forEach { vEvent: VEvent -> 43 | try{ 44 | val holiday = assembleHoliday(vEvent, state) 45 | if (holidayRepository.findOneBySummaryAndStateCodeAndYear(summary = holiday.summary, stateCode = holiday.stateCode, year = holiday.year) == null) { 46 | holidayRepository.save(holiday) 47 | } 48 | }catch (e: Exception){ 49 | logger.error("Could not extract event $e for state $state") 50 | } 51 | 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/config/ConfigurationProperties.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.config 2 | 3 | import de.paulbrejla.holidays.domain.AllOpen 4 | import de.paulbrejla.holidays.domain.NoArg 5 | import org.springframework.boot.context.properties.ConfigurationProperties 6 | 7 | @ConfigurationProperties(prefix = "loader") 8 | @AllOpen 9 | @NoArg 10 | data class LoaderProperties( 11 | var source: String, 12 | var remoteURL: String, 13 | var branch: String, 14 | var filePath: String, 15 | var authToken: String? = null 16 | ) -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/config/SwaggerConfiguration.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import springfox.documentation.builders.PathSelectors 6 | 7 | import springfox.documentation.builders.RequestHandlerSelectors 8 | 9 | import springfox.documentation.spi.DocumentationType 10 | 11 | import springfox.documentation.spring.web.plugins.Docket 12 | 13 | 14 | @Configuration 15 | class SwaggerConfiguration { 16 | @Bean 17 | fun api(): Docket? { 18 | return Docket(DocumentationType.SWAGGER_2) 19 | .select() 20 | .apis(RequestHandlerSelectors.basePackage("de.paulbrejla.holidays.rest")) 21 | .paths(PathSelectors.any()) 22 | .build() 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/config/WebMvcConfiguration.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.web.servlet.config.annotation.EnableWebMvc 5 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 7 | 8 | 9 | @Configuration 10 | @EnableWebMvc 11 | class WebMvcConfiguration: WebMvcConfigurer { 12 | override fun addResourceHandlers(registry: ResourceHandlerRegistry) { 13 | registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/") 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/domain/AllOpen.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.domain 2 | 3 | /** 4 | * Created by paul on 16.07.17. 5 | */ 6 | annotation class AllOpen -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/domain/Enums.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.domain 2 | 3 | enum class State { 4 | 5 | BW,BY,BE,BB,HB,HH,HE,MV,NI,NW,RP,SL,SN,ST,SH,TH 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/domain/Models.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.domain 2 | 3 | import java.io.Serializable 4 | import java.time.LocalDate 5 | import javax.persistence.* 6 | 7 | 8 | @AllOpen 9 | @NoArg 10 | @Entity 11 | data class Holiday(@Id 12 | @GeneratedValue(strategy= GenerationType.AUTO) var id: Long, 13 | @Enumerated(EnumType.STRING) var stateCode: State, 14 | val year: Int, 15 | var summary: String, 16 | var start: LocalDate, 17 | var end: LocalDate, 18 | var slug: String) : Serializable -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/domain/NoArg.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.domain 2 | 3 | /** 4 | * Created by paul on 30.06.17. 5 | */ 6 | annotation class NoArg -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/Assembler.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure 2 | 3 | fun assembleStateFromFileName(fileName: String) = fileName.split("_")[1] -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/Dto.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure 2 | 3 | import biweekly.ICalendar 4 | 5 | data class CalendarDto(val state: String, val calendars: List) -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/api/HolidayRepository.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure.api 2 | 3 | import de.paulbrejla.holidays.domain.Holiday 4 | import de.paulbrejla.holidays.domain.State 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | 7 | interface HolidayRepository : JpaRepository { 8 | fun findOneBySummaryAndStateCodeAndYear(summary: String, stateCode: State, year: Int): Holiday? 9 | fun findAllByStateCode(stateCode: State): List 10 | fun findAllByStateCodeAndYear(stateCode: State, year: Int): List 11 | 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/loader/api/CalendarLoader.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure.loader.api 2 | 3 | import de.paulbrejla.holidays.infrastructure.CalendarDto 4 | 5 | interface CalendarLoader { 6 | 7 | fun loadCalendarFiles() : List 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/loader/impl/GitCalendarLoaderGateway.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure.loader.impl 2 | 3 | import biweekly.Biweekly 4 | import de.paulbrejla.holidays.config.LoaderProperties 5 | import de.paulbrejla.holidays.infrastructure.CalendarDto 6 | import de.paulbrejla.holidays.infrastructure.assembleStateFromFileName 7 | import de.paulbrejla.holidays.infrastructure.loader.api.CalendarLoader 8 | import org.eclipse.jgit.api.Git 9 | import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription 10 | import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository 11 | import org.eclipse.jgit.lib.RepositoryCache 12 | import org.eclipse.jgit.revwalk.* 13 | import org.eclipse.jgit.transport.RefSpec 14 | import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider 15 | import org.eclipse.jgit.treewalk.TreeWalk 16 | import org.eclipse.jgit.treewalk.filter.PathFilter 17 | import org.slf4j.Logger 18 | import org.slf4j.LoggerFactory 19 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 20 | import org.springframework.stereotype.Component 21 | import java.io.ByteArrayOutputStream 22 | import javax.annotation.PostConstruct 23 | 24 | @Component 25 | @ConditionalOnProperty(prefix = "loader", name = ["source"], havingValue = "git") 26 | class GitCalendarLoaderGateway(val loaderProperties: LoaderProperties) : CalendarLoader { 27 | val logger: Logger = LoggerFactory.getLogger(this::class.java) 28 | private val refSpec: String = "+refs/heads/*:refs/heads/*" 29 | 30 | override fun loadCalendarFiles(): List { 31 | return readFromGit().map { 32 | CalendarDto(assembleStateFromFileName(it.first), Biweekly.parse(it.second).all()).also { cal -> 33 | logger.info("Imported ${cal.calendars.size} entries for ${cal.state}") 34 | } 35 | } 36 | } 37 | 38 | @PostConstruct 39 | fun postConstruct() { 40 | logger.info("Using ${this.javaClass.name} for ICS import.") 41 | } 42 | 43 | private fun readFromGit(): List> { 44 | val repoDesc = DfsRepositoryDescription() 45 | val repo = InMemoryRepository(repoDesc) 46 | 47 | val git = Git(repo) 48 | git.fetch() 49 | .setForceUpdate(true) 50 | .setRemote(loaderProperties.remoteURL) 51 | .setRefSpecs(RefSpec(refSpec)).also { fc -> 52 | loaderProperties.authToken?.let { 53 | fc.setCredentialsProvider(UsernamePasswordCredentialsProvider(loaderProperties.authToken, "")) 54 | } 55 | } 56 | .call() 57 | 58 | val latestCommit: RevCommit = assembleLatestCommit(repo, loaderProperties.branch) 59 | val tree = latestCommit.tree 60 | val treeWalk = assembleTreeWithFilters(repo, tree, loaderProperties.filePath) 61 | 62 | return extractCalendarFiles(treeWalk, repo) 63 | } 64 | 65 | private fun assembleLatestCommit(repo: InMemoryRepository, branch: String): RevCommit { 66 | val lastCommitId = repo.resolve("refs/heads/$branch") 67 | val revWalk = RevWalk(repo) 68 | revWalk.sort(RevSort.COMMIT_TIME_DESC, true) 69 | return revWalk.parseCommit(lastCommitId) 70 | } 71 | 72 | private fun extractCalendarFiles( 73 | treeWalk: TreeWalk, 74 | repo: InMemoryRepository 75 | ): List> { 76 | val calendarFiles = mutableListOf>() 77 | 78 | while (treeWalk.next()) { 79 | try { 80 | val loader = repo.open(treeWalk.getObjectId(0)) 81 | 82 | val stream = ByteArrayOutputStream() 83 | loader.copyTo(stream) 84 | val calendarFileWithFilename = Pair(treeWalk.pathString.substringAfterLast("/"), stream.toString()) 85 | calendarFiles.add(calendarFileWithFilename) 86 | } catch (e: Exception) { 87 | logger.error("Error extracting object from git: $e") 88 | } 89 | } 90 | repo.close() 91 | RepositoryCache.clear() 92 | return calendarFiles 93 | } 94 | 95 | private fun assembleTreeWithFilters( 96 | repo: InMemoryRepository, 97 | tree: RevTree?, 98 | filePath: String 99 | ): TreeWalk { 100 | val treeWalk = TreeWalk(repo) 101 | treeWalk.addTree(tree) 102 | treeWalk.isRecursive = true 103 | treeWalk.filter = PathFilter.create(filePath) 104 | 105 | return treeWalk 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/infrastructure/loader/impl/LocalFilesystemCalendarLoaderGateway.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure.loader.impl 2 | 3 | import de.paulbrejla.holidays.infrastructure.loader.api.CalendarLoader 4 | import biweekly.Biweekly 5 | import de.paulbrejla.holidays.infrastructure.CalendarDto 6 | import de.paulbrejla.holidays.infrastructure.assembleStateFromFileName 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.beans.factory.annotation.Value 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 11 | import org.springframework.core.io.Resource 12 | import org.springframework.stereotype.Component 13 | import javax.annotation.PostConstruct 14 | 15 | @Component 16 | @ConditionalOnProperty(prefix = "loader", name = ["source"], havingValue = "filesystem") 17 | class LocalFilesystemCalendarLoaderGateway : CalendarLoader { 18 | val logger: Logger = LoggerFactory.getLogger(this::class.java) 19 | 20 | @Value("classpath:holidays/*.ics") 21 | private val calendarFiles: Array? = null 22 | 23 | @PostConstruct 24 | fun postConstruct() { 25 | logger.info("Using ${this.javaClass.name} for ICS import.") 26 | } 27 | 28 | override fun loadCalendarFiles(): List { 29 | return calendarFiles?.map { 30 | CalendarDto(assembleStateFromFileName(it.filename!!.toString()), 31 | calendars = Biweekly.parse(it.inputStream).all()) 32 | }!!.toList() 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/mvc/api/ViewController.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.mvc.api 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping 4 | 5 | interface ViewController { 6 | 7 | @RequestMapping("/") 8 | fun frontPage() : String 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/mvc/impl/ViewControllerImpl.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.mvc.impl 2 | 3 | import de.paulbrejla.holidays.mvc.api.ViewController 4 | import org.springframework.stereotype.Controller 5 | 6 | @Controller 7 | class ViewControllerImpl : ViewController { 8 | 9 | override fun frontPage(): String { 10 | return "index" 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/rest/Assembler.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest 2 | 3 | import de.paulbrejla.holidays.domain.Holiday 4 | 5 | fun assembleHolidayDto(holiday: Holiday) = HolidayDto( 6 | start = holiday.start, 7 | end = holiday.end, 8 | year = holiday.year, 9 | stateCode = holiday.stateCode, 10 | name = holiday.summary, 11 | slug = holiday.slug) -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/rest/Dto.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat 4 | import de.paulbrejla.holidays.domain.State 5 | import java.time.LocalDate 6 | 7 | data class HolidayDto( 8 | @get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") val start: LocalDate, 9 | @get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") val end: LocalDate, 10 | val year: Int, 11 | val stateCode: State, 12 | val name: String, 13 | val slug: String 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/rest/api/HolidayWsV1.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest.api 2 | 3 | import de.paulbrejla.holidays.domain.State 4 | import de.paulbrejla.holidays.rest.HolidayDto 5 | import org.springframework.web.bind.annotation.PathVariable 6 | import org.springframework.web.bind.annotation.RequestMapping 7 | import org.springframework.web.bind.annotation.RequestMethod 8 | 9 | 10 | @RequestMapping("/api/v1", produces = ["application/json"]) 11 | interface HolidayWsV1 { 12 | 13 | @RequestMapping("/holidays", "/holidays.json", method = [RequestMethod.GET]) 14 | fun getHolidays(): List 15 | 16 | @RequestMapping("/holidays/{state}.json", "/holidays/{state}", method = [RequestMethod.GET]) 17 | fun getHolidaysForState(@PathVariable("state") state: State): List 18 | 19 | @RequestMapping("/holidays/{state}/{year}.json", "/holidays/{state}/{year}", method = [RequestMethod.GET]) 20 | fun getHolidaysForStateAndYear(@PathVariable("state") state: State, @PathVariable("year") year: Int): List 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/rest/impl/HolidayWsV1Impl.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest.impl 2 | 3 | import de.paulbrejla.holidays.application.api.HolidayService 4 | import de.paulbrejla.holidays.domain.State 5 | import de.paulbrejla.holidays.rest.HolidayDto 6 | import de.paulbrejla.holidays.rest.api.HolidayWsV1 7 | import org.springframework.http.HttpStatus 8 | import org.springframework.web.bind.annotation.CrossOrigin 9 | import org.springframework.web.bind.annotation.PathVariable 10 | import org.springframework.web.bind.annotation.RequestMethod 11 | import org.springframework.web.bind.annotation.RestController 12 | import org.springframework.web.server.ResponseStatusException 13 | import java.lang.Exception 14 | 15 | 16 | @CrossOrigin(origins = ["*"], allowedHeaders = ["*"], methods = [RequestMethod.GET]) 17 | @RestController 18 | class HolidayWsV1Impl(val holidayService: HolidayService) : HolidayWsV1 { 19 | 20 | override fun getHolidaysForState(@PathVariable("state") state: State): List { 21 | return holidayService.findHolidays(forState = state) 22 | } 23 | 24 | override fun getHolidays(): List { 25 | return handleApplicationCall { holidayService.findHolidays() } 26 | } 27 | 28 | override fun getHolidaysForStateAndYear(@PathVariable("state") state: State, @PathVariable("year") year: Int): List { 29 | return handleApplicationCall { holidayService.findHolidays(forState = state, andYear = year) } 30 | } 31 | 32 | private inline fun handleApplicationCall( 33 | call: () -> T 34 | ): List { 35 | return try { 36 | val result = call() 37 | result as List 38 | } catch (e: Exception) { 39 | throw ResponseStatusException(HttpStatus.NOT_FOUND, "Could not extract holidays for your query.") 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/de/paulbrejla/holidays/rest/impl/RateLimitFilter.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest.impl 2 | 3 | import de.paulbrejla.holidays.application.api.RateLimitService 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.http.HttpStatus 6 | import org.springframework.stereotype.Component 7 | import org.springframework.web.server.ResponseStatusException 8 | import javax.servlet.Filter 9 | import javax.servlet.FilterChain 10 | import javax.servlet.ServletException 11 | import javax.servlet.ServletRequest 12 | import javax.servlet.ServletResponse 13 | import javax.servlet.http.HttpServletResponse 14 | 15 | @Component 16 | class RateLimitFilter(val rateLimitService: RateLimitService) : Filter { 17 | @Value("\${rateLimit.globalBucket.id}") 18 | var globalBucketId: String = "" 19 | 20 | @Value("\${rateLimit.globalBucket.capacity}") 21 | var globalBucketCapacity: Long = 0 22 | 23 | 24 | override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) { 25 | if (!this.shouldFulfillRequestWithinRateLimit(globalBucketId)) { // Maybe add additional x-rate-limit headers later. 26 | (response as HttpServletResponse).apply { 27 | this.status = HttpStatus.TOO_MANY_REQUESTS.value() 28 | this.addHeader("X-RateLimit-Limit", globalBucketCapacity.toString()) 29 | this.addHeader("X-RateLimit-Remaining", "0") 30 | } 31 | } else { 32 | chain!!.doFilter(request, response) 33 | } 34 | } 35 | 36 | private fun shouldFulfillRequestWithinRateLimit(bucketId: String): Boolean { 37 | return rateLimitService.resolveBucket(bucketId) 38 | .tryConsumeAndReturnRemaining(1).run { 39 | if (this.isConsumed) 40 | true 41 | else 42 | false 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: holidays-api 4 | mcv: 5 | pathmatch: 6 | matching-strategy: ant_path_matcher 7 | h2: 8 | console: 9 | enabled: false 10 | path: /h2-console 11 | server: 12 | port: ${SERVER_PORT:80} 13 | error: 14 | include-stacktrace: never 15 | loader: 16 | source: ${LOADER_SOURCE} 17 | remoteURL: ${LOADER_REMOTE_URL} 18 | branch: ${LOADER_BRANCH} 19 | filePath: ${LOADER_FILE_PATH} 20 | authToken: ${LOADER_AUTH_TOKEN} 21 | rateLimit: 22 | globalBucket: 23 | id: ${RATE_LIMIT_GLOBAL_BUCKET_ID:global} 24 | capacity: ${RATE_LIMIT_GLOBAL_BUCKET_CAPACITY:500} 25 | -------------------------------------------------------------------------------- /src/main/resources/holidays/.gitignore: -------------------------------------------------------------------------------- 1 | *.ics -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | 19 | Deutsche Ferientermine als JSON 20 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |

Deutsche Ferientermine

31 |

Via JSON -und XML-Schnittstelle

32 | 49 |
50 |

Verwendung (bitte lies mich, wirklich!)

51 |

Die Benutzung ist für private und kommerzielle Zwecke kostenfrei. Bitte bedenke bei der Verwendung, dass 52 | ich 53 | keine kommerziellen Zwecke verfolge und die Hostingkosten für dieses Projekt selber trage. Sämtliche 54 | Änderungen passieren in meiner Freizeit. Ich habe mittlerweile keine Verwendung mehr für diese Daten. 55 |

56 |
Richtigkeit und Bereitstellung der Daten
57 |

Sämtliche Datumsangaben sind ohne Gewähr. Ich übernehme weder Verantwortung für die Richtigkeit der 58 | Daten 59 | noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können. 60 | Wenn du 61 | sicher sein möchtest dass diese Daten stimmen und Erreichbarkeit sicherstellen möchtest, kannst du 62 | dieses Projekt unter der MIT-Lizenz selber hosten und mit von dir geprüften Kalenderdaten 63 | füttern.

64 | Hier entlang zum GitHub-Repository 65 |
66 | Wenn die Schnittstelle für dich nützlich ist, freue ich mich aber über eine Spende für einen Kaffee. 67 |
68 |
69 | 70 | 71 | 74 | 76 |
77 |
78 |

79 | 80 |
Hello World
81 |

82 | Die Schnittstelle ist unter https://ferien-api.de/api/v1/holidays erreichbar. 83 | Du kannst die API direkt über Swagger / OpenAPI ausprobieren: Swagger / 84 | OpenAPI. 85 | Dort kannst du dir auch 86 | für die Sprache und das Framework Deiner Wahl Client SDKs generieren. 87 |

88 |

89 | Um alle kommenden Ferientermine für alle Bundesländer zu erhalten, starte wie folgt: 90 |

 91 |         $ curl https://ferien-api.de/api/v1/holidays
 92 | 
 93 |         [
 94 |            {
 95 |               "start":"2017-04-09T22:00",
 96 |               "end":"2017-04-21T22:00",
 97 |               "year":2017,
 98 |               "stateCode":"BW",
 99 |               "name":"osterferien",
100 |               "slug":"osterferien-2017-BW"
101 |            },..
102 |         ]
103 |        
104 | Die Datumsangaben werden als UTC zurückgeliefert. 105 |

106 | 107 |
Ferientermine nach Bundesland
108 |

109 | Alle Ferientermine für ein Bundesland erhältst du über 110 | https://ferien-api.de/api/v1/holidays/{stateCode}, 111 | wobei stateCode dem zweistelligen Code des Bundeslandes entspricht. 112 |

113 |         $ curl https://ferien-api.de/api/v1/holidays/HB
114 | 
115 |         [
116 |            {
117 |               "start":"2017-01-29T23:00",
118 |               "end":"2017-01-31T23:00",
119 |               "year":2017,
120 |               "stateCode":"HB",
121 |               "name":"winterferien",
122 |               "slug":"winterferien-2017-HB"
123 |            },
124 |            {
125 |               "start":"2017-04-09T22:00",
126 |               "end":"2017-04-22T22:00",
127 |               "year":2017,
128 |               "stateCode":"HB",
129 |               "name":"osterferien",
130 |               "slug":"osterferien-2017-HB"
131 |            },..
132 |         ]
133 |        
134 | Die möglichen Codes für die Bundesländer findest du zum Beispiel unter https://de.wikipedia.org/wiki/ISO_3166-2:DE. 136 |

137 | 138 |
Ferientermine nach Bundesland und Jahr
139 |

140 | Alle Ferientermine nach Bundesland und erhältst du über https://ferien-api.de/api/v1/holidays/{stateCode}/{year}. 141 |

142 |         $ curl https://ferien-api.de/api/v1/holidays/HB/2017
143 | 
144 |         [
145 |            {
146 |               "start":"2017-01-29T23:00",
147 |               "end":"2017-01-31T23:00",
148 |               "year":2017,
149 |               "stateCode":"HB",
150 |               "name":"winterferien",
151 |               "slug":"winterferien-2017-HB"
152 |            },
153 |            {
154 |               "start":"2017-04-09T22:00",
155 |               "end":"2017-04-22T22:00",
156 |               "year":2017,
157 |               "stateCode":"HB",
158 |               "name":"osterferien",
159 |               "slug":"osterferien-2017-HB"
160 |            },..
161 |         ]
162 |        
163 |

164 | 165 |
Rückgabewert & Format
166 |

167 | Du kannst das Format der zurückgelieferten Daten ändern indem du einfach .json oder .xml an das Ende der 168 | URL 169 | anhängst. 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |
FeldTypBeschreibung
startDate ISO8601 (UTC)Ferienbeginn
endDate ISO8601 (UTC)Ferienende
stateCodeStringZweistelliger Code des Bundeslandes
nameStringName der Schulferien
slugStringKombination aus Ferienname, Jahr und Bundesland
208 | 209 |

Parameter
210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 |
FeldTypLänge
StateCodeString (siehe https://de.wikipedia.org/wiki/ISO_3166-2:DE) 222 | 2
YearInt4
233 |

234 |
235 |

Mithelfen

236 |

237 | Dieses Projekt ist unter der MIT License lizenziert. 238 | Du findest den Quellcode auf GitHub unter https://github.com/paulbrejla/ferien-api. 239 | Dort kannst 240 | du dich direkt an der Weiterentwicklung beteiligen. 241 |

242 |
243 |

Kontakt und Feedback

244 |

Die Ferien-Schnittstelle ist ein Projekt von Paul Brejla. 245 | Für Featurewünsche oder Bugs kannst du gerne ein Issue oder einen Pull Request unter https://github.com/paulbrejla/ferien-api 247 | eröffnen. 248 | Bei Rückfragen kannst du weiter unten einen Kommentar hinterlassen oder mich unter 249 | paul(at)paulbrejla(punkt)com erreichen. 250 |

251 |
252 |
253 |
254 |
Impressum
255 |
Paul Brejla
256 |
Schellerdamm 9
257 |
21079 Hamburg
258 |
259 |
260 |
261 |
262 |
LinkedIn
263 |
264 | https://www.linkedin.com/in/paulbrejla/ 265 |
266 |
Xing
267 |
268 | https://www.xing.com/profile/Paul_Brejla 269 |
270 |
GitHub
271 |
https://github.com/paulbrejla
272 | 273 |
274 | 275 |
276 | 277 |
278 |
279 | 299 | 302 | 303 |
304 |
305 |
306 |
307 | 314 | 315 | -------------------------------------------------------------------------------- /src/test/kotlin/de/paulbrejla/holidays/HolidaysApiApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import org.springframework.test.context.junit4.SpringRunner 6 | 7 | @SpringBootTest 8 | class HolidaysApiApplicationTests { 9 | 10 | @Test 11 | fun contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/kotlin/de/paulbrejla/holidays/infrastructure/loader/impl/LocalFilesystemCalendarLoaderGatewayTest.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.infrastructure.loader.impl 2 | 3 | import de.paulbrejla.holidays.infrastructure.loader.api.CalendarLoader 4 | import org.junit.jupiter.api.Test 5 | 6 | import org.junit.jupiter.api.Assertions.* 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.test.context.SpringBootTest 9 | 10 | @SpringBootTest 11 | internal class LocalFilesystemCalendarLoaderGatewayTest { 12 | 13 | @Autowired 14 | lateinit var calendarLoader: CalendarLoader 15 | 16 | @Test 17 | fun `calendar files are loaded`() { 18 | // Given 19 | val eventCount = 5 20 | 21 | // When 22 | val calendars = calendarLoader.loadCalendarFiles() 23 | 24 | // Then 25 | assertNotNull(calendars.first().calendars) 26 | assertEquals(calendars.first().calendars.first().events.count(), eventCount) 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/de/paulbrejla/holidays/rest/AssemblerKtTest.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest 2 | 3 | import de.paulbrejla.holidays.domain.Holiday 4 | import de.paulbrejla.holidays.domain.State 5 | import org.junit.jupiter.api.Test 6 | 7 | import org.junit.jupiter.api.Assertions.* 8 | import java.time.LocalDate 9 | 10 | class AssemblerKtTest { 11 | 12 | @Test 13 | fun `a HolidayDto is assembled from a Holiday`() { 14 | // Given 15 | val holiday = Holiday(id = 1, stateCode = State.HB, year = 2020, summary = "Winterferien Bremen", 16 | start = LocalDate.now(), end = LocalDate.now().plusYears(2), slug = "ferien-hb") 17 | 18 | // When 19 | val holidayDto = assembleHolidayDto(holiday) 20 | 21 | // Then 22 | assertNotNull(holidayDto) 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/de/paulbrejla/holidays/rest/impl/HolidayWsV1ImplITest.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest.impl 2 | 3 | import de.paulbrejla.holidays.application.api.HolidayService 4 | import de.paulbrejla.holidays.rest.api.HolidayWsV1 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.TestInstance 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 10 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 11 | import org.springframework.boot.test.context.SpringBootTest 12 | import org.springframework.test.context.junit.jupiter.SpringExtension 13 | import org.springframework.test.web.servlet.MockMvc 14 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders 15 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers 16 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 17 | 18 | 19 | @AutoConfigureMockMvc 20 | @SpringBootTest 21 | @ExtendWith( 22 | SpringExtension::class 23 | ) 24 | @TestInstance(TestInstance.Lifecycle.PER_METHOD) 25 | class HolidayWsV1ImplITest { 26 | @Autowired 27 | lateinit var mvc: MockMvc 28 | 29 | @Test 30 | fun `api responds with a 429 if rate limit is reached after 10 requests`() { 31 | repeat(11) { rep -> 32 | mvc.perform( 33 | MockMvcRequestBuilders 34 | .get("/api/v1/holidays/HB") 35 | ) 36 | .andExpect { 37 | if (rep <= 10) { 38 | status().isOk 39 | } else { 40 | status().isTooManyRequests 41 | } 42 | } 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/test/kotlin/de/paulbrejla/holidays/rest/impl/HolidayWsV1ImplTest.kt: -------------------------------------------------------------------------------- 1 | package de.paulbrejla.holidays.rest.impl 2 | 3 | import de.paulbrejla.holidays.application.api.HolidayService 4 | import de.paulbrejla.holidays.domain.State 5 | import de.paulbrejla.holidays.infrastructure.api.HolidayRepository 6 | import de.paulbrejla.holidays.infrastructure.loader.api.CalendarLoader 7 | import de.paulbrejla.holidays.rest.api.HolidayWsV1 8 | import org.junit.jupiter.api.Test 9 | 10 | import org.junit.jupiter.api.Assertions.* 11 | import org.junit.jupiter.api.BeforeAll 12 | import org.junit.jupiter.api.TestInstance 13 | import org.junit.jupiter.api.extension.ExtendWith 14 | import org.springframework.beans.factory.annotation.Autowired 15 | import org.springframework.boot.test.context.SpringBootTest 16 | import org.springframework.test.context.junit.jupiter.SpringExtension 17 | 18 | @SpringBootTest 19 | @ExtendWith( 20 | SpringExtension::class) 21 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 22 | internal class HolidayWsV1ImplTest { 23 | 24 | @Autowired lateinit var holidayWsV1: HolidayWsV1 25 | @Autowired lateinit var holidayService: HolidayService 26 | 27 | @BeforeAll 28 | fun setUp(){ 29 | holidayService.loadHolidays() 30 | } 31 | 32 | @Test 33 | fun `returns holidays for a given state`() { 34 | // Given 35 | val state = State.BW 36 | val expectedSize = 5 37 | 38 | // When 39 | val holidays = holidayWsV1.getHolidaysForState(state) 40 | 41 | // Then 42 | assertNotNull(holidays) 43 | assertEquals(expectedSize, holidays.size) 44 | } 45 | 46 | @Test 47 | fun `returns all holidays`() { 48 | // Given 49 | val expectedSize = holidayService.findHolidays().size 50 | 51 | // When 52 | val holidays = holidayWsV1.getHolidays() 53 | 54 | // Then 55 | assertNotNull(holidays) 56 | assertEquals(expectedSize, holidays.size) 57 | 58 | } 59 | 60 | @Test 61 | fun `returns holidays for state and year`() { 62 | // Given 63 | val state = State.BW 64 | val expectedSize = 5 65 | val year = 2017 66 | 67 | // When 68 | val holidays = holidayWsV1.getHolidaysForStateAndYear(state, year) 69 | 70 | // Then 71 | assertNotNull(holidays) 72 | assertEquals(expectedSize, holidays.size) 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: holidays-api 4 | mcv: 5 | pathmatch: 6 | matching-strategy: ant_path_matcher 7 | h2: 8 | console: 9 | enabled: false 10 | path: /h2-console 11 | server: 12 | port: ${SERVER_PORT:80} 13 | loader: 14 | source: filesystem 15 | remoteURL: 16 | branch: master 17 | filePath: 18 | rateLimit: 19 | globalBucket: 20 | id: ${RATE_LIMIT_GLOBAL_BUCKET_ID:global} 21 | capacity: ${RATE_LIMIT_GLOBAL_BUCKET_CAPACITY:10} 22 | -------------------------------------------------------------------------------- /src/test/resources/holidays/de/ferien_baden-wuerttemberg_2017.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:FERIEN_API 5 | METHOD:PUBLISH 6 | 7 | BEGIN:VEVENT 8 | DTSTAMP:20170922T052539Z 9 | UID:1 10 | DTSTART;VALUE=DATE:20170410 11 | DTEND;VALUE=DATE:20170422 12 | SUMMARY:Osterferien 13 | END:VEVENT 14 | 15 | BEGIN:VEVENT 16 | DTSTAMP:20170922T052539Z 17 | UID:2 18 | DTSTART;VALUE=DATE:20170606 19 | DTEND;VALUE=DATE:20170617 20 | SUMMARY:Pfingstferien 21 | END:VEVENT 22 | 23 | BEGIN:VEVENT 24 | DTSTAMP:20170922T052539Z 25 | UID:3 26 | DTSTART;VALUE=DATE:20170727 27 | DTEND;VALUE=DATE:20170910 28 | SUMMARY:Sommerferien 29 | END:VEVENT 30 | 31 | BEGIN:VEVENT 32 | DTSTAMP:20170922T052539Z 33 | UID:4 34 | DTSTART;VALUE=DATE:20171030 35 | DTEND;VALUE=DATE:20171104 36 | SUMMARY:Herbstferien 37 | END:VEVENT 38 | 39 | BEGIN:VEVENT 40 | DTSTAMP:20170922T052539Z 41 | UID:5 42 | DTSTART;VALUE=DATE:20171222 43 | DTEND;VALUE=DATE:20180106 44 | SUMMARY:Weihnachtsferien 45 | END:VEVENT 46 | 47 | END:VCALENDAR 48 | -------------------------------------------------------------------------------- /src/test/resources/holidays/de/ferien_bremen_2017.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:FERIEN_API 5 | METHOD:PUBLISH 6 | 7 | BEGIN:VEVENT 8 | DTSTAMP:20170922T052539Z 9 | UID:1 10 | DTSTART;VALUE=DATE:20170410 11 | DTEND;VALUE=DATE:20170422 12 | SUMMARY:Osterferien 13 | END:VEVENT 14 | 15 | BEGIN:VEVENT 16 | DTSTAMP:20170922T052539Z 17 | UID:2 18 | DTSTART;VALUE=DATE:20170606 19 | DTEND;VALUE=DATE:20170617 20 | SUMMARY:Pfingstferien 21 | END:VEVENT 22 | 23 | BEGIN:VEVENT 24 | DTSTAMP:20170922T052539Z 25 | UID:3 26 | DTSTART;VALUE=DATE:20170727 27 | DTEND;VALUE=DATE:20170910 28 | SUMMARY:Sommerferien 29 | END:VEVENT 30 | 31 | BEGIN:VEVENT 32 | DTSTAMP:20170922T052539Z 33 | UID:4 34 | DTSTART;VALUE=DATE:20171030 35 | DTEND;VALUE=DATE:20171104 36 | SUMMARY:Herbstferien 37 | END:VEVENT 38 | 39 | BEGIN:VEVENT 40 | DTSTAMP:20170922T052539Z 41 | UID:5 42 | DTSTART;VALUE=DATE:20171222 43 | DTEND;VALUE=DATE:20180106 44 | SUMMARY:Weihnachtsferien 45 | END:VEVENT 46 | 47 | END:VCALENDAR 48 | -------------------------------------------------------------------------------- /src/test/resources/holidays/ferien_baden-wuerttemberg_2017.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:FERIEN_API 5 | METHOD:PUBLISH 6 | 7 | BEGIN:VEVENT 8 | DTSTAMP:20170922T052539Z 9 | UID:1 10 | DTSTART;VALUE=DATE:20170410 11 | DTEND;VALUE=DATE:20170422 12 | SUMMARY:Osterferien 13 | END:VEVENT 14 | 15 | BEGIN:VEVENT 16 | DTSTAMP:20170922T052539Z 17 | UID:2 18 | DTSTART;VALUE=DATE:20170606 19 | DTEND;VALUE=DATE:20170617 20 | SUMMARY:Pfingstferien 21 | END:VEVENT 22 | 23 | BEGIN:VEVENT 24 | DTSTAMP:20170922T052539Z 25 | UID:3 26 | DTSTART;VALUE=DATE:20170727 27 | DTEND;VALUE=DATE:20170910 28 | SUMMARY:Sommerferien 29 | END:VEVENT 30 | 31 | BEGIN:VEVENT 32 | DTSTAMP:20170922T052539Z 33 | UID:4 34 | DTSTART;VALUE=DATE:20171030 35 | DTEND;VALUE=DATE:20171104 36 | SUMMARY:Herbstferien 37 | END:VEVENT 38 | 39 | BEGIN:VEVENT 40 | DTSTAMP:20170922T052539Z 41 | UID:5 42 | DTSTART;VALUE=DATE:20171222 43 | DTEND;VALUE=DATE:20180106 44 | SUMMARY:Weihnachtsferien 45 | END:VEVENT 46 | 47 | END:VCALENDAR 48 | --------------------------------------------------------------------------------