├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── Wiki ├── Error Architecture.md ├── In-memory database.md └── code-formatter.md ├── docker-compose.yml ├── hooks └── pre-commit ├── icon.png ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── pl │ │ └── wrapper │ │ └── parking │ │ ├── ParkingApplication.java │ │ ├── facade │ │ ├── ParkingHistoricDataService.java │ │ ├── ParkingRequestStatsService.java │ │ ├── ParkingService.java │ │ ├── ParkingStatsService.java │ │ ├── domain │ │ │ ├── historic │ │ │ │ ├── HistoricDataEntry.java │ │ │ │ ├── ParkingHistoricController.java │ │ │ │ └── ParkingHistoricDataServiceImpl.java │ │ │ ├── main │ │ │ │ ├── ParkingController.java │ │ │ │ └── ParkingServiceImpl.java │ │ │ └── stats │ │ │ │ ├── parking │ │ │ │ ├── ParkingStatsController.java │ │ │ │ └── ParkingStatsServiceImpl.java │ │ │ │ └── request │ │ │ │ ├── ParkingRequestStatsController.java │ │ │ │ └── ParkingRequestStatsServiceImpl.java │ │ └── dto │ │ │ ├── historicData │ │ │ ├── HistoricDayData.java │ │ │ ├── HistoricDayParkingData.java │ │ │ ├── HistoricPeriodParkingData.java │ │ │ └── TimestampEntry.java │ │ │ ├── main │ │ │ └── NominatimLocation.java │ │ │ └── stats │ │ │ ├── parking │ │ │ ├── ParkingStatsResponse.java │ │ │ ├── basis │ │ │ │ ├── OccupancyInfo.java │ │ │ │ ├── ParkingInfo.java │ │ │ │ └── ParkingStats.java │ │ │ ├── daily │ │ │ │ ├── CollectiveDailyParkingStats.java │ │ │ │ └── DailyParkingStatsResponse.java │ │ │ └── weekly │ │ │ │ ├── CollectiveWeeklyParkingStats.java │ │ │ │ └── WeeklyParkingStatsResponse.java │ │ │ └── request │ │ │ └── EndpointStats.java │ │ ├── infrastructure │ │ ├── configuration │ │ │ ├── CorsConfiguration.java │ │ │ ├── DbConfiguration.java │ │ │ └── SwaggerConfig.java │ │ ├── error │ │ │ ├── Error.java │ │ │ ├── ErrorWrapper.java │ │ │ ├── HandleResult.java │ │ │ ├── ParkingError.java │ │ │ └── Result.java │ │ ├── exception │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── InvalidCallException.java │ │ │ ├── NominatimClientException.java │ │ │ └── PwrApiNotRespondingException.java │ │ ├── inMemory │ │ │ ├── InMemoryRepository.java │ │ │ ├── InMemoryRepositoryImpl.java │ │ │ ├── ParkingDataRepository.java │ │ │ ├── ParkingRequestRepository.java │ │ │ └── dto │ │ │ │ ├── parking │ │ │ │ ├── AvailabilityData.java │ │ │ │ └── ParkingData.java │ │ │ │ └── request │ │ │ │ ├── EndpointData.java │ │ │ │ ├── EndpointDataFactory.java │ │ │ │ └── TimeframeStatistic.java │ │ ├── interceptor │ │ │ ├── ParkingRequestInterceptor.java │ │ │ └── ParkingRequestInterceptorConfig.java │ │ ├── nominatim │ │ │ ├── client │ │ │ │ └── NominatimClient.java │ │ │ └── configuration │ │ │ │ └── NominatimClientConfig.java │ │ ├── util │ │ │ └── DateTimeUtils.java │ │ └── validation │ │ │ └── validIds │ │ │ ├── IdValidator.java │ │ │ └── ValidIds.java │ │ └── pwrResponseHandler │ │ ├── PwrApiServerCaller.java │ │ ├── configuration │ │ ├── CacheConfig.java │ │ ├── CacheCustomizer.java │ │ └── WebClientConfig.java │ │ ├── domain │ │ ├── PwrApiCaller.java │ │ └── PwrApiServerCallerImpl.java │ │ └── dto │ │ ├── Address.java │ │ └── ParkingResponse.java └── resources │ ├── application.properties │ └── schema.sql └── test ├── java └── pl │ └── wrapper │ └── parking │ ├── ParkingApplicationTests.java │ ├── facade │ └── domain │ │ ├── historic │ │ └── ParkingHistoricDataServiceImplTest.java │ │ ├── main │ │ ├── ParkingControllerIT.java │ │ ├── ParkingControllerTest.java │ │ └── ParkingServiceImplTest.java │ │ └── stats │ │ ├── parking │ │ ├── ParkingStatsControllerIT.java │ │ └── ParkingStatsServiceImplTest.java │ │ └── request │ │ ├── ParkingRequestStatsControllerTest.java │ │ └── ParkingRequestStatsServiceImplTest.java │ ├── infrastructure │ ├── inMemory │ │ └── InMemoryRepositoryTest.java │ └── nominatim │ │ └── configuration │ │ └── NominatimClientTests.java │ └── pwrResponseHandler │ ├── configuration │ └── WebClientTest.java │ └── domain │ ├── PwrApiCaller.java │ └── PwrApiCallerTest.java └── resources ├── application.properties └── schema.sql /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 21 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '21' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | data/statistics/** 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/maven:3.9.9-eclipse-temurin-21 AS build 2 | COPY . /build/ 3 | WORKDIR /build 4 | RUN mvn -B clean package --file pom.xml 5 | 6 | FROM docker.io/library/openjdk:21 7 | WORKDIR /app 8 | COPY --from=build /build/target/parking-*-SNAPSHOT.jar parking.jar 9 | EXPOSE 8080 10 | ENTRYPOINT ["java", "-jar", "parking.jar"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parkingi API 2 | ![img.png](icon.png) 3 | 4 | ## Wrapper do API parkingów Politechniki Wrocławskiej 5 | ### Żadne rozwiązanie informatycznie nie jest wieczne. Nie inaczej przedstawia się sprawa z API do parkingów naszej Uczelni. Dane w przestarzałym formacie, jeden endpoint, który zwraca je wszystkie bez ładu i składu… Na szczęście powstało narzędzie, które opakowuje, modernizuje i znacząco ulepsza system Politechniki. 6 | 7 | 8 | 9 | #### Czas trwania: Od 11.2024 do 03.2025 10 | #### Kategoria: wrapper do API 11 | #### Technologie: Spring Boot 3.3, PostgreSQL 12 | 13 | ### Jakie ulepszenia wprowadzamy? 14 | Udostępniamy wszystkie dane w ustrukturyzowanej formie JSONów, upewniając się, że brakujące parametry parkingów i ujemne wolne miejsca parkingowe nie będą straszyć użytkownika, a wiadomością o możliwym błędzie będzie więcej niż złowrogie 400 BAD_REQUEST. 15 | Pozwalamy filtrować i sortować dostępne parkingi ze względu na ich parametry (nazwę, symbol, ilość wolnych miejsc parkingowych, godziny otwarcia) oraz ze względu na ich lokalizację. 16 | ### Jakie nowe funkcjonalności wprowadzamy? 17 | Tutaj najlepiej przedstawić serię krótkich historii i odpowiedzi na zawarte w nich problemy… 18 | 19 | - H1: Jesteś studentem i od zawsze parkujesz na swoim ulubionym parkingu, ale pewnego dnia nagle zajęcia masz w innym budynku po drugiej stronie miasta. Gdzie zaparkujesz? Gdzie znajdziesz wolne miejsce o 13:05 we wtorek lub 17:30 w piątek? 20 | - A1: Nic trudnego, dzięki naszemu narzędziu z łatwością uzyskasz średnią ilość wolnych miejsc z ostatnich kilku tygodni na danym parkingu w dany dzień i godzinę. 21 | 22 | 23 | - H2: Zajmujesz się zbadaniem wpływu wydarzeń organizowanych na Politechnice na życie studentów i pracowników uczelni. Albo planujesz nowe wydarzenie i nie wiesz czy parkingi Politechniki wystarczą dla planowanej liczby gości, ale za to wiesz, że kilka miesięcy temu odbyło się coś podobnego… A może po prostu jesteś ciekawy, ile było wolnych miejsc na parkingu Wrońskiego o godzinie 18:41 dnia 1 stycznia ubiegłego roku? 24 | - A2: Takie informacje również udostępniamy, oczywiście, zaczynając od czasu od kiedy nasza aplikacja działa. 25 | 26 | 27 | - H3: Jesteś ciekaw o co ludzie pytają nasz serwer najczęściej? Kiedy pytają najczęściej? 28 | - A3: Nie powiemy ci kto dokładnie, ale ktoś\* średnio przynajmniej raz w tygodniu o trzeciej w nocy szuka najbliższego 29 | mu otwartego parkingu naszej uczelni i dostaje odpowiedź 404 NOT_FOUND. Nie wierzysz? Skorzystaj z statystyk zapytań 30 | do serwera i zobacz sam o co, kiedy i z jakim skutkiem inni pytają najczęściej. 31 | \* - „ktoś” został wymyślony na rzecz historii, ale naprawdę możesz takie rzeczy sprawdzić. 32 | 33 | ### Zespół: 34 | - Ignacy Smoliński; GitHub: Leadman5555 (Project Manager, Tech Lead) 35 | - Mateusz Płaska; GitHub: mateusz-plaska (Backend developer) 36 | - Dominik Korwek; GitHub: dominikkorwek (Backend developer) 37 | - Jan Samokar; GitHub: sxlecquer (Backend developer) 38 | - Aliaksei Samoshyn; GitHub: Kawaban (Wsparcie w tworzeniu tasków) 39 | 40 | 41 | -------------------------------------------------------------------------------- /Wiki/Error Architecture.md: -------------------------------------------------------------------------------- 1 | ## Error Handling Architecture 2 | 3 | # 1. Result Interface 4 | 5 | The Result interface is a core part of the architecture that wraps return values from methods, ensuring safe and unambiguous handling of success and error cases without throwing exceptions. 6 | 7 | Components: 8 | Success: Represents a successful operation and contains the resulting data. 9 | Failure: Wraps error information using the Error interface. 10 | Methods: 11 | isSuccess(): Checks whether the operation succeeded. 12 | getData(): Returns the data if the operation was successful. 13 | getError(): Returns the error if the operation failed. 14 | Safety: 15 | Calling getData() on a Failure or getError() on a Success throws an InvalidCallException. 16 | 17 | # 2. Returning Values from Services 18 | 19 | Instead of throwing exceptions or returning null, services always return a Result object. 20 | 21 | Usage Example: 22 | On success: Result.success(data). 23 | On failure: Result.failure(new ErrorSubclass()). 24 | 25 | This ensures that services explicitly communicate whether the operation succeeded or failed, delegating further handling to controllers. 26 | 27 | # 3. Handling Results in Controllers 28 | 29 | All controllers extend the abstract class HandleResult, which provides a standardized approach to processing Result objects. 30 | 31 | How HandleResult Works: 32 | The handleResult method: 33 | If Result is Success, it returns a ResponseEntity with an HTTP success status and the data as JSON. 34 | If Result is Failure, it maps the error to an ErrorWrapper and returns an HTTP error response with the appropriate status. 35 | 36 | Benefits: 37 | Avoids repetitive code in controllers. 38 | Ensures standardized API responses (consistent data and error formats). 39 | 40 | Parameters of handleResult: 41 | Result toHandle: The Result object returned by the service. 42 | HttpStatus onSuccess: The HTTP status to use in case of success. 43 | String uri: The endpoint URI (used for logging errors). 44 | 45 | # 4. Global Exception Handler 46 | 47 | Using @ControllerAdvice, the application has a centralized mechanism to handle uncaught exceptions globally. 48 | 49 | How It Works: 50 | Each handled exception type (e.g., InvalidCallException) has a dedicated method in the handler. 51 | For each exception, an ErrorWrapper is created with: 52 | The error message. 53 | The expected HTTP status. 54 | The URI where the error occurred. 55 | The actual HTTP status (e.g., 500 or 400). 56 | 57 | Examples of Exception Handling: 58 | InvalidCallException: Returns HTTP 400 (Bad Request) with a message like "Invalid call." 59 | JsonProcessingException: Returns HTTP 500 (Internal Server Error) with a message like "JSON processing error." 60 | 61 | This ensures that even unhandled or unexpected exceptions are communicated in a client-friendly and consistent format. 62 | 63 | # 5. ErrorWrapper 64 | 65 | ErrorWrapper is a record that encapsulates error information in a standardized way. 66 | 67 | Fields: 68 | errorMessage: A descriptive error message. 69 | expectedStatus: The expected HTTP status if the operation succeeded. 70 | uri: The endpoint where the error occurred. 71 | occurredStatus: The actual HTTP status of the error. 72 | 73 | By using ErrorWrapper, clients always receive clear, predictable error responses. 74 | 75 | # 6. Example Flow in a Controller 76 | 77 | Success Scenario: 78 | The service returns Result.success(42L). 79 | handleResult generates a response with HTTP 200 (OK) and a JSON body containing 42. 80 | 81 | Error Scenario: 82 | The service returns Result.failure(new ParkingError.ParkingNotFoundById(id)). 83 | handleResult maps the error to an ErrorWrapper and generates a response with HTTP 400 (Bad Request) and a message like "Invalid Parking ID: {id}". 84 | -------------------------------------------------------------------------------- /Wiki/In-memory database.md: -------------------------------------------------------------------------------- 1 | # In-Memory Database Overview 2 | ## Location of Serialized Data 3 | The location for serialized data is defined in the application.properties file. 4 | Base path: /data/statistics. 5 | 6 | ## InMemoryRepository Interface 7 | A generic interface with two parameters: 8 | K: The key, which must extend Serializable. 9 | V: The value, which must extend Serializable. 10 | 11 | Methods Defined in Interface 12 | 13 | void add(K key, V value) 14 | Adds a new entry to the repository. 15 | 16 | V get(K key) 17 | Retrieves the value associated with the given key. 18 | 19 | Set fetchAllKeys() 20 | Fetches all keys stored in the repository. 21 | 22 | Set> fetchAllEntries() 23 | Fetches all key-value pairs as a set of entries. 24 | 25 | ## InMemoryRepositoryImpl Class 26 | 27 | A concrete implementation of InMemoryRepository with the following structure: 28 | Fields: 29 | 30 | protected final transient File file 31 | The file used for storing serialized data. 32 | 33 | protected Map dataMap 34 | The in-memory data map storing all key-value pairs. 35 | 36 | protected V defaultValue 37 | Default value for the repository (used when no value is found for a key). 38 | 39 | Feature: 40 | 41 | Serialization: 42 | Scheduled serialization of all dataMap values to the file: 43 | Triggered periodically. 44 | Ensures data is serialized before the program exits. 45 | Deserialization: 46 | Automatically deserializes data from the file when an instance is created. 47 | 48 | ## Creating a Custom Repository 49 | 50 | You can create a custom repository by extending InMemoryRepositoryImpl. 51 | Steps: 52 | 53 | Create a new class that extends InMemoryRepositoryImpl. 54 | Use a constructor to configure: 55 | The file path for serialized data. 56 | The map type for dataMap. 57 | The default value for entries (e.g., an empty object or null). 58 | 59 | Example 60 | 61 | @Component 62 | public class ParkingDataRepository extends InMemoryRepositoryImpl { 63 | 64 | public ParkingDataRepository(@Value("${custom}") String saveToLocationPath) { 65 | super( 66 | saveToLocationPath, // File path for serialized data 67 | new HashMap<>(), // Choose the map type for in-memory storage 68 | null // Default value for entries if there is no key in database 69 | ); 70 | } 71 | } -------------------------------------------------------------------------------- /Wiki/code-formatter.md: -------------------------------------------------------------------------------- 1 | # Configuration Instruction 2 | 3 | ## Prerequisites 4 | 5 | 1. Make sure that you have maven installed on your machine. If not, you can download it (you need BINARY zip archive) from [here](https://maven.apache.org/download.cgi). 6 | 2. Set the `MAVEN_HOME` environment variable to the path of your maven installation. You can find information about how to do it [here](https://phoenixnap.com/kb/install-maven-windows). 7 | 8 | ## GitHooks configuration 9 | 10 | 1. Execute maven command `mvn clean install` in the root directory of the project. 11 | 2. Now, when you commit your changes, the pre-commit hook will apply code formatter to automatically fix your code style. 12 | 13 | ## Code Formatter usage 14 | 15 | 1. If you want to format your code manually, you can execute maven command `mvn spotless:apply` in the root directory of the project. It will format your code according to the code style standards. 16 | 2. Now code formatter supports Java, POM XML and Markdown files. 17 | 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | container_name: spring-boot-app 6 | ports: 7 | - "8080:8080" 8 | environment: 9 | - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/${PARKINGIAPI_DB_NAME} 10 | - SPRING_DATASOURCE_USERNAME=${PARKINGIAPI_DB_USERNAME} 11 | - SPRING_DATASOURCE_PASSWORD=${PARKINGIAPI_DB_PASSWORD} 12 | depends_on: 13 | - db 14 | 15 | db: 16 | image: postgres:15.4 17 | container_name: parkingi-db 18 | environment: 19 | POSTGRES_USER: ${PARKINGIAPI_DB_USERNAME} 20 | POSTGRES_PASSWORD: ${PARKINGIAPI_DB_PASSWORD} 21 | POSTGRES_DB: ${PARKINGIAPI_DB_NAME} 22 | ports: 23 | - "5432:5432" 24 | volumes: 25 | - postgres_data:/var/lib/postgresql/data 26 | 27 | volumes: 28 | postgres_data: -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | $"MAVEN_HOME"/mvn spotless:apply 4 | 5 | for file in $(git diff --name-only --cached) 6 | do 7 | if [ -f "$file" ]; then 8 | git add "$file" 9 | fi 10 | done 11 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solvro/backend-parking-api-wrapper/617029999acaed3e013388a80d068650544fe8bd/icon.png -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 3.3.4 8 | 9 | 10 | 11 | pl.wrapper 12 | parking 13 | 0.0.1-SNAPSHOT 14 | parking 15 | Wrapper for old API 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-validation 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-webflux 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-data-jpa 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | true 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-configuration-processor 62 | true 63 | 64 | 65 | com.diffplug.spotless 66 | spotless-maven-plugin 67 | 2.41.1 68 | 69 | 70 | com.squareup.okhttp3 71 | okhttp 72 | 5.0.0-alpha.12 73 | test 74 | 75 | 76 | com.squareup.okhttp3 77 | mockwebserver 78 | 5.0.0-alpha.12 79 | test 80 | 81 | 82 | io.projectreactor 83 | reactor-test 84 | 3.7.0 85 | test 86 | 87 | 88 | org.springdoc 89 | springdoc-openapi-starter-webmvc-ui 90 | 2.5.0 91 | 92 | 93 | io.hypersistence 94 | hypersistence-utils-hibernate-63 95 | 3.8.2 96 | 97 | 98 | org.postgresql 99 | postgresql 100 | runtime 101 | 102 | 103 | com.h2database 104 | h2 105 | test 106 | 107 | 108 | jakarta.persistence 109 | jakarta.persistence-api 110 | 111 | 112 | org.hibernate.orm 113 | hibernate-core 114 | 6.6.3.Final 115 | 116 | 117 | org.springframework 118 | spring-tx 119 | 120 | 121 | 122 | 123 | 124 | 125 | org.springframework.boot 126 | spring-boot-maven-plugin 127 | 128 | 129 | 130 | com.diffplug.spotless 131 | spotless-maven-plugin 132 | 2.41.1 133 | 134 | 135 | 136 | src/main/java/**/*.java 137 | src/test/java/**/*.java 138 | 139 | 140 | 141 | 2.39.0 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | pom.xml 153 | 154 | 155 | false 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | **/*.md 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | apply 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/ParkingApplication.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class ParkingApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ParkingApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/ParkingHistoricDataService.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade; 2 | 3 | import java.time.LocalDate; 4 | import java.util.List; 5 | import org.springframework.lang.Nullable; 6 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayParkingData; 7 | import pl.wrapper.parking.facade.dto.historicData.HistoricPeriodParkingData; 8 | 9 | public interface ParkingHistoricDataService { 10 | List getDataForDay(LocalDate forDate); 11 | 12 | HistoricDayParkingData getDataForDay(LocalDate forDate, int parkingId); 13 | 14 | HistoricPeriodParkingData getDataForPeriod(LocalDate fromDate, @Nullable LocalDate toDate, int parkingId); 15 | 16 | List getDataForPeriod(LocalDate fromDate, @Nullable LocalDate toDate); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/ParkingRequestStatsService.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import pl.wrapper.parking.facade.dto.stats.request.EndpointStats; 6 | 7 | public interface ParkingRequestStatsService { 8 | Map getBasicRequestStats(); 9 | 10 | Map>> getRequestStatsForTimes(); 11 | 12 | List> getRequestPeakTimes(); 13 | 14 | Map getDailyRequestStats(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/ParkingService.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade; 2 | 3 | import java.util.List; 4 | import org.springframework.lang.Nullable; 5 | import pl.wrapper.parking.infrastructure.error.Result; 6 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 7 | 8 | public interface ParkingService { 9 | 10 | List getAllWithFreeSpots(@Nullable Boolean opened); 11 | 12 | Result getWithTheMostFreeSpots(@Nullable Boolean opened); 13 | 14 | Result getClosestParking(String address); 15 | 16 | Result getByName(String name, @Nullable Boolean opened); 17 | 18 | Result getById(Integer id, @Nullable Boolean opened); 19 | 20 | Result getBySymbol(String symbol, @Nullable Boolean opened); 21 | 22 | List getByParams( 23 | @Nullable String symbol, 24 | @Nullable Integer id, 25 | @Nullable String name, 26 | @Nullable Boolean opened, 27 | @Nullable Boolean hasFreeSpots); 28 | 29 | Object getChartForToday(Integer forId); 30 | 31 | List getAllChartsForToday(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/ParkingStatsService.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade; 2 | 3 | import java.time.DayOfWeek; 4 | import java.time.LocalTime; 5 | import java.util.List; 6 | import org.springframework.lang.Nullable; 7 | import pl.wrapper.parking.facade.dto.stats.parking.ParkingStatsResponse; 8 | import pl.wrapper.parking.facade.dto.stats.parking.daily.CollectiveDailyParkingStats; 9 | import pl.wrapper.parking.facade.dto.stats.parking.daily.DailyParkingStatsResponse; 10 | import pl.wrapper.parking.facade.dto.stats.parking.weekly.CollectiveWeeklyParkingStats; 11 | import pl.wrapper.parking.facade.dto.stats.parking.weekly.WeeklyParkingStatsResponse; 12 | 13 | public interface ParkingStatsService { 14 | List getParkingStats( 15 | @Nullable List parkingIds, @Nullable DayOfWeek dayOfWeek, LocalTime time); 16 | 17 | List getDailyParkingStats(@Nullable List parkingIds, DayOfWeek dayOfWeek); 18 | 19 | List getWeeklyParkingStats(@Nullable List parkingIds); 20 | 21 | List getCollectiveDailyParkingStats( 22 | @Nullable List parkingIds, DayOfWeek dayOfWeek); 23 | 24 | List getCollectiveWeeklyParkingStats(@Nullable List parkingIds); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/historic/HistoricDataEntry.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.historic; 2 | 3 | import io.hypersistence.utils.hibernate.type.array.IntArrayType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.NamedNativeQuery; 8 | import jakarta.persistence.NamedQueries; 9 | import jakarta.persistence.NamedQuery; 10 | import jakarta.persistence.Table; 11 | import java.time.LocalDate; 12 | import lombok.AllArgsConstructor; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | import org.hibernate.annotations.Parameter; 16 | import org.hibernate.annotations.Type; 17 | 18 | @Entity 19 | @Getter 20 | @Table(name = "historic_data") 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | @NamedQueries({ 24 | @NamedQuery( 25 | name = "HistoricData.periodQuery", 26 | query = "SELECT data FROM HistoricDataEntry data WHERE data.date >= :from AND data.date <= :to"), 27 | @NamedQuery( 28 | name = "HistoricData.fromQuery", 29 | query = "SELECT data FROM HistoricDataEntry data WHERE data.date >= :from") 30 | }) 31 | @NamedNativeQuery( 32 | name = "HistoricData.atQuery", 33 | query = "SELECT data_table FROM historic.historic_data WHERE date = :at") 34 | class HistoricDataEntry { 35 | 36 | @Id 37 | @Column(name = "date", nullable = false) 38 | private LocalDate date; 39 | 40 | @Type(value = IntArrayType.class, parameters = @Parameter(name = IntArrayType.SQL_ARRAY_TYPE, value = "smallint")) 41 | @Column(name = "data_table", columnDefinition = "smallint[][]", nullable = false) 42 | private short[][] parkingInfo; 43 | 44 | HistoricDataEntry(int parkingCount, int timeframeCount, LocalDate date) { 45 | this.date = date; 46 | this.parkingInfo = new short[parkingCount][timeframeCount]; 47 | for (int i = 0; i < parkingCount; i++) { 48 | for (int j = 0; j < timeframeCount; j++) { 49 | this.parkingInfo[i][j] = -1; 50 | } 51 | } 52 | } 53 | 54 | public void addValue(int parkingId, int forTimeframe, int freeSpots) { 55 | this.parkingInfo[parkingId - 1][forTimeframe] = (short) freeSpots; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/historic/ParkingHistoricController.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.historic; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.media.ArraySchema; 6 | import io.swagger.v3.oas.annotations.media.Content; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import jakarta.validation.constraints.Max; 11 | import jakarta.validation.constraints.Min; 12 | import java.time.LocalDate; 13 | import java.util.List; 14 | import lombok.AllArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.format.annotation.DateTimeFormat; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PathVariable; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RequestParam; 23 | import org.springframework.web.bind.annotation.RestController; 24 | import pl.wrapper.parking.facade.ParkingHistoricDataService; 25 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayParkingData; 26 | import pl.wrapper.parking.facade.dto.historicData.HistoricPeriodParkingData; 27 | 28 | @RestController 29 | @AllArgsConstructor 30 | @Slf4j 31 | @Tag( 32 | name = "Parking API Historic", 33 | description = 34 | "Endpoints for accessing historic data. Free spots with value=-1 mean that data couldn't have been fetched at that time. Missing/null days in periods mean the same.") 35 | @RequestMapping("/historic") 36 | class ParkingHistoricController { 37 | 38 | private ParkingHistoricDataService parkingHistoricDataService; 39 | 40 | @Operation( 41 | summary = "Get historic data for the given day and parking of given id", 42 | parameters = { 43 | @Parameter( 44 | name = "forDay", 45 | description = "Date in ISO date format", 46 | required = true, 47 | example = "2025-07-29") 48 | }, 49 | responses = { 50 | @ApiResponse( 51 | responseCode = "200", 52 | description = "Historic data retrieved successfully", 53 | content = 54 | @Content( 55 | mediaType = MediaType.APPLICATION_JSON_VALUE, 56 | schema = @Schema(implementation = HistoricDayParkingData.class))), 57 | @ApiResponse(responseCode = "404", description = "No data for the given parking lot for the given date") 58 | }) 59 | @GetMapping(path = "/day/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 60 | public ResponseEntity getHistoricDataForDayAndId( 61 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam("forDay") LocalDate forDay, 62 | @PathVariable(name = "id") @Min(1) @Max(5) Integer parkingId) { 63 | HistoricDayParkingData dataForDay = parkingHistoricDataService.getDataForDay(forDay, parkingId); 64 | if (dataForDay == null) return ResponseEntity.notFound().build(); 65 | return ResponseEntity.ok(dataForDay); 66 | } 67 | 68 | @Operation( 69 | summary = "Get historic data for the given day for all parking lots", 70 | parameters = { 71 | @Parameter( 72 | name = "forDay", 73 | description = "Date in ISO date format", 74 | required = true, 75 | example = "2025-07-29") 76 | }, 77 | responses = { 78 | @ApiResponse( 79 | responseCode = "200", 80 | description = "Historic data retrieved successfully", 81 | content = 82 | @Content( 83 | mediaType = MediaType.APPLICATION_JSON_VALUE, 84 | array = 85 | @ArraySchema( 86 | schema = 87 | @Schema( 88 | implementation = 89 | HistoricDayParkingData.class)))), 90 | @ApiResponse(responseCode = "404", description = "No data for the given date") 91 | }) 92 | @GetMapping(path = "/day", produces = MediaType.APPLICATION_JSON_VALUE) 93 | public ResponseEntity> getHistoricDataForDay( 94 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam("forDay") LocalDate forDay) { 95 | List dataForDay = parkingHistoricDataService.getDataForDay(forDay); 96 | if (dataForDay == null) return ResponseEntity.notFound().build(); 97 | return ResponseEntity.ok(dataForDay); 98 | } 99 | 100 | @Operation( 101 | summary = 102 | "Get all available historic data for the given period for the given id, both ends inclusive. If no end data given, get until today.", 103 | parameters = { 104 | @Parameter( 105 | name = "fromDate", 106 | description = "Date in ISO date format", 107 | required = true, 108 | example = "2025-07-29"), 109 | @Parameter(name = "toDate", description = "Date in ISO date format", example = "2025-08-29") 110 | }, 111 | responses = { 112 | @ApiResponse( 113 | responseCode = "200", 114 | description = "Historic data retrieved successfully", 115 | content = 116 | @Content( 117 | mediaType = MediaType.APPLICATION_JSON_VALUE, 118 | schema = @Schema(implementation = HistoricPeriodParkingData.class))), 119 | @ApiResponse( 120 | responseCode = "404", 121 | description = "No data for the given parking lot for the given period") 122 | }) 123 | @GetMapping(path = "/period/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 124 | public ResponseEntity getHistoricDataForPeriodAndId( 125 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam("fromDate") LocalDate fromDate, 126 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam(value = "toDate", required = false) 127 | LocalDate toDate, 128 | @PathVariable(name = "id") @Min(1) @Max(5) Integer parkingId) { 129 | HistoricPeriodParkingData dataForPeriod = 130 | parkingHistoricDataService.getDataForPeriod(fromDate, toDate, parkingId); 131 | if (dataForPeriod == null) return ResponseEntity.notFound().build(); 132 | return ResponseEntity.ok(dataForPeriod); 133 | } 134 | 135 | @Operation( 136 | summary = 137 | "Get all available historic data for the given period for all parking lots, both ends inclusive. If no end data given, get until today.", 138 | parameters = { 139 | @Parameter( 140 | name = "fromDate", 141 | description = "Date in ISO date format", 142 | required = true, 143 | example = "2025-07-29"), 144 | @Parameter(name = "toDate", description = "Date in ISO date format", example = "2025-08-29") 145 | }, 146 | responses = { 147 | @ApiResponse( 148 | responseCode = "200", 149 | description = "Historic data retrieved successfully", 150 | content = 151 | @Content( 152 | mediaType = MediaType.APPLICATION_JSON_VALUE, 153 | array = 154 | @ArraySchema( 155 | schema = 156 | @Schema( 157 | implementation = 158 | HistoricPeriodParkingData.class)))), 159 | @ApiResponse(responseCode = "404", description = "No data for the given period") 160 | }) 161 | @GetMapping(path = "/period", produces = MediaType.APPLICATION_JSON_VALUE) 162 | public ResponseEntity> getHistoricDataForPeriod( 163 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam("fromDate") LocalDate fromDate, 164 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @RequestParam(value = "toDate", required = false) 165 | LocalDate toDate) { 166 | List dataForPeriod = parkingHistoricDataService.getDataForPeriod(fromDate, toDate); 167 | if (dataForPeriod == null) return ResponseEntity.notFound().build(); 168 | return ResponseEntity.ok(dataForPeriod); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/historic/ParkingHistoricDataServiceImpl.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.historic; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.PersistenceContext; 5 | import jakarta.persistence.Query; 6 | import jakarta.persistence.TypedQuery; 7 | import java.time.LocalDate; 8 | import java.time.LocalTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.time.temporal.ChronoUnit; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.scheduling.annotation.Scheduled; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | import pl.wrapper.parking.facade.ParkingHistoricDataService; 18 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayData; 19 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayParkingData; 20 | import pl.wrapper.parking.facade.dto.historicData.HistoricPeriodParkingData; 21 | import pl.wrapper.parking.facade.dto.historicData.TimestampEntry; 22 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 23 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 24 | 25 | @Service 26 | @Transactional(readOnly = true) 27 | class ParkingHistoricDataServiceImpl implements ParkingHistoricDataService { 28 | 29 | @PersistenceContext 30 | private EntityManager em; 31 | 32 | private final int intervalLength; 33 | 34 | private final int intervalCount; 35 | 36 | private final PwrApiServerCaller pwrApiServerCaller; 37 | 38 | private final List formattedStartTimes; 39 | 40 | public ParkingHistoricDataServiceImpl( 41 | PwrApiServerCaller pwrApiServerCaller, @Value("${historic.data-update.minutes}") Integer intervalLength) { 42 | this.pwrApiServerCaller = pwrApiServerCaller; 43 | this.intervalLength = intervalLength; 44 | intervalCount = calculateTimeframesCount(intervalLength); 45 | this.formattedStartTimes = getFormattedStartTimes(intervalLength, intervalCount); 46 | } 47 | 48 | @SuppressWarnings("unchecked") 49 | @Override 50 | public List getDataForDay(LocalDate forDate) { 51 | List fetchedData = (List) createAtQuery(forDate).getResultList(); 52 | if (fetchedData.isEmpty()) return null; 53 | return parseTableForDay(fetchedData.getFirst(), forDate); 54 | } 55 | 56 | @SuppressWarnings("unchecked") 57 | @Override 58 | public HistoricDayParkingData getDataForDay(LocalDate forDate, int parkingId) { 59 | List fetchedData = (List) createAtQuery(forDate).getResultList(); 60 | if (fetchedData.isEmpty()) return null; 61 | return parseTableForDay(parkingId, fetchedData.getFirst(), forDate); 62 | } 63 | 64 | @Override 65 | public HistoricPeriodParkingData getDataForPeriod(LocalDate fromDate, LocalDate toDate, int parkingId) { 66 | List fetchedData = fetchDataForPeriod(fromDate, toDate); 67 | if (fetchedData == null) return null; 68 | return parseTableForPeriod(fetchedData, parkingId); 69 | } 70 | 71 | @Override 72 | public List getDataForPeriod(LocalDate fromDate, LocalDate toDate) { 73 | List fetchedData = fetchDataForPeriod(fromDate, toDate); 74 | if (fetchedData == null) return null; 75 | return parseTableForPeriod(fetchedData); 76 | } 77 | 78 | List fetchDataForPeriod(LocalDate fromDate, LocalDate toDate) { 79 | List fetchedData; 80 | if (toDate == null) fetchedData = createFromQuery(fromDate).getResultList(); 81 | else fetchedData = createPeriodQuery(fromDate, toDate).getResultList(); 82 | if (fetchedData.isEmpty()) return null; 83 | return fetchedData; 84 | } 85 | 86 | TypedQuery createPeriodQuery(LocalDate fromDate, LocalDate toDate) { 87 | return em.createNamedQuery("HistoricData.periodQuery", HistoricDataEntry.class) 88 | .setParameter("from", fromDate) 89 | .setParameter("to", toDate); 90 | } 91 | 92 | TypedQuery createFromQuery(LocalDate fromDate) { 93 | return em.createNamedQuery("HistoricData.fromQuery", HistoricDataEntry.class) 94 | .setParameter("from", fromDate); 95 | } 96 | 97 | Query createAtQuery(LocalDate atDate) { 98 | return em.createNamedQuery("HistoricData.atQuery").setParameter("at", atDate); 99 | } 100 | 101 | private HistoricDayParkingData parseTableForDay(int parkingId, short[][] dataTable, LocalDate forDate) { 102 | if (parkingId >= dataTable.length) return null; 103 | return new HistoricDayParkingData( 104 | (short) parkingId, new HistoricDayData(forDate, getTimestampedList(dataTable, parkingId))); 105 | } 106 | 107 | private List parseTableForDay(short[][] dataTable, LocalDate forDate) { 108 | List resultList = new ArrayList<>(dataTable.length + 1); 109 | for (int i = 0; i < dataTable.length; i++) 110 | resultList.add(new HistoricDayParkingData( 111 | (short) i, new HistoricDayData(forDate, getTimestampedList(dataTable, i)))); 112 | return resultList; 113 | } 114 | 115 | private List getTimestampedList(short[][] dataTable, int parkingId) { 116 | int bound = dataTable[parkingId].length; 117 | List entryList = new ArrayList<>(bound + 1); 118 | for (int j = 0; j < bound; j++) 119 | entryList.add(new TimestampEntry(formattedStartTimes.get(j), dataTable[parkingId][j])); 120 | return entryList; 121 | } 122 | 123 | private HistoricPeriodParkingData parseTableForPeriod(List dataEntries, int parkingId) { 124 | return new HistoricPeriodParkingData( 125 | (short) parkingId, 126 | dataEntries.stream() 127 | .map(data -> new HistoricDayData( 128 | data.getDate(), getTimestampedList(data.getParkingInfo(), parkingId))) 129 | .toList()); 130 | } 131 | 132 | private List parseTableForPeriod(List dataEntries) { 133 | int parkingCount = dataEntries.getFirst().getParkingInfo().length; 134 | List> dataLists = new ArrayList<>(parkingCount + 1); 135 | List resultList = new ArrayList<>(parkingCount + 1); 136 | for (int i = 0; i < parkingCount; i++) dataLists.add(new ArrayList<>()); 137 | 138 | dataEntries.forEach(data -> { 139 | short[][] dataTable = data.getParkingInfo(); 140 | LocalDate currentDate = data.getDate(); 141 | for (int i = 0; i < dataTable.length; i++) 142 | dataLists.get(i).add(new HistoricDayData(currentDate, getTimestampedList(dataTable, i))); 143 | }); 144 | for (int i = 0; i < parkingCount; i++) 145 | resultList.add(new HistoricPeriodParkingData((short) i, dataLists.get(i))); 146 | return resultList; 147 | } 148 | 149 | @Scheduled(cron = "30 */${historic.data-update.minutes} * * * *") 150 | @Transactional 151 | void storeNewData() { 152 | List fetchedData = pwrApiServerCaller.fetchParkingData(); 153 | LocalDate today = LocalDate.now(); 154 | HistoricDataEntry entryForToday = em.find(HistoricDataEntry.class, today); 155 | if (entryForToday == null) { 156 | entryForToday = new HistoricDataEntry(fetchedData.size(), intervalCount, today); 157 | em.persist(entryForToday); 158 | } 159 | int currentIntervalIndex = mapTimeToTimeframeIndex(LocalTime.now(), intervalLength); 160 | for (ParkingResponse parkingData : fetchedData) { 161 | entryForToday.addValue(parkingData.parkingId(), currentIntervalIndex, parkingData.freeSpots()); 162 | } 163 | } 164 | 165 | private static int calculateTimeframesCount(int timeframeLengthInMinutes) { 166 | return (int) Math.ceil((double) 24 * 60 / timeframeLengthInMinutes); 167 | } 168 | 169 | private static int mapTimeToTimeframeIndex(LocalTime time, int intervalLength) { 170 | return (int) ChronoUnit.MINUTES.between(LocalTime.MIDNIGHT, time) / intervalLength; 171 | } 172 | 173 | private static List getFormattedStartTimes(int intervalLength, int intervalCount) { 174 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); 175 | LocalTime currentTime = LocalTime.MIDNIGHT; 176 | List formattedStartTimes = new ArrayList<>(); 177 | for (int i = 0; i < intervalCount; i++) { 178 | formattedStartTimes.add(currentTime.format(formatter)); 179 | currentTime = currentTime.plusMinutes(intervalLength); 180 | } 181 | return formattedStartTimes; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/main/ParkingController.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.main; 2 | 3 | import static pl.wrapper.parking.infrastructure.error.HandleResult.handleResult; 4 | 5 | import io.swagger.v3.oas.annotations.Operation; 6 | import io.swagger.v3.oas.annotations.Parameter; 7 | import io.swagger.v3.oas.annotations.media.ArraySchema; 8 | import io.swagger.v3.oas.annotations.media.Content; 9 | import io.swagger.v3.oas.annotations.media.Schema; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 11 | import io.swagger.v3.oas.annotations.tags.Tag; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.validation.constraints.Max; 14 | import jakarta.validation.constraints.Min; 15 | import java.util.List; 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.RequestParam; 24 | import org.springframework.web.bind.annotation.RestController; 25 | import pl.wrapper.parking.facade.ParkingService; 26 | import pl.wrapper.parking.infrastructure.error.ErrorWrapper; 27 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 28 | 29 | @RestController 30 | @RequiredArgsConstructor 31 | @Slf4j 32 | @Tag( 33 | name = "Parking API Main", 34 | description = "Endpoints for managing parking-related operations with up-to-date information") 35 | class ParkingController { 36 | private final ParkingService parkingService; 37 | 38 | @Operation(summary = "Get list of parking lots with free spots from all/opened/closed.") 39 | @ApiResponse( 40 | responseCode = "200", 41 | description = "list of parking lots", 42 | content = 43 | @Content( 44 | mediaType = "application/json", 45 | array = @ArraySchema(schema = @Schema(implementation = ParkingResponse.class)))) 46 | @GetMapping(path = "/free", produces = MediaType.APPLICATION_JSON_VALUE) 47 | public ResponseEntity> getAllParkingWithFreeSpots( 48 | @Parameter(description = "search in opened parking lots") @RequestParam(required = false) Boolean opened) { 49 | log.info("Finding all parking with free spots"); 50 | return new ResponseEntity<>(parkingService.getAllWithFreeSpots(opened), HttpStatus.OK); 51 | } 52 | 53 | @Operation(summary = "Get parking lot with the most free spots from all/opened/closed parking lots.") 54 | @ApiResponse( 55 | responseCode = "200", 56 | description = "parking found", 57 | content = 58 | @Content(mediaType = "application/json", schema = @Schema(implementation = ParkingResponse.class))) 59 | @ApiResponse( 60 | responseCode = "404", 61 | description = "parking not found", 62 | content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorWrapper.class))) 63 | @GetMapping(path = "/free/top", produces = MediaType.APPLICATION_JSON_VALUE) 64 | public ResponseEntity getParkingWithTheMostFreeSpots( 65 | @Parameter(description = "search in opened parking lots") @RequestParam(required = false) Boolean opened, 66 | HttpServletRequest request) { 67 | log.info("Finding parking with the most free spots"); 68 | return handleResult(parkingService.getWithTheMostFreeSpots(opened), HttpStatus.OK, request.getRequestURI()); 69 | } 70 | 71 | @Operation( 72 | summary = "Find the closest parking by given address.", 73 | parameters = 74 | @Parameter( 75 | name = "address", 76 | description = "The address to find the closest parking for", 77 | required = true, 78 | example = "Flower 20, 50-337 Wroclaw"), 79 | responses = { 80 | @ApiResponse( 81 | responseCode = "200", 82 | description = "Closest parking found", 83 | content = 84 | @Content( 85 | mediaType = MediaType.APPLICATION_JSON_VALUE, 86 | schema = @Schema(implementation = ParkingResponse.class))), 87 | @ApiResponse( 88 | responseCode = "404", 89 | description = "No parking found for the given address", 90 | content = 91 | @Content( 92 | mediaType = MediaType.APPLICATION_JSON_VALUE, 93 | schema = @Schema(implementation = ErrorWrapper.class))) 94 | }) 95 | @GetMapping(path = "/address", produces = MediaType.APPLICATION_JSON_VALUE) 96 | public ResponseEntity getClosestParking( 97 | @RequestParam("address") String address, HttpServletRequest request) { 98 | log.info("Finding closest parking for address: {}", address); 99 | return handleResult(parkingService.getClosestParking(address), HttpStatus.OK, request.getRequestURI()); 100 | } 101 | 102 | @Operation(summary = "Fetch a parking lot by name.") 103 | @ApiResponse( 104 | responseCode = "200", 105 | description = "parking found", 106 | content = 107 | @Content(mediaType = "application/json", schema = @Schema(implementation = ParkingResponse.class))) 108 | @ApiResponse( 109 | responseCode = "404", 110 | description = "parking not found", 111 | content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorWrapper.class))) 112 | @GetMapping(path = "/name", produces = MediaType.APPLICATION_JSON_VALUE) 113 | public ResponseEntity getParkingByName( 114 | @Parameter(description = "parking name") @RequestParam String name, 115 | @Parameter(description = "is parking opened") @RequestParam(required = false) Boolean opened, 116 | HttpServletRequest request) { 117 | log.info("Received request: get parking by name: {}", name); 118 | return handleResult(parkingService.getByName(name, opened), HttpStatus.OK, request.getRequestURI()); 119 | } 120 | 121 | @Operation(summary = "Fetch a parking lot by id.") 122 | @ApiResponse( 123 | responseCode = "200", 124 | description = "parking found", 125 | content = 126 | @Content(mediaType = "application/json", schema = @Schema(implementation = ParkingResponse.class))) 127 | @ApiResponse( 128 | responseCode = "404", 129 | description = "parking not found", 130 | content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorWrapper.class))) 131 | @GetMapping(path = "/id", produces = MediaType.APPLICATION_JSON_VALUE) 132 | public ResponseEntity getParkingById( 133 | @Parameter(description = "parking id") @RequestParam Integer id, 134 | @Parameter(description = "is parking opened") @RequestParam(required = false) Boolean opened, 135 | HttpServletRequest request) { 136 | log.info("Received request: get parking by id: {}", id); 137 | return handleResult(parkingService.getById(id, opened), HttpStatus.OK, request.getRequestURI()); 138 | } 139 | 140 | @Operation(summary = "Fetch a parking lot by symbol.") 141 | @ApiResponse( 142 | responseCode = "200", 143 | description = "parking found", 144 | content = 145 | @Content(mediaType = "application/json", schema = @Schema(implementation = ParkingResponse.class))) 146 | @ApiResponse( 147 | responseCode = "404", 148 | description = "parking not found", 149 | content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorWrapper.class))) 150 | @GetMapping(path = "/symbol", produces = MediaType.APPLICATION_JSON_VALUE) 151 | public ResponseEntity getParkingBySymbol( 152 | @Parameter(description = "parking symbol") @RequestParam String symbol, 153 | @Parameter(description = "is parking opened") @RequestParam(required = false) Boolean opened, 154 | HttpServletRequest request) { 155 | log.info("Received request: get parking by symbol: {}", symbol); 156 | return handleResult(parkingService.getBySymbol(symbol, opened), HttpStatus.OK, request.getRequestURI()); 157 | } 158 | 159 | @Operation(summary = "Get list of parking lots by name/id/symbol/if opened/has free spots") 160 | @ApiResponse( 161 | responseCode = "200", 162 | description = "list with parking lots", 163 | content = 164 | @Content( 165 | mediaType = "application/json", 166 | array = @ArraySchema(schema = @Schema(implementation = ParkingResponse.class)))) 167 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 168 | public ResponseEntity> getParkingByParams( 169 | @Parameter(description = "parking symbol") @RequestParam(required = false) String symbol, 170 | @Parameter(description = "parking id") @RequestParam(required = false) Integer id, 171 | @Parameter(description = "parking name") @RequestParam(required = false) String name, 172 | @Parameter(description = "is parking opened") @RequestParam(required = false) Boolean opened, 173 | @Parameter(description = "if parking has free spots") @RequestParam(required = false) Boolean freeSpots) { 174 | log.info( 175 | "Received request: get parking by symbol: {}, id: {}, name: {} and hasFreeSpots: {}", 176 | symbol, 177 | id, 178 | name, 179 | freeSpots); 180 | return new ResponseEntity<>(parkingService.getByParams(symbol, id, name, opened, freeSpots), HttpStatus.OK); 181 | } 182 | 183 | @Operation(summary = "Fetch the chart for today for parking lot of given Id.") 184 | @ApiResponse( 185 | responseCode = "200", 186 | description = "Chart fetched", 187 | content = @Content(mediaType = "application/json")) 188 | @GetMapping(path = "/chart/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 189 | public ResponseEntity getChartForToday(@PathVariable("id") @Min(1) @Max(5) Integer id) { 190 | return ResponseEntity.ok(parkingService.getChartForToday(id)); 191 | } 192 | 193 | @Operation(summary = "Fetch the chart for today for parking lot of given Id.") 194 | @ApiResponse( 195 | responseCode = "200", 196 | description = "Chart fetched", 197 | content = @Content(mediaType = "application/json")) 198 | @GetMapping(path = "/chart", produces = MediaType.APPLICATION_JSON_VALUE) 199 | public ResponseEntity> getAllChartsForToday() { 200 | return ResponseEntity.ok(parkingService.getAllChartsForToday()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/main/ParkingServiceImpl.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.main; 2 | 3 | import java.util.Comparator; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import java.util.function.Predicate; 8 | import java.util.stream.Stream; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.lang.Nullable; 11 | import org.springframework.stereotype.Service; 12 | import pl.wrapper.parking.facade.ParkingService; 13 | import pl.wrapper.parking.facade.dto.main.NominatimLocation; 14 | import pl.wrapper.parking.infrastructure.error.ParkingError; 15 | import pl.wrapper.parking.infrastructure.error.Result; 16 | import pl.wrapper.parking.infrastructure.nominatim.client.NominatimClient; 17 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 18 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 19 | 20 | @Service 21 | @Slf4j 22 | record ParkingServiceImpl(PwrApiServerCaller pwrApiServerCaller, NominatimClient nominatimClient) 23 | implements ParkingService { 24 | 25 | @Override 26 | public List getAllWithFreeSpots(@Nullable Boolean opened) { 27 | Predicate predicate = generatePredicateForParams(null, null, null, opened, true); 28 | return getStreamOfFilteredFetchedParkingLots(predicate).toList(); 29 | } 30 | 31 | @Override 32 | public Result getWithTheMostFreeSpots(@Nullable Boolean opened) { 33 | Predicate predicate = generatePredicateForParams(null, null, null, opened, null); 34 | return getStreamOfFilteredFetchedParkingLots(predicate) 35 | .max(Comparator.comparingInt(ParkingResponse::freeSpots)) 36 | .map(this::handleFoundParking) 37 | .orElse(Result.failure(new ParkingError.NoFreeParkingSpotsAvailable())); 38 | } 39 | 40 | @Override 41 | public Result getClosestParking(String address) { 42 | Optional geoLocation = 43 | nominatimClient.search(address, "json").next().blockOptional(); 44 | return geoLocation 45 | .map(location -> findClosestParking(location, pwrApiServerCaller.fetchParkingData()) 46 | .map(Result::success) 47 | .orElse(Result.failure(new ParkingError.ParkingNotFoundByAddress(address)))) 48 | .orElseGet(() -> { 49 | log.info("No geocoding results for address: {}", address); 50 | return Result.failure(new ParkingError.ParkingNotFoundByAddress(address)); 51 | }); 52 | } 53 | 54 | @Override 55 | public Result getByName(String name, @Nullable Boolean opened) { 56 | Predicate predicate = generatePredicateForParams(null, null, name, opened, null); 57 | return findParking(predicate) 58 | .map(this::handleFoundParking) 59 | .orElse(Result.failure(new ParkingError.ParkingNotFoundByName(name))); 60 | } 61 | 62 | @Override 63 | public Result getById(Integer id, @Nullable Boolean opened) { 64 | Predicate predicate = generatePredicateForParams(null, id, null, opened, null); 65 | return findParking(predicate) 66 | .map(this::handleFoundParking) 67 | .orElse(Result.failure(new ParkingError.ParkingNotFoundById(id))); 68 | } 69 | 70 | @Override 71 | public Result getBySymbol(String symbol, @Nullable Boolean opened) { 72 | Predicate predicate = generatePredicateForParams(symbol, null, null, opened, null); 73 | return findParking(predicate) 74 | .map(this::handleFoundParking) 75 | .orElse(Result.failure(new ParkingError.ParkingNotFoundBySymbol(symbol))); 76 | } 77 | 78 | @Override 79 | public List getByParams( 80 | @Nullable String symbol, 81 | @Nullable Integer id, 82 | @Nullable String name, 83 | @Nullable Boolean opened, 84 | @Nullable Boolean hasFreeSpots) { 85 | Predicate predicate = generatePredicateForParams(symbol, id, name, opened, hasFreeSpots); 86 | return getStreamOfFilteredFetchedParkingLots(predicate).toList(); 87 | } 88 | 89 | @Override 90 | public Object getChartForToday(Integer forId) { 91 | return pwrApiServerCaller.getAllCharsForToday().get(forId - 1); 92 | } 93 | 94 | @Override 95 | public List getAllChartsForToday() { 96 | return pwrApiServerCaller.getAllCharsForToday(); 97 | } 98 | 99 | private Stream getStreamOfFilteredFetchedParkingLots( 100 | Predicate filteringPredicate) { 101 | return pwrApiServerCaller.fetchParkingData().stream().filter(filteringPredicate); 102 | } 103 | 104 | private Optional findParking(Predicate predicate) { 105 | return getStreamOfFilteredFetchedParkingLots(predicate).findFirst(); 106 | } 107 | 108 | private Optional findClosestParking( 109 | NominatimLocation location, List parkingLots) { 110 | double lat = location.latitude(); 111 | double lon = location.longitude(); 112 | 113 | return parkingLots.stream() 114 | .min(Comparator.comparingDouble(parking -> haversineDistance( 115 | lat, 116 | lon, 117 | parking.address().geoLatitude(), 118 | parking.address().geoLongitude()))); 119 | } 120 | 121 | private static double haversineDistance(double lat1, double lon1, double lat2, double lon2) { 122 | final int EARTH_RADIUS = 6371; 123 | 124 | double havLat = (1 - Math.cos(Math.toRadians(lat2 - lat1))) / 2; 125 | double havLon = (1 - Math.cos(Math.toRadians(lon2 - lon1))) / 2; 126 | double haversine = havLat + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * havLon; 127 | 128 | return 2 * EARTH_RADIUS * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); 129 | } 130 | 131 | private Result handleFoundParking(ParkingResponse found) { 132 | return Result.success(found); 133 | } 134 | 135 | private Predicate generatePredicateForParams( 136 | String symbol, Integer id, String name, Boolean isOpened, Boolean hasFreeSpots) { 137 | Predicate predicate = parking -> true; 138 | if (symbol != null) 139 | predicate = predicate.and( 140 | parking -> symbol.toLowerCase().contains(parking.symbol().toLowerCase())); 141 | if (id != null) predicate = predicate.and(parking -> Objects.equals(id, parking.parkingId())); 142 | if (name != null) 143 | predicate = predicate.and( 144 | parking -> name.toLowerCase().contains(parking.name().toLowerCase())); 145 | if (isOpened != null) predicate = predicate.and(parking -> Objects.equals(isOpened, parking.isOpened())); 146 | if (hasFreeSpots != null) predicate = predicate.and(parking -> hasFreeSpots == (parking.freeSpots() > 0)); 147 | 148 | return predicate; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/stats/request/ParkingRequestStatsController.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.stats.request; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.media.ArraySchema; 5 | import io.swagger.v3.oas.annotations.media.Content; 6 | import io.swagger.v3.oas.annotations.media.ExampleObject; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import java.util.List; 11 | import java.util.Map; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | import pl.wrapper.parking.facade.ParkingRequestStatsService; 21 | import pl.wrapper.parking.facade.dto.stats.request.EndpointStats; 22 | 23 | @RestController 24 | @RequiredArgsConstructor 25 | @Slf4j 26 | @Tag(name = "Parking API Request Stats", description = "Endpoints for request statistics") 27 | @RequestMapping("/stats/requests") 28 | class ParkingRequestStatsController { 29 | private final ParkingRequestStatsService parkingRequestStatsService; 30 | 31 | @Operation( 32 | summary = "get basic request statistics", 33 | description = "Returns stats as request count, successful request count, success rate for each endpoint") 34 | @ApiResponse( 35 | responseCode = "200", 36 | description = "map of basic statistics, key - endpoint name, value - endpoint stats", 37 | content = 38 | @Content( 39 | mediaType = "application/json", 40 | schema = @Schema(implementation = Map.class), 41 | examples = 42 | @ExampleObject( 43 | name = "example", 44 | description = "key: String -> value: EndpointStats", 45 | value = 46 | "{ parkings/free: { totalRequests: 3, successfulRequests: 2, successRate: 0.67 }, parkings: { totalRequests: 5, successfulRequests: 3, successRate: 0.6 } }"))) 47 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 48 | public ResponseEntity> getBasicRequestStats() { 49 | log.info("Fetching parking requests stats"); 50 | Map result = parkingRequestStatsService.getBasicRequestStats(); 51 | return new ResponseEntity<>(result, HttpStatus.OK); 52 | } 53 | 54 | @Operation( 55 | summary = "get request statistics in timeframes", 56 | description = "Returns the list of pairs(timeframe, average number of requests) for each endpoint") 57 | @ApiResponse( 58 | responseCode = "200", 59 | description = 60 | "map of statistics in timeframes, key - endpoint name, value - list of timeframes with average number of requests", 61 | content = 62 | @Content( 63 | mediaType = "application/json", 64 | schema = @Schema(implementation = Map.class), 65 | examples = 66 | @ExampleObject( 67 | name = "example", 68 | description = "key: String -> value: Array[{String, Double}]", 69 | value = 70 | "{ parkings/free: [{ 00:00 - 00:30: 0.4 }, {00:30 - 01:00: 0.32 }, ...], parkings: [{ 00:00 - 00:30: 0.31 }, {00:30 - 01:00: 0.26 }, ...] }"))) 71 | @GetMapping(path = "/times", produces = MediaType.APPLICATION_JSON_VALUE) 72 | public ResponseEntity>>> getRequestStatsForTimes() { 73 | log.info("Fetching parking requests stats for times"); 74 | Map>> result = parkingRequestStatsService.getRequestStatsForTimes(); 75 | return new ResponseEntity<>(result, HttpStatus.OK); 76 | } 77 | 78 | @Operation( 79 | summary = "get request peak times", 80 | description = "Returns the top 3 peak times based on average number of requests in a timeframe") 81 | @ApiResponse( 82 | responseCode = "200", 83 | description = "list of 3 peak times with average number of requests", 84 | content = 85 | @Content( 86 | mediaType = "application/json", 87 | array = @ArraySchema(schema = @Schema(implementation = Map.Entry.class)), 88 | examples = 89 | @ExampleObject( 90 | name = "example", 91 | description = "Array[{String, Double}]", 92 | value = 93 | "[{ 13:00 - 13:30: 23.0 }, { 18:30 - 19:00: 21.2 }, { 9:00 - 9:30: 18.4 }]"))) 94 | @GetMapping(path = "/peak-times", produces = MediaType.APPLICATION_JSON_VALUE) 95 | public ResponseEntity>> getRequestPeakTimes() { 96 | log.info("Fetching peak request times"); 97 | List> result = parkingRequestStatsService.getRequestPeakTimes(); 98 | return new ResponseEntity<>(result, HttpStatus.OK); 99 | } 100 | 101 | @Operation( 102 | summary = "get daily request statistics", 103 | description = "Returns the average number of requests per day for each endpoint") 104 | @ApiResponse( 105 | responseCode = "200", 106 | description = "map of daily statistics, key - endpoint name, value - average number of requests per day", 107 | content = 108 | @Content( 109 | mediaType = "application/json", 110 | schema = @Schema(implementation = Map.class), 111 | examples = 112 | @ExampleObject( 113 | name = "example", 114 | description = "key: String -> value: Double", 115 | value = "{ parkings/free: 2.8, parkings/free/top: 2.5, parkings: 4.0 "))) 116 | @GetMapping(path = "/day", produces = MediaType.APPLICATION_JSON_VALUE) 117 | public ResponseEntity> getDailyRequestStats() { 118 | log.info("Fetching parking requests stats daily"); 119 | Map result = parkingRequestStatsService.getDailyRequestStats(); 120 | return new ResponseEntity<>(result, HttpStatus.OK); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/domain/stats/request/ParkingRequestStatsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.stats.request; 2 | 3 | import java.time.LocalTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | import org.springframework.stereotype.Service; 8 | import pl.wrapper.parking.facade.ParkingRequestStatsService; 9 | import pl.wrapper.parking.facade.dto.stats.request.EndpointStats; 10 | import pl.wrapper.parking.infrastructure.inMemory.ParkingRequestRepository; 11 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.EndpointData; 12 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.TimeframeStatistic; 13 | 14 | @Service 15 | class ParkingRequestStatsServiceImpl implements ParkingRequestStatsService { 16 | 17 | private final ParkingRequestRepository requestRepository; 18 | 19 | private final List formattedTimeframes; 20 | 21 | public ParkingRequestStatsServiceImpl(ParkingRequestRepository requestRepository) { 22 | this.requestRepository = requestRepository; 23 | this.formattedTimeframes = getFormattedTimeframes( 24 | requestRepository.getTotalEndpoint().getTimeframeLength(), 25 | requestRepository.getTotalEndpoint().getTimeframeStatistics().length); 26 | } 27 | 28 | @Override 29 | public Map getBasicRequestStats() { 30 | return requestRepository.fetchAllEntries().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> { 31 | EndpointData endpointData = entry.getValue(); 32 | return new EndpointStats( 33 | endpointData.getRequestCount(), endpointData.getSuccessCount(), endpointData.getSuccessRate()); 34 | })); 35 | } 36 | 37 | @Override 38 | public Map>> getRequestStatsForTimes() { 39 | return requestRepository.fetchAllEntries().stream() 40 | .collect(Collectors.toMap(Map.Entry::getKey, entry -> getTimeframesWithAverage(entry.getValue()))); 41 | } 42 | 43 | @Override 44 | public List> getRequestPeakTimes() { 45 | EndpointData totalEndpoint = requestRepository.getTotalEndpoint(); 46 | List> timeframesWithAverage = getTimeframesWithAverage(totalEndpoint); 47 | 48 | return timeframesWithAverage.stream() 49 | .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) 50 | .limit(3) 51 | .toList(); 52 | } 53 | 54 | @Override 55 | public Map getDailyRequestStats() { 56 | return requestRepository.fetchAllEntries().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> { 57 | EndpointData endpointData = entry.getValue(); 58 | double sumOfAverages = 0.0; 59 | for (TimeframeStatistic timeframeStatistic : endpointData.getTimeframeStatistics()) { 60 | sumOfAverages += timeframeStatistic.getAverageNumberOfRequests(); 61 | } 62 | return sumOfAverages; 63 | })); 64 | } 65 | 66 | private List> getTimeframesWithAverage(EndpointData endpointData) { 67 | List> averages = new ArrayList<>(); 68 | for (int i = 0; i < endpointData.getTimeframeStatistics().length; ++i) { 69 | Double average = endpointData.getTimeframeStatistics()[i].getAverageNumberOfRequests(); 70 | averages.add(Map.entry(formattedTimeframes.get(i), average)); 71 | } 72 | return averages; 73 | } 74 | 75 | private List getFormattedTimeframes(int timeframeLength, int maxTimeframesCount) { 76 | List timeframes = new ArrayList<>(); 77 | for (int i = 0; i < maxTimeframesCount; ++i) { 78 | LocalTime start = LocalTime.MIDNIGHT.plusMinutes((long) i * timeframeLength); 79 | LocalTime end = start.plusMinutes(timeframeLength); 80 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); 81 | timeframes.add(formatter.format(start) + " - " + formatter.format(end)); 82 | } 83 | return timeframes; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/historicData/HistoricDayData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.historicData; 2 | 3 | import io.swagger.v3.oas.annotations.media.ArraySchema; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import java.time.LocalDate; 6 | import java.util.List; 7 | 8 | public record HistoricDayData( 9 | @Schema(type = "string", format = "date", example = "2025-08-23") LocalDate atDate, 10 | @ArraySchema(schema = @Schema(implementation = TimestampEntry.class)) List data) {} 11 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/historicData/HistoricDayParkingData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.historicData; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record HistoricDayParkingData( 6 | short parkingId, @Schema(implementation = HistoricDayData.class) HistoricDayData data) {} 7 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/historicData/HistoricPeriodParkingData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.historicData; 2 | 3 | import io.swagger.v3.oas.annotations.media.ArraySchema; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import java.util.List; 6 | 7 | public record HistoricPeriodParkingData( 8 | short parkingId, 9 | @ArraySchema(schema = @Schema(implementation = HistoricDayData.class)) List dataList) {} 10 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/historicData/TimestampEntry.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.historicData; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record TimestampEntry( 6 | @Schema(type = "string", format = "time", example = "12:45") String timestamp, short freeSpots) {} 7 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/main/NominatimLocation.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.main; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record NominatimLocation(@JsonProperty("lat") double latitude, @JsonProperty("lon") double longitude) {} 6 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/ParkingStatsResponse.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingInfo; 5 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingStats; 6 | 7 | public record ParkingStatsResponse( 8 | @Schema(implementation = ParkingInfo.class) ParkingInfo parkingInfo, 9 | @Schema(implementation = ParkingStats.class) ParkingStats stats) {} 10 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/basis/OccupancyInfo.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.basis; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import java.time.DayOfWeek; 5 | import java.time.LocalTime; 6 | 7 | public record OccupancyInfo( 8 | @Schema(examples = "WEDNESDAY") DayOfWeek dayOfWeek, 9 | @Schema(type = "string", format = "time", example = "15:30:00") LocalTime time) {} 10 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/basis/ParkingInfo.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.basis; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Builder; 5 | 6 | @Builder 7 | public record ParkingInfo(@Schema(example = "3") int parkingId, @Schema(example = "54") int totalSpots) {} 8 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/basis/ParkingStats.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.basis; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Builder; 5 | 6 | @Builder 7 | public record ParkingStats( 8 | @Schema(example = "0.723") double averageAvailability, @Schema(example = "37") int averageFreeSpots) {} 9 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/daily/CollectiveDailyParkingStats.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.daily; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import java.time.LocalTime; 5 | import java.util.Map; 6 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingInfo; 7 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingStats; 8 | 9 | public record CollectiveDailyParkingStats( 10 | @Schema(implementation = ParkingInfo.class) ParkingInfo parkingInfo, Map statsMap) {} 11 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/daily/DailyParkingStatsResponse.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.daily; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import java.time.LocalTime; 5 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingInfo; 6 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingStats; 7 | 8 | public record DailyParkingStatsResponse( 9 | @Schema(implementation = ParkingInfo.class) ParkingInfo parkingInfo, 10 | @Schema(implementation = ParkingStats.class) ParkingStats stats, 11 | @Schema(type = "string", format = "time", example = "15:30:00") LocalTime maxOccupancyAt, 12 | @Schema(type = "string", format = "time", example = "02:30:00") LocalTime minOccupancyAt) {} 13 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/weekly/CollectiveWeeklyParkingStats.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.weekly; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import java.time.DayOfWeek; 5 | import java.time.LocalTime; 6 | import java.util.Map; 7 | import org.springframework.http.MediaType; 8 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingInfo; 9 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingStats; 10 | 11 | public record CollectiveWeeklyParkingStats( 12 | @Schema(implementation = ParkingInfo.class) ParkingInfo parkingInfo, 13 | @Schema( 14 | contentMediaType = MediaType.APPLICATION_JSON_VALUE, 15 | example = "{\"MONDAY\": " 16 | + "{\"08:00:00\": {\"averageAvailability\": 0.723, \"averageFreeSpots\": 37}," 17 | + "\"08:10:00\": {\"averageAvailability\": 0.69, \"averageFreeSpots\": 33}}," 18 | + "\"TUESDAY\": " 19 | + "{\"09:30:00\": {\"averageAvailability\": 0.431, \"averageFreeSpots\": 23}," 20 | + "\"09:40:00\": {\"averageAvailability\": 0.411, \"averageFreeSpots\": 21}}}") 21 | Map> statsMap) {} 22 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/parking/weekly/WeeklyParkingStatsResponse.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.parking.weekly; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import pl.wrapper.parking.facade.dto.stats.parking.basis.OccupancyInfo; 5 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingInfo; 6 | import pl.wrapper.parking.facade.dto.stats.parking.basis.ParkingStats; 7 | 8 | public record WeeklyParkingStatsResponse( 9 | @Schema(implementation = ParkingInfo.class) ParkingInfo parkingInfo, 10 | @Schema(implementation = ParkingStats.class) ParkingStats stats, 11 | @Schema(implementation = OccupancyInfo.class) OccupancyInfo maxOccupancyInfo, 12 | @Schema(implementation = OccupancyInfo.class) OccupancyInfo minOccupancyInfo) {} 13 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/facade/dto/stats/request/EndpointStats.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.dto.stats.request; 2 | 3 | public record EndpointStats(long totalRequests, long successfulRequests, double successRate) {} 4 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/configuration/CorsConfiguration.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.configuration; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | @RequiredArgsConstructor 12 | class CorsConfiguration { 13 | @Bean 14 | public WebMvcConfigurer corsConfigurer() { 15 | return new WebMvcConfigurer() { 16 | @Override 17 | public void addCorsMappings(@NonNull CorsRegistry registry) { 18 | registry.addMapping("/**") 19 | .allowedOrigins("*") 20 | .allowedHeaders("*") 21 | .allowedMethods("GET"); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/configuration/DbConfiguration.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.configuration; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.transaction.annotation.EnableTransactionManagement; 5 | 6 | @Configuration 7 | @EnableTransactionManagement 8 | public class DbConfiguration {} 9 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/configuration/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.configuration; 2 | 3 | import io.swagger.v3.oas.models.OpenAPI; 4 | import io.swagger.v3.oas.models.info.Info; 5 | import io.swagger.v3.oas.models.servers.Server; 6 | import java.util.List; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | class SwaggerConfig { 12 | @Bean 13 | public OpenAPI customOpenAPI() { 14 | Info info = new Info(); 15 | info.title("Parking API"); 16 | info.description("Wrapper for old PWr API for parking lots"); 17 | return new OpenAPI() 18 | .servers(List.of( 19 | new Server() 20 | .url("https://parking-api.topwr.solvro.pl/parkingiAPI") 21 | .description("HTTPS server version"), 22 | new Server().url("http://localhost:8080/parkingiAPI").description("HTTP server version"))) 23 | .info(info); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/error/Error.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.error; 2 | 3 | public sealed interface Error permits ParkingError {} 4 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/error/ErrorWrapper.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.error; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import org.springframework.http.HttpStatus; 5 | 6 | public record ErrorWrapper( 7 | @Schema(example = "An error has occurred") String errorMessage, 8 | @Schema(example = "INTERNAL_SERVER_ERROR") HttpStatus expectedStatus, 9 | @Schema(example = "/v1/parkings/name") String uri, 10 | @Schema(example = "INTERNAL_SERVER_ERROR") HttpStatus occurredStatus) {} 11 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/error/HandleResult.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.error; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.ObjectWriter; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | import java.text.SimpleDateFormat; 8 | import lombok.SneakyThrows; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | 12 | public class HandleResult { 13 | private static final ObjectWriter ow = new ObjectMapper() 14 | .enable(SerializationFeature.INDENT_OUTPUT) 15 | .registerModule(new JavaTimeModule()) 16 | .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) 17 | .writerWithDefaultPrettyPrinter(); 18 | 19 | @SneakyThrows 20 | public static ResponseEntity handleResult(Result toHandle, HttpStatus onSuccess, String uri) { 21 | if (toHandle.isSuccess()) return new ResponseEntity<>(ow.writeValueAsString(toHandle.getData()), onSuccess); 22 | Error error = toHandle.getError(); 23 | ErrorWrapper errorWrapper = getInfoByError(error, uri, onSuccess); 24 | return new ResponseEntity<>(ow.writeValueAsString(errorWrapper), errorWrapper.occurredStatus()); 25 | } 26 | 27 | private static ErrorWrapper getInfoByError(Error error, String uri, HttpStatus onSuccess) { 28 | return switch (error) { 29 | case ParkingError.ParkingNotFoundBySymbol e -> new ErrorWrapper( 30 | "Parking of symbol: " + e.symbol() + " not found", onSuccess, uri, HttpStatus.NOT_FOUND); 31 | case ParkingError.ParkingNotFoundById e -> new ErrorWrapper( 32 | "Parking of id: " + e.id() + " not found", onSuccess, uri, HttpStatus.NOT_FOUND); 33 | case ParkingError.ParkingNotFoundByName e -> new ErrorWrapper( 34 | "Parking of name: " + e.name() + " not found", onSuccess, uri, HttpStatus.NOT_FOUND); 35 | case ParkingError.ParkingNotFoundByAddress e -> new ErrorWrapper( 36 | "Parking of address: " + e.address() + " not found", onSuccess, uri, HttpStatus.NOT_FOUND); 37 | case ParkingError.NoFreeParkingSpotsAvailable ignored -> new ErrorWrapper( 38 | "No free parking spots available", onSuccess, uri, HttpStatus.NOT_FOUND); 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/error/ParkingError.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.error; 2 | 3 | public sealed interface ParkingError extends Error { 4 | // remember to add newly error to connected to its controller 5 | record ParkingNotFoundBySymbol(String symbol) implements ParkingError {} 6 | 7 | record ParkingNotFoundById(Integer id) implements ParkingError {} 8 | 9 | record ParkingNotFoundByName(String name) implements ParkingError {} 10 | 11 | record ParkingNotFoundByAddress(String address) implements ParkingError {} 12 | 13 | record NoFreeParkingSpotsAvailable() implements ParkingError {} 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/error/Result.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.error; 2 | 3 | import pl.wrapper.parking.infrastructure.exception.InvalidCallException; 4 | 5 | public interface Result { 6 | static Result success(T data) { 7 | return new Success<>(data); 8 | } 9 | 10 | static Failure failure(Error error) { 11 | return new Failure<>(error); 12 | } 13 | 14 | boolean isSuccess(); 15 | 16 | T getData(); 17 | 18 | Error getError(); 19 | 20 | record Success(T data) implements Result { 21 | @Override 22 | public boolean isSuccess() { 23 | return true; 24 | } 25 | 26 | @Override 27 | public T getData() { 28 | return data; 29 | } 30 | 31 | @Override 32 | public Error getError() { 33 | throw new InvalidCallException("Call getError() on Success"); 34 | } 35 | } 36 | 37 | record Failure(Error error) implements Result { 38 | @Override 39 | public boolean isSuccess() { 40 | return false; 41 | } 42 | 43 | @Override 44 | public T getData() { 45 | throw new InvalidCallException("Call getData() on Failure"); 46 | } 47 | 48 | @Override 49 | public Error getError() { 50 | return error; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.exception; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import java.net.ConnectException; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.core.serializer.support.SerializationFailedException; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.MissingServletRequestParameterException; 12 | import org.springframework.web.bind.annotation.ControllerAdvice; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.method.annotation.HandlerMethodValidationException; 15 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 16 | import org.springframework.web.reactive.function.client.WebClientRequestException; 17 | import org.springframework.web.servlet.resource.NoResourceFoundException; 18 | import pl.wrapper.parking.infrastructure.error.ErrorWrapper; 19 | 20 | @ControllerAdvice 21 | @RequiredArgsConstructor 22 | @Slf4j 23 | class GlobalExceptionHandler { 24 | 25 | @ExceptionHandler(Exception.class) 26 | public ResponseEntity handleGeneralException(Exception e, HttpServletRequest request) { 27 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 28 | String message = "An error has occurred"; 29 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 30 | logError(message, request.getRequestURI(), e); 31 | return new ResponseEntity<>(errorWrapper, status); 32 | } 33 | 34 | @ExceptionHandler(HandlerMethodValidationException.class) 35 | public ResponseEntity handleValidationException( 36 | HandlerMethodValidationException e, HttpServletRequest request) { 37 | HttpStatus status = HttpStatus.BAD_REQUEST; 38 | String message = "Validation error for method parameters/variables"; 39 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 40 | logError(message, request.getRequestURI(), e); 41 | return new ResponseEntity<>(errorWrapper, status); 42 | } 43 | 44 | @ExceptionHandler(NoResourceFoundException.class) 45 | public ResponseEntity handleNoResourceException( 46 | NoResourceFoundException e, HttpServletRequest request) { 47 | HttpStatus status = HttpStatus.BAD_REQUEST; 48 | String message = "Invalid call URL"; 49 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 50 | logError(message, request.getRequestURI(), e); 51 | return new ResponseEntity<>(errorWrapper, status); 52 | } 53 | 54 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 55 | public ResponseEntity handleMethodArgumentTypeMismatchException( 56 | MethodArgumentTypeMismatchException ex, HttpServletRequest request) { 57 | HttpStatus status = HttpStatus.BAD_REQUEST; 58 | String message = "Argument type mismatch"; 59 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 60 | logError(message, request.getRequestURI(), ex); 61 | return new ResponseEntity<>(errorWrapper, status); 62 | } 63 | 64 | @ExceptionHandler(MissingServletRequestParameterException.class) 65 | public ResponseEntity handleMissingServletRequestParameterException( 66 | MissingServletRequestParameterException ex, HttpServletRequest request) { 67 | HttpStatus status = HttpStatus.BAD_REQUEST; 68 | String message = String.format("'%s' parameter is missing", ex.getParameterName()); 69 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 70 | logError(message, request.getRequestURI(), ex); 71 | return new ResponseEntity<>(errorWrapper, status); 72 | } 73 | 74 | @ExceptionHandler(NominatimClientException.class) 75 | public ResponseEntity handleNominatimClientException( 76 | NominatimClientException ex, HttpServletRequest request) { 77 | HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; 78 | String message = ex.getMessage(); 79 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 80 | logError(message, request.getRequestURI(), ex); 81 | return new ResponseEntity<>(errorWrapper, status); 82 | } 83 | 84 | @ExceptionHandler(InvalidCallException.class) 85 | public ResponseEntity handleInvalidCallException(InvalidCallException e, HttpServletRequest request) { 86 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 87 | String message = "Invalid call"; 88 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 89 | logError(message, request.getRequestURI(), e); 90 | return new ResponseEntity<>(errorWrapper, status); 91 | } 92 | 93 | @ExceptionHandler({PwrApiNotRespondingException.class, WebClientRequestException.class, ConnectException.class}) 94 | public ResponseEntity handlePwrApiNotRespondingException(Exception e, HttpServletRequest request) { 95 | HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; 96 | String message = "PWR Api not responding"; 97 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 98 | logError(message, request.getRequestURI(), e); 99 | return new ResponseEntity<>(errorWrapper, status); 100 | } 101 | 102 | @ExceptionHandler(JsonProcessingException.class) 103 | public ResponseEntity handleJsonProcessingException( 104 | JsonProcessingException e, HttpServletRequest request) { 105 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 106 | String message = "Json processing error"; 107 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 108 | logError(message, request.getRequestURI(), e); 109 | return new ResponseEntity<>(errorWrapper, status); 110 | } 111 | 112 | @ExceptionHandler(ClassCastException.class) 113 | public ResponseEntity handleClassCastException(ClassCastException e, HttpServletRequest request) { 114 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 115 | String message = "Class cast exception"; 116 | ErrorWrapper errorWrapper = new ErrorWrapper(message, status, request.getRequestURI(), status); 117 | logError(message, request.getRequestURI(), e); 118 | return new ResponseEntity<>(errorWrapper, status); 119 | } 120 | 121 | @ExceptionHandler(SerializationFailedException.class) 122 | public ResponseEntity handleClassCastException( 123 | SerializationFailedException e, HttpServletRequest request) { 124 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 125 | ErrorWrapper errorWrapper = new ErrorWrapper(e.getMessage(), status, request.getRequestURI(), status); 126 | logError(e.getMessage(), request.getRequestURI(), e); 127 | return new ResponseEntity<>(errorWrapper, status); 128 | } 129 | 130 | private void logError(String message, String uri, T e) { 131 | log.error("{} at uri: {}; Details: {}", message, uri, e.getMessage()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/exception/InvalidCallException.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.exception; 2 | 3 | public class InvalidCallException extends RuntimeException { 4 | public InvalidCallException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/exception/NominatimClientException.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.exception; 2 | 3 | public class NominatimClientException extends RuntimeException { 4 | public NominatimClientException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/exception/PwrApiNotRespondingException.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.exception; 2 | 3 | public class PwrApiNotRespondingException extends RuntimeException { 4 | public PwrApiNotRespondingException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/InMemoryRepository.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | import java.util.Set; 6 | 7 | public interface InMemoryRepository { 8 | void add(K key, V value); 9 | 10 | V get(K key); 11 | 12 | Set fetchAllKeys(); 13 | 14 | Set> fetchAllEntries(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/InMemoryRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import jakarta.annotation.PreDestroy; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.ObjectInputStream; 10 | import java.io.ObjectOutputStream; 11 | import java.io.Serializable; 12 | import java.util.Collection; 13 | import java.util.Collections; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import org.springframework.core.serializer.support.SerializationFailedException; 17 | import org.springframework.scheduling.annotation.Scheduled; 18 | 19 | public abstract class InMemoryRepositoryImpl 20 | implements InMemoryRepository { 21 | 22 | protected final transient File file; 23 | protected Map dataMap; 24 | protected final V defaultValue; 25 | 26 | public InMemoryRepositoryImpl(String filePath, Map map, V defaultValue) { 27 | this.file = new File(filePath); 28 | this.defaultValue = defaultValue; 29 | 30 | this.dataMap = map; 31 | } 32 | 33 | public Collection values() { 34 | return dataMap.values(); 35 | } 36 | 37 | @Override 38 | public void add(K key, V value) { 39 | dataMap.put(key, value); 40 | } 41 | 42 | @Override 43 | public Set fetchAllKeys() { 44 | return Collections.unmodifiableSet(dataMap.keySet()); 45 | } 46 | 47 | @Override 48 | public Set> fetchAllEntries() { 49 | return dataMap.entrySet(); 50 | } 51 | 52 | @Override 53 | public V get(K key) { 54 | return dataMap.getOrDefault(key, defaultValue); 55 | } 56 | 57 | @PostConstruct 58 | @SuppressWarnings("unchecked") 59 | protected void init() { 60 | if (!file.exists()) return; 61 | 62 | try (FileInputStream fileIn = new FileInputStream(file); 63 | ObjectInputStream in = new ObjectInputStream(fileIn)) { 64 | 65 | this.dataMap = (Map) in.readObject(); 66 | } catch (IOException | ClassNotFoundException e) { 67 | throw new SerializationFailedException(createExceptionForIOE("Deserialization", e)); 68 | } 69 | } 70 | 71 | @PreDestroy 72 | private void selfSerialize() { 73 | File parent = file.getParentFile(); 74 | if (parent != null && !parent.exists()) 75 | if (!parent.mkdirs()) 76 | throw new SerializationFailedException( 77 | "Failed to create directory for path: " + file.getAbsolutePath()); 78 | 79 | try (FileOutputStream fileOut = new FileOutputStream(file); 80 | ObjectOutputStream out = new ObjectOutputStream(fileOut)) { 81 | 82 | out.writeObject(dataMap); 83 | } catch (IOException e) { 84 | throw new SerializationFailedException(createExceptionForIOE("Serialization", e)); 85 | } 86 | } 87 | 88 | @Scheduled(fixedRateString = "#{60 * 1000 * ${serialization.timeStamp.inMinutes}}", initialDelay = 10 * 1000) 89 | protected void periodicSerialize() { 90 | selfSerialize(); 91 | } 92 | 93 | private static String createExceptionForIOE(String methodType, E e) { 94 | return methodType + " failed for: " + InMemoryRepositoryImpl.class.getSimpleName() + ". Message: " 95 | + e.getMessage(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/ParkingDataRepository.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory; 2 | 3 | import java.time.DayOfWeek; 4 | import java.time.LocalDateTime; 5 | import java.time.LocalTime; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.concurrent.TimeUnit; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.stereotype.Component; 14 | import pl.wrapper.parking.infrastructure.inMemory.dto.parking.AvailabilityData; 15 | import pl.wrapper.parking.infrastructure.inMemory.dto.parking.ParkingData; 16 | import pl.wrapper.parking.infrastructure.util.DateTimeUtils; 17 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 18 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 19 | 20 | @Component("parkingDataRepository") 21 | @Slf4j 22 | public class ParkingDataRepository extends InMemoryRepositoryImpl { 23 | 24 | @Value("${pwr-api.data-fetch.minutes}") 25 | private Integer minuteInterval; 26 | 27 | private final PwrApiServerCaller pwrApiServerCaller; 28 | 29 | public ParkingDataRepository( 30 | @Value("${serialization.location.parkingData}") String saveToLocationPath, 31 | PwrApiServerCaller pwrApiServerCaller) { 32 | super(saveToLocationPath, new HashMap<>(), null); 33 | this.pwrApiServerCaller = pwrApiServerCaller; 34 | } 35 | 36 | @Scheduled(fixedRateString = "${pwr-api.data-fetch.minutes}", timeUnit = TimeUnit.MINUTES) 37 | private void handleData() { 38 | LocalDateTime currentDateTime = DateTimeUtils.roundToNearestInterval(LocalDateTime.now(), minuteInterval); 39 | LocalTime currentTime = currentDateTime.toLocalTime(); 40 | DayOfWeek currentDay = currentDateTime.getDayOfWeek(); 41 | 42 | log.info("Saving parking data with rounded time: {}, day: {}", currentTime, currentDay); 43 | 44 | List parkings = pwrApiServerCaller.fetchParkingData(); 45 | for (ParkingResponse parking : parkings) { 46 | int parkingId = parking.parkingId(); 47 | double availability = (double) parking.freeSpots() / parking.totalSpots(); 48 | 49 | ParkingData parkingData = get(parkingId); 50 | if (parkingData == null) { 51 | parkingData = ParkingData.builder() 52 | .parkingId(parkingId) 53 | .totalSpots(parking.totalSpots()) 54 | .freeSpotsHistory(new HashMap<>()) 55 | .build(); 56 | } 57 | 58 | Map dailyHistory = 59 | parkingData.freeSpotsHistory().computeIfAbsent(currentDay, k -> new HashMap<>()); 60 | AvailabilityData availabilityData = 61 | dailyHistory.computeIfAbsent(currentTime, k -> new AvailabilityData(0, 0.0)); 62 | 63 | int newSampleCount = availabilityData.sampleCount() + 1; 64 | double newAvgAvailability = 65 | (availabilityData.averageAvailability() * availabilityData.sampleCount() + availability) 66 | / newSampleCount; 67 | AvailabilityData newAvailabilityData = new AvailabilityData(newSampleCount, newAvgAvailability); 68 | 69 | dailyHistory.put(currentTime, newAvailabilityData); 70 | add(parkingId, parkingData); 71 | } 72 | 73 | log.info("Parking data saved successfully. Storage updated."); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/ParkingRequestRepository.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory; 2 | 3 | import java.time.LocalTime; 4 | import java.util.HashMap; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.EndpointData; 11 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.EndpointDataFactory; 12 | 13 | @Slf4j 14 | @Component("parkingRequestRepository") 15 | public class ParkingRequestRepository extends InMemoryRepositoryImpl { 16 | private static final String TOTAL_ENDPOINT_NAME = "total"; 17 | private final EndpointDataFactory endpointDataFactory; 18 | 19 | @Autowired 20 | public ParkingRequestRepository( 21 | @Value("${serialization.location.ParkingRequests}") String saveToLocationPath, 22 | EndpointDataFactory endpointDataFactory) { 23 | super(saveToLocationPath, new HashMap<>(), null); 24 | this.endpointDataFactory = endpointDataFactory; 25 | dataMap.put(TOTAL_ENDPOINT_NAME, endpointDataFactory.create()); 26 | } 27 | 28 | public EndpointData getTotalEndpoint() { 29 | return dataMap.get(TOTAL_ENDPOINT_NAME); 30 | } 31 | 32 | public void updateRequestEndpointData(String requestURI, boolean isSuccessful, LocalTime requestTime) { 33 | dataMap.computeIfAbsent(requestURI, key -> endpointDataFactory.create()) 34 | .registerRequest(isSuccessful, requestTime); 35 | getTotalEndpoint().registerRequest(isSuccessful, requestTime); 36 | } 37 | 38 | @Scheduled(cron = "0 */${timeframe.default.length.inMinutes} * * * *") 39 | public void updateAverages() { 40 | log.info("Updating the average number of requests for each endpoint"); 41 | 42 | LocalTime currentTime = LocalTime.now(); 43 | values().forEach(endpointData -> endpointData.recalculateAverageForPreviousTimeframe(currentTime)); 44 | 45 | int previousTimeframeIndex = getTotalEndpoint().getPreviousTimeframeIndex(currentTime); 46 | double totalAverage = dataMap.entrySet().stream() 47 | .filter(entry -> !entry.getKey().equals(TOTAL_ENDPOINT_NAME)) 48 | .mapToDouble(entry -> entry.getValue() 49 | .getTimeframeStatistics()[previousTimeframeIndex] 50 | .getAverageNumberOfRequests()) 51 | .sum(); 52 | 53 | getTotalEndpoint().getTimeframeStatistics()[previousTimeframeIndex].setAverageNumberOfRequests(totalAverage); 54 | 55 | log.info("The averages have been updated"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/dto/parking/AvailabilityData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory.dto.parking; 2 | 3 | import java.io.Serializable; 4 | 5 | public record AvailabilityData(int sampleCount, double averageAvailability) implements Serializable {} 6 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/dto/parking/ParkingData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory.dto.parking; 2 | 3 | import java.io.Serializable; 4 | import java.time.DayOfWeek; 5 | import java.time.LocalTime; 6 | import java.util.Map; 7 | import lombok.Builder; 8 | 9 | @Builder 10 | public record ParkingData( 11 | int parkingId, int totalSpots, Map> freeSpotsHistory) 12 | implements Serializable {} 13 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/dto/request/EndpointData.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory.dto.request; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | import java.math.RoundingMode; 6 | import java.time.LocalTime; 7 | import java.time.temporal.ChronoUnit; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @AllArgsConstructor 14 | @Getter 15 | public class EndpointData implements Serializable { 16 | private long successCount; 17 | private long requestCount; 18 | private final TimeframeStatistic[] timeframeStatistics; 19 | private final int timeframeLength; 20 | 21 | public EndpointData(int timeframeLength) { 22 | this.successCount = 0; 23 | this.requestCount = 0; 24 | this.timeframeLength = timeframeLength; 25 | this.timeframeStatistics = new TimeframeStatistic[calculateTimeframesCount(timeframeLength)]; 26 | for (int i = 0; i < timeframeStatistics.length; i++) { 27 | timeframeStatistics[i] = new TimeframeStatistic(); 28 | } 29 | } 30 | 31 | public double getSuccessRate() { 32 | if (requestCount == 0) return 0.0; 33 | return BigDecimal.valueOf((double) successCount / requestCount * 100) 34 | .setScale(2, RoundingMode.HALF_UP) 35 | .doubleValue(); 36 | } 37 | 38 | public void registerRequest(boolean isSuccessful, LocalTime requestTime) { 39 | requestCount++; 40 | if (isSuccessful) { 41 | successCount++; 42 | } 43 | int timeframe = mapToTimeframeIndex(requestTime); 44 | timeframeStatistics[timeframe].registerRequest(); 45 | } 46 | 47 | public void recalculateAverageForPreviousTimeframe(LocalTime currentTimeframeTime) { 48 | timeframeStatistics[getPreviousTimeframeIndex(currentTimeframeTime)].recalculateAverage(); 49 | } 50 | 51 | public int getPreviousTimeframeIndex(LocalTime currentTimeframeTime) { 52 | return (mapToTimeframeIndex(currentTimeframeTime) - 1 + timeframeStatistics.length) 53 | % timeframeStatistics.length; 54 | } 55 | 56 | private int calculateTimeframesCount(int timeframeLengthInMinutes) { 57 | return (int) Math.ceil((double) 24 * 60 / timeframeLengthInMinutes); 58 | } 59 | 60 | private int mapToTimeframeIndex(LocalTime time) { 61 | return (int) ChronoUnit.MINUTES.between(LocalTime.MIDNIGHT, time) / timeframeLength; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/dto/request/EndpointDataFactory.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory.dto.request; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component("endpointDataFactory") 7 | public class EndpointDataFactory { 8 | @Value("${timeframe.default.length.inMinutes}") 9 | private int timeframeLength; 10 | 11 | public EndpointData create() { 12 | return new EndpointData(timeframeLength); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/inMemory/dto/request/TimeframeStatistic.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory.dto.request; 2 | 3 | import java.io.Serializable; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Setter 10 | @Getter 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class TimeframeStatistic implements Serializable { 14 | private double averageNumberOfRequests; 15 | private int totalNumberOfRequests; 16 | private int numberOfAverageCalculations; 17 | 18 | public void registerRequest() { 19 | totalNumberOfRequests++; 20 | } 21 | 22 | public void recalculateAverage() { 23 | averageNumberOfRequests = (averageNumberOfRequests * numberOfAverageCalculations + totalNumberOfRequests) 24 | / (numberOfAverageCalculations + 1); 25 | numberOfAverageCalculations++; 26 | totalNumberOfRequests = 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/interceptor/ParkingRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import java.time.LocalTime; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import pl.wrapper.parking.infrastructure.inMemory.ParkingRequestRepository; 12 | 13 | @Slf4j 14 | @Component 15 | @RequiredArgsConstructor 16 | public class ParkingRequestInterceptor implements HandlerInterceptor { 17 | private final ParkingRequestRepository parkingRequestRepository; 18 | 19 | @Override 20 | public void afterCompletion( 21 | HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 22 | boolean isSuccessful = HttpStatus.Series.valueOf(response.getStatus()) == HttpStatus.Series.SUCCESSFUL; 23 | parkingRequestRepository.updateRequestEndpointData(request.getRequestURI(), isSuccessful, LocalTime.now()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/interceptor/ParkingRequestInterceptorConfig.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.interceptor; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | import pl.wrapper.parking.infrastructure.inMemory.ParkingRequestRepository; 8 | 9 | @Configuration 10 | @RequiredArgsConstructor 11 | public class ParkingRequestInterceptorConfig implements WebMvcConfigurer { 12 | private final ParkingRequestRepository parkingRequestRepository; 13 | 14 | @Override 15 | public void addInterceptors(InterceptorRegistry registry) { 16 | registry.addInterceptor(new ParkingRequestInterceptor(parkingRequestRepository)) 17 | .addPathPatterns("/**") 18 | .excludePathPatterns("/stats/**") 19 | .excludePathPatterns("/swagger-ui/**") 20 | .excludePathPatterns("/v3/api-docs/**"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/nominatim/client/NominatimClient.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.nominatim.client; 2 | 3 | import org.springframework.web.bind.annotation.RequestParam; 4 | import org.springframework.web.service.annotation.GetExchange; 5 | import org.springframework.web.service.annotation.HttpExchange; 6 | import pl.wrapper.parking.facade.dto.main.NominatimLocation; 7 | import reactor.core.publisher.Flux; 8 | 9 | @HttpExchange 10 | public interface NominatimClient { 11 | 12 | @GetExchange("/search") 13 | Flux search( 14 | @RequestParam("q") String query, @RequestParam(value = "format", defaultValue = "jsonv2") String format); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/nominatim/configuration/NominatimClientConfig.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.nominatim.configuration; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.http.HttpStatusCode; 7 | import org.springframework.web.reactive.function.client.WebClient; 8 | import org.springframework.web.reactive.function.client.support.WebClientAdapter; 9 | import org.springframework.web.service.invoker.HttpServiceProxyFactory; 10 | import pl.wrapper.parking.infrastructure.exception.NominatimClientException; 11 | import pl.wrapper.parking.infrastructure.nominatim.client.NominatimClient; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Configuration 15 | class NominatimClientConfig { 16 | 17 | @Value("${maps.api.url}") 18 | private String mapsUrl; 19 | 20 | @Bean 21 | public NominatimClient nominatimClient() { 22 | WebClient webClient = WebClient.builder() 23 | .baseUrl(mapsUrl) 24 | .defaultStatusHandler(HttpStatusCode::isError, resp -> resp.bodyToMono(String.class) 25 | .flatMap(body -> Mono.error(new NominatimClientException(body)))) 26 | .build(); 27 | HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)) 28 | .build(); 29 | 30 | return factory.createClient(NominatimClient.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/util/DateTimeUtils.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.util; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class DateTimeUtils { 6 | 7 | public static LocalDateTime roundToNearestInterval(LocalDateTime dateTime, int minuteInterval) { 8 | if (minuteInterval <= 0) { 9 | throw new IllegalArgumentException("Minute interval must be positive"); 10 | } 11 | 12 | int minutes = dateTime.getHour() * 60 + dateTime.getMinute(); 13 | int roundedMinutes = (minutes / minuteInterval) * minuteInterval; 14 | 15 | return dateTime.toLocalDate().atStartOfDay().plusMinutes(roundedMinutes); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/validation/validIds/IdValidator.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.validation.validIds; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | import java.util.List; 6 | 7 | class IdValidator implements ConstraintValidator> { 8 | 9 | private static final int[] ALLOWED_IDS = {1, 2, 3, 4, 5}; 10 | 11 | @Override 12 | public boolean isValid(List requestList, ConstraintValidatorContext constraintValidatorContext) { 13 | return requestList == null || allMatch(requestList); 14 | } 15 | 16 | private static boolean allMatch(List toCheck) { 17 | for (int id : toCheck) { 18 | boolean isValid = false; 19 | for (int allowedId : ALLOWED_IDS) { 20 | if (id == allowedId) { 21 | isValid = true; 22 | break; 23 | } 24 | } 25 | if (!isValid) return false; 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/infrastructure/validation/validIds/ValidIds.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.validation.validIds; 2 | 3 | import jakarta.validation.Constraint; 4 | import jakarta.validation.Payload; 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Documented 12 | @Constraint(validatedBy = IdValidator.class) 13 | @Target({ElementType.FIELD, ElementType.PARAMETER}) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface ValidIds { 16 | String message() default "One or more of provided ID values is invalid"; 17 | 18 | Class[] groups() default {}; 19 | 20 | Class[] payload() default {}; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/PwrApiServerCaller.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler; 2 | 3 | import java.util.List; 4 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 5 | 6 | public interface PwrApiServerCaller { 7 | List fetchParkingData(); 8 | 9 | List getAllCharsForToday(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/configuration/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.configuration; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.EnableCaching; 5 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @EnableCaching 10 | @Configuration 11 | class CacheConfig { 12 | @Bean 13 | public CacheManager cacheManager() { 14 | return new ConcurrentMapCacheManager("parkingListCache", "chartCache"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/configuration/CacheCustomizer.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.configuration; 2 | 3 | import java.util.List; 4 | import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; 5 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | class CacheCustomizer implements CacheManagerCustomizer { 10 | 11 | @Override 12 | public void customize(ConcurrentMapCacheManager cacheManager) { 13 | cacheManager.setCacheNames(List.of("parkingListCache", "chartCache")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/configuration/WebClientConfig.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import org.springframework.util.LinkedMultiValueMap; 7 | import org.springframework.util.MultiValueMap; 8 | import org.springframework.web.reactive.function.client.ClientResponse; 9 | import org.springframework.web.reactive.function.client.ExchangeFilterFunction; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | import pl.wrapper.parking.infrastructure.exception.PwrApiNotRespondingException; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Configuration 15 | class WebClientConfig { 16 | 17 | @Profile("prod") 18 | @Bean 19 | public WebClient webClient() { 20 | MultiValueMap headers = new LinkedMultiValueMap<>(); 21 | headers.add("Accept", "application/json"); 22 | headers.add("Accept-Encoding", "gzip"); 23 | headers.add("Accept-Language", "pl"); 24 | headers.add("Referer", "https://iparking.pwr.edu.pl"); 25 | headers.add("X-Requested-With", "XMLHttpRequest"); 26 | headers.add("Connection", "keep-alive"); 27 | return WebClient.builder() 28 | .baseUrl("https://iparking.pwr.edu.pl/modules/iparking/scripts/ipk_operations.php") 29 | .defaultHeaders(httpHeaders -> httpHeaders.addAll(headers)) 30 | .filter(buildRetryFilter()) 31 | .filter(ExchangeFilterFunction.ofResponseProcessor(WebClientConfig::responseFilter)) 32 | .build(); 33 | } 34 | 35 | static Mono responseFilter(ClientResponse response) { 36 | if (response.statusCode().isError()) 37 | return response.bodyToMono(String.class) 38 | .flatMap(body -> Mono.error(new PwrApiNotRespondingException(body))); 39 | return Mono.just(response); 40 | } 41 | 42 | static ExchangeFilterFunction buildRetryFilter() { 43 | return (request, next) -> next.exchange(request) 44 | .onErrorResume(PwrApiNotRespondingException.class, ex -> { 45 | System.out.println("retry"); 46 | return next.exchange(request); 47 | }) 48 | .retry(2); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/domain/PwrApiCaller.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.domain; 2 | 3 | import java.time.LocalTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.lang.Nullable; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.reactive.function.client.WebClient; 15 | import pl.wrapper.parking.pwrResponseHandler.dto.Address; 16 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | 20 | @Profile("prod") 21 | @Component 22 | @RequiredArgsConstructor 23 | public final class PwrApiCaller { 24 | 25 | private final WebClient webClient; 26 | 27 | public Mono> fetchParkingPlaces() { 28 | return webClient 29 | .post() 30 | .contentType(MediaType.APPLICATION_JSON) 31 | .bodyValue(createDummyParkingMap()) 32 | .retrieve() 33 | .bodyToMono(HashMap.class) 34 | .flatMap(parkingResponses -> { 35 | try { 36 | return Mono.just(parseResponse(parkingResponses.get("places"))); 37 | } catch (ClassCastException e) { 38 | return Mono.error(e); 39 | } 40 | }); 41 | } 42 | 43 | private static HashMap createDummyParkingMap() { 44 | HashMap body = new HashMap<>(); 45 | body.put("o", "get_parks"); 46 | return body; 47 | } 48 | 49 | @SuppressWarnings("unchecked") 50 | private static List parseResponse(Object unparsedResponse) throws ClassCastException { 51 | List returnList = new ArrayList<>(); 52 | ArrayList firstTierCastList = (ArrayList) unparsedResponse; 53 | int parkingId = 0; 54 | for (Object currentParkingPrecast : firstTierCastList) { 55 | LinkedHashMap currentParking; 56 | try { 57 | currentParking = (LinkedHashMap) currentParkingPrecast; 58 | } catch (ClassCastException e) { 59 | continue; 60 | } 61 | int boundlessFreeSpots = Integer.parseInt(currentParking.getOrDefault("liczba_miejsc", "0")); 62 | int totalSpots = Integer.parseInt(currentParking.getOrDefault("places", "0")); 63 | ParkingResponse currentResponse = ParkingResponse.builder() 64 | .parkingId(++parkingId) 65 | .name(currentParking.getOrDefault("nazwa", "unknown")) 66 | .freeSpots(Math.max(0, Math.min(totalSpots, boundlessFreeSpots))) 67 | .symbol(currentParking.getOrDefault("symbol", "unknown")) 68 | .openingHours(PwrApiCaller.getParsedTime(currentParking.get("open_hour"))) 69 | .closingHours(PwrApiCaller.getParsedTime(currentParking.get("close_hour"))) 70 | .totalSpots(totalSpots) 71 | .address(new Address( 72 | currentParking.getOrDefault("address", "unknown").strip(), 73 | Float.parseFloat(currentParking.get("geo_lat")), 74 | Float.parseFloat(currentParking.get("geo_lan")))) 75 | .trend(Short.parseShort(currentParking.getOrDefault("trend", "0"))) 76 | .urlToPhoto(currentParking.getOrDefault("photo", "unknown")) 77 | .build(); 78 | returnList.add(currentResponse); 79 | } 80 | return returnList; 81 | } 82 | 83 | private static LocalTime getParsedTime(@Nullable String time) { 84 | if (time == null) return null; 85 | return LocalTime.parse(time, DateTimeFormatter.ISO_LOCAL_TIME); 86 | } 87 | 88 | private static HashMap createDummyChartMap(int forId) { 89 | HashMap body = new HashMap<>(); 90 | body.put("o", "get_today_chart"); 91 | body.put("i", String.valueOf(forId)); 92 | return body; 93 | } 94 | 95 | private static final Integer[] ID_MAPPER = {4, 2, 5, 6, 7}; 96 | 97 | private Mono fetchParkingChart(int forId) { 98 | return webClient 99 | .post() 100 | .contentType(MediaType.APPLICATION_JSON) 101 | .bodyValue(createDummyChartMap(forId)) 102 | .retrieve() 103 | .bodyToMono(HashMap.class) 104 | .flatMap(Mono::just); 105 | } 106 | 107 | public Mono> fetchAllParkingCharts() { 108 | return Flux.fromArray(ID_MAPPER).flatMap(this::fetchParkingChart).collectList(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/domain/PwrApiServerCallerImpl.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.domain; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.TimeUnit; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.cache.annotation.CacheEvict; 8 | import org.springframework.cache.annotation.Cacheable; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Service; 11 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 12 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 13 | 14 | @Service 15 | @Slf4j 16 | @RequiredArgsConstructor 17 | public class PwrApiServerCallerImpl implements PwrApiServerCaller { 18 | 19 | private static final int CACHE_TTL_MIN = 3; 20 | private final PwrApiCaller pwrApiCaller; 21 | 22 | @Override 23 | @Cacheable("parkingListCache") 24 | public List fetchParkingData() { 25 | log.info("Fetching new data from Pwr api."); 26 | List data = pwrApiCaller.fetchParkingPlaces().block(); 27 | log.info("Data fetched successfully"); 28 | return data; 29 | } 30 | 31 | @CacheEvict( 32 | value = {"parkingListCache", "chartCache"}, 33 | allEntries = true) 34 | @Scheduled(fixedRate = CACHE_TTL_MIN, timeUnit = TimeUnit.MINUTES) 35 | public void flushCache() { 36 | log.info("Cache flushed. New data can be fetched."); 37 | } 38 | 39 | @Override 40 | @Cacheable("chartCache") 41 | public List getAllCharsForToday() { 42 | log.info("Fetching new chart data from Pwr api."); 43 | List charts = pwrApiCaller.fetchAllParkingCharts().block(); 44 | log.info("Charts fetched successfully"); 45 | return charts; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/dto/Address.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record Address( 6 | @Schema(example = "Example 201, 11-041 Wrocław") String streetAddress, 7 | @Schema(example = "21.37") float geoLatitude, 8 | @Schema(example = "-4.20") float geoLongitude) {} 9 | -------------------------------------------------------------------------------- /src/main/java/pl/wrapper/parking/pwrResponseHandler/dto/ParkingResponse.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import java.time.LocalTime; 6 | import lombok.Builder; 7 | import org.springframework.lang.Nullable; 8 | 9 | @Builder 10 | public record ParkingResponse( 11 | @Schema(example = "4") int parkingId, 12 | @Schema(example = "33") int freeSpots, 13 | @Schema(example = "97") int totalSpots, 14 | @Schema(example = "best parking") String name, 15 | @Schema(example = "WRO") String symbol, 16 | @Schema(type = "string", format = "time", example = "08:00:00") @Nullable LocalTime openingHours, 17 | @Schema(type = "string", format = "time", example = "22:00:00") @Nullable LocalTime closingHours, 18 | @Schema(implementation = Address.class) Address address, 19 | @Schema(type = "short", example = "0") short trend, 20 | @Schema(type = "string", example = "/images/photos/geo-l01.jpg") String urlToPhoto) { 21 | 22 | @JsonIgnore 23 | public boolean isOpened() { 24 | LocalTime now = LocalTime.now(); 25 | return openingHours == null || closingHours == null || now.isAfter(openingHours) && now.isBefore(closingHours); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=parking 2 | spring.profiles.active=prod 3 | 4 | springdoc.swagger-ui.path=/docs 5 | 6 | server.servlet.context-path=/parkingiAPI 7 | server.port=8080 8 | 9 | maps.api.url=https://nominatim.openstreetmap.org 10 | 11 | pwr-api.data-fetch.minutes=10 12 | 13 | serialization.timeStamp.inMinutes=10 14 | serialization.location=data/statistics 15 | serialization.location.ParkingRequests=${serialization.location}/requests 16 | serialization.location.parkingData=${serialization.location}/data 17 | 18 | timeframe.default.length.inMinutes=30 19 | 20 | springdoc.swagger-ui.enabled=true 21 | springdoc.api-docs.enabled=true 22 | 23 | spring.jpa.hibernate.ddl-auto=update 24 | spring.jpa.defer-datasource-initialization=true 25 | spring.jpa.open-in-view=false 26 | spring.jpa.properties.hibernate.order_updates=true 27 | spring.jpa.properties.hibernate.order_inserts=true 28 | spring.datasource.url=jdbc:postgresql://${PARKINGIAPI_DB_HOST}:5432/${PARKINGIAPI_DB_NAME} 29 | spring.datasource.username=${PARKINGIAPI_DB_USERNAME} 30 | spring.datasource.password=${PARKINGIAPI_DB_PASSWORD} 31 | spring.datasource.driver-class-name=org.postgresql.Driver 32 | spring.sql.init.schema-locations=classpath:/schema.sql 33 | spring.sql.init.mode=always 34 | 35 | #changing the below value with the current implementation will cause the timestamps for older values to diverge 36 | historic.data-update.minutes=5 37 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS historic; 2 | CREATE TABLE IF NOT EXISTS historic.historic_data(data_table SMALLINT[][] NOT NULL, date DATE NOT NULL PRIMARY KEY); -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/ParkingApplicationTests.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ParkingApplicationTests { 8 | 9 | @Test 10 | void contextLoads() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/historic/ParkingHistoricDataServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.historic; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.Mockito.doReturn; 5 | import static org.mockito.Mockito.when; 6 | 7 | import jakarta.persistence.EntityManager; 8 | import jakarta.persistence.Query; 9 | import jakarta.persistence.TypedQuery; 10 | import java.time.LocalDate; 11 | import java.time.LocalTime; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.InjectMocks; 17 | import org.mockito.Mock; 18 | import org.mockito.Spy; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayData; 21 | import pl.wrapper.parking.facade.dto.historicData.HistoricDayParkingData; 22 | import pl.wrapper.parking.facade.dto.historicData.HistoricPeriodParkingData; 23 | import pl.wrapper.parking.facade.dto.historicData.TimestampEntry; 24 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | class ParkingHistoricDataServiceImplTest { 28 | 29 | @Mock 30 | private PwrApiServerCaller pwrApiServerCaller; 31 | 32 | private final Integer intervalLength = 480; 33 | 34 | @Mock 35 | private Query atQuery; 36 | 37 | @Mock 38 | private TypedQuery periodQuery; 39 | 40 | @Mock 41 | private TypedQuery fromQuery; 42 | 43 | @Mock 44 | private EntityManager em; 45 | 46 | @InjectMocks 47 | @Spy 48 | private final ParkingHistoricDataServiceImpl parkingHistoricDataService = 49 | new ParkingHistoricDataServiceImpl(pwrApiServerCaller, intervalLength); 50 | 51 | @Test 52 | void testGetDataForDay_ValidDate_ReturnsParkingData() { 53 | LocalDate testDate = LocalDate.of(2023, 10, 1); 54 | short[][] testData = { 55 | {10, 20, 30}, 56 | {5, 15, 25} 57 | }; 58 | List testList = new ArrayList<>(); 59 | testList.add(testData); 60 | HistoricDayParkingData expectedData = new HistoricDayParkingData( 61 | (short) 1, 62 | new HistoricDayData( 63 | testDate, 64 | List.of( 65 | new TimestampEntry("00:00", (short) 5), 66 | new TimestampEntry("08:00", (short) 15), 67 | new TimestampEntry("16:00", (short) 25)))); 68 | 69 | doReturn(atQuery).when(parkingHistoricDataService).createAtQuery(testDate); 70 | when(atQuery.getResultList()).thenReturn(testList); 71 | HistoricDayParkingData actualData = parkingHistoricDataService.getDataForDay(testDate, 1); 72 | 73 | assertNotNull(actualData); 74 | assertEquals(expectedData, actualData); 75 | } 76 | 77 | @Test 78 | void testGetDataForDay_EmptyData_ReturnsNull() { 79 | LocalDate testDate = LocalDate.of(2023, 10, 1); 80 | doReturn(atQuery).when(parkingHistoricDataService).createAtQuery(testDate); 81 | when(atQuery.getResultList()).thenReturn(List.of()); 82 | List actualData = parkingHistoricDataService.getDataForDay(testDate); 83 | 84 | assertNull(actualData); 85 | } 86 | 87 | @Test 88 | void testGetDataForDay_MultipleParkingLots_ReturnsCorrectData() { 89 | LocalDate testDate = LocalDate.of(2023, 10, 2); 90 | short[][] testData = { 91 | {15, 25, 35}, 92 | {10, 20, 30}, 93 | {5, 10, 15} 94 | }; 95 | List testList = new ArrayList<>(); 96 | testList.add(testData); 97 | List expectedData = List.of( 98 | new HistoricDayParkingData( 99 | (short) 0, 100 | new HistoricDayData( 101 | testDate, 102 | List.of( 103 | new TimestampEntry("00:00", (short) 15), 104 | new TimestampEntry("08:00", (short) 25), 105 | new TimestampEntry("16:00", (short) 35)))), 106 | new HistoricDayParkingData( 107 | (short) 1, 108 | new HistoricDayData( 109 | testDate, 110 | List.of( 111 | new TimestampEntry("00:00", (short) 10), 112 | new TimestampEntry("08:00", (short) 20), 113 | new TimestampEntry("16:00", (short) 30)))), 114 | new HistoricDayParkingData( 115 | (short) 2, 116 | new HistoricDayData( 117 | testDate, 118 | List.of( 119 | new TimestampEntry("00:00", (short) 5), 120 | new TimestampEntry("08:00", (short) 10), 121 | new TimestampEntry("16:00", (short) 15))))); 122 | 123 | doReturn(atQuery).when(parkingHistoricDataService).createAtQuery(testDate); 124 | when(atQuery.getResultList()).thenReturn(testList); 125 | List actualData = parkingHistoricDataService.getDataForDay(testDate); 126 | 127 | assertNotNull(actualData); 128 | assertEquals(expectedData.size(), actualData.size()); 129 | assertEquals(expectedData, actualData); 130 | } 131 | 132 | @Test 133 | void testGetDataForPeriod_ValidData_ReturnsHistoricPeriodParkingData() { 134 | LocalDate fromDate = LocalDate.of(2023, 10, 1); 135 | LocalDate toDate = LocalDate.of(2023, 10, 3); 136 | int parkingId = 1; 137 | 138 | List testData = List.of( 139 | new HistoricDataEntry(fromDate, new short[][] { 140 | {10, 20, 30}, 141 | {5, 15, 25} 142 | }), 143 | new HistoricDataEntry(toDate, new short[][] { 144 | {15, 25, 35}, 145 | {10, 20, 30} 146 | })); 147 | 148 | doReturn(periodQuery).when(parkingHistoricDataService).createPeriodQuery(fromDate, toDate); 149 | when(periodQuery.getResultList()).thenReturn(testData); 150 | 151 | HistoricPeriodParkingData actualData = parkingHistoricDataService.getDataForPeriod(fromDate, toDate, parkingId); 152 | 153 | assertNotNull(actualData); 154 | assertEquals((short) parkingId, actualData.parkingId()); 155 | assertEquals(2, actualData.dataList().size()); 156 | 157 | HistoricDayData firstDay = actualData.dataList().getFirst(); 158 | assertEquals(fromDate, firstDay.atDate()); 159 | assertEquals(3, firstDay.data().size()); 160 | assertEquals("00:00", firstDay.data().getFirst().timestamp()); 161 | assertEquals((short) 5, firstDay.data().getFirst().freeSpots()); 162 | } 163 | 164 | @Test 165 | void testGetDataForPeriod_NoData_ReturnsNull() { 166 | LocalDate fromDate = LocalDate.of(2023, 10, 1); 167 | LocalDate toDate = LocalDate.of(2023, 10, 3); 168 | int parkingId = 1; 169 | 170 | doReturn(periodQuery).when(parkingHistoricDataService).createPeriodQuery(fromDate, toDate); 171 | when(periodQuery.getResultList()).thenReturn(List.of()); 172 | 173 | HistoricPeriodParkingData actualData = parkingHistoricDataService.getDataForPeriod(fromDate, toDate, parkingId); 174 | assertNull(actualData); 175 | } 176 | 177 | @Test 178 | void testGetDataForPeriod_FromDateOnly_ReturnsHistoricData() { 179 | LocalDate fromDate = LocalDate.of(2023, 10, 1); 180 | int parkingId = 1; 181 | 182 | List testData = List.of(new HistoricDataEntry(fromDate, new short[][] { 183 | {15, 25, 35}, 184 | {5, 10, 15} 185 | })); 186 | 187 | doReturn(fromQuery).when(parkingHistoricDataService).createFromQuery(fromDate); 188 | when(fromQuery.getResultList()).thenReturn(testData); 189 | 190 | HistoricPeriodParkingData actualData = parkingHistoricDataService.getDataForPeriod(fromDate, null, parkingId); 191 | 192 | assertNotNull(actualData); 193 | assertEquals((short) parkingId, actualData.parkingId()); 194 | assertEquals(1, actualData.dataList().size()); 195 | assertEquals(fromDate, actualData.dataList().getFirst().atDate()); 196 | } 197 | 198 | @Test 199 | void testCalculateTimeframesCount() throws Exception { 200 | java.lang.reflect.Method method = 201 | ParkingHistoricDataServiceImpl.class.getDeclaredMethod("calculateTimeframesCount", int.class); 202 | method.setAccessible(true); 203 | 204 | int timeframeLength = 60; 205 | int expectedCount = 24; 206 | int actualCount = (int) method.invoke(null, timeframeLength); 207 | assertEquals(expectedCount, actualCount); 208 | 209 | timeframeLength = 30; 210 | expectedCount = 48; 211 | actualCount = (int) method.invoke(null, timeframeLength); 212 | assertEquals(expectedCount, actualCount); 213 | 214 | timeframeLength = 5; 215 | expectedCount = 288; 216 | actualCount = (int) method.invoke(null, timeframeLength); 217 | assertEquals(expectedCount, actualCount); 218 | } 219 | 220 | @Test 221 | void testMapTimeToTimeframeIndex() throws Exception { 222 | 223 | java.lang.reflect.Method mapTimeToTimeframeIndexMethod = ParkingHistoricDataServiceImpl.class.getDeclaredMethod( 224 | "mapTimeToTimeframeIndex", LocalTime.class, int.class); 225 | mapTimeToTimeframeIndexMethod.setAccessible(true); 226 | 227 | LocalTime time = LocalTime.MIDNIGHT; 228 | int intervalLength = 60; 229 | int expectedIndex = 0; 230 | int actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 231 | assertEquals(expectedIndex, actualIndex); 232 | 233 | time = LocalTime.of(1, 15); 234 | expectedIndex = 1; 235 | actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 236 | assertEquals(expectedIndex, actualIndex); 237 | 238 | time = LocalTime.of(1, 26); 239 | intervalLength = 30; 240 | expectedIndex = 2; 241 | actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 242 | assertEquals(expectedIndex, actualIndex); 243 | 244 | time = LocalTime.of(2, 15); 245 | intervalLength = 15; 246 | expectedIndex = 9; 247 | actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 248 | assertEquals(expectedIndex, actualIndex); 249 | 250 | time = LocalTime.of(9, 0); 251 | intervalLength = 180; 252 | expectedIndex = 3; 253 | actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 254 | assertEquals(expectedIndex, actualIndex); 255 | 256 | time = LocalTime.of(23, 59); 257 | intervalLength = 5; 258 | expectedIndex = 287; 259 | actualIndex = (int) mapTimeToTimeframeIndexMethod.invoke(null, time, intervalLength); 260 | assertEquals(expectedIndex, actualIndex); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/main/ParkingControllerIT.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.main; 2 | 3 | import static org.hamcrest.CoreMatchers.anything; 4 | import static org.hamcrest.CoreMatchers.is; 5 | import static org.mockito.Mockito.*; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 8 | 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.mock.mockito.MockBean; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import pl.wrapper.parking.facade.ParkingHistoricDataService; 21 | import pl.wrapper.parking.facade.dto.main.NominatimLocation; 22 | import pl.wrapper.parking.infrastructure.nominatim.client.NominatimClient; 23 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 24 | import pl.wrapper.parking.pwrResponseHandler.dto.Address; 25 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 26 | import reactor.core.publisher.Flux; 27 | 28 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 29 | @AutoConfigureMockMvc 30 | public class ParkingControllerIT { 31 | @Autowired 32 | private MockMvc mockMvc; 33 | 34 | @Autowired 35 | private ObjectMapper objectMapper; 36 | 37 | @MockBean 38 | private PwrApiServerCaller pwrApiServerCaller; 39 | 40 | @MockBean 41 | private NominatimClient nominatimClient; 42 | 43 | @MockBean 44 | private ParkingHistoricDataService parkingHistoricDataService; 45 | 46 | private List parkings; 47 | 48 | @BeforeEach 49 | void setUp() { 50 | parkings = List.of( 51 | ParkingResponse.builder() 52 | .parkingId(1) 53 | .name("Parking 1") 54 | .symbol("P1") 55 | .address(new Address("street 1", 37.1f, -158.8f)) 56 | .build(), 57 | ParkingResponse.builder() 58 | .parkingId(2) 59 | .name("Parking 2") 60 | .symbol("P2") 61 | .address(new Address("street 2", -44.4f, 123.6f)) 62 | .build()); 63 | } 64 | 65 | @Test 66 | void getClosestParking_returnClosestParking() throws Exception { 67 | String address = "test place"; 68 | NominatimLocation location = new NominatimLocation(37.0, -158.0); 69 | 70 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.just(location)); 71 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings); 72 | 73 | mockMvc.perform(get("/address").queryParam("address", address).accept(MediaType.APPLICATION_JSON)) 74 | .andExpect(status().isOk()) 75 | .andExpect(jsonPath("$.parkingId", is(1))) 76 | .andExpect(jsonPath("$.name", is("Parking 1"))) 77 | .andExpect(jsonPath("$.address.geoLatitude").value(37.1f)) 78 | .andExpect(jsonPath("$.address.geoLongitude").value(-158.8f)); 79 | } 80 | 81 | @Test 82 | void getClosestParking_returnNotFound_whenNoResultsFromApi() throws Exception { 83 | String address = "non-existent address"; 84 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.empty()); 85 | 86 | mockMvc.perform(get("/address").queryParam("address", address).accept(MediaType.APPLICATION_JSON)) 87 | .andExpect(status().isNotFound()) 88 | .andExpect(jsonPath("$.errorMessage", anything())); 89 | 90 | verify(pwrApiServerCaller, never()).fetchParkingData(); 91 | } 92 | 93 | @Test 94 | void getClosestParking_returnNotFound_whenNoParkingsAvailable() throws Exception { 95 | String address = "test place"; 96 | NominatimLocation location = new NominatimLocation(37.0, -158.0); 97 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.just(location)); 98 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(Collections.emptyList()); 99 | 100 | mockMvc.perform(get("/address").queryParam("address", address).accept(MediaType.APPLICATION_JSON)) 101 | .andExpect(status().isNotFound()) 102 | .andExpect(jsonPath("$.errorMessage", anything())); 103 | } 104 | 105 | @Test 106 | public void getParkingByParams_returnAllParkings_whenNoParamsGiven() throws Exception { 107 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings); 108 | 109 | mockMvc.perform(get("").accept(MediaType.APPLICATION_JSON)) 110 | .andExpect(status().isOk()) 111 | .andExpect(content().json(objectMapper.writeValueAsString(parkings))); 112 | } 113 | 114 | @Test 115 | public void getParkingBySymbol_returnFoundParking() throws Exception { 116 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings); 117 | 118 | mockMvc.perform(get("/symbol").accept(MediaType.APPLICATION_JSON).queryParam("symbol", "P1")) 119 | .andExpect(status().isOk()) 120 | .andExpect(jsonPath("$.parkingId", is(1))) 121 | .andExpect(jsonPath("$.name", is("Parking 1"))) 122 | .andExpect(jsonPath("$.address.geoLatitude").value(37.1f)) 123 | .andExpect(jsonPath("$.address.geoLongitude").value(-158.8f)); 124 | } 125 | 126 | @Test 127 | public void getParkingByName_returnNoParkings() throws Exception { 128 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings); 129 | 130 | mockMvc.perform(get("/name").accept(MediaType.APPLICATION_JSON).queryParam("name", "Non-existent name")) 131 | .andExpect(status().isNotFound()) 132 | .andExpect(jsonPath("$.errorMessage", anything())); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/main/ParkingServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.main; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import static org.mockito.Mockito.*; 6 | 7 | import java.time.LocalTime; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | import pl.wrapper.parking.facade.dto.main.NominatimLocation; 17 | import pl.wrapper.parking.infrastructure.error.ParkingError; 18 | import pl.wrapper.parking.infrastructure.error.Result; 19 | import pl.wrapper.parking.infrastructure.nominatim.client.NominatimClient; 20 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 21 | import pl.wrapper.parking.pwrResponseHandler.dto.Address; 22 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 23 | import reactor.core.publisher.Flux; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | public class ParkingServiceImplTest { 27 | @Mock 28 | private PwrApiServerCaller pwrApiServerCaller; 29 | 30 | @Mock 31 | private NominatimClient nominatimClient; 32 | 33 | private ParkingServiceImpl parkingService; 34 | 35 | private List parkings1; 36 | private List parkings2; 37 | 38 | @BeforeEach 39 | void setUp() { 40 | parkingService = new ParkingServiceImpl(pwrApiServerCaller, nominatimClient); 41 | parkings1 = List.of( 42 | ParkingResponse.builder() 43 | .parkingId(1) 44 | .name("Parking 1") 45 | .symbol("P1") 46 | .address(new Address("street 1", 37.1f, -158.8f)) 47 | .build(), 48 | ParkingResponse.builder() 49 | .parkingId(2) 50 | .name("Parking 2") 51 | .symbol("P2") 52 | .address(new Address("street 2", -44.4f, 123.6f)) 53 | .build()); 54 | parkings2 = List.of( 55 | ParkingResponse.builder() 56 | .parkingId(1) 57 | .name("Parking 1") 58 | .symbol("P1") 59 | .freeSpots(0) 60 | .openingHours(null) 61 | .closingHours(null) 62 | .build(), 63 | ParkingResponse.builder() 64 | .parkingId(2) 65 | .name("Parking 2") 66 | .symbol("P2") 67 | .freeSpots(325) 68 | .openingHours(LocalTime.NOON) 69 | .closingHours(LocalTime.NOON) 70 | .build(), 71 | ParkingResponse.builder() 72 | .parkingId(3) 73 | .name("Parking 3") 74 | .symbol("P3") 75 | .freeSpots(117) 76 | .openingHours(LocalTime.NOON) 77 | .closingHours(LocalTime.NOON) 78 | .build(), 79 | ParkingResponse.builder() 80 | .parkingId(4) 81 | .name("Parking 4") 82 | .symbol("P4") 83 | .freeSpots(51) 84 | .openingHours(null) 85 | .closingHours(null) 86 | .build()); 87 | } 88 | 89 | @Test 90 | void getClosestParking_returnSuccessWithClosestParking() { 91 | String address = "test place"; 92 | NominatimLocation location = new NominatimLocation(37.0, -158.0); 93 | 94 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.just(location)); 95 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings1); 96 | 97 | Result result = parkingService.getClosestParking(address); 98 | assertThat(result.isSuccess()).isTrue(); 99 | assertThat(result.getData()).matches(p -> p.name().equals("Parking 1")); 100 | 101 | verify(nominatimClient).search(address, "json"); 102 | verify(pwrApiServerCaller).fetchParkingData(); 103 | } 104 | 105 | @Test 106 | void getClosestParking_returnFailureOfAddressNotFound_whenNoResultsFromApi() { 107 | String address = "non-existent address"; 108 | 109 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.empty()); 110 | 111 | Result result = parkingService.getClosestParking(address); 112 | assertThat(result.isSuccess()).isFalse(); 113 | assertThat(result.getError()).isInstanceOf(ParkingError.ParkingNotFoundByAddress.class); 114 | 115 | verify(nominatimClient).search(address, "json"); 116 | verify(pwrApiServerCaller, never()).fetchParkingData(); 117 | } 118 | 119 | @Test 120 | void getClosestParking_returnFailureOfAddressNotFound_whenNoParkingsAvailable() { 121 | String address = "test place"; 122 | NominatimLocation location = new NominatimLocation(37.0, -158.0); 123 | 124 | when(nominatimClient.search(eq(address), anyString())).thenReturn(Flux.just(location)); 125 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(Collections.emptyList()); 126 | 127 | Result result = parkingService.getClosestParking(address); 128 | assertThat(result.isSuccess()).isFalse(); 129 | assertThat(result.getError()).isInstanceOf(ParkingError.ParkingNotFoundByAddress.class); 130 | 131 | verify(nominatimClient).search(address, "json"); 132 | verify(pwrApiServerCaller).fetchParkingData(); 133 | } 134 | 135 | @Test() 136 | void getAllParkingsWithFreeSpots_shouldReturnList() { 137 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 138 | List result = parkingService.getAllWithFreeSpots(null); 139 | 140 | assertEquals(3, result.size()); 141 | assertTrue(result.stream().allMatch(parking -> parking.freeSpots() > 0)); 142 | } 143 | 144 | @Test 145 | void getOpenedParkingsWithFreeSpots_shouldReturnList() { 146 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 147 | List result = parkingService.getAllWithFreeSpots(true); 148 | 149 | assertEquals(1, result.size()); 150 | assertTrue(result.stream().allMatch(parking -> parking.freeSpots() > 0 && parking.isOpened())); 151 | } 152 | 153 | @Test 154 | void getOpenedParkingsWithFreeSpots_shouldReturnEmptyList() { 155 | List parkingDataLocal = new ArrayList<>(parkings2); 156 | parkingDataLocal.remove(3); 157 | 158 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkingDataLocal); 159 | List result = parkingService.getAllWithFreeSpots(true); 160 | 161 | assertEquals(0, result.size()); 162 | } 163 | 164 | @Test 165 | void getClosedParkingsWithFreeSpots_shouldReturnList() { 166 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 167 | List result = parkingService.getAllWithFreeSpots(false); 168 | 169 | assertEquals(2, result.size()); 170 | assertTrue(result.stream().allMatch(parking -> parking.freeSpots() > 0 && !parking.isOpened())); 171 | } 172 | 173 | @Test 174 | void getParkingWithTheMostFreeSpacesFromAll_shouldReturnSuccessResult() { 175 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 176 | Result result = parkingService.getWithTheMostFreeSpots(null); 177 | 178 | assertTrue(result.isSuccess()); 179 | assertEquals(325, result.getData().freeSpots()); 180 | assertEquals("P2", result.getData().symbol()); 181 | } 182 | 183 | @Test 184 | void getParkingWithTheMostFreeSpacesFromOpened_shouldReturnSuccessResult() { 185 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 186 | Result result = parkingService.getWithTheMostFreeSpots(true); 187 | 188 | assertTrue(result.isSuccess()); 189 | assertEquals(51, result.getData().freeSpots()); 190 | assertEquals("P4", result.getData().symbol()); 191 | } 192 | 193 | @Test 194 | void getParkingWithTheMostFreeSpacesFromClosed_shouldReturnSuccessResult() { 195 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkings2); 196 | Result result = parkingService.getWithTheMostFreeSpots(false); 197 | 198 | assertTrue(result.isSuccess()); 199 | assertEquals(325, result.getData().freeSpots()); 200 | assertEquals("P2", result.getData().symbol()); 201 | } 202 | 203 | @Test 204 | void getParkingWithTheMostFreeSpacesFromClosed_shouldReturnNotFoundError() { 205 | List parkingDataLocal = new ArrayList<>(parkings2); 206 | parkingDataLocal.remove(2); 207 | parkingDataLocal.remove(1); 208 | when(pwrApiServerCaller.fetchParkingData()).thenReturn(parkingDataLocal); 209 | Result result = parkingService.getWithTheMostFreeSpots(false); 210 | 211 | assertFalse(result.isSuccess()); 212 | assertInstanceOf(ParkingError.NoFreeParkingSpotsAvailable.class, result.getError()); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/stats/parking/ParkingStatsControllerIT.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.stats.parking; 2 | 3 | import static java.time.DayOfWeek.*; 4 | import static org.hamcrest.CoreMatchers.anything; 5 | import static org.hamcrest.CoreMatchers.is; 6 | import static org.mockito.Mockito.*; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | 11 | import java.time.LocalTime; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import org.hamcrest.Matchers; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.boot.test.mock.mockito.MockBean; 22 | import org.springframework.http.MediaType; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | import pl.wrapper.parking.infrastructure.inMemory.ParkingDataRepository; 25 | import pl.wrapper.parking.infrastructure.inMemory.dto.parking.AvailabilityData; 26 | import pl.wrapper.parking.infrastructure.inMemory.dto.parking.ParkingData; 27 | 28 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 29 | @AutoConfigureMockMvc 30 | public class ParkingStatsControllerIT { 31 | 32 | @Autowired 33 | private MockMvc mockMvc; 34 | 35 | @MockBean 36 | private ParkingDataRepository dataRepository; 37 | 38 | private List parkingData; 39 | 40 | @BeforeEach 41 | public void setUp() { 42 | parkingData = List.of( 43 | ParkingData.builder() 44 | .parkingId(1) 45 | .totalSpots(100) 46 | .freeSpotsHistory(Map.of( 47 | MONDAY, 48 | Map.of( 49 | LocalTime.of(10, 0), new AvailabilityData(1, 0.8), 50 | LocalTime.of(12, 0), new AvailabilityData(1, 0.5)), 51 | TUESDAY, 52 | Map.of(LocalTime.of(10, 0), new AvailabilityData(1, 0.7)))) 53 | .build(), 54 | ParkingData.builder() 55 | .parkingId(2) 56 | .totalSpots(200) 57 | .freeSpotsHistory(Map.of( 58 | MONDAY, Map.of(LocalTime.of(10, 0), new AvailabilityData(1, 0.6)), 59 | WEDNESDAY, Map.of(LocalTime.of(14, 0), new AvailabilityData(1, 0.9)))) 60 | .build()); 61 | } 62 | 63 | @Test 64 | void getParkingStats_withDayOfWeekAndTime_returnCorrectStats() throws Exception { 65 | when(dataRepository.values()).thenReturn(parkingData); 66 | 67 | mockMvc.perform(get("/stats") 68 | .accept(MediaType.APPLICATION_JSON) 69 | .queryParam("day_of_week", "MONDAY") 70 | .queryParam("time", "10:07:15")) 71 | .andExpect(status().isOk()) 72 | .andExpect(jsonPath("$.length()", is(2))) 73 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 74 | .andExpect(jsonPath("$[0].stats.averageAvailability", is(0.8))) 75 | .andExpect(jsonPath("$[1].parkingInfo.parkingId", is(2))) 76 | .andExpect(jsonPath("$[1].stats.averageAvailability", is(0.6))); 77 | } 78 | 79 | @Test 80 | void getParkingStats_withWeirdIdListAndTime_returnCorrectStats() throws Exception { 81 | when(dataRepository.fetchAllKeys()).thenReturn(Set.of(1, 2)); 82 | when(dataRepository.get(anyInt())).thenReturn(parkingData.get(0), parkingData.get(1)); 83 | 84 | mockMvc.perform(get("/stats") 85 | .accept(MediaType.APPLICATION_JSON) 86 | .queryParam("time", "10:07:15") 87 | .queryParam("ids", "1", "2", "3")) 88 | .andExpect(status().isOk()) 89 | .andExpect(jsonPath("$.length()", is(2))) 90 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 91 | .andExpect(jsonPath("$[0].stats.averageAvailability", is(0.75))) 92 | .andExpect(jsonPath("$[1].parkingInfo.parkingId", is(2))) 93 | .andExpect(jsonPath("$[1].stats.averageAvailability", is(0.6))); 94 | 95 | verify(dataRepository, never()).values(); 96 | } 97 | 98 | @Test 99 | void getParkingStats_withEmptyDataRepository_returnEmptyList() throws Exception { 100 | when(dataRepository.values()).thenReturn(List.of()); 101 | 102 | mockMvc.perform(get("/stats") 103 | .accept(MediaType.APPLICATION_JSON) 104 | .queryParam("time", "10:07:15") 105 | .queryParam("ids", "1", "2")) 106 | .andExpect(status().isOk()) 107 | .andExpect(jsonPath("$.length()", is(0))); 108 | } 109 | 110 | @Test 111 | void getParkingStats_withIncorrectTimeFormat_returnBadRequest() throws Exception { 112 | mockMvc.perform(get("/stats").accept(MediaType.APPLICATION_JSON).queryParam("time", "incorrect")) 113 | .andExpect(status().isBadRequest()) 114 | .andExpect(jsonPath("$.errorMessage", anything())); 115 | } 116 | 117 | @Test 118 | void getDailyParkingStats_withIdList_returnCorrectDailyStats() throws Exception { 119 | when(dataRepository.fetchAllKeys()).thenReturn(Set.of(1, 2)); 120 | when(dataRepository.get(anyInt())).thenReturn(parkingData.get(0), parkingData.get(1)); 121 | 122 | mockMvc.perform(get("/stats/daily") 123 | .accept(MediaType.APPLICATION_JSON) 124 | .queryParam("day_of_week", "MONDAY") 125 | .queryParam("ids", "1")) 126 | .andExpect(status().isOk()) 127 | .andExpect(jsonPath("$.length()", is(1))) 128 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 129 | .andExpect(jsonPath("$[0].stats.averageAvailability", is(0.65))) 130 | .andExpect(jsonPath("$[0].maxOccupancyAt", is("12:00:00"))) 131 | .andExpect(jsonPath("$[0].minOccupancyAt", is("10:00:00"))); 132 | 133 | verify(dataRepository, never()).values(); 134 | } 135 | 136 | @Test 137 | void getDailyParkingStats_withMissingDayOfWeek_returnBadRequest() throws Exception { 138 | mockMvc.perform(get("/stats/daily")) 139 | .andExpect(status().isBadRequest()) 140 | .andExpect(jsonPath("$.errorMessage", anything())); 141 | } 142 | 143 | @Test 144 | void getWeeklyParkingStats_withEmptyIdList_returnCorrectWeeklyStats() throws Exception { 145 | when(dataRepository.values()).thenReturn(parkingData); 146 | 147 | mockMvc.perform(get("/stats/weekly").accept(MediaType.APPLICATION_JSON)) 148 | .andExpect(status().isOk()) 149 | .andExpect(jsonPath("$.length()", is(2))) 150 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 151 | .andExpect(jsonPath("$[0].stats.averageAvailability", Matchers.closeTo(0.666, 0.0011))) 152 | .andExpect(jsonPath("$[0].maxOccupancyInfo.dayOfWeek", is("MONDAY"))) 153 | .andExpect(jsonPath("$[0].maxOccupancyInfo.time", is("12:00:00"))) 154 | .andExpect(jsonPath("$[0].minOccupancyInfo.dayOfWeek", is("MONDAY"))) 155 | .andExpect(jsonPath("$[0].minOccupancyInfo.time", is("10:00:00"))) 156 | .andExpect(jsonPath("$[1].parkingInfo.parkingId", is(2))) 157 | .andExpect(jsonPath("$[1].stats.averageAvailability", is(0.75))) 158 | .andExpect(jsonPath("$[1].maxOccupancyInfo.dayOfWeek", is("MONDAY"))) 159 | .andExpect(jsonPath("$[1].maxOccupancyInfo.time", is("10:00:00"))) 160 | .andExpect(jsonPath("$[1].minOccupancyInfo.dayOfWeek", is("WEDNESDAY"))) 161 | .andExpect(jsonPath("$[1].minOccupancyInfo.time", is("14:00:00"))); 162 | } 163 | 164 | @Test 165 | void getCollectiveDailyParkingStats_withWeirdIdList_returnCorrectCollectiveDailyStats() throws Exception { 166 | when(dataRepository.fetchAllKeys()).thenReturn(Set.of(1, 2)); 167 | when(dataRepository.get(1)).thenReturn(parkingData.getFirst()); 168 | 169 | mockMvc.perform(get("/stats/daily/collective") 170 | .accept(MediaType.APPLICATION_JSON) 171 | .queryParam("day_of_week", "MONDAY") 172 | .queryParam("ids", "1")) 173 | .andExpect(status().isOk()) 174 | .andExpect(jsonPath("$.length()", is(1))) 175 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 176 | .andExpect(jsonPath("$[0].statsMap", Matchers.aMapWithSize(2))) 177 | .andExpect(jsonPath("$[0].statsMap['10:00'].averageAvailability", is(0.8))) 178 | .andExpect(jsonPath("$[0].statsMap['12:00'].averageAvailability", is(0.5))); 179 | 180 | verify(dataRepository, never()).values(); 181 | } 182 | 183 | @Test 184 | void getCollectiveDailyParkingStats_withMissingDayOfWeek_returnBadRequest() throws Exception { 185 | mockMvc.perform(get("/stats/daily/collective")) 186 | .andExpect(status().isBadRequest()) 187 | .andExpect(jsonPath("$.errorMessage", anything())); 188 | } 189 | 190 | @Test 191 | void getCollectiveWeeklyParkingStats_withoutIdList_returnCorrectCollectiveWeeklyStats() throws Exception { 192 | when(dataRepository.values()).thenReturn(parkingData); 193 | 194 | mockMvc.perform(get("/stats/weekly/collective")) 195 | .andExpect(status().isOk()) 196 | .andExpect(jsonPath("$.length()", is(2))) 197 | .andExpect(jsonPath("$[0].parkingInfo.parkingId", is(1))) 198 | .andExpect(jsonPath("$[0].statsMap", Matchers.aMapWithSize(2))) 199 | .andExpect(jsonPath("$[0].statsMap['MONDAY']", Matchers.aMapWithSize(2))) 200 | .andExpect(jsonPath("$[0].statsMap['TUESDAY']", Matchers.aMapWithSize(1))) 201 | .andExpect(jsonPath("$[0].statsMap['MONDAY']['10:00'].averageAvailability", is(0.8))) 202 | .andExpect(jsonPath("$[0].statsMap['MONDAY']['12:00'].averageAvailability", is(0.5))) 203 | .andExpect(jsonPath("$[0].statsMap['TUESDAY']['10:00'].averageAvailability", is(0.7))) 204 | .andExpect(jsonPath("$[1].parkingInfo.parkingId", is(2))) 205 | .andExpect(jsonPath("$[1].statsMap", Matchers.aMapWithSize(2))) 206 | .andExpect(jsonPath("$[1].statsMap['MONDAY']", Matchers.aMapWithSize(1))) 207 | .andExpect(jsonPath("$[1].statsMap['WEDNESDAY']", Matchers.aMapWithSize(1))) 208 | .andExpect(jsonPath("$[1].statsMap['MONDAY']['10:00'].averageAvailability", is(0.6))) 209 | .andExpect(jsonPath("$[1].statsMap['WEDNESDAY']['14:00'].averageAvailability", is(0.9))); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/stats/request/ParkingRequestStatsControllerTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.stats.request; 2 | 3 | import static org.mockito.Mockito.when; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 16 | import org.springframework.boot.test.mock.mockito.MockBean; 17 | import org.springframework.context.annotation.ComponentScan; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import pl.wrapper.parking.facade.ParkingRequestStatsService; 21 | import pl.wrapper.parking.facade.dto.stats.request.EndpointStats; 22 | 23 | @WebMvcTest(ParkingRequestStatsController.class) 24 | @ComponentScan({ 25 | "pl.wrapper.parking.infrastructure", 26 | "pl.wrapper.parking.facade.main", 27 | "pl.wrapper.parking.pwrResponseHandler.domain" 28 | }) 29 | public class ParkingRequestStatsControllerTest { 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @MockBean 34 | private ParkingRequestStatsService parkingRequestStatsService; 35 | 36 | @Test 37 | void getBasicRequestStats_shouldReturnData() throws Exception { 38 | Map stats = Map.of( 39 | "parkings/free", new EndpointStats(3, 2, 0.67), 40 | "parkings", new EndpointStats(5, 3, 0.6)); 41 | 42 | when(parkingRequestStatsService.getBasicRequestStats()).thenReturn(stats); 43 | 44 | mockMvc.perform(get("/stats/requests").accept(MediaType.APPLICATION_JSON)) 45 | .andExpect(status().isOk()) 46 | .andExpect(content().json(new ObjectMapper().writeValueAsString(stats))); 47 | } 48 | 49 | @Test 50 | void getBasicRequestStats_shouldReturnEmptyData() throws Exception { 51 | when(parkingRequestStatsService.getBasicRequestStats()).thenReturn(Collections.emptyMap()); 52 | 53 | mockMvc.perform(get("/stats/requests").accept(MediaType.APPLICATION_JSON)) 54 | .andExpect(status().isOk()) 55 | .andExpect(content().json("{}")); 56 | } 57 | 58 | @Test 59 | void getRequestStatsForTimes_shouldReturnData() throws Exception { 60 | Map>> timeStats = Map.of( 61 | "parkings/free", List.of(Map.entry("00:00 - 00:30", 0.4), Map.entry("00:30 - 01:00", 0.32)), 62 | "parkings", List.of(Map.entry("00:00 - 00:30", 0.31), Map.entry("00:30 - 01:00", 0.26))); 63 | 64 | when(parkingRequestStatsService.getRequestStatsForTimes()).thenReturn(timeStats); 65 | 66 | mockMvc.perform(get("/stats/requests/times").accept(MediaType.APPLICATION_JSON)) 67 | .andExpect(status().isOk()) 68 | .andExpect(content().json(new ObjectMapper().writeValueAsString(timeStats))); 69 | } 70 | 71 | @Test 72 | void getRequestStatsForTimes_shouldReturnEmptyData() throws Exception { 73 | when(parkingRequestStatsService.getRequestStatsForTimes()).thenReturn(Collections.emptyMap()); 74 | 75 | mockMvc.perform(get("/stats/requests/times").accept(MediaType.APPLICATION_JSON)) 76 | .andExpect(status().isOk()) 77 | .andExpect(content().json("{}")); 78 | } 79 | 80 | @Test 81 | void getRequestPeakTimes_shouldReturnData() throws Exception { 82 | List> peakTimesStats = List.of( 83 | Map.entry("13:00 - 13:30", 23.0), Map.entry("18:30 - 19:00", 21.2), Map.entry("9:00 - 9:30", 18.4)); 84 | 85 | when(parkingRequestStatsService.getRequestPeakTimes()).thenReturn(peakTimesStats); 86 | 87 | mockMvc.perform(get("/stats/requests/peak-times").accept(MediaType.APPLICATION_JSON)) 88 | .andExpect(status().isOk()) 89 | .andExpect(content().json(new ObjectMapper().writeValueAsString(peakTimesStats))); 90 | } 91 | 92 | @Test 93 | void getRequestPeakTimes_shouldReturnEmptyData() throws Exception { 94 | when(parkingRequestStatsService.getRequestPeakTimes()).thenReturn(Collections.emptyList()); 95 | 96 | mockMvc.perform(get("/stats/requests/peak-times").accept(MediaType.APPLICATION_JSON)) 97 | .andExpect(status().isOk()) 98 | .andExpect(content().json("[]")); 99 | } 100 | 101 | @Test 102 | void getDailyRequestStats_shouldReturnData() throws Exception { 103 | Map dailyStats = Map.of( 104 | "parkings/free", 2.8, 105 | "parkings/free/top", 2.5, 106 | "parkings", 4.0); 107 | 108 | when(parkingRequestStatsService.getDailyRequestStats()).thenReturn(dailyStats); 109 | 110 | mockMvc.perform(get("/stats/requests/day").accept(MediaType.APPLICATION_JSON)) 111 | .andExpect(status().isOk()) 112 | .andExpect(content().json(new ObjectMapper().writeValueAsString(dailyStats))); 113 | } 114 | 115 | @Test 116 | void getDailyRequestStats_shouldReturnEmptyData() throws Exception { 117 | when(parkingRequestStatsService.getDailyRequestStats()).thenReturn(Collections.emptyMap()); 118 | 119 | mockMvc.perform(get("/stats/requests/day").accept(MediaType.APPLICATION_JSON)) 120 | .andExpect(status().isOk()) 121 | .andExpect(content().json("{}")); 122 | } 123 | 124 | @Test 125 | void getDailyRequestStats_missingData_shouldReturnData() throws Exception { 126 | Map dailyStats = new HashMap<>(); 127 | dailyStats.put("parkings/free", 2.8); 128 | dailyStats.put("parkings/free/top", null); 129 | 130 | when(parkingRequestStatsService.getDailyRequestStats()).thenReturn(dailyStats); 131 | 132 | mockMvc.perform(get("/stats/requests/day").accept(MediaType.APPLICATION_JSON)) 133 | .andExpect(status().isOk()) 134 | .andExpect(content().json(new ObjectMapper().writeValueAsString(dailyStats))); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/facade/domain/stats/request/ParkingRequestStatsServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.facade.domain.stats.request; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Set; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import pl.wrapper.parking.facade.dto.stats.request.EndpointStats; 16 | import pl.wrapper.parking.infrastructure.inMemory.ParkingRequestRepository; 17 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.EndpointData; 18 | import pl.wrapper.parking.infrastructure.inMemory.dto.request.TimeframeStatistic; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class ParkingRequestStatsServiceImplTest { 22 | 23 | @Mock 24 | private ParkingRequestRepository requestRepository; 25 | 26 | private ParkingRequestStatsServiceImpl requestStatsService; 27 | 28 | private Set> dataEntries; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | EndpointData totalEndpoint = EndpointData.builder() 33 | .timeframeStatistics(new TimeframeStatistic[] { 34 | new TimeframeStatistic(10.0, 9, 4), 35 | new TimeframeStatistic(15.0, 15, 4), 36 | new TimeframeStatistic(23.0, 21, 3), 37 | new TimeframeStatistic(18.4, 17, 4), 38 | new TimeframeStatistic(21.2, 19, 5) 39 | }) 40 | .successCount(46) 41 | .requestCount(50) 42 | .timeframeLength(30) 43 | .build(); 44 | 45 | when(requestRepository.getTotalEndpoint()).thenReturn(totalEndpoint); 46 | 47 | requestStatsService = new ParkingRequestStatsServiceImpl(requestRepository); 48 | 49 | dataEntries = Set.of( 50 | Map.entry( 51 | "parkings/free", 52 | EndpointData.builder() 53 | .successCount(10) 54 | .requestCount(12) 55 | .timeframeLength(30) 56 | .timeframeStatistics(new TimeframeStatistic[] { 57 | new TimeframeStatistic(2.0, 5, 2), 58 | new TimeframeStatistic(3.0, 7, 3), 59 | new TimeframeStatistic(4.0, 8, 3) 60 | }) 61 | .build()), 62 | Map.entry( 63 | "parkings", 64 | EndpointData.builder() 65 | .successCount(26) 66 | .requestCount(29) 67 | .timeframeLength(30) 68 | .timeframeStatistics(new TimeframeStatistic[] { 69 | new TimeframeStatistic(9.1, 12, 7), 70 | new TimeframeStatistic(13.2, 14, 8), 71 | new TimeframeStatistic(18.7, 20, 7), 72 | new TimeframeStatistic(14.4, 19, 9) 73 | }) 74 | .build()), 75 | Map.entry( 76 | "parkings/address", 77 | EndpointData.builder() 78 | .successCount(6) 79 | .requestCount(13) 80 | .timeframeLength(30) 81 | .timeframeStatistics(new TimeframeStatistic[] { 82 | new TimeframeStatistic(2.5, 5, 4), new TimeframeStatistic(3.0, 6, 4) 83 | }) 84 | .build())); 85 | } 86 | 87 | @Test 88 | void getBasicRequestStats_shouldReturnData() { 89 | when(requestRepository.fetchAllEntries()).thenReturn(dataEntries); 90 | 91 | Map result = requestStatsService.getBasicRequestStats(); 92 | 93 | assertThat(result).hasSize(3); 94 | assertThat(result.get("parkings/free")) 95 | .extracting(EndpointStats::successfulRequests, EndpointStats::totalRequests, EndpointStats::successRate) 96 | .containsExactly(10L, 12L, 83.33); 97 | assertThat(result.get("parkings")) 98 | .extracting(EndpointStats::successfulRequests, EndpointStats::totalRequests, EndpointStats::successRate) 99 | .containsExactly(26L, 29L, 89.66); 100 | assertThat(result.get("parkings/address")) 101 | .extracting(EndpointStats::successfulRequests, EndpointStats::totalRequests, EndpointStats::successRate) 102 | .containsExactly(6L, 13L, 46.15); 103 | } 104 | 105 | @Test 106 | void getBasicRequestStats_shouldReturnEmptyData() { 107 | when(requestRepository.fetchAllEntries()).thenReturn(Collections.emptySet()); 108 | 109 | Map result = requestStatsService.getBasicRequestStats(); 110 | 111 | assertThat(result).isEmpty(); 112 | } 113 | 114 | @Test 115 | void getRequestStatsForTimes_shouldReturnData() { 116 | when(requestRepository.fetchAllEntries()).thenReturn(dataEntries); 117 | 118 | Map>> result = requestStatsService.getRequestStatsForTimes(); 119 | 120 | assertThat(result).hasSize(3); 121 | assertThat(result.get("parkings/free")) 122 | .extracting(Map.Entry::getKey, Map.Entry::getValue) 123 | .containsExactly(tuple("00:00 - 00:30", 2.0), tuple("00:30 - 01:00", 3.0), tuple("01:00 - 01:30", 4.0)); 124 | assertThat(result.get("parkings")) 125 | .extracting(Map.Entry::getKey, Map.Entry::getValue) 126 | .containsExactly( 127 | tuple("00:00 - 00:30", 9.1), 128 | tuple("00:30 - 01:00", 13.2), 129 | tuple("01:00 - 01:30", 18.7), 130 | tuple("01:30 - 02:00", 14.4)); 131 | assertThat(result.get("parkings/address")) 132 | .extracting(Map.Entry::getKey, Map.Entry::getValue) 133 | .containsExactly(tuple("00:00 - 00:30", 2.5), tuple("00:30 - 01:00", 3.0)); 134 | } 135 | 136 | @Test 137 | void getRequestStatsForTimes_shouldReturnEmptyData() { 138 | when(requestRepository.fetchAllEntries()).thenReturn(Collections.emptySet()); 139 | 140 | Map>> result = requestStatsService.getRequestStatsForTimes(); 141 | 142 | assertThat(result).isEmpty(); 143 | } 144 | 145 | @Test 146 | void getRequestPeakTimes_shouldReturnData() { 147 | List> result = requestStatsService.getRequestPeakTimes(); 148 | 149 | assertThat(result) 150 | .hasSize(3) 151 | .extracting(Map.Entry::getKey, Map.Entry::getValue) 152 | .containsExactly( 153 | tuple("01:00 - 01:30", 23.0), tuple("02:00 - 02:30", 21.2), tuple("01:30 - 02:00", 18.4)); 154 | } 155 | 156 | @Test 157 | void getRequestPeakTimes_shouldReturnEmptyData() { 158 | when(requestRepository.getTotalEndpoint()) 159 | .thenReturn(EndpointData.builder() 160 | .timeframeStatistics(new TimeframeStatistic[] {}) 161 | .build()); 162 | 163 | List> result = requestStatsService.getRequestPeakTimes(); 164 | 165 | assertThat(result).isEmpty(); 166 | } 167 | 168 | @Test 169 | void getRequestPeakTimes_missingTimeframe_shouldReturnData() { 170 | when(requestRepository.getTotalEndpoint()) 171 | .thenReturn(EndpointData.builder() 172 | .timeframeStatistics(new TimeframeStatistic[] { 173 | new TimeframeStatistic(26.5, 18, 5), new TimeframeStatistic(31.7, 21, 6), 174 | }) 175 | .build()); 176 | 177 | List> result = requestStatsService.getRequestPeakTimes(); 178 | 179 | assertThat(result) 180 | .hasSize(2) 181 | .extracting(Map.Entry::getKey, Map.Entry::getValue) 182 | .containsExactly(tuple("00:30 - 01:00", 31.7), tuple("00:00 - 00:30", 26.5)); 183 | } 184 | 185 | @Test 186 | void getDailyRequestStats_shouldReturnData() { 187 | when(requestRepository.fetchAllEntries()).thenReturn(dataEntries); 188 | 189 | Map result = requestStatsService.getDailyRequestStats(); 190 | 191 | assertThat(result).hasSize(3); 192 | assertThat(result.get("parkings/free")).isEqualTo(9.0); 193 | assertThat(result.get("parkings")).isEqualTo(55.4); 194 | assertThat(result.get("parkings/address")).isEqualTo(5.5); 195 | } 196 | 197 | @Test 198 | void getDailyRequestStats_shouldReturnEmptyData() { 199 | when(requestRepository.fetchAllEntries()).thenReturn(Collections.emptySet()); 200 | 201 | Map result = requestStatsService.getDailyRequestStats(); 202 | 203 | assertThat(result).isEmpty(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/infrastructure/inMemory/InMemoryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.inMemory; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.io.File; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class InMemoryRepositoryTest { 14 | 15 | private InMemoryRepositoryTestImpl inMemoryRepository; 16 | private Integer id; 17 | private String value = "value"; 18 | private static final String path = "data/statistics/tests"; 19 | 20 | static class InMemoryRepositoryTestImpl extends InMemoryRepositoryImpl { 21 | 22 | public InMemoryRepositoryTestImpl(String filePath, Map map, String defaultValue) { 23 | super(filePath, map, defaultValue); 24 | } 25 | 26 | public void testSerialize() { 27 | periodicSerialize(); 28 | } 29 | 30 | public void testDeserialize() { 31 | init(); 32 | } 33 | 34 | public void deleteData() { 35 | dataMap.clear(); 36 | } 37 | } 38 | 39 | @BeforeEach 40 | void setUp() { 41 | inMemoryRepository = new InMemoryRepositoryTestImpl(path, new HashMap<>(), null); 42 | value = "value"; 43 | id = 10; 44 | } 45 | 46 | @SuppressWarnings("ResultOfMethodCallIgnored") 47 | @AfterEach 48 | void tearDown() { 49 | File file = new File(path); 50 | file.delete(); 51 | } 52 | 53 | @Test 54 | void shouldReturnObject() { 55 | inMemoryRepository.add(id, value); 56 | 57 | int first = inMemoryRepository.fetchAllKeys().stream().findFirst().orElseThrow(); 58 | 59 | assertEquals(first, id); 60 | assertEquals(inMemoryRepository.get(first), value); 61 | } 62 | 63 | @Test 64 | void shouldSerializationRunCorrectly() { 65 | inMemoryRepository.add(id, value); 66 | 67 | inMemoryRepository.testSerialize(); 68 | inMemoryRepository.deleteData(); 69 | 70 | assertTrue(inMemoryRepository.fetchAllKeys().isEmpty()); 71 | 72 | inMemoryRepository.testDeserialize(); 73 | assertEquals(inMemoryRepository.get(id), value); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/infrastructure/nominatim/configuration/NominatimClientTests.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.infrastructure.nominatim.configuration; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import java.io.IOException; 7 | import java.util.List; 8 | import mockwebserver3.MockResponse; 9 | import mockwebserver3.MockWebServer; 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import pl.wrapper.parking.facade.dto.main.NominatimLocation; 17 | import pl.wrapper.parking.infrastructure.exception.NominatimClientException; 18 | import pl.wrapper.parking.infrastructure.nominatim.client.NominatimClient; 19 | import reactor.core.publisher.Flux; 20 | import reactor.test.StepVerifier; 21 | 22 | @SpringBootTest 23 | @ContextConfiguration(classes = NominatimClientConfig.class) 24 | public class NominatimClientTests { 25 | private static MockWebServer mockWebServer; 26 | private final ObjectMapper objectMapper = new ObjectMapper(); 27 | 28 | @Autowired 29 | private NominatimClient nominatimClient; 30 | 31 | @BeforeAll 32 | static void beforeAll() throws IOException { 33 | mockWebServer = new MockWebServer(); 34 | mockWebServer.start(); 35 | System.setProperty("maps.api.url", mockWebServer.url("/").toString()); 36 | } 37 | 38 | @AfterAll 39 | static void afterAll() throws IOException { 40 | mockWebServer.shutdown(); 41 | } 42 | 43 | @Test 44 | void returnLocations_whenSearchSuccessful() throws IOException { 45 | double lat1 = 53.8927406; 46 | double lon1 = 25.3019590; 47 | double lat2 = 56.9339537; 48 | double lon2 = 13.7331028; 49 | 50 | List locations = 51 | List.of(new NominatimLocation(lat1, lon1), new NominatimLocation(lat2, lon2)); 52 | 53 | mockWebServer.enqueue(new MockResponse() 54 | .newBuilder() 55 | .body(objectMapper.writeValueAsString(locations)) 56 | .addHeader("Content-Type", "application/json") 57 | .build()); 58 | 59 | Flux locationFlux = nominatimClient.search("Lida", "json"); 60 | StepVerifier.create(locationFlux) 61 | .expectNextMatches(loc -> loc.latitude() == lat1 && loc.longitude() == lon1) 62 | .expectNextMatches(loc -> loc.latitude() == lat2 && loc.longitude() == lon2) 63 | .verifyComplete(); 64 | } 65 | 66 | @Test 67 | void returnNothing_whenLocationNotFound() throws IOException { 68 | List locations = List.of(); 69 | 70 | mockWebServer.enqueue(new MockResponse() 71 | .newBuilder() 72 | .body(objectMapper.writeValueAsString(locations)) 73 | .addHeader("Content-Type", "application/json") 74 | .build()); 75 | 76 | Flux locationFlux = nominatimClient.search("Non-existent", "json"); 77 | StepVerifier.create(locationFlux).expectNextCount(0).verifyComplete(); 78 | } 79 | 80 | @Test 81 | void throwException_whenSearchFailed() { 82 | String message = "Internal Server Error"; 83 | 84 | mockWebServer.enqueue( 85 | new MockResponse().newBuilder().code(500).body(message).build()); 86 | 87 | Flux locationFlux = nominatimClient.search("Lida", "json"); 88 | StepVerifier.create(locationFlux) 89 | .expectErrorSatisfies(error -> assertThat(error) 90 | .isInstanceOf(NominatimClientException.class) 91 | .hasMessageContaining(message)) 92 | .verify(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/pwrResponseHandler/configuration/WebClientTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.configuration; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.mockito.Mockito.*; 6 | import static pl.wrapper.parking.pwrResponseHandler.configuration.WebClientConfig.buildRetryFilter; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mockito; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.reactive.function.client.ClientResponse; 12 | import org.springframework.web.reactive.function.client.ExchangeFilterFunction; 13 | import org.springframework.web.reactive.function.client.ExchangeFunction; 14 | import org.springframework.web.reactive.function.client.WebClient; 15 | import pl.wrapper.parking.infrastructure.exception.PwrApiNotRespondingException; 16 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 17 | import pl.wrapper.parking.pwrResponseHandler.domain.PwrApiCaller; 18 | import pl.wrapper.parking.pwrResponseHandler.domain.PwrApiServerCallerImpl; 19 | import reactor.core.publisher.Mono; 20 | import reactor.test.StepVerifier; 21 | 22 | class WebClientTest { 23 | 24 | @Test 25 | void testResponseFilterAndRetryFilterIntegration() { 26 | ClientResponse forbiddenResponse = ClientResponse.create(HttpStatus.FORBIDDEN) 27 | .body("Access Denied") 28 | .build(); 29 | ExchangeFunction mockExchangeFunction = mock(ExchangeFunction.class); 30 | when(mockExchangeFunction.exchange(any())).thenReturn(Mono.just(forbiddenResponse)); 31 | WebClient webClientWithFilters = WebClient.builder() 32 | .filter(buildRetryFilter()) 33 | .filter(ExchangeFilterFunction.ofResponseProcessor(WebClientConfig::responseFilter)) 34 | .exchangeFunction(mockExchangeFunction) 35 | .build(); 36 | 37 | Mono response = 38 | webClientWithFilters.get().uri("/mock-uri").retrieve().bodyToMono(String.class); 39 | 40 | StepVerifier.create(response) 41 | .expectErrorMatches(throwable -> throwable instanceof PwrApiNotRespondingException 42 | && throwable.getMessage().equals("Access Denied")) 43 | .verify(); 44 | verify(mockExchangeFunction, times(4)).exchange(any()); 45 | } 46 | 47 | @Test 48 | void shouldReturnException() { 49 | Exception provided = new ClassCastException("simulated"); 50 | 51 | PwrApiCaller apiCaller = Mockito.mock(PwrApiCaller.class); 52 | Mockito.when(apiCaller.fetchParkingPlaces()).thenReturn(Mono.error(provided)); 53 | 54 | PwrApiServerCaller pwrApiServerCaller = new PwrApiServerCallerImpl(apiCaller); 55 | Exception e = assertThrows(provided.getClass(), pwrApiServerCaller::fetchParkingData); 56 | 57 | assertEquals(provided.getMessage(), e.getMessage()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/pwrResponseHandler/domain/PwrApiCaller.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.domain; 2 | 3 | import java.time.LocalTime; 4 | import java.util.List; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.stereotype.Component; 8 | import pl.wrapper.parking.pwrResponseHandler.dto.Address; 9 | import pl.wrapper.parking.pwrResponseHandler.dto.ParkingResponse; 10 | import reactor.core.publisher.Mono; 11 | 12 | @Profile("test") 13 | @Component 14 | @RequiredArgsConstructor 15 | public final class PwrApiCaller { 16 | 17 | public Mono> fetchParkingPlaces() { 18 | return Mono.just(List.of( 19 | ParkingResponse.builder() 20 | .parkingId(1) 21 | .freeSpots(15) 22 | .totalSpots(50) 23 | .name("Central Parking") 24 | .symbol("CEN") 25 | .openingHours(LocalTime.of(8, 0)) 26 | .closingHours(LocalTime.of(20, 0)) 27 | .address(new Address("Main St", 10.21f, 4.32f)) 28 | .build(), 29 | ParkingResponse.builder() 30 | .parkingId(2) 31 | .freeSpots(5) 32 | .totalSpots(30) 33 | .name("Westside Parking") 34 | .symbol("WSP") 35 | .openingHours(LocalTime.of(6, 0)) 36 | .closingHours(LocalTime.of(22, 0)) 37 | .address(new Address("West St", 10f, -4f)) 38 | .build(), 39 | ParkingResponse.builder() 40 | .parkingId(3) 41 | .freeSpots(0) 42 | .totalSpots(100) 43 | .name("Airport Parking") 44 | .symbol("AIR") 45 | .openingHours(null) 46 | .closingHours(null) 47 | .address(new Address("Airport Rd", 13.0f, 2.0f)) 48 | .build(), 49 | ParkingResponse.builder() 50 | .parkingId(4) 51 | .freeSpots(10) 52 | .totalSpots(60) 53 | .name("Eastside Parking") 54 | .symbol("ESP") 55 | .openingHours(LocalTime.of(7, 30)) 56 | .closingHours(LocalTime.of(21, 30)) 57 | .address(new Address("East St", 12.0f, 12.0f)) 58 | .build(), 59 | ParkingResponse.builder() 60 | .parkingId(5) 61 | .freeSpots(25) 62 | .totalSpots(50) 63 | .name("Downtown Parking") 64 | .symbol("DTP") 65 | .openingHours(LocalTime.of(8, 0)) 66 | .closingHours(LocalTime.of(18, 0)) 67 | .address(new Address("Downtown Ln", 0.3f, 2.1f)) 68 | .build())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/pl/wrapper/parking/pwrResponseHandler/domain/PwrApiCallerTest.java: -------------------------------------------------------------------------------- 1 | package pl.wrapper.parking.pwrResponseHandler.domain; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.Mockito; 8 | import pl.wrapper.parking.pwrResponseHandler.PwrApiServerCaller; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class PwrApiCallerTest { 12 | @Test 13 | void shouldReturnException() { 14 | Exception provided = new ClassCastException("simulated"); 15 | 16 | PwrApiCaller apiCaller = Mockito.mock(PwrApiCaller.class); 17 | Mockito.when(apiCaller.fetchParkingPlaces()).thenReturn(Mono.error(provided)); 18 | 19 | PwrApiServerCaller pwrApiServerCaller = new PwrApiServerCallerImpl(apiCaller); 20 | Exception e = assertThrows(provided.getClass(), pwrApiServerCaller::fetchParkingData); 21 | 22 | assertEquals(provided.getMessage(), e.getMessage()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=parking 2 | spring.profiles.active=test 3 | server.servlet.context-path=/v1 4 | server.port=8080 5 | 6 | maps.api.url=https://nominatim.openstreetmap.org 7 | 8 | pwr-api.data-fetch.minutes=10 9 | 10 | serialization.timeStamp.inMinutes=10 11 | 12 | serialization.location=data/statistics 13 | serialization.location.ParkingRequests=${serialization.location}/requests 14 | serialization.location.parkingData=${serialization.location}/data 15 | 16 | timeframe.default.length.inMinutes=30 17 | 18 | spring.jpa.defer-datasource-initialization=true 19 | spring.sql.init.schema-locations=classpath:/schema.sql 20 | spring.sql.init.mode=always 21 | spring.jpa.open-in-view=false 22 | spring.jpa.properties.hibernate.order_updates=true 23 | spring.jpa.properties.hibernate.order_inserts=true 24 | 25 | spring.datasource.url=jdbc:h2:mem:libraryH2 26 | spring.datasource.driver-class-name=org.h2.Driver 27 | spring.datasource.username=user 28 | spring.datasource.password=password 29 | 30 | historic.data-update.minutes=480 -------------------------------------------------------------------------------- /src/test/resources/schema.sql: -------------------------------------------------------------------------------- 1 | -- noinspection SqlResolveForFile 2 | 3 | DROP SCHEMA IF EXISTS test CASCADE; 4 | CREATE SCHEMA test; 5 | -- h2 and hibernate do not support 2d arrays; this schema is purely to make hibernate quiet down during tests 6 | CREATE TABLE test.historic_data(data_table TINYINT ARRAY NOT NULL, date DATE NOT NULL PRIMARY KEY); --------------------------------------------------------------------------------