feat: initial commit - Z3 backend (Clean Architecture / DDD)

This commit is contained in:
Jérémy Munsch 2026-11-01 14:50:34 +01:00
commit 4efa3d8bbf
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
566 changed files with 40611 additions and 0 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
#*
#!target/*-runner
#!target/*-runner.jar
#!target/lib/*
#!target/quarkus-app/*

46
.gitignore vendored Normal file
View file

@ -0,0 +1,46 @@
#Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
release.properties
.flattened-pom.xml
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/
# KEYS
*.pem

56
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,56 @@
image: quay.io/buildah/stable
stages:
- test
- release
- build
branch-release:
image: docker.io/bitnami/git
variables:
GIT_STRATEGY: clone
stage: release
script:
# create branch ; if the tag is v2024.01, the branch will be rel/2024.01 (without the v)
- git switch -c rel/${CI_COMMIT_TAG:1}
- git push origin rel/${CI_COMMIT_TAG:1}
rules:
# only for tags like v2024.01, v2024.01.01, v2024.01-1, v2024.01.01-1
- if: $CI_COMMIT_TAG =~ /^v\d{4}\.\d{2}(\.\d{2})?(-\d+)?$/
build-staging:
stage: build
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:staging
before_script:
- buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- buildah bud -t "$IMAGE_TAG" --build-arg GITLAB_MVN_TOKEN=$CI_JOB_TOKEN .
- buildah push "$IMAGE_TAG"
only:
- main
build-feat:
stage: build
before_script:
# CI_COMMIT_BRANCH without "feat/" prefix
- export IMAGE_TAG=$CI_REGISTRY_IMAGE:${CI_COMMIT_BRANCH/feat\//feat-}
- buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- buildah bud -t "$IMAGE_TAG" --build-arg GITLAB_MVN_TOKEN=$CI_JOB_TOKEN .
- buildah push "$IMAGE_TAG"
rules:
- when: manual
if: '$CI_COMMIT_BRANCH =~ /^feat\//'
build-release:
stage: build
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:latest
before_script:
- buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- buildah bud -t "$IMAGE_TAG" --build-arg GITLAB_MVN_TOKEN=$CI_JOB_TOKEN .
- buildah push "$IMAGE_TAG"
only:
- /^rel\//

1
.mvn/wrapper/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
maven-wrapper.jar

View file

@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public final class MavenWrapperDownloader
{
private static final String WRAPPER_VERSION = "3.2.0";
private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) );
public static void main( String[] args )
{
log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION );
if ( args.length != 2 )
{
System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" );
System.exit( 1 );
}
try
{
log( " - Downloader started" );
final URL wrapperUrl = new URL( args[0] );
final String jarPath = args[1].replace( "..", "" ); // Sanitize path
final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize();
downloadFileFromURL( wrapperUrl, wrapperJarPath );
log( "Done" );
}
catch ( IOException e )
{
System.err.println( "- Error downloading: " + e.getMessage() );
if ( VERBOSE )
{
e.printStackTrace();
}
System.exit( 1 );
}
}
private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath )
throws IOException
{
log( " - Downloading to: " + wrapperJarPath );
if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null )
{
final String username = System.getenv( "MVNW_USERNAME" );
final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray();
Authenticator.setDefault( new Authenticator()
{
@Override
protected PasswordAuthentication getPasswordAuthentication()
{
return new PasswordAuthentication( username, password );
}
} );
}
try ( InputStream inStream = wrapperUrl.openStream() )
{
Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING );
}
log( " - Downloader complete" );
}
private static void log( String msg )
{
if ( VERBOSE )
{
System.out.println( msg );
}
}
}

18
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View file

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

245
CHANGELOG.md Normal file
View file

@ -0,0 +1,245 @@
## (2025-03-27)
## 1.1.0 (2025-03-27)
### Features
* **global:** Add some statistics resources to get some graphics ([8f12c0f](https://gitlab.pegase.tech/z3/z3-backend/commit/8f12c0f1947ff9c61e1dbe9fe608d4e6b03c8f21))
* **global:** Add statistics domain and simplify UUID handling ([377249d](https://gitlab.pegase.tech/z3/z3-backend/commit/377249d5de7f3ec788c3372b661dbb61d638a2b9))
### Bug Fixes
* **domain:** Allow Referer path to be empty ([1884049](https://gitlab.pegase.tech/z3/z3-backend/commit/188404953b2171df239c0a10b775bae79bda8919))
* **domain:** DomainNameService could not reliably allow assign a fqdn to a brand that already owns it ([07e02be](https://gitlab.pegase.tech/z3/z3-backend/commit/07e02be02d2c025148726a0e44e92e383b707fa8)), closes [#13](https://gitlab.pegase.tech/z3/z3-backend/issues/13)
* **domain:** Fix CreateFingerPrintTest ([bce237d](https://gitlab.pegase.tech/z3/z3-backend/commit/bce237d76428f7498a53b33c396067fe43277572))
* **domain:** Link Statistic for visits was no correctly using nullable ([7e885a4](https://gitlab.pegase.tech/z3/z3-backend/commit/7e885a4558390b565f9ae406dbd956b68a5f3868))
* **infrastructure:** GeoIpServiceImpl was not loading correctly resources files ([e918708](https://gitlab.pegase.tech/z3/z3-backend/commit/e918708cb995b54aac3f4db800ad625017c55d4f))
* **infrastructure:** UserAgentAnalyser was not loading correctly yml rule files ([190dc6e](https://gitlab.pegase.tech/z3/z3-backend/commit/190dc6e790cdf1e34364cdd75fc3a1df10ff0e87))
## 1.0.5 (2025-03-05)
### Features
* **global:** Allow brands to have an associated list of Fqdn ([fe7708d](https://gitlab.pegase.tech/z3/z3-backend/commit/fe7708da0baf8954ab4a0703a6d3b5f143fb9aa2))
* **global:** Link Export now uses brand first available fqdn or the resolver fqdn ([011c261](https://gitlab.pegase.tech/z3/z3-backend/commit/011c2613b3ceb604df8b69a42629d87459de1440))
* **global:** Link resolve change of behavior and using `host` parameter ([d56b0a6](https://gitlab.pegase.tech/z3/z3-backend/commit/d56b0a6043dbcd94e4bb9091093809c59c4ac329))
### Bug Fixes
* **application:** Craete and update brand properly validates fqdns ([93492f9](https://gitlab.pegase.tech/z3/z3-backend/commit/93492f96e30c8bf7c4d27d3c9d6f912bdf770dc4))
* **application:** Fix create brand request validation ([639c577](https://gitlab.pegase.tech/z3/z3-backend/commit/639c5774c6eae62b98293d5c8a27da080eddb571))
* **application:** Fixes [#4](https://gitlab.pegase.tech/z3/z3-backend/issues/4) choosing tag while importing links ([ad37d91](https://gitlab.pegase.tech/z3/z3-backend/commit/ad37d91fbf4bf53f8677334a0389e67d251f8227))
* **application:** Handle Unicode BOM on csv file import ([214c827](https://gitlab.pegase.tech/z3/z3-backend/commit/214c827c39b11a4e5e5b28205605fac1e20b5cb2))
* **application:** RedirectLinkOutputTest was wrong after rebase with main ([22d74c1](https://gitlab.pegase.tech/z3/z3-backend/commit/22d74c19fced37424c21c437d75f636f8bd341f2))
* **domain:** Closes [#7](https://gitlab.pegase.tech/z3/z3-backend/issues/7) ([0300d7f](https://gitlab.pegase.tech/z3/z3-backend/commit/0300d7f5fcc3e3aee7fc7d79c19b2fb6f6aefac2))
* **domain:** Correct Validation error field name on Create and Update brand ([96ca5f0](https://gitlab.pegase.tech/z3/z3-backend/commit/96ca5f0c3b066852f7ad38f72be6f44f1a46f2c6))
* **domain:** Prevent link upsert on other brands while importing ([a81ffde](https://gitlab.pegase.tech/z3/z3-backend/commit/a81ffdeeafa527a64a805250a3ecd0137ccdff7c))
* **infrastructure:** LinkRepository could output null for count queries ([9ed8300](https://gitlab.pegase.tech/z3/z3-backend/commit/9ed8300ede944574d96f91d835b07f6bdb20389e))
* **infrastructure:** ReportingSubscriptionDatabaseRepository fix querying by userUuid ([82c24bf](https://gitlab.pegase.tech/z3/z3-backend/commit/82c24bf5acb24acf936e3bd484b6fa4e2e831ced))
* **infrastructure:** Simplify repositories cache usage ([f939928](https://gitlab.pegase.tech/z3/z3-backend/commit/f939928d812ff9b3c9d0b4f0b50b03968683ab82))
## 1.0.3 (2025-01-09)
### ⚠ BREAKING CHANGES
* **global:** SendMessage can now output it success status
### Features
* **global:** Add Reporting Subscriptions for users ([da78839](https://gitlab.pegase.tech/z3/z3-backend/commit/da78839458761d5be909e71785c0af9a2810fe72))
* **global:** Send scheduled invalid links reports ([fc059b5](https://gitlab.pegase.tech/z3/z3-backend/commit/fc059b576506084fdebf17f2f95bcc7666e4cef3))
### Bug Fixes
* **cli:** Fix Hibernate warning on compilation ([0b8dec5](https://gitlab.pegase.tech/z3/z3-backend/commit/0b8dec52decba550bc059a8ce68e9dd645c43971))
* **domain:** GetGlobalStatistics count is wrong + add more tests ([8df73ea](https://gitlab.pegase.tech/z3/z3-backend/commit/8df73ea5ea75423dbca7b3346408ef5f2bfe263c))
* **domain:** Remove lang3 dep in SendReports use case ([60d3e72](https://gitlab.pegase.tech/z3/z3-backend/commit/60d3e72d242069eeb1fd325775a074fd29e28876))
* **domain:** When Link is updated it now changes its updatedAt timestamp ([8eeb8d9](https://gitlab.pegase.tech/z3/z3-backend/commit/8eeb8d934da239ac0aeb5a2e863798012eeab85b))
* **global:** GetLinksStatistics regression fix ([e9cc5df](https://gitlab.pegase.tech/z3/z3-backend/commit/e9cc5dfd6881ae853d9e2b80b298b09108ccf67c))
* **global:** Use Flyway to apply a migration with seeded data ([be00a1a](https://gitlab.pegase.tech/z3/z3-backend/commit/be00a1a5807adf9445574d9083bda91423b8baba))
* **global:** Use ZonedDateTime as value-object and return them from presenter for Links usecases ([d0ee9a6](https://gitlab.pegase.tech/z3/z3-backend/commit/d0ee9a673cc592c63f950dc3b1cc523cda2893c7))
### Code Refactoring
* **global:** SendMessage can now output it success status ([6b68299](https://gitlab.pegase.tech/z3/z3-backend/commit/6b68299c853021112f68fa016cf7e99a883d3a42))
## 1.0.2 (2024-12-10)
### Features
* **application:** Ajout d'un message sur une demande de redirection d'un lien invalide ([4c28c8f](https://gitlab.pegase.tech/z3/z3-backend/commit/4c28c8f67c08c68f727c222b933451d8aea8b01f))
* **domain:** Allow to put any text on source and slugify it on import ([a9f69b7](https://gitlab.pegase.tech/z3/z3-backend/commit/a9f69b75ebc21a5853584bc358b830d305a77cc2))
* **global:** Allow to set tag while importing links ([3656677](https://gitlab.pegase.tech/z3/z3-backend/commit/3656677833632159dfc284904c5b107b8176b609))
### Bug Fixes
* **application:** Add no-cache header for public redirection ([20c85f7](https://gitlab.pegase.tech/z3/z3-backend/commit/20c85f7c1f563c4448176b736c25262727193372))
* **application:** Correction of import for user requests ([9942f65](https://gitlab.pegase.tech/z3/z3-backend/commit/9942f658baf69c75362c4085837863d3621f90b3))
* **application:** Slugify source on create link endpoint ([3e760a3](https://gitlab.pegase.tech/z3/z3-backend/commit/3e760a321dc79e809706252d5992df63542bfdfb))
* **domain:** Import links for catalog was not setting correct brand ([02d6507](https://gitlab.pegase.tech/z3/z3-backend/commit/02d650728ad394c0df86993ef6f59051cf1436d5))
* **domain:** Import links was returning the wrong imported links number ([9ec5c2d](https://gitlab.pegase.tech/z3/z3-backend/commit/9ec5c2dc377ebf859e8c1b8d8579501b8ec93cc2))
* **domain:** Reset user password was not persisting the updated credential ([5e811bd](https://gitlab.pegase.tech/z3/z3-backend/commit/5e811bd3c428054fa00dc5dfb300b16900b74d19))
* **infrastructure:** Slugifyer now replaces undescores ([34ab963](https://gitlab.pegase.tech/z3/z3-backend/commit/34ab963a68464be0ff0257b5fec91154b0d229c5))
## 1.0.1 (2024-12-08)
### ⚠ BREAKING CHANGES
* **global:** Allow to assign some brands to any user
### Features
* **global:** Allow to assign some brands to any user ([6e820e7](https://gitlab.pegase.tech/z3/z3-backend/commit/6e820e772e690e9f129ce55b63fb377da9c4d1fb))
* **global:** Change user password ([acb99eb](https://gitlab.pegase.tech/z3/z3-backend/commit/acb99ebcec6a6de5480a3ef3f57ffa274192339d))
* **global:** Complete Global statistics feature ([0f5074d](https://gitlab.pegase.tech/z3/z3-backend/commit/0f5074da602002af7988656781afa302c3cdd711))
* **global:** Get statistics for Brand, Catalog and Tag ([26db02b](https://gitlab.pegase.tech/z3/z3-backend/commit/26db02b5df14151972fbfab8c73f6072267b5df6))
* **global:** Reset user password feature ([a59ae02](https://gitlab.pegase.tech/z3/z3-backend/commit/a59ae027ff944c182f168bdf89406f27a8d39091))
### Bug Fixes
* **application:** Add not null to create and update users requests ([a35dfe4](https://gitlab.pegase.tech/z3/z3-backend/commit/a35dfe40430a889361118049b1fcea882e05d5df))
* **application:** Allow admin users to update users ([13f141c](https://gitlab.pegase.tech/z3/z3-backend/commit/13f141c24e6512d79ab6250a59f6341fee2c7c5f))
* **application:** Remove Role check on get brand resource ([492d842](https://gitlab.pegase.tech/z3/z3-backend/commit/492d842664842e1baa477e2fbbf75e1f228b06bd))
## 0.0.1 (2024-09-10)
## (2025-03-27)
### Features
* **global:** Add some statistics resources to get some graphics ([8f12c0f](https://gitlab.pegase.tech/z3/z3-backend/commit/8f12c0f1947ff9c61e1dbe9fe608d4e6b03c8f21))
* **global:** Add statistics domain and simplify UUID handling ([377249d](https://gitlab.pegase.tech/z3/z3-backend/commit/377249d5de7f3ec788c3372b661dbb61d638a2b9))
### Bug Fixes
* **domain:** Allow Referer path to be empty ([1884049](https://gitlab.pegase.tech/z3/z3-backend/commit/188404953b2171df239c0a10b775bae79bda8919))
* **domain:** Link Statistic for visits was no correctly using nullable ([7e885a4](https://gitlab.pegase.tech/z3/z3-backend/commit/7e885a4558390b565f9ae406dbd956b68a5f3868))
## (2025-03-05)
## [1.0.5](https://gitlab.pegase.tech/z3/z3-backend/compare/1.0.4...1.0.5) (2025-02-04)
### Features
* **global:** Allow brands to have an associated list of
Fqdn ([fe7708d](https://gitlab.pegase.tech/z3/z3-backend/commit/fe7708da0baf8954ab4a0703a6d3b5f143fb9aa2))
* **global:** Link Export now uses brand first available fqdn or the resolver
fqdn ([011c261](https://gitlab.pegase.tech/z3/z3-backend/commit/011c2613b3ceb604df8b69a42629d87459de1440))
* **global:** Link resolve change of behavior and using `host`
parameter ([d56b0a6](https://gitlab.pegase.tech/z3/z3-backend/commit/d56b0a6043dbcd94e4bb9091093809c59c4ac329))
### Bug Fixes
* **application:** Craete and update brand properly validates
fqdns ([93492f9](https://gitlab.pegase.tech/z3/z3-backend/commit/93492f96e30c8bf7c4d27d3c9d6f912bdf770dc4))
* **application:** RedirectLinkOutputTest was wrong after rebase with
main ([22d74c1](https://gitlab.pegase.tech/z3/z3-backend/commit/22d74c19fced37424c21c437d75f636f8bd341f2))
* **domain:** Correct Validation error field name on Create and Update
brand ([96ca5f0](https://gitlab.pegase.tech/z3/z3-backend/commit/96ca5f0c3b066852f7ad38f72be6f44f1a46f2c6))
## (2025-03-05)
## [1.0.4](https://gitlab.pegase.tech/z3/z3-backend/compare/1.0.3...1.0.4) (2025-02-04)
### Bug Fixes
* **application:** Fix create brand request
validation ([639c577](https://gitlab.pegase.tech/z3/z3-backend/commit/639c5774c6eae62b98293d5c8a27da080eddb571))
* **application:** Fixes [#4](https://gitlab.pegase.tech/z3/z3-backend/issues/4) choosing tag while
importing
links ([ad37d91](https://gitlab.pegase.tech/z3/z3-backend/commit/ad37d91fbf4bf53f8677334a0389e67d251f8227))
* **application:** Handle Unicode BOM on csv file
import ([214c827](https://gitlab.pegase.tech/z3/z3-backend/commit/214c827c39b11a4e5e5b28205605fac1e20b5cb2))
* **domain:**
Closes [#7](https://gitlab.pegase.tech/z3/z3-backend/issues/7) ([0300d7f](https://gitlab.pegase.tech/z3/z3-backend/commit/0300d7f5fcc3e3aee7fc7d79c19b2fb6f6aefac2))
* **domain:** Prevent link upsert on other brands while
importing ([a81ffde](https://gitlab.pegase.tech/z3/z3-backend/commit/a81ffdeeafa527a64a805250a3ecd0137ccdff7c))
* **infrastructure:** LinkRepository could output null for count
queries ([9ed8300](https://gitlab.pegase.tech/z3/z3-backend/commit/9ed8300ede944574d96f91d835b07f6bdb20389e))
* **infrastructure:** ReportingSubscriptionDatabaseRepository fix querying by
userUuid ([82c24bf](https://gitlab.pegase.tech/z3/z3-backend/commit/82c24bf5acb24acf936e3bd484b6fa4e2e831ced))
* **infrastructure:** Simplify repositories cache
usage ([f939928](https://gitlab.pegase.tech/z3/z3-backend/commit/f939928d812ff9b3c9d0b4f0b50b03968683ab82))
## [1.0.3](https://gitlab.pegase.tech/z3/z3-backend/compare/1.0.2...1.0.3) (2025-01-09)
### ⚠ BREAKING CHANGES
* **global:** SendMessage can now output it success status
### Features
* **global:** Add Reporting Subscriptions for
users ([da78839](https://gitlab.pegase.tech/z3/z3-backend/commit/da78839458761d5be909e71785c0af9a2810fe72))
* **global:** Send scheduled invalid links
reports ([fc059b5](https://gitlab.pegase.tech/z3/z3-backend/commit/fc059b576506084fdebf17f2f95bcc7666e4cef3))
### Bug Fixes
* **cli:** Fix Hibernate warning on
compilation ([0b8dec5](https://gitlab.pegase.tech/z3/z3-backend/commit/0b8dec52decba550bc059a8ce68e9dd645c43971))
* **domain:** GetGlobalStatistics count is wrong + add more
tests ([8df73ea](https://gitlab.pegase.tech/z3/z3-backend/commit/8df73ea5ea75423dbca7b3346408ef5f2bfe263c))
* **domain:** Remove lang3 dep in SendReports use
case ([60d3e72](https://gitlab.pegase.tech/z3/z3-backend/commit/60d3e72d242069eeb1fd325775a074fd29e28876))
* **domain:** When Link is updated it now changes its updatedAt
timestamp ([8eeb8d9](https://gitlab.pegase.tech/z3/z3-backend/commit/8eeb8d934da239ac0aeb5a2e863798012eeab85b))
* **global:** GetLinksStatistics regression
fix ([e9cc5df](https://gitlab.pegase.tech/z3/z3-backend/commit/e9cc5dfd6881ae853d9e2b80b298b09108ccf67c))
* **global:** Use Flyway to apply a migration with seeded
data ([be00a1a](https://gitlab.pegase.tech/z3/z3-backend/commit/be00a1a5807adf9445574d9083bda91423b8baba))
* **global:** Use ZonedDateTime as value-object and return them from presenter for Links
usecases ([d0ee9a6](https://gitlab.pegase.tech/z3/z3-backend/commit/d0ee9a673cc592c63f950dc3b1cc523cda2893c7))
### Code Refactoring
* **global:** SendMessage can now output it success
status ([6b68299](https://gitlab.pegase.tech/z3/z3-backend/commit/6b68299c853021112f68fa016cf7e99a883d3a42))
## [1.0.2](https://gitlab.pegase.tech/z3/z3-backend/compare/1.0.1...1.0.2) (2024-12-10)
### Features
* **application:** Ajout d'un message sur une demande de redirection d'un lien
invalide ([4c28c8f](https://gitlab.pegase.tech/z3/z3-backend/commit/4c28c8f67c08c68f727c222b933451d8aea8b01f))
* **domain:** Allow to put any text on source and slugify it on
import ([a9f69b7](https://gitlab.pegase.tech/z3/z3-backend/commit/a9f69b75ebc21a5853584bc358b830d305a77cc2))
* **global:** Allow to set tag while importing
links ([3656677](https://gitlab.pegase.tech/z3/z3-backend/commit/3656677833632159dfc284904c5b107b8176b609))
### Bug Fixes
* **application:** Add no-cache header for public
redirection ([20c85f7](https://gitlab.pegase.tech/z3/z3-backend/commit/20c85f7c1f563c4448176b736c25262727193372))
* **application:** Correction of import for user
requests ([9942f65](https://gitlab.pegase.tech/z3/z3-backend/commit/9942f658baf69c75362c4085837863d3621f90b3))
* **application:** Slugify source on create link
endpoint ([3e760a3](https://gitlab.pegase.tech/z3/z3-backend/commit/3e760a321dc79e809706252d5992df63542bfdfb))
* **domain:** Import links for catalog was not setting correct
brand ([02d6507](https://gitlab.pegase.tech/z3/z3-backend/commit/02d650728ad394c0df86993ef6f59051cf1436d5))
* **domain:** Import links was returning the wrong imported links
number ([9ec5c2d](https://gitlab.pegase.tech/z3/z3-backend/commit/9ec5c2dc377ebf859e8c1b8d8579501b8ec93cc2))
* **domain:** Reset user password was not persisting the updated
credential ([5e811bd](https://gitlab.pegase.tech/z3/z3-backend/commit/5e811bd3c428054fa00dc5dfb300b16900b74d19))
* **infrastructure:** Slugifyer now replaces
undescores ([34ab963](https://gitlab.pegase.tech/z3/z3-backend/commit/34ab963a68464be0ff0257b5fec91154b0d229c5))
## [1.0.1](https://gitlab.pegase.tech/z3/z3-backend/compare/a59ae027ff944c182f168bdf89406f27a8d39091...1.0.1) (2024-12-08)
### ⚠ BREAKING CHANGES
* **global:** Allow to assign some brands to any user
### Features
* **global:** Allow to assign some brands to any
user ([6e820e7](https://gitlab.pegase.tech/z3/z3-backend/commit/6e820e772e690e9f129ce55b63fb377da9c4d1fb))
* **global:** Change user
password ([acb99eb](https://gitlab.pegase.tech/z3/z3-backend/commit/acb99ebcec6a6de5480a3ef3f57ffa274192339d))
* **global:** Complete Global statistics
feature ([0f5074d](https://gitlab.pegase.tech/z3/z3-backend/commit/0f5074da602002af7988656781afa302c3cdd711))
* **global:** Get statistics for Brand, Catalog and
Tag ([26db02b](https://gitlab.pegase.tech/z3/z3-backend/commit/26db02b5df14151972fbfab8c73f6072267b5df6))
* **global:** Reset user password
feature ([a59ae02](https://gitlab.pegase.tech/z3/z3-backend/commit/a59ae027ff944c182f168bdf89406f27a8d39091))
### Bug Fixes
* **application:** Add not null to create and update users
requests ([a35dfe4](https://gitlab.pegase.tech/z3/z3-backend/commit/a35dfe40430a889361118049b1fcea882e05d5df))
* **application:** Allow admin users to update
users ([13f141c](https://gitlab.pegase.tech/z3/z3-backend/commit/13f141c24e6512d79ab6250a59f6341fee2c7c5f))
* **application:** Remove Role check on get brand
resource ([492d842](https://gitlab.pegase.tech/z3/z3-backend/commit/492d842664842e1baa477e2fbbf75e1f228b06bd))

64
Dockerfile Normal file
View file

@ -0,0 +1,64 @@
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build
ARG GITLAB_MVN_TOKEN
COPY <<EOF /mvn-settings.xml
<settings>
<servers>
<server>
<id>gitlab-maven</id>
<configuration>
<httpHeaders>
<property>
<name>Job-Token</name>
<value>$GITLAB_MVN_TOKEN</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>
EOF
COPY --chown=quarkus:quarkus config /code/config
COPY --chown=quarkus:quarkus mvnw /code/mvnw
COPY --chown=quarkus:quarkus .mvn /code/.mvn
COPY --chown=quarkus:quarkus pom.xml /code/pom.xml
COPY --chown=quarkus:quarkus z3-application/pom.xml /code/z3-application/pom.xml
COPY --chown=quarkus:quarkus z3-application/src /code/z3-application/src
COPY --chown=quarkus:quarkus z3-cli/pom.xml /code/z3-cli/pom.xml
COPY --chown=quarkus:quarkus z3-cli/src /code/z3-cli/src
COPY --chown=quarkus:quarkus z3-domain/pom.xml /code/z3-domain/pom.xml
COPY --chown=quarkus:quarkus z3-domain/src /code/z3-domain/src
COPY --chown=quarkus:quarkus z3-infrastructure/pom.xml /code/z3-infrastructure/pom.xml
COPY --chown=quarkus:quarkus z3-infrastructure/src /code/z3-infrastructure/src
COPY --chown=quarkus:quarkus z3-shared/pom.xml /code/z3-shared/pom.xml
COPY --chown=quarkus:quarkus z3-shared/src /code/z3-shared/src
COPY --chown=quarkus:quarkus z3-statistics-domain/pom.xml /code/z3-statistics-domain/pom.xml
COPY --chown=quarkus:quarkus z3-statistics-domain/src /code/z3-statistics-domain/src
COPY --chown=quarkus:quarkus z3-statistics-infrastructure/pom.xml /code/z3-statistics-infrastructure/pom.xml
COPY --chown=quarkus:quarkus z3-statistics-infrastructure/src /code/z3-statistics-infrastructure/src
USER quarkus
WORKDIR /code
RUN ./mvnw package -DskipTests -Dquarkus.package.jar.type=uber-jar -s /mvn-settings.xml
FROM registry.access.redhat.com/ubi8/openjdk-21:1.20
ENV LANGUAGE='en_US:en'
COPY --from=build --chown=185 /code/z3-application/target/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

176
README.md Normal file
View file

@ -0,0 +1,176 @@
# Z3 – A link to the path
*Z3* est un raccourcisseur d'URL.
*Z3* s'inspire dans la forme des principes de clean architecture et de DDD. On trouve ainsi quatre
modules Maven :
- `z3`: module parent ;
- `z3-domain` : contient les entités, les objets-valeur, les use cases et les ports ;
- `z3-infrastructure` : contient les implémentations des ports (adapters) ;
- `z3-application` : contient les services applicatifs.
Le nom *Z3* est un clin d'œil à *Zelda 3 : A Link to the Past*, un jeu vidéo sorti en 1991 sur Super
Nintendo.
## GENARATE CHANGELOG
- Package node à util `https://github.com/conventional-changelog/conventional-changelog`
- Commande à utiliser `conventional-changelog -p conventionalcommits -i CHANGELOG.md -s`
## SCHEDULED TASKS
Liste des taches planifiés :
- Envoyer les rapports de liens invalides -> `APP_REPORT_INVALID_LINKS`
## CLI
- Pour lancer la cli en DEV : `mvn quarkus:dev -Dquarkus.args=seed
- Ou `java -jar target/z3-cli-1.1.0-SNAPSHOT-runner.jar seed`
## Configuration
### BDD supportées
- Postgres
### LOG
- Le loglevel peut être set avec `APP_LOG_LEVEL` -> `APP_LOG_LEVEL=DEBUG`
### UPLOADS
**! Cette valeur dois être raccord avec la partie front (Sveltekit pas exemple) !**
- La taille max des uploads peut être définie `APP_MAX_UPLOAD_SIZE` -> `APP_MAX_UPLOAD_SIZE=100M`
### SECURITE
**! Toutes les informations de configuration de cette partie doivent être sauvegardés pour conserver
des données exploitables !**
#### JWT
Usage :
```text
APP_JWT_ISSUER=http://localhost:8080/issuer
APP_JWT_PUBLIC_KEY=publicKey.pem
APP_JWT_PRIVATE_KEY=privateKey.pem
```
Génerer des clés :
```shell
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
```
#### ENCRYPTION
example :
```text
AES_KEY="password"
AES_SALT="salt"
AES_KEY_LENGTH=256
```
Notes :
- `KEY` Est une valeur texte quelconque
- `SALT` Est une valeur texte quelconque
- `KEY_LENGTH` Devrais être laissée à 256
### MAIL
Exemple (pour mailpit) :
```text
APP_MAIL_FROM=z3@example.net
APP_MAIL_HOST=127.0.0.1
APP_MAIL_PORT=1025
APP_MAIL_START_TLS=OPTIONAL
APP_MAIL_TLS=false
APP_MAIL_USERNAME=null
APP_MAIL_PASSWORD=null
APP_MAIL_DEV_SIMULATE=false
```
Notes :
- `APP_MAIL_FROM` Est l'adresse email qui est inscrite dans le mail dans la partie `FROM:`
- `MAIL_DEV_SIMULATE` Permet de simuler l'envoie de mails et donc de se passer d'un serveur SMTP.
### Création d'un compte initial
**! Cette partie nécessite un serveur SMTP, en local il est possible d'utiliser Mailpit par
exemple !**
Mailpit :
```shell
podman run -d \
--name=mailpit \
--restart unless-stopped \
-p 8025:8025 \
-p 1025:1025 \
axllent/mailpit
```
Exemple :
```text
APP_CREATE_USER_ON_START=toto@example.net
```
Notes :
- `CREATE_USER_ON_START` Peut etre vide, s'il ne l'est pas un nouvel utilisateur **Administrateur**
sera crée. Les informations de connexion seront envoyés par email.
## TESTS
```sh
mvn clean verify
```
Reports for each modules can be found under target dirs.
## SECURITY
### Checking
Check for vulnerabilities
First time running requires downloading a list of CVE.
You can request an API key from [here](https://nvd.nist.gov/developers/request-an-api-key) to
accelerate downloading theses. They are stored in
`~/.m2/repository/org/owasp/dependency-check-data/`.
You can find more information form
the [plugin Github repository](https://github.com/jeremylong/DependencyCheck).
```sh
mvn org.owasp:dependency-check-maven:check
```
To pass an api key you can use, or replace `$ENV_API_KEY_VAR` directly with the api key.
```sh
mvn verify -Ddependency-check-maven.api_key=$ENV_API_KEY_VAR
```
### Report
You may find report after ward in `target/dependency-check-report.html`
They are several reports configured but you can add more looking
at [the documentation](https://jeremylong.github.io/DependencyCheck/dependency-check-maven/configuration.html).
Available are `HTML, XML, CSV, JSON, JUNIT, SARIF, JENKINS, GITLAB, ALL`.
Now Z3 has enabled `HTML, JSON, SARIF and GITLAB`.

9190
Uml/Z3Backend.gaphor Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
meta {
name: Change Password
type: http
seq: 3
}
put {
url: http://localhost:8080/api/v1/auth/password
body: json
auth: inherit
}
body:json {
{
"password" : "asd",
"new-password" : "asd"
}
}

View file

@ -0,0 +1,28 @@
meta {
name: Login
type: http
seq: 1
}
post {
url: http://localhost:8080/api/v1/auth/login
body: json
auth: none
}
headers {
Content-Type: application/json
Accept: application/json
}
body:json {
{
"login": "z3.admin",
"password": "g4nWmGs0w^"
}
}
tests {
let response = res.getBody();
bru.setVar("jwt_token", response.token);
}

View file

@ -0,0 +1,16 @@
meta {
name: Me
type: http
seq: 2
}
get {
url: http://localhost:8080/api/v1/auth/me
body: none
auth: inherit
}
headers {
Content-Type: application/json
Accept: application/json
}

View file

@ -0,0 +1,3 @@
meta {
name: Auth
}

View file

@ -0,0 +1,26 @@
meta {
name: Create Brand
type: http
seq: 2
}
post {
url: http://localhost:8080/api/v1/brands
body: json
auth: inherit
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"name" : "super brand 2",
"fqdns": [
"example.net",
"example.io"
]
}
}

View file

@ -0,0 +1,15 @@
meta {
name: Get Brand
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/brands/2abe8b15-b204-4021-9847-6437188ec6d8
body: none
auth: inherit
}
headers {
Accept: application/json
}

View file

@ -0,0 +1,18 @@
meta {
name: List Brands
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/brands?page=1&perPage=15
body: none
auth: inherit
}
params:query {
page: 1
perPage: 15
~orderBy: source
~direction: desc
}

View file

@ -0,0 +1,26 @@
meta {
name: Update Brand
type: http
seq: 3
}
put {
url: http://localhost:8080/api/v1/brands/super-brand-2
body: json
auth: inherit
}
headers {
Content-Type: application/json
Accept: application/json
}
body:json {
{
"name": "Super Brand 2",
"fqdns": [
"example.net",
"example.io"
]
}
}

View file

@ -0,0 +1,3 @@
meta {
name: Brands
}

View file

@ -0,0 +1,23 @@
meta {
name: Create Catalog
type: http
seq: 1
}
post {
url: http://localhost:8080/api/v1/catalogs
body: json
auth: inherit
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"name" : "super catalog",
"brand": "1e86a493-ee75-494d-8da5-c61971c37b67"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Delete Catalog
type: http
seq: 3
}
delete {
url: http://localhost:8080/api/v1/catalogs/super-tag
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: Get Catalog
type: http
seq: 5
}
get {
url: http://localhost:8080/api/v1/catalogs/super-catalog
body: none
auth: inherit
}
headers {
Accept: application/json
}

View file

@ -0,0 +1,19 @@
meta {
name: List Catalogs
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/catalogs?page=1&brand=b5a90f20-dd1c-4a8a-b820-aed3aa62e857&perPage=15
body: none
auth: inherit
}
params:query {
page: 1
brand: b5a90f20-dd1c-4a8a-b820-aed3aa62e857
perPage: 15
~orderBy: source
~direction: desc
}

View file

@ -0,0 +1,18 @@
meta {
name: Update Catalog
type: http
seq: 2
}
put {
url: http://localhost:8080/api/v1/catalogs/97295da4-d7c8-4e15-86fb-81ee9f53a47e
body: json
auth: inherit
}
body:json {
{
"name" : "super tagwrere",
"color": "#fff"
}
}

View file

@ -0,0 +1,3 @@
meta {
name: Catalogs
}

View file

@ -0,0 +1,26 @@
meta {
name: Create Link
type: http
seq: 3
}
post {
url: http://localhost:8080/api/v1/links
body: json
auth: inherit
}
headers {
Content-Type: application/json
Accept: application/json
}
body:json {
{
"source" : "testkey",
"target" : "https://example.org",
"is_external": true,
"tag": "",
"catalog": ""
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Get Link
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/links/3cisvgdo7r8fdqo774ci3apagt
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Link export no brand
type: http
seq: 5
}
get {
url: http://localhost:8080/api/v1/links-export
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Link export with brand
type: http
seq: 6
}
get {
url: http://localhost:8080/api/v1/links-export/brands/new-branddd
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Link export with catalog
type: http
seq: 7
}
get {
url: http://localhost:8080/api/v1/links-export/catalogs/new-catalog
body: none
auth: inherit
}

View file

@ -0,0 +1,19 @@
meta {
name: Link import
type: http
seq: 8
}
post {
url: http://localhost:8080/api/v1/links-import/brands/1e86a493-ee75-494d-8da5-c61971c37b67
body: multipartForm
auth: inherit
}
headers {
Accept: application/json
}
body:multipart-form {
file: @file(postman-cloud:///1efaca96-04b8-4ff0-968f-73a0020ae3a0)
}

View file

@ -0,0 +1,22 @@
meta {
name: List Links
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/links?page=1&perPage=15&orderBy=source&direction=desc
body: none
auth: inherit
}
params:query {
page: 1
perPage: 15
orderBy: source
direction: desc
~tag: 1fefab47-ec13-4f9e-815a-1cef5e7d7db4
~searchOn: source
~keywords: example.net
~catalog: 77657d43-7d33-48cd-88aa-7f73fa6d6ded
}

View file

@ -0,0 +1,11 @@
meta {
name: For all Brands
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/links/statistics/brands
body: none
auth: inherit
}

View file

@ -0,0 +1,19 @@
meta {
name: For all Catalogs of
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/links/statistics/catalog?brand=582807fe-7a37-462d-97e4-10f30d055abb
body: none
auth: bearer
}
params:query {
brand: 582807fe-7a37-462d-97e4-10f30d055abb
}
auth:bearer {
token: {{jwt_token}}
}

View file

@ -0,0 +1,19 @@
meta {
name: For all Tag of
type: http
seq: 5
}
get {
url: http://localhost:8080/api/v1/links/statistics/tag?brand=582807fe-7a37-462d-97e4-10f30d055abb
body: none
auth: bearer
}
params:query {
brand: 582807fe-7a37-462d-97e4-10f30d055abb
}
auth:bearer {
token: {{jwt_token}}
}

View file

@ -0,0 +1,15 @@
meta {
name: Global Statistics For Brand
type: http
seq: 3
}
get {
url: http://localhost:8080/api/v1/links/statistics/582807fe-7a37-462d-97e4-10f30d055abb
body: none
auth: bearer
}
auth:bearer {
token: {{jwt_token}}
}

View file

@ -0,0 +1,15 @@
meta {
name: Global Statistics
type: http
seq: 2
}
get {
url: http://localhost:8080/api/v1/links/statistics
body: none
auth: bearer
}
auth:bearer {
token: {{jwt_token}}
}

View file

@ -0,0 +1,3 @@
meta {
name: Statistics
}

View file

@ -0,0 +1,20 @@
meta {
name: Update Link
type: http
seq: 2
}
put {
url: http://localhost:8080/api/v1/links/cle-1058
body: json
auth: inherit
}
body:json {
{
"target": "https://example.net",
"is_external": true,
"tag": "",
"catalog": ""
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Verify Link
type: http
seq: 9
}
put {
url: http://localhost:8080/api/v1/links/3cisvgdo7r8fdqo774ci3apagt/verify
body: none
auth: inherit
}

View file

@ -0,0 +1,3 @@
meta {
name: Links
}

View file

@ -0,0 +1,29 @@
meta {
name: Link Redirection With Additionals
type: http
seq: 3
}
get {
url: http://localhost:8080/presentation-de-aout?ip-address=80.0.2.1&dnt=false&lang=fr&region=FR&referrer=https%3A%2F%2Fgoogle.com%2Fi-love-linux&screen-width=1200&screen-height=1080&screen-dpi=4&user-agent=Mozilla%2F5.0%20%28compatible%3B%20Sosospider%2F2.0%3B%20%2Bhttp%3A%2F%2Fhelp.soso.com%2Fwebspider.htm%29&fingerprint=fingerprint
body: none
auth: none
}
params:query {
ip-address: 80.0.2.1
dnt: false
lang: fr
region: FR
referrer: https%3A%2F%2Fgoogle.com%2Fi-love-linux
screen-width: 1200
screen-height: 1080
screen-dpi: 4
user-agent: Mozilla%2F5.0%20%28compatible%3B%20Sosospider%2F2.0%3B%20%2Bhttp%3A%2F%2Fhelp.soso.com%2Fwebspider.htm%29
fingerprint: fingerprint
~user-agent: Mozilla%252F5.0%2520%2528compatible%253B%2520Sosospider%252F2.0%253B%2520%252Bhttp%253A%252F%252Fhelp.soso.com%252Fwebspider.htm%2529
~user-agent: Python-urllib%2F1.17
~user-agent: gloubiboulga
~user-agent: Mozilla%2F5.0%20%28X11%3B%20Linux%20x86_64%3B%20rv%3A136.0%29%20Gecko%2F20100101%20Firefox%2F136.0
~user-agent: Mozilla%2F5.0%20%28compatible%3B%202GDPR%2F1.2%3B%20https%3A%2F%2F2gdpr.com%29
}

View file

@ -0,0 +1,11 @@
meta {
name: Link Redirection slim
type: http
seq: 4
}
get {
url: http://localhost:8080/test
body: none
auth: none
}

View file

@ -0,0 +1,25 @@
meta {
name: Link Redirection
type: http
seq: 2
}
get {
url: http://localhost:8080/test?ip-address=80.0.2.1&dnt=false&lang=fr&region=FR&referrer=https%3A%2F%2Fgoogle.com%2Fi-love-linux&user-agent=Mozilla%2F5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F133.0.0.0%20Safari%2F537.36&screen-width=1200&screen-height=1080&screen-dpi=4
body: none
auth: none
}
params:query {
ip-address: 80.0.2.1
dnt: false
lang: fr
region: FR
referrer: https%3A%2F%2Fgoogle.com%2Fi-love-linux
user-agent: Mozilla%2F5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F133.0.0.0%20Safari%2F537.36
screen-width: 1200
screen-height: 1080
screen-dpi: 4
~user-agent: Mozilla%2F5.0%20%28X11%3B%20Linux%20x86_64%3B%20rv%3A136.0%29%20Gecko%2F20100101%20Firefox%2F136.0
~user-agent: gloubiboulga
}

View file

@ -0,0 +1,11 @@
meta {
name: Root Url
type: http
seq: 1
}
get {
url: http://localhost:8080
body: none
auth: none
}

View file

@ -0,0 +1,3 @@
meta {
name: Public
}

View file

@ -0,0 +1,11 @@
meta {
name: Get Reporting Subscriptions
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/reporting/subscriptions/b12f92c0-361c-40bd-9fbc-48c83fcccc81
body: none
auth: inherit
}

View file

@ -0,0 +1,19 @@
meta {
name: Update Reproting Subscriptions
type: http
seq: 2
}
post {
url: http://localhost:8080/api/v1/reporting/subscriptions/b12f92c0-361c-40bd-9fbc-48c83fcccc81
body: json
auth: inherit
}
body:json {
{
"brands": [
"582807fe-7a37-462d-97e4-10f30d055abb"
]
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Invalid Links Per Week By Brands
type: http
seq: 5
}
get {
url: http://localhost:8080/api/v1/statistics/invalid-links/per-week-per-brand
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Most Frequent Referrers With Brands
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/statistics/top/referrers
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Top Visited Sources With Brands
type: http
seq: 3
}
get {
url: http://localhost:8080/api/v1/statistics/top/sources
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Unique Visitors By Catalogs
type: http
seq: 2
}
get {
url: http://localhost:8080/api/v1/statistics/unique-visitors/catalogs
body: none
auth: inherit
}

View file

@ -0,0 +1,11 @@
meta {
name: Unique Visitors by Brands
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/statistics/unique-visitors/brands
body: none
auth: inherit
}

View file

@ -0,0 +1,23 @@
meta {
name: Create Tag
type: http
seq: 1
}
post {
url: http://localhost:8080/api/v1/tags
body: json
auth: inherit
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"name" : "super tag",
"color": "#fff"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Delete Tag
type: http
seq: 3
}
delete {
url: http://localhost:8080/api/v1/brands/super-tag
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: Get Tag
type: http
seq: 5
}
get {
url: http://localhost:8080/api/v1/tags/c1c9205d-19e5-469f-83fa-b40837ea0fbe
body: none
auth: inherit
}
headers {
Accept: application/json
}

View file

@ -0,0 +1,17 @@
meta {
name: List Tags
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/tags?page=1
body: none
auth: inherit
}
params:query {
page: 1
~orderBy: source
~direction: desc
}

View file

@ -0,0 +1,18 @@
meta {
name: Update Tag
type: http
seq: 2
}
put {
url: http://localhost:8080/api/v1/tags/97295da4-d7c8-4e15-86fb-81ee9f53a47e
body: json
auth: inherit
}
body:json {
{
"name" : "super tagwrere",
"color": "#fff"
}
}

View file

@ -0,0 +1,3 @@
meta {
name: Tags
}

View file

@ -0,0 +1,28 @@
meta {
name: Create User
type: http
seq: 1
}
post {
url: http://localhost:8080/api/v1/users
body: json
auth: inherit
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"firstName" : "new first name",
"lastName" : "new last name",
"email" : "new.email@examspwle.net",
"role": "ADMINISTRATOR",
"login": "sdsd",
"password": "NewPassword12.",
"brands": []
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Delete User
type: http
seq: 3
}
delete {
url: http://localhost:8080/api/v1/users/2c98a5a6-029b-4c9a-a88b-53da3b86c65b
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: Get User
type: http
seq: 4
}
get {
url: http://localhost:8080/api/v1/users/4fa30f46-d116-499e-9f3f-aa3afe04dabf
body: none
auth: inherit
}
headers {
Accept: application/json
}

View file

@ -0,0 +1,18 @@
meta {
name: List Users
type: http
seq: 6
}
get {
url: http://localhost:8080/api/v1/users?direction=desc&page=1&perPage=3
body: none
auth: inherit
}
params:query {
direction: desc
page: 1
perPage: 3
~orderBy: source
}

View file

@ -0,0 +1,15 @@
meta {
name: Reset User Password
type: http
seq: 5
}
post {
url: http://localhost:8080/api/v1/users/5380d8b1-52d4-4986-8de9-d92ae7c0c970/reset
body: none
auth: inherit
}
headers {
Accept: application/json
}

View file

@ -0,0 +1,20 @@
meta {
name: Update User
type: http
seq: 2
}
put {
url: http://localhost:8080/api/v1/users/4beed79f-ef03-4beb-b691-9556ae38ac76
body: json
auth: inherit
}
body:json {
{
"firstName" : "new first name",
"lastName" : "new last name",
"email" : "new.email@example.net",
"brands": []
}
}

View file

@ -0,0 +1,3 @@
meta {
name: Users
}

View file

@ -0,0 +1,9 @@
{
"version": "1",
"name": "Z3",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View file

@ -0,0 +1,20 @@
meta {
name: Z3
}
headers {
Accept-Language: fr
}
auth {
mode: bearer
}
auth:bearer {
token: {{jwt_token}}
}
script:pre-request {
req.setMaxRedirects(0);
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<suppress checks="MissingJavadocType" files=".*\.java"/>
<suppress checks="MissingJavadocMethod" files=".*\.java"/>
<suppress checks="VariableDeclarationUsageDistance" files=".*\.java"/>
</suppressions>

420
config/google_checks.xml Normal file
View file

@ -0,0 +1,420 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="${org.checkstyle.google.severity}" default="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/filefilters/index.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressWarningsFilter"/>
<!-- https://checkstyle.org/filters/suppressionfilter.html -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
<module name="SuppressWithNearbyTextFilter">
<property name="nearbyTextPattern"
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
<property name="checkPattern" value="$1"/>
<property name="lineRange" value="$2"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="100"/>
<property name="ignorePattern"
value="^package.*|^import.*|href\s*=\s*&quot;[^&quot;]*&quot;|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF, LITERAL_SWITCH, LITERAL_CASE"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_RETURN,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, LITERAL_FINALLY, DO_WHILE, ELLIPSIS,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_CATCH, LAMBDA,
LITERAL_YIELD, LITERAL_CASE, LITERAL_WHEN"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
TYPE_EXTENSION_AND, LITERAL_WHEN"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="WhitespaceAround"/>
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT
or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
//*[self::RCURLY][parent::SLIST[count(./*)=1]]"/>
</module>
<module name="RegexpSinglelineJava">
<property name="format" value="\{[ ]+\}"/>
<property name="message" value="Empty blocks should have no spaces. Empty blocks
may only be represented as '{}' when not part of a
multi-block statement (4.1.3)"/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, CTOR_CALL,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="allowNewlineParagraph" value="false"/>
</module>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="protected"/>
<property name="allowMissingPropertyJavadoc" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MissingJavadocMethod"/>
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
or self::RECORD_DEF or self::ENUM_DEF]
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MethodName"/>
<property name="query" value="//METHOD_DEF[
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
]/IDENT"/>
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
</module>
<module name="SingleLineJavadoc"/>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
<module name="SuppressWarningsHolder"/>
<module name="SuppressionCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)"/>
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)"/>
<property name="checkFormat" value="$1"/>
</module>
<module name="SuppressWithNearbyCommentFilter">
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
<property name="checkFormat" value="$1"/>
<!-- The check is suppressed in the next line of code after the comment -->
<property name="influenceFormat" value="1"/>
</module>
</module>
</module>

308
mvnw vendored Executable file
View file

@ -0,0 +1,308 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

205
mvnw.cmd vendored Executable file
View file

@ -0,0 +1,205 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

201
pom.xml Normal file
View file

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>z3</artifactId>
<build>
<plugins>
<plugin>
<artifactId>dependency-check-maven</artifactId>
<configuration>
<assemblyAnalyzerEnabled>false</assemblyAnalyzerEnabled>
<formats>
<format>HTML</format>
<format>SARIF</format>
<format>JSON</format>
<format>GITLAB</format>
</formats>
<nuspecAnalyzerEnabled>false</nuspecAnalyzerEnabled>
<nvdApiKey>${dependency-check-maven.api_key}</nvdApiKey>
<ossindexAnalyzerEnabled>false</ossindexAnalyzerEnabled>
<retireJsAnalyzerEnabled>false</retireJsAnalyzerEnabled>
</configuration>
<groupId>org.owasp</groupId>
<version>${dependency-check-maven.version}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- FIX warnings about maybe mockito, maybe removed when bumping quarkus-->
<argLine>-XX:+EnableDynamicAgentLoading -Xshare:off</argLine>
<!-- To remove JBoss Log error on build and testing for all modules-->
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
<version>${surefire-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<configLocation>/config/google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<linkXRef>false</linkXRef>
<suppressionsLocation>/config/checkstyle-suppressions.xml</suppressionsLocation>
</configuration>
<dependencies>
<dependency>
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
<id>check-style</id>
<phase>validate</phase>
</execution>
</executions>
<groupId>org.apache.maven.plugins</groupId>
<version>${checkstyle-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<artifactId>dependency-check-maven</artifactId>
<groupId>org.owasp</groupId>
<version>${dependency-check-maven.version}</version>
</dependency>
<dependency>
<artifactId>happy-core</artifactId>
<groupId>tech.pegase.happy</groupId>
<version>${pegase-happy.version}</version>
</dependency>
<dependency>
<artifactId>quarkus-smallrye-jwt</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-cache</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>rest-assured</artifactId>
<groupId>io.rest-assured</groupId>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>jspecify</artifactId>
<groupId>org.jspecify</groupId>
<version>${jspecify.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<groupId>${quarkus.platform.group-id}</groupId>
<scope>import</scope>
<type>pom</type>
<version>${quarkus.platform.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
<url>https://gitlab.pegase.tech/api/v4/projects/68/packages/maven</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
<url>https://gitlab.pegase.tech/api/v4/projects/68/packages/maven</url>
</snapshotRepository>
</distributionManagement>
<groupId>tech.pegase.z3</groupId>
<modelVersion>4.0.0</modelVersion>
<modules>
<module>z3-domain</module>
<module>z3-infrastructure</module>
<module>z3-application</module>
<module>z3-shared</module>
<module>z3-cli</module>
<module>z3-statistics-domain</module>
<module>z3-statistics-infrastructure</module>
</modules>
<packaging>pom</packaging>
<profiles>
<profile>
<activation>
<property>
<name>native</name>
</property>
</activation>
<id>native</id>
<properties>
<quarkus.native.enabled>true</quarkus.native.enabled>
<skipITs>false</skipITs>
</properties>
</profile>
</profiles>
<properties>
<apache-tika.version>3.0.0</apache-tika.version>
<blaze-persistence.version>1.6.14</blaze-persistence.version>
<bouncycastle-bcpkix.version>1.79</bouncycastle-bcpkix.version>
<checkstyle-maven-plugin.version>3.6.0</checkstyle-maven-plugin.version>
<checkstyle.version>10.21.1</checkstyle.version>
<commons-csv.version>1.13.0</commons-csv.version>
<commons-validator.version>1.9.0</commons-validator.version>
<crawler-detect.version>1.0.0</crawler-detect.version>
<dependency-check-maven.api_key/>
<dependency-check-maven.version>12.0.0</dependency-check-maven.version>
<failsafe-plugin.version>3.5.2</failsafe-plugin.version>
<jacoco.version>0.8.12</jacoco.version>
<jspecify.version>1.0.0</jspecify.version>
<log4j2.version>2.24.3</log4j2.version>
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
<maven.compiler.release>21</maven.compiler.release>
<maxmind-geoip2.version>2.15.0</maxmind-geoip2.version>
<net-datafaker.version>2.4.2</net-datafaker.version>
<opencsv.version>5.9</opencsv.version>
<pegase-happy.version>1.0.1</pegase-happy.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.17.6</quarkus.platform.version>
<seancfoley-ipaddress.version>5.5.1</seancfoley-ipaddress.version>
<skipITs>true</skipITs>
<slugify-dep-icu4j-icu.version>76.1</slugify-dep-icu4j-icu.version>
<slugify.version>3.0.7</slugify.version>
<spring-security.version>6.4.2</spring-security.version>
<surefire-plugin.version>3.5.2</surefire-plugin.version>
<yauaa-useragent.version>7.30.0</yauaa-useragent.version>
<z3.version>1.1.0-SNAPSHOT</z3.version>
<z3.version>1.1.0-SNAPSHOT</z3.version>
</properties>
<repositories>
<repository>
<id>gitlab-maven</id>
<url>https://gitlab.pegase.tech/api/v4/projects/68/packages/maven</url>
</repository>
</repositories>
<version>1.1.0-SNAPSHOT</version>
</project>

View file

@ -0,0 +1,43 @@
# DATABASE
APP_DB_KIND=
APP_DB_USERNAME=
APP_DB_PASSWORD=
APP_DB_JDBC_URL=
# EXPORT
APP_RESOLVER_FQDN=example.net
# LOG
APP_LOG_LEVEL=INFO
# MAX UPLOAD SIZE
APP_MAX_UPLOAD_SIZE=100M
# SECURITY
APP_JWT_ISSUER=
APP_JWT_PUBLIC_KEY=
APP_JWT_PRIVATE_KEY=
# ENCRYPTION
AES_KEY=
AES_SALT=
AES_KEY_LENGTH=
# MAIL
APP_MAIL_FROM=z3@example.net
APP_MAIL_HOST=localhost
APP_MAIL_PORT=1025
APP_MAIL_START_TLS=OPTIONAL
APP_MAIL_TLS=false
APP_MAIL_USERNAME=null
APP_MAIL_PASSWORD=null
APP_MAIL_DEV_SIMULATE=false
# - ACCOUNT CREATION - optional - an email is sent
APP_CREATE_USER_ON_START=
# - SCHEDULING
# By default, the syntax used for cron expressions is based on Quartz
# - https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
# You can change the syntax using the following property:
# quarkus.scheduler.cron-type=unix
APP_REPORT_INVALID_LINKS=

187
z3-application/pom.xml Normal file
View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>z3-application</artifactId>
<build>
<plugins>
<plugin>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
<extensions>true</extensions>
<groupId>${quarkus.platform.group-id}</groupId>
<version>${quarkus.platform.version}</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- FIX warnings about maybe mockito, maybe removed when bumping quarkus-->
<argLine>-XX:+EnableDynamicAgentLoading -Xshare:off</argLine>
<!-- To remove JBoss Log error on build and testing for all modules-->
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
<quarkus.jacoco.data-file>${project.build.directory}/coverage-reports/jacoco.exec
</quarkus.jacoco.data-file>
<quarkus.jacoco.report-location>${project.build.directory}/coverage-jacoco
</quarkus.jacoco.report-location>
<quarkus.jacoco.reuse-data-file>true</quarkus.jacoco.reuse-data-file>
</systemPropertyVariables>
</configuration>
<version>${surefire-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<version>${failsafe-plugin.version}</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<artifactId>z3-domain</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>${z3.version}</version>
</dependency>
<dependency>
<artifactId>z3-infrastructure</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>${z3.version}</version>
</dependency>
<dependency>
<artifactId>z3-statistics-domain</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>${z3.version}</version>
</dependency>
<dependency>
<artifactId>z3-statistics-infrastructure</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>${z3.version}</version>
</dependency>
<dependency>
<artifactId>z3-shared</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>${z3.version}</version>
</dependency>
<dependency>
<artifactId>quarkus-arc</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-rest</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-rest-jackson</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-narayana-jta</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-hibernate-validator</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-scheduler</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-vertx</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>quarkus-jdbc-h2</artifactId>
<groupId>io.quarkus</groupId>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>quarkus-flyway</artifactId>
<groupId>io.quarkus</groupId>
</dependency>
<dependency>
<artifactId>flyway-database-postgresql</artifactId>
<groupId>org.flywaydb</groupId>
</dependency>
<dependency>
<artifactId>quarkus-junit5-mockito</artifactId>
<groupId>io.quarkus</groupId>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>quarkus-jacoco</artifactId>
<groupId>io.quarkus</groupId>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>happy-quarkus</artifactId>
<groupId>tech.pegase.happy</groupId>
<version>${pegase-happy.version}</version>
</dependency>
<dependency>
<artifactId>opencsv</artifactId>
<groupId>com.opencsv</groupId>
<version>${opencsv.version}</version>
</dependency>
<dependency>
<artifactId>tika-core</artifactId>
<groupId>org.apache.tika</groupId>
<version>${apache-tika.version}</version>
</dependency>
<dependency>
<artifactId>ipaddress</artifactId>
<groupId>com.github.seancfoley</groupId>
<version>${seancfoley-ipaddress.version}</version>
</dependency>
</dependencies>
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>z3</artifactId>
<groupId>tech.pegase.z3</groupId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<properties>
<maven.compiler.source>22</maven.compiler.source>
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View file

@ -0,0 +1,23 @@
package tech.pegase.z3.application.api;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import org.jspecify.annotations.NullMarked;
@NullMarked
@Path("/")
public class RootResource {
@Inject
public RootResource() {}
@GET
@Path("/")
@PermitAll
public Response getRoot() {
return Response.ok().entity("Z3 App running").build();
}
}

View file

@ -0,0 +1,57 @@
package tech.pegase.z3.application.api.v1.auth;
import io.quarkus.security.Authenticated;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.context.UserContext;
import tech.pegase.z3.application.exception.ServerErrorException;
import tech.pegase.z3.application.presenter.auth.ChangePasswordPresenter;
import tech.pegase.z3.application.presenter.auth.ChangePasswordPresenter.MessageDto;
import tech.pegase.z3.application.request.auth.ChangePasswordRequest;
import tech.pegase.z3.domain.usecase.users.ChangeUserPassword;
@Path("/api/v1/auth")
@Authenticated
public class ChangePasswordResource {
private final UseCaseExecutor useCaseExecutor;
private final ChangeUserPassword changeUserPassword;
private final ChangePasswordPresenter changePasswordPresenter;
private final UserContext userContext;
ChangePasswordResource(
UseCaseExecutor executor,
ChangeUserPassword useCase,
ChangePasswordPresenter presenter,
UserContext userContext) {
useCaseExecutor = executor;
changeUserPassword = useCase;
changePasswordPresenter = presenter;
this.userContext = userContext;
}
@PUT
@Path("/password")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<MessageDto> updateUser(@Valid ChangePasswordRequest request) {
var userContextId = userContext.getId();
if (userContextId == null) {
throw new ServerErrorException("Cannot get user context");
}
return useCaseExecutor
.forUseCase(changeUserPassword)
.withParameter(
new ChangeUserPassword.Input.WhereId(
userContextId, request.getPassword(), request.getNewPassword()))
.withPresenter(changePasswordPresenter)
.run();
}
}

View file

@ -0,0 +1,50 @@
package tech.pegase.z3.application.api.v1.auth;
import io.quarkus.security.Authenticated;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.context.UserContext;
import tech.pegase.z3.application.exception.ServerErrorException;
import tech.pegase.z3.application.presenter.user.GetUserPresenter;
import tech.pegase.z3.application.presenter.user.GetUserPresenter.UserDto;
import tech.pegase.z3.domain.usecase.users.GetUser;
@Path("/api/v1/auth")
@ApplicationScoped
public class GetProfileResource {
private final UseCaseExecutor useCaseExecutor;
private final GetUser getUserUseCase;
private final GetUserPresenter getUserPresenter;
private final UserContext userContext;
public GetProfileResource(
UseCaseExecutor executor,
GetUser getUserUseCase,
GetUserPresenter getUserPresenter,
UserContext userContext) {
this.useCaseExecutor = executor;
this.getUserUseCase = getUserUseCase;
this.getUserPresenter = getUserPresenter;
this.userContext = userContext;
}
@GET
@Path("/me")
@Authenticated
public RestResponse<UserDto> currentUser() {
var userContextId = userContext.getId();
if (userContextId == null) {
throw new ServerErrorException("Cannot get user context");
}
return useCaseExecutor
.forUseCase(getUserUseCase)
.withParameter(new GetUser.Input.ForId(userContextId))
.withPresenter(getUserPresenter)
.run();
}
}

View file

@ -0,0 +1,45 @@
package tech.pegase.z3.application.api.v1.auth;
import jakarta.annotation.security.PermitAll;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.validation.Valid;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.RestResponse;
import org.jspecify.annotations.NullMarked;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.auth.AuthenticateUserPresenter;
import tech.pegase.z3.application.presenter.auth.AuthenticateUserPresenter.JwtTokenJsonDto;
import tech.pegase.z3.application.request.auth.AuthRequest;
import tech.pegase.z3.domain.usecase.auth.AuthenticateUser;
@NullMarked
@Path("/api/v1/auth")
@ApplicationScoped
public class LoginResource {
private final UseCaseExecutor useCaseExecutor;
private final AuthenticateUser authenticateUserUseCase;
private final AuthenticateUserPresenter authenticateUserPresenter;
public LoginResource(
UseCaseExecutor executor,
AuthenticateUser useCase,
AuthenticateUserPresenter presenter) {
this.useCaseExecutor = executor;
this.authenticateUserUseCase = useCase;
this.authenticateUserPresenter = presenter;
}
@POST
@Path("/login")
@PermitAll
public RestResponse<JwtTokenJsonDto> auth(@Valid AuthRequest request) {
// TODO: add rate limiting.
return useCaseExecutor
.forUseCase(authenticateUserUseCase)
.withParameter(
new AuthenticateUser.Input.Authenticate(request.getLogin(), request.getPassword()))
.withPresenter(authenticateUserPresenter)
.run();
}
}

View file

@ -0,0 +1,4 @@
@NullMarked
package tech.pegase.z3.application.api.v1.auth;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,48 @@
package tech.pegase.z3.application.api.v1.brand;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.brand.CreateBrandPresenter;
import tech.pegase.z3.application.presenter.brand.CreateBrandPresenter.BrandDto;
import tech.pegase.z3.application.request.brand.CreateBrandRequest;
import tech.pegase.z3.domain.usecase.brands.CreateBrand;
import tech.pegase.z3.domain.usecase.brands.CreateBrand.Input;
@Authenticated
@Path("/api/v1/brands")
public class CreateBrandResource {
private final UseCaseExecutor useCaseExecutor;
private final CreateBrand createBrand;
private final CreateBrandPresenter createBrandPresenter;
@Inject
public CreateBrandResource(
UseCaseExecutor executor, CreateBrand useCase, CreateBrandPresenter presenter) {
this.useCaseExecutor = executor;
this.createBrand = useCase;
this.createBrandPresenter = presenter;
}
@POST
@Path("/")
@RolesAllowed("ADMINISTRATOR")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<BrandDto> create(@Valid CreateBrandRequest request) {
var parameter = new Input.With(request.getName(), request.getFqdns());
return useCaseExecutor
.forUseCase(createBrand)
.withParameter(parameter)
.withPresenter(createBrandPresenter)
.run();
}
}

View file

@ -0,0 +1,43 @@
package tech.pegase.z3.application.api.v1.brand;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.brand.GetBrandPresenter;
import tech.pegase.z3.application.presenter.brand.GetBrandPresenter.BrandDto;
import tech.pegase.z3.domain.usecase.brands.GetBrand;
import tech.pegase.z3.domain.usecase.brands.GetBrand.Input.ForSlug;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/brands")
@Authenticated
public class GetBrandResource {
private final UseCaseExecutor useCaseExecutor;
private final GetBrand getBrand;
private final GetBrandPresenter getBrandResponsePresenter;
@Inject
public GetBrandResource(
UseCaseExecutor executor, GetBrand useCase, GetBrandPresenter presenter) {
this.useCaseExecutor = executor;
this.getBrand = useCase;
this.getBrandResponsePresenter = presenter;
}
@GET
@Path("/{brand}")
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<BrandDto> getBrand(@PathParam("brand") Slug brand) {
return useCaseExecutor
.forUseCase(getBrand)
.withParameter(new ForSlug(brand))
.withPresenter(getBrandResponsePresenter)
.run();
}
}

View file

@ -0,0 +1,51 @@
package tech.pegase.z3.application.api.v1.brand;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.dto.PaginatedDto;
import tech.pegase.z3.application.presenter.brand.ListBrandsPresenter;
import tech.pegase.z3.application.presenter.brand.ListBrandsPresenter.BrandDto;
import tech.pegase.z3.application.request.brand.PaginatedBrandListRequest;
import tech.pegase.z3.domain.usecase.brands.ListBrands;
import tech.pegase.z3.shared.pagination.Pagination;
@Path("/api/v1/brands")
@Authenticated
public class ListBrandsResource {
private final UseCaseExecutor useCaseExecutor;
private final ListBrands listBrands;
private final ListBrandsPresenter listBrandsPresenter;
@Inject
public ListBrandsResource(
UseCaseExecutor executor, ListBrands useCase, ListBrandsPresenter presenter) {
this.useCaseExecutor = executor;
this.listBrands = useCase;
this.listBrandsPresenter = presenter;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<PaginatedDto<BrandDto>> getPaginatedBrandList(
@BeanParam @Valid PaginatedBrandListRequest request) {
return useCaseExecutor
.forUseCase(listBrands)
.withParameter(getParameterFromRequest(request))
.withPresenter(listBrandsPresenter)
.run();
}
private ListBrands.Input getParameterFromRequest(PaginatedBrandListRequest request) {
var pagination = Pagination.fromPageNumberAndPageSize(request.getPage(), request.getPerPage());
return new ListBrands.Input.WithPagination(pagination);
}
}

View file

@ -0,0 +1,45 @@
package tech.pegase.z3.application.api.v1.brand;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.brand.UpdateBrandPresenter;
import tech.pegase.z3.application.presenter.brand.UpdateBrandPresenter.BrandDto;
import tech.pegase.z3.application.request.brand.UpdateBrandRequest;
import tech.pegase.z3.domain.usecase.brands.UpdateBrand;
import tech.pegase.z3.domain.usecase.brands.UpdateBrand.Input.ForSlug;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/brands")
@Authenticated
public class UpdateBrandResource {
private final UseCaseExecutor useCaseExecutor;
private final UpdateBrand updateBrandUseCase;
private final UpdateBrandPresenter updateBrandPresenter;
@Inject
UpdateBrandResource(
UseCaseExecutor executor, UpdateBrand useCase, UpdateBrandPresenter presenter) {
useCaseExecutor = executor;
updateBrandUseCase = useCase;
updateBrandPresenter = presenter;
}
@PUT
@Path("/{brand}")
@RolesAllowed("ADMINISTRATOR")
public RestResponse<BrandDto> updateBrand(
@PathParam("brand") Slug brand, @Valid UpdateBrandRequest request) {
return useCaseExecutor
.forUseCase(updateBrandUseCase)
.withParameter(new ForSlug(brand, request.getName(), request.getFqdns()))
.withPresenter(updateBrandPresenter)
.run();
}
}

View file

@ -0,0 +1,4 @@
@NullMarked
package tech.pegase.z3.application.api.v1.brand;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,48 @@
package tech.pegase.z3.application.api.v1.catalog;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.catalog.CreateCatalogPresenter;
import tech.pegase.z3.application.presenter.catalog.CreateCatalogPresenter.CatalogDto;
import tech.pegase.z3.application.request.catalog.CreateCatalogRequest;
import tech.pegase.z3.domain.usecase.catalogs.CreateCatalog;
import tech.pegase.z3.domain.usecase.catalogs.CreateCatalog.Input;
@Authenticated
@Path("/api/v1/catalogs")
public class CreateCatalogResource {
private final UseCaseExecutor useCaseExecutor;
private final CreateCatalog createCatalog;
private final CreateCatalogPresenter createCatalogPresenter;
@Inject
public CreateCatalogResource(
UseCaseExecutor executor,
CreateCatalog useCase,
CreateCatalogPresenter presenter) {
this.useCaseExecutor = executor;
this.createCatalog = useCase;
this.createCatalogPresenter = presenter;
}
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<CatalogDto> create(@Valid CreateCatalogRequest request) {
Input parameter = new Input.With(request.getName(), request.getBrand());
return useCaseExecutor
.forUseCase(createCatalog)
.withParameter(parameter)
.withPresenter(createCatalogPresenter)
.run();
}
}

View file

@ -0,0 +1,43 @@
package tech.pegase.z3.application.api.v1.catalog;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.catalog.GetCatalogPresenter;
import tech.pegase.z3.application.presenter.catalog.GetCatalogPresenter.CatalogDto;
import tech.pegase.z3.domain.usecase.catalogs.GetCatalog;
import tech.pegase.z3.domain.usecase.catalogs.GetCatalog.Input.ForSlug;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/catalogs")
@Authenticated
public class GetCatalogResource {
private final UseCaseExecutor useCaseExecutor;
private final GetCatalog getCatalog;
private final GetCatalogPresenter getCatalogResponsePresenter;
@Inject
public GetCatalogResource(
UseCaseExecutor executor, GetCatalog useCase, GetCatalogPresenter presenter) {
this.useCaseExecutor = executor;
this.getCatalog = useCase;
this.getCatalogResponsePresenter = presenter;
}
@GET
@Path("/{catalog}")
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<CatalogDto> getCatalog(@PathParam("catalog") Slug catalog) {
return useCaseExecutor
.forUseCase(getCatalog)
.withParameter(new ForSlug(catalog))
.withPresenter(getCatalogResponsePresenter)
.run();
}
}

View file

@ -0,0 +1,58 @@
package tech.pegase.z3.application.api.v1.catalog;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.dto.PaginatedDto;
import tech.pegase.z3.application.presenter.catalog.ListCatalogsPresenter;
import tech.pegase.z3.application.presenter.catalog.ListCatalogsPresenter.CatalogDto;
import tech.pegase.z3.application.request.catalog.PaginatedCatalogListRequest;
import tech.pegase.z3.domain.usecase.catalogs.ListCatalogs;
import tech.pegase.z3.shared.pagination.Pagination;
@Path("/api/v1/catalogs")
@Authenticated
public class ListCatalogsResource {
private final UseCaseExecutor useCaseExecutor;
private final ListCatalogs listCatalogs;
private final ListCatalogsPresenter listCatalogsPresenter;
@Inject
public ListCatalogsResource(
UseCaseExecutor executor, ListCatalogs useCase, ListCatalogsPresenter presenter) {
this.useCaseExecutor = executor;
this.listCatalogs = useCase;
this.listCatalogsPresenter = presenter;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<PaginatedDto<CatalogDto>> getPaginatedCatalogList(
@BeanParam @Valid PaginatedCatalogListRequest request) {
return useCaseExecutor
.forUseCase(listCatalogs)
.withParameter(getParameterFromRequest(request))
.withPresenter(listCatalogsPresenter)
.run();
}
private ListCatalogs.Input getParameterFromRequest(PaginatedCatalogListRequest request) {
var pagination =
Pagination.fromPageNumberAndPageSize(request.getPage(), request.getPerPage());
var brand = request.getBrand();
if (brand != null) {
return new ListCatalogs.Input.WithPaginationForBrand(brand, pagination);
}
return new ListCatalogs.Input.WithPagination(pagination);
}
}

View file

@ -0,0 +1,45 @@
package tech.pegase.z3.application.api.v1.catalog;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.dto.MessageDto;
import tech.pegase.z3.application.presenter.catalog.RemoveCatalogPresenter;
import tech.pegase.z3.domain.usecase.catalogs.RemoveCatalog;
import tech.pegase.z3.domain.usecase.catalogs.RemoveCatalog.Input.ForSlug;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/catalogs")
@Authenticated
public class RemoveCatalogResource {
private final UseCaseExecutor useCaseExecutor;
private final RemoveCatalog removeCatalogUseCase;
private final RemoveCatalogPresenter removeCatalogPresenter;
@Inject
RemoveCatalogResource(
UseCaseExecutor executor,
RemoveCatalog useCase,
RemoveCatalogPresenter presenter) {
useCaseExecutor = executor;
removeCatalogUseCase = useCase;
removeCatalogPresenter = presenter;
}
@DELETE
@Path("/{catalog}")
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<MessageDto> removeCatalog(@PathParam("catalog") Slug catalog) {
return useCaseExecutor
.forUseCase(removeCatalogUseCase)
.withParameter(new ForSlug(catalog))
.withPresenter(removeCatalogPresenter)
.run();
}
}

View file

@ -0,0 +1,50 @@
package tech.pegase.z3.application.api.v1.catalog;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.catalog.UpdateCatalogPresenter;
import tech.pegase.z3.application.presenter.catalog.UpdateCatalogPresenter.CatalogDto;
import tech.pegase.z3.application.request.catalog.UpdateCatalogRequest;
import tech.pegase.z3.domain.usecase.catalogs.UpdateCatalog;
import tech.pegase.z3.domain.usecase.catalogs.UpdateCatalog.Input.ForSlug;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/catalogs")
@Authenticated
public class UpdateCatalogResource {
private final UseCaseExecutor useCaseExecutor;
private final UpdateCatalog updateCatalogUseCase;
private final UpdateCatalogPresenter updateCatalogPresenter;
@Inject
UpdateCatalogResource(
UseCaseExecutor executor,
UpdateCatalog useCase,
UpdateCatalogPresenter presenter) {
useCaseExecutor = executor;
updateCatalogUseCase = useCase;
updateCatalogPresenter = presenter;
}
@PUT
@Path("/{catalog}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<CatalogDto> updateCatalog(
@PathParam("catalog") Slug catalog, @Valid UpdateCatalogRequest request) {
return useCaseExecutor
.forUseCase(updateCatalogUseCase)
.withParameter(new ForSlug(catalog, request.getName()))
.withPresenter(updateCatalogPresenter)
.run();
}
}

View file

@ -0,0 +1,4 @@
@NullMarked
package tech.pegase.z3.application.api.v1.catalog;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,62 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.link.CreateLinkPresenter;
import tech.pegase.z3.application.presenter.link.CreateLinkPresenter.LinkDto;
import tech.pegase.z3.application.request.link.CreateLinkRequest;
import tech.pegase.z3.domain.usecase.links.CreateLink;
import tech.pegase.z3.domain.usecase.links.CreateLink.Input.CommonLinkInput;
import tech.pegase.z3.domain.usecase.links.CreateLink.Input.WithSourceAndUrl;
import tech.pegase.z3.domain.usecase.links.CreateLink.Input.WithUrl;
@Path("/api/v1/links")
@Authenticated
public class CreateLinkResource {
private final UseCaseExecutor useCaseExecutor;
private final CreateLink createLink;
private final CreateLinkPresenter createLinkPresenter;
@Inject
public CreateLinkResource(
UseCaseExecutor executor, CreateLink useCase, CreateLinkPresenter presenter) {
this.useCaseExecutor = executor;
this.createLink = useCase;
this.createLinkPresenter = presenter;
}
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<LinkDto> create(CreateLinkRequest request) {
var commonLinkInput = from(request);
CreateLink.Input parameter =
request.getSource() == null
? new WithUrl(commonLinkInput)
: new WithSourceAndUrl(request.getSource(), commonLinkInput);
return useCaseExecutor
.forUseCase(createLink)
.withParameter(parameter)
.withPresenter(createLinkPresenter)
.run();
}
CommonLinkInput from(CreateLinkRequest request) {
var target = request.getTarget();
var isExternal = request.isExternal();
var brandUuid = request.getBrand();
var tagUuid = request.getTag();
var catalogUuid = request.getCatalog();
return new CommonLinkInput(target, isExternal, brandUuid, tagUuid, catalogUuid);
}
}

View file

@ -0,0 +1,217 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.resteasy.reactive.RestResponse;
import org.jspecify.annotations.Nullable;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.brand.GetBrandPresenter;
import tech.pegase.z3.application.presenter.catalog.GetCatalogPresenter;
import tech.pegase.z3.application.presenter.link.CsvPartialExportLinkPresenter;
import tech.pegase.z3.application.presenter.link.CsvPartialExportLinkPresenter.PartialExport;
import tech.pegase.z3.domain.usecase.brands.GetBrand;
import tech.pegase.z3.domain.usecase.catalogs.GetCatalog;
import tech.pegase.z3.domain.usecase.links.ExportLinks;
import tech.pegase.z3.domain.usecase.links.ExportLinks.Input;
import tech.pegase.z3.domain.valueobject.Slug;
@Path("/api/v1/links-export")
@Authenticated
public class ExportLinksResource {
private final UseCaseExecutor useCaseExecutor;
private final ExportLinks exportLinks;
private final GetBrand getBrand;
private final GetCatalog getCatalog;
private final CsvPartialExportLinkPresenter csvPartialExportLinkPresenter;
@Inject
public ExportLinksResource(
UseCaseExecutor executor,
ExportLinks useCase,
GetBrand getBrandUseCase,
GetCatalog getCatalogUseCase,
CsvPartialExportLinkPresenter presenter) {
this.useCaseExecutor = executor;
this.exportLinks = useCase;
this.getBrand = getBrandUseCase;
this.getCatalog = getCatalogUseCase;
this.csvPartialExportLinkPresenter = presenter;
}
@GET
@Path("/")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public RestResponse<StreamingOutput> exportLink(
@QueryParam("per-page") @Valid @jakarta.annotation.Nullable @Nullable @Min(1) @Max(50)
Integer perPage) {
try {
perPage = Optional.ofNullable(perPage).orElse(50);
csvPartialExportLinkPresenter.setWithHeaders(true);
var output = genOutput(perPage);
return buildResponse(output, "external_links");
} catch (IllegalStateException e) {
return RestResponse.status(Response.Status.BAD_REQUEST);
}
}
@GET
@Path("/brands/{brand: [a-zA-Z0-9-]+}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public RestResponse<StreamingOutput> exportBrandLinks(
@PathParam("brand") Slug brand,
@QueryParam("per-page") @Valid @jakarta.annotation.Nullable @Nullable @Min(1) @Max(50)
Integer perPage) {
try {
perPage = Optional.ofNullable(perPage).orElse(50);
var resolvedBrand = resolveBrand(brand);
if (resolvedBrand == null) {
return RestResponse.status(Status.NOT_FOUND);
}
csvPartialExportLinkPresenter.setWithHeaders(true);
var output = genBrandOutput(brand, perPage);
return buildResponse(output, brand.toString());
} catch (IllegalStateException e) {
return RestResponse.status(Response.Status.BAD_REQUEST);
}
}
@GET
@Path("/catalogs/{catalog: [a-zA-Z0-9-]+}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public RestResponse<StreamingOutput> exportCatalogLinks(
@PathParam("catalog") Slug catalog,
@QueryParam("per-page") @Valid @jakarta.annotation.Nullable @Nullable @Min(1) @Max(50)
Integer perPage) {
try {
perPage = Optional.ofNullable(perPage).orElse(50);
var resolvedCatalog = resolveCatalog(catalog);
if (resolvedCatalog == null) {
return RestResponse.status(Status.NOT_FOUND);
}
csvPartialExportLinkPresenter.setWithHeaders(true);
var output = genCatalogOutput(catalog, perPage);
return buildResponse(output, catalog.toString());
} catch (IllegalStateException e) {
return RestResponse.status(Response.Status.BAD_REQUEST);
}
}
private GetBrandPresenter.@Nullable BrandDto resolveBrand(Slug brandSlug) {
try (var response =
useCaseExecutor
.forUseCase(getBrand)
.withParameter(new GetBrand.Input.ForSlug(brandSlug))
.withPresenter(
new GetBrandPresenter() {
@Override
public RestResponse<@Nullable BrandDto> present(GetBrand.Output output) {
if (output instanceof GetBrand.Output.Found) {
return RestResponse.ok(BrandDto.of(((GetBrand.Output.Found) output).brand()));
}
return RestResponse.notFound();
}
})
.run()) {
return response.getStatusInfo().equals(Status.NOT_FOUND) ? null : response.getEntity();
}
}
private GetCatalogPresenter.@Nullable CatalogDto resolveCatalog(Slug catalogSlug) {
try (var response =
useCaseExecutor
.forUseCase(getCatalog)
.withParameter(new GetCatalog.Input.ForSlug(catalogSlug))
.withPresenter(
new GetCatalogPresenter() {
@Override
public RestResponse<@Nullable CatalogDto> present(GetCatalog.Output output) {
if (output instanceof GetCatalog.Output.Found) {
return RestResponse.ok(
CatalogDto.of(((GetCatalog.Output.Found) output).catalog()));
}
return RestResponse.notFound();
}
})
.run()) {
return response.getStatusInfo().equals(Status.NOT_FOUND) ? null : response.getEntity();
}
}
private StreamingOutput genBrandOutput(Slug brandSlug, int perPage) {
return genOutputWith(
page -> new Input.PageForBrand(brandSlug, new Input.Pagination(page, perPage)));
}
private StreamingOutput genCatalogOutput(Slug catalogSlug, int perPage) {
return genOutputWith(
page -> new Input.PageForCatalog(catalogSlug, new Input.Pagination(page, perPage)));
}
private StreamingOutput genOutput(int perPage) {
return genOutputWith(page -> new Input.PageForNonBranded(new Input.Pagination(page, perPage)));
}
private StreamingOutput genOutputWith(Function<Integer, Input> inputFunction) {
return out -> {
var page = 0;
PartialExport exportPart;
do {
exportPart =
useCaseExecutor
.forUseCase(exportLinks)
.withParameter(inputFunction.apply(page++))
.withPresenter(csvPartialExportLinkPresenter)
.run();
if (exportPart instanceof PartialExport.Part(var partContent)) {
out.write(partContent.getBytes(StandardCharsets.UTF_8));
out.flush();
}
} while (exportPart instanceof PartialExport.Part);
};
}
private RestResponse<StreamingOutput> buildResponse(StreamingOutput output, String exportName) {
var appName = ConfigProvider.getConfig().getValue("quarkus.application.name", String.class);
return RestResponse.ResponseBuilder.create(Status.OK, output)
.header(
"Content-Disposition",
"attachment; filename=\"%s-export-%s-%s.csv\";"
.formatted(
appName, exportName, new SimpleDateFormat("dd-MM-yyyy").format(new Date())))
.header("Content-Type", "application/csv; charset=UTF-8")
.header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
.header("Pragma", "no-cache")
.build();
}
}

View file

@ -0,0 +1,43 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.link.GetLinkPresenter;
import tech.pegase.z3.application.presenter.link.GetLinkPresenter.LinkDto;
import tech.pegase.z3.domain.usecase.links.GetLink;
import tech.pegase.z3.domain.usecase.links.GetLink.Input.ForSource;
import tech.pegase.z3.shared.valueobject.Source;
@Path("/api/v1/links")
@Authenticated
public class GetLinkResource {
private final UseCaseExecutor useCaseExecutor;
private final GetLink getLink;
private final GetLinkPresenter getLinkResponsePresenter;
@Inject
public GetLinkResource(UseCaseExecutor executor, GetLink useCase, GetLinkPresenter presenter) {
this.useCaseExecutor = executor;
this.getLink = useCase;
this.getLinkResponsePresenter = presenter;
}
@GET
@Path("/{source: [a-zA-Z0-9/._~!$'()*+,;=:@-]+}")
@Produces(MediaType.APPLICATION_JSON)
/* Ici source correspond à la partie path + query + anchor d'une URL sur un QR code. */
public RestResponse<LinkDto> getLink(@PathParam("source") Source source) {
return useCaseExecutor
.forUseCase(getLink)
.withParameter(new ForSource(source))
.withPresenter(getLinkResponsePresenter)
.run();
}
}

View file

@ -0,0 +1,231 @@
package tech.pegase.z3.application.api.v1.link;
import com.opencsv.CSVParser;
import com.opencsv.CSVParserBuilder;
import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.exceptions.CsvException;
import com.opencsv.exceptions.CsvValidationException;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.tika.Tika;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import org.jspecify.annotations.Nullable;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.exception.ServerErrorException;
import tech.pegase.z3.application.exception.ValidationException;
import tech.pegase.z3.application.exception.ValidationException.ValidationError;
import tech.pegase.z3.application.presenter.link.ImportLinksPresenter;
import tech.pegase.z3.application.presenter.link.ImportLinksPresenter.ImportFailuresDto;
import tech.pegase.z3.domain.usecase.links.ImportLinks;
import tech.pegase.z3.domain.usecase.links.ImportLinks.Input.ImportLine;
@Path("/api/v1/links-import/")
@Authenticated
public class ImportLinksResource {
private static final Logger logger = Logger.getLogger(ImportLinksResource.class.getName());
private final UseCaseExecutor useCaseExecutor;
private final ImportLinks importLinks;
private final ImportLinksPresenter importLinksPresenter;
@Inject
public ImportLinksResource(
UseCaseExecutor executor, ImportLinks useCase, ImportLinksPresenter presenter) {
this.useCaseExecutor = executor;
this.importLinks = useCase;
this.importLinksPresenter = presenter;
}
@POST
@Path("/brands/{brand: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<ImportFailuresDto> importForBrand(
@PathParam("brand") UUID brandUuid,
@Nullable @RestForm("tag") UUID tagUuid,
@RestForm("file") @PartType("application/octet-stream") FileUpload file) {
try {
var importLines = uploadedFileToImportLine(file);
return useCaseExecutor
.forUseCase(importLinks)
.withParameter(new ImportLinks.Input.ImportForBrand(brandUuid, tagUuid, importLines))
.withPresenter(importLinksPresenter)
.run();
} catch (IllegalStateException | CsvException e) {
logger.debug(e.getMessage(), e);
return RestResponse.status(Response.Status.BAD_REQUEST);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
throw new RuntimeException(e);
}
}
@POST
@Path("/catalogs/{catalog: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<ImportFailuresDto> importForCatalog(
@PathParam("catalog") UUID catalogUuid,
@Nullable @RestForm("tag") UUID tagUuid,
@RestForm("file") @PartType("application/octet-stream") FileUpload file) {
try {
var importLines = uploadedFileToImportLine(file);
return useCaseExecutor
.forUseCase(importLinks)
.withParameter(new ImportLinks.Input.ImportForCatalog(catalogUuid, tagUuid, importLines))
.withPresenter(importLinksPresenter)
.run();
} catch (IllegalStateException | CsvException e) {
logger.debug(e.getMessage(), e);
return RestResponse.status(Response.Status.BAD_REQUEST);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
throw new RuntimeException(e);
}
}
private List<ImportLinks.Input.ImportForBrand.ImportLine> uploadedFileToImportLine(
FileUpload file) throws IOException, CsvException {
var requiredColumns = List.of("source", "target", "isExternal");
var delimiterToUse = detectDelimiterWithColumns(file, requiredColumns, ';');
assertFileHasHeader(file, requiredColumns, delimiterToUse);
assertUploadedFileIsPlainText(file);
List<ImportLinks.Input.ImportForBrand.ImportLine> output = new ArrayList<>();
try (var inputStream = Files.newInputStream(file.uploadedFile())) {
var csvReader = fromInputStream(inputStream, delimiterToUse);
var lines = csvReader.readAll();
// Skip header line.
lines.removeFirst();
for (var line : lines) {
var importLine = listToImportLine(List.of(line));
output.add(importLine);
}
return output;
}
}
private Character detectDelimiterWithColumns(
FileUpload file, List<String> columns, Character defaultDelimiter) {
var availableDelimiters = Arrays.asList(';', ',');
return availableDelimiters.stream()
.filter(
delimiter -> {
try (var inputStream = Files.newInputStream(file.uploadedFile())) {
var csvReader = fromInputStream(inputStream, delimiter);
var firstLine = List.of(csvReader.readNext());
return columns.equals(firstLine);
} catch (CsvValidationException e) {
return false;
} catch (IOException e) {
logger.warn(e.getMessage(), e);
throw new RuntimeException(e);
}
})
.findFirst()
.orElse(defaultDelimiter);
}
private ImportLine listToImportLine(List<String> values) {
if (values.size() < 3) {
throw new IllegalStateException("Import lines requires at least three values");
}
var source = values.getFirst();
var url = values.get(1);
var isExternal = Boolean.parseBoolean(values.get(2));
return new ImportLine(source, url, isExternal);
}
private void assertFileHasHeader(FileUpload file, List<String> columns, Character delimiter)
throws IOException {
try (var inputStream = Files.newInputStream(file.uploadedFile())) {
var csvReader = fromInputStream(inputStream, delimiter);
var firstLine = List.of(csvReader.readNext());
if (firstLine.size() != 3) {
throw ValidationException.of(
ValidationError.of("file", "La première ligne est invalide, elle à besoin 3 columns"));
}
if (!columns.equals(firstLine)) {
throw ValidationException.of(
ValidationError.of(
"file",
"La première ligne est invalide, il faut 'source', 'target', 'isExternal'"));
}
} catch (CsvValidationException e) {
logger.debug(e.getMessage());
throw new ServerErrorException(e);
}
}
private void assertUploadedFileIsPlainText(FileUpload file) throws IOException {
var guessedMimeType = guessMimeType(file);
if (!guessedMimeType.equals(MediaType.TEXT_PLAIN)) {
logger.debug("File is not plain text");
throw ValidationException.of(
ValidationError.of("file", "File le fichier doit contenir du texte/csv uniquement"));
}
}
private CSVReader fromInputStream(InputStream inputStream, Character delimiter)
throws IOException {
var bomInputStream =
BOMInputStream.builder().setInputStream(inputStream).setInclude(false).get();
var reader = new InputStreamReader(new BufferedInputStream(bomInputStream));
return getCsvReader(reader, delimiter);
}
private CSVReader getCsvReader(Reader reader, Character delimiter) {
var builder = new CSVReaderBuilder(reader);
var parser = getCsvParser(delimiter);
builder.withCSVParser(parser);
return builder.build();
}
private CSVParser getCsvParser(Character delimiter) {
var csvParserBuilder = new CSVParserBuilder();
csvParserBuilder.withSeparator(delimiter);
return csvParserBuilder.build();
}
private String guessMimeType(FileUpload file) throws IOException {
var tika = new Tika();
return tika.detect(new File(file.uploadedFile().toUri()));
}
}

View file

@ -0,0 +1,100 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Objects;
import org.jboss.resteasy.reactive.RestResponse;
import org.jspecify.annotations.Nullable;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.dto.PaginatedDto;
import tech.pegase.z3.application.exception.ValidationException;
import tech.pegase.z3.application.exception.ValidationException.ValidationError;
import tech.pegase.z3.application.presenter.link.ListLinksPresenter;
import tech.pegase.z3.application.presenter.link.ListLinksPresenter.LinkDto;
import tech.pegase.z3.application.request.link.PaginatedLinkListRequest;
import tech.pegase.z3.domain.usecase.links.ListLinks;
import tech.pegase.z3.domain.valueobject.concepts.Ordering;
import tech.pegase.z3.domain.valueobject.concepts.Ordering.Direction;
import tech.pegase.z3.domain.valueobject.concepts.Searching;
import tech.pegase.z3.domain.valueobject.orderings.LinkOrdering;
import tech.pegase.z3.domain.valueobject.orderings.LinkOrdering.Field;
import tech.pegase.z3.domain.valueobject.searchings.LinkSearching;
import tech.pegase.z3.shared.pagination.Pagination;
@Path("/api/v1/links")
@Authenticated
public class ListLinksResource {
private final UseCaseExecutor useCaseExecutor;
private final ListLinks listLinks;
private final ListLinksPresenter listLinksPresenter;
@Inject
public ListLinksResource(
UseCaseExecutor executor, ListLinks useCase, ListLinksPresenter presenter) {
this.useCaseExecutor = executor;
this.listLinks = useCase;
this.listLinksPresenter = presenter;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public RestResponse<PaginatedDto<LinkDto>> getPaginatedLinkList(
@BeanParam @Valid PaginatedLinkListRequest request) {
return useCaseExecutor
.forUseCase(listLinks)
.withParameter(getParameterFromRequest(request))
.withPresenter(listLinksPresenter)
.run();
}
private ListLinks.Input getParameterFromRequest(PaginatedLinkListRequest request) {
var pagination = Pagination.fromPageNumberAndPageSize(request.getPage(), request.getPerPage());
var orderBy = request.getOrderBy();
var direction = request.getDirection();
var searchOn = request.getSearchOn();
var keyword = request.getKeywords();
var filters = request.getFilters();
var defaultOrdering = LinkOrdering.of(Field.SOURCE, Direction.NATURAL);
assertCanOrder(orderBy, direction);
assertCanSearch(searchOn, keyword);
var ordering =
(orderBy != null && direction != null) ? LinkOrdering.of(orderBy, direction) : null;
var searching =
(searchOn != null && keyword != null) ? LinkSearching.of(searchOn, keyword) : null;
if (searching != null) {
return new ListLinks.Input.WithPaginationAndSearchingAllSorted(
pagination, filters, Objects.requireNonNullElse(ordering, defaultOrdering), searching);
}
return new ListLinks.Input.WithPaginationSorted(
pagination, filters, Objects.requireNonNullElse(ordering, defaultOrdering));
}
private void assertCanOrder(
LinkOrdering.@Nullable Field orderBy, Ordering.@Nullable Direction direction) {
if (orderBy != null ^ direction != null) {
throw ValidationException.of(
ValidationError.of("orderBy", "orderBy est requis avec direction"),
ValidationError.of("direction", "direction est requis avec orderBy"));
}
}
private void assertCanSearch(
LinkSearching.@Nullable Field searchOn, Searching.@Nullable SearchValue keywords) {
if (searchOn != null ^ keywords != null) {
throw ValidationException.of(
ValidationError.of("searchOn", "searchOn est requis avec keywords"),
ValidationError.of("keywords", "keywords est requis avec searchOn"));
}
}
}

View file

@ -0,0 +1,99 @@
package tech.pegase.z3.application.api.v1.link;
import io.vertx.core.eventbus.EventBus;
import jakarta.annotation.Nullable;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import java.time.ZonedDateTime;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.events.LinkRedirectedEvent;
import tech.pegase.z3.application.presenter.link.ResolveLinkResponsePresenter;
import tech.pegase.z3.application.request.link.RedirectLinkRequest;
import tech.pegase.z3.domain.usecase.links.RedirectLink;
import tech.pegase.z3.domain.usecase.links.RedirectLink.Input;
import tech.pegase.z3.domain.usecase.links.RedirectLink.Output;
import tech.pegase.z3.domain.valueobject.Fqdn;
import tech.pegase.z3.shared.valueobject.Source;
@Path("/")
public class RedirectLinkResource {
private final Fqdn resolverFqdn;
private final UseCaseExecutor useCaseExecutor;
private final RedirectLink redirectLink;
private final ResolveLinkResponsePresenter redirectLinkResponsePresenter;
private final EventBus eventBus;
public RedirectLinkResource(
@ConfigProperty(name = "z3.export.resolver-fqdn") String resolverFqdn,
UseCaseExecutor executor,
RedirectLink useCase,
ResolveLinkResponsePresenter presenter,
EventBus eventBus) {
this.useCaseExecutor = executor;
this.redirectLink = useCase;
this.redirectLinkResponsePresenter = presenter;
this.resolverFqdn = new Fqdn(resolverFqdn);
this.eventBus = eventBus;
}
@GET
@Path("/{source: [a-zA-Z0-9/._~!$'()*+,;=:@-]+}")
@PermitAll
public Response redirect(
@PathParam("source") Source source,
@QueryParam("host") @Valid @Nullable Fqdn host,
@Valid RedirectLinkRequest request) {
// * Use default Fqdn if none provided
var actualHost = host != null ? host : resolverFqdn;
return useCaseExecutor
.forUseCase(redirectLink)
.withParameter(new Input.ResolveWith(resolverFqdn, actualHost, source))
.withPresenter(
new ResolveLinkResponsePresenter() {
@Override
public Response present(Output output) {
var linkIsResolved = output instanceof Output.Found;
dispatchVisitedLink(source, linkIsResolved, request);
return redirectLinkResponsePresenter.present(output);
}
})
.run();
}
private void dispatchVisitedLink(
Source source, Boolean linkIsResolved, RedirectLinkRequest request) {
var ipAddress = request.getIpAddress();
var doNotTrack = request.getDoNotTrack();
var requestedAt = ZonedDateTime.now();
var locale = request.getLocale();
var referrerUri = request.getReferrerUri();
var userAgent = request.getUserAgent();
var screenProperties = request.getScreenProperties();
var fingerprint = request.getFingerprint();
var eventPojo =
new LinkRedirectedEvent(
source,
linkIsResolved,
requestedAt,
doNotTrack,
ipAddress,
locale,
referrerUri,
userAgent,
fingerprint,
screenProperties);
eventBus.publish("LinkRedirectedEvent", eventPojo);
}
}

View file

@ -0,0 +1,53 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.link.UpdateLinkPresenter;
import tech.pegase.z3.application.presenter.link.UpdateLinkPresenter.LinkDto;
import tech.pegase.z3.application.request.link.UpdateLinkRequest;
import tech.pegase.z3.domain.usecase.links.UpdateLink;
import tech.pegase.z3.domain.usecase.links.UpdateLink.Input.ForSource;
import tech.pegase.z3.shared.valueobject.Source;
@Path("/api/v1/links")
@Authenticated
public class UpdateLinkResource {
private final UseCaseExecutor useCaseExecutor;
private final UpdateLink updateLinkUseCase;
private final UpdateLinkPresenter updateLinkPresenter;
@Inject
UpdateLinkResource(UseCaseExecutor executor, UpdateLink useCase, UpdateLinkPresenter presenter) {
useCaseExecutor = executor;
updateLinkUseCase = useCase;
updateLinkPresenter = presenter;
}
@PUT
@Path("/{source: [a-zA-Z0-9/._~!$'()*+,;=:@-]+}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public RestResponse<LinkDto> updateLink(
@PathParam("source") Source source, @Valid UpdateLinkRequest request) {
return useCaseExecutor
.forUseCase(updateLinkUseCase)
.withParameter(
new ForSource(
source,
request.getTarget(),
request.isExternal(),
request.getTag(),
request.getCatalog()))
.withPresenter(updateLinkPresenter)
.run();
}
}

View file

@ -0,0 +1,44 @@
package tech.pegase.z3.application.api.v1.link;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import tech.pegase.happy.core.usecase.UseCaseExecutor;
import tech.pegase.z3.application.presenter.link.VerifyLinkPresenter;
import tech.pegase.z3.application.presenter.link.VerifyLinkPresenter.LinkValidityDto;
import tech.pegase.z3.domain.usecase.links.VerifyLink;
import tech.pegase.z3.domain.usecase.links.VerifyLink.Input;
import tech.pegase.z3.shared.valueobject.Source;
@Path("/api/v1/links")
@Authenticated
public class VerifyLinkResource {
private final UseCaseExecutor useCaseExecutor;
private final VerifyLink verifyLink;
private final VerifyLinkPresenter verifyLinkPresenter;
@Inject
public VerifyLinkResource(
UseCaseExecutor executor, VerifyLink useCase, VerifyLinkPresenter presenter) {
this.useCaseExecutor = executor;
this.verifyLink = useCase;
this.verifyLinkPresenter = presenter;
}
@PUT
@Path("/{source: [a-zA-Z0-9/._~!$'()*+,;=:@-]+}/verify")
@Produces(MediaType.APPLICATION_JSON)
/* Ici source correspond à la partie path + query + anchor d'une URL sur un QR code. */
public RestResponse<LinkValidityDto> verifyLinkValidity(@PathParam("source") Source source) {
return useCaseExecutor
.forUseCase(verifyLink)
.withParameter(new Input.Verify(source))
.withPresenter(verifyLinkPresenter)
.run();
}
}

View file

@ -0,0 +1,4 @@
@NullMarked
package tech.pegase.z3.application.api.v1.link;
import org.jspecify.annotations.NullMarked;

Some files were not shown because too many files have changed in this diff Show more