Init (1)
Some checks failed
Maven build / build (push) Failing after 54s

This commit is contained in:
Guillaume Dugas
2026-02-19 09:58:35 +01:00
parent 7ad05e5b14
commit 90ac1fe6b4
44 changed files with 2395 additions and 7 deletions

5
.dockerignore Normal file
View File

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

View File

@@ -18,6 +18,11 @@ jobs:
java-version: '21' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
- uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
- name: Set versions - name: Set versions
run: | run: |
VERSION_BASE=$RELEASE_VERSION_BASE VERSION_BASE=$RELEASE_VERSION_BASE

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
#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/
# TLS Certificates
.certs/

19
.mvn/settings.xml Normal file
View File

@@ -0,0 +1,19 @@
<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>gitea</id>
<configuration>
<httpHeaders>
<property>
<name>Authorization</name>
<value>token ${env.MAVEN_ACCESS_TOKEN}</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>

3
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip

47
Blocks.http Normal file
View File

@@ -0,0 +1,47 @@
### Render a simple template
POST http://localhost:8080/blocks/template/render
Content-Type: application/json
{
"definitions": {
"defsId": [
{
"name": "html",
"inputs": {
"name": "toto"
},
"slots": {
"content": [
{
"name": "html",
"inputs": {
"name": "slot content"
}
}
]
}
}
]
}
}
### Render a block configuration
POST http://localhost:8080/blocks/configuration/render
Content-Type: application/json
{
"definitions": {
"defsId": [
{
"name": "product",
"inputs": {
"product": {
"title": "TEA",
"name": "Visiting Eiffel Tower"
}
},
"slots": {}
}
]
}
}

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# ublock
This project uses Quarkus, the Supersonic Subatomic Java Framework.
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>.
## Running the application in dev mode
You can run your application in dev mode that enables live coding using:
```shell script
./mvnw quarkus:dev
```
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at <http://localhost:8080/q/dev/>.
## Packaging and running the application
The application can be packaged using:
```shell script
./mvnw package
```
It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory.
Be aware that its not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory.
The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.
If you want to build an _über-jar_, execute the following command:
```shell script
./mvnw package -Dquarkus.package.jar.type=uber-jar
```
The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`.
## Creating a native executable
You can create a native executable using:
```shell script
./mvnw package -Dnative
```
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
```shell script
./mvnw package -Dnative -Dquarkus.native.container-build=true
```
You can then execute your native executable with: `./target/ublock-1.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/maven-tooling>.
## Related Guides
- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it
## Provided Code
### REST
Easily start your REST Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)

View File

@@ -11,9 +11,9 @@
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
<artifactId>bom</artifactId>
<name>Bom</name>
<packaging>pom</packaging> <packaging>pom</packaging>
<artifactId>bom</artifactId>
<name>Compositor Bom</name>
<properties> <properties>
<compiler-plugin.version>3.14.1</compiler-plugin.version> <compiler-plugin.version>3.14.1</compiler-plugin.version>
@@ -29,12 +29,30 @@
<surefire-plugin.version>3.5.4</surefire-plugin.version> <surefire-plugin.version>3.5.4</surefire-plugin.version>
<!-- APP PROPS --> <!-- APP PROPS -->
<compositor.version>${project.version}</compositor.version>
<lombok.version>1.18.42</lombok.version> <lombok.version>1.18.42</lombok.version>
<pebble.version>4.1.0</pebble.version> <pebble.version>4.1.0</pebble.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>bom</artifactId>
<version>${compositor.version}</version>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>compositor</artifactId>
<version>${compositor.version}</version>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core</artifactId>
<version>${compositor.version}</version>
</dependency>
<dependency> <dependency>
<groupId>io.pebbletemplates</groupId> <groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId> <artifactId>pebble</artifactId>

29
domain/pom.xml Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>bom</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../bom/pom.xml</relativePath>
</parent>
<properties>
<maven.compiler.release>8</maven.compiler.release>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<artifactId>compositor</artifactId>
<name>Compositor Domain</name>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,4 +1,4 @@
package fr.cnd.compositor.blocks.models; package fr.cnd.compositor.models;
import lombok.*; import lombok.*;

View File

@@ -1,4 +1,4 @@
package fr.cnd.compositor.blocks.models; package fr.cnd.compositor.models;
import lombok.*; import lombok.*;

View File

@@ -1,4 +1,4 @@
package fr.cnd.compositor.blocks.models; package fr.cnd.compositor.models;
import lombok.*; import lombok.*;

View File

@@ -0,0 +1,63 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>core-deployment</artifactId>
<name>Core - Deployment</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson-deployment</artifactId>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package fr.cnd.compositor.core.deployment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
class CoreProcessor {
private static final String FEATURE = "core";
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}
}

View File

@@ -0,0 +1,20 @@
package fr.cnd.compositor.blocks.blocks;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
/**
* Implémentation de test pour {@link BlockTemplate}.
*/
@Identifier("test-block")
@ApplicationScoped
public class TestBlockTemplate implements BlockTemplate {
@Override
public Uni<String> render(BlockRenderDefinition definition) {
return Uni.createFrom().item("<div>Test Block</div>");
}
}

View File

@@ -0,0 +1,63 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.models.BlockRenderDefinition;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@QuarkusTest
class AbstractPebbleBlockTemplateTest {
@Inject
PebbleBlockEngine engine;
@Test
void shouldRenderStaticTemplate() {
AbstractPebbleBlockTemplate template = createTemplate("<div>Hello</div>");
String result = template.render(BlockRenderDefinition.builder().build()).await().indefinitely();
assertEquals("<div>Hello</div>", result);
}
@Test
void shouldRenderTemplateWithContext() {
AbstractPebbleBlockTemplate template = createTemplate("<div>Hello {{ name }}</div>");
Map<String, Object> context = Map.of("name", "World");
String result = template.render(BlockRenderDefinition.builder()
.inputs(context)
.build()).await().indefinitely();
assertEquals("<div>Hello World</div>", result);
}
@Test
void shouldRenderTemplateWithEmptyContext() {
AbstractPebbleBlockTemplate template = createTemplate("<span>Static content</span>");
final Map<String, Object> context = new HashMap<>();
String result = template.render(BlockRenderDefinition.builder()
.inputs(context)
.build()).await().indefinitely();
assertEquals("<span>Static content</span>", result);
}
private AbstractPebbleBlockTemplate createTemplate(String templateContent) {
AbstractPebbleBlockTemplate template = new AbstractPebbleBlockTemplate() {
@Override
public Uni<String> getTemplate() {
return Uni.createFrom().item(templateContent);
}
};
template.setEngine(engine);
return template;
}
}

View File

@@ -0,0 +1,117 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.blocks.specs.BlockEngine;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests unitaires pour {@link PebbleBlockEngine}.
* <p>
* Cette classe vérifie le comportement du moteur de rendu Pebble
* qui implémente l'interface {@link BlockEngine}.
*/
@QuarkusTest
class PebbleBlockEngineTest {
@Inject
PebbleBlockEngine pebbleBlockEngine;
@Test
void shouldRenderSimpleTemplate() {
String content = "Hello World";
String result = pebbleBlockEngine.render(content, Collections.emptyMap())
.await().indefinitely();
assertEquals("Hello World", result);
}
@Test
void shouldRenderTemplateWithVariable() {
String content = "Hello {{ name }}!";
Map<String, Object> context = Map.of("name", "Guillaume");
String result = pebbleBlockEngine.render(content, context)
.await().indefinitely();
assertEquals("Hello Guillaume!", result);
}
@Test
void shouldRenderTemplateWithMultipleVariables() {
String content = "{{ greeting }} {{ name }}, you have {{ count }} messages.";
Map<String, Object> context = Map.of(
"greeting", "Bonjour",
"name", "Alice",
"count", 5
);
String result = pebbleBlockEngine.render(content, context)
.await().indefinitely();
assertEquals("Bonjour Alice, you have 5 messages.", result);
}
@Test
void shouldRenderTemplateWithConditional() {
String content = "{% if active %}Active{% else %}Inactive{% endif %}";
String resultActive = pebbleBlockEngine.render(content, Map.of("active", true))
.await().indefinitely();
String resultInactive = pebbleBlockEngine.render(content, Map.of("active", false))
.await().indefinitely();
assertEquals("Active", resultActive);
assertEquals("Inactive", resultInactive);
}
@Test
void shouldRenderTemplateWithLoop() {
String content = "{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}";
Map<String, Object> context = Map.of("items", java.util.List.of("A", "B", "C"));
String result = pebbleBlockEngine.render(content, context)
.await().indefinitely();
assertEquals("A, B, C", result);
}
@Test
void shouldHandleEmptyContext() {
String content = "Static content only";
String result = pebbleBlockEngine.render(content, Collections.emptyMap())
.await().indefinitely();
assertEquals("Static content only", result);
}
@Test
void shouldHandleMissingVariableGracefully() {
String content = "Hello {{ name }}!";
String result = pebbleBlockEngine.render(content, Collections.emptyMap())
.await().indefinitely();
assertEquals("Hello !", result);
}
@Test
void shouldThrowExceptionForInvalidTemplate() {
String content = "{% invalid syntax %}";
RuntimeException exception = assertThrows(RuntimeException.class, () ->
pebbleBlockEngine.render(content, Collections.emptyMap())
.await().indefinitely()
);
assertTrue(exception.getMessage().contains("Failed to render template content"));
}
}

View File

@@ -0,0 +1,54 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests unitaires pour {@link CDIBlockTemplateResolver}.
* <p>
* Cette classe vérifie le comportement du resolver qui recherche
* les instances de {@link BlockTemplate} par leur annotation {@code @Identifier}.
*/
@QuarkusTest
class CDIBlockTemplateResolverTest {
@Inject
CDIBlockTemplateResolver resolver;
/**
* Vérifie que le resolver retourne null lorsque le template n'existe pas.
*/
@Test
void shouldReturnNullWhenTemplateNotFound() {
BlockTemplate result = resolver.resolveTemplate("unknown-template").await().indefinitely();
assertNull(result);
}
/**
* Vérifie que le resolver trouve et retourne le template correspondant à l'identifiant.
*/
@Test
void shouldResolveTemplateTemplateByIdentifier() {
BlockTemplate result = resolver.resolveTemplate("test-block").await().indefinitely();
assertNotNull(result);
}
/**
* Vérifie que le template résolu peut effectuer un rendu.
*/
@Test
void shouldResolveTemplateWorkingTemplate() {
BlockTemplate result = resolver.resolveTemplate("test-block").await().indefinitely();
assertNotNull(result);
String rendered = result.render(BlockRenderDefinition.builder().build()).await().indefinitely();
assertEquals("<div>Test Block</div>", rendered);
}
}

View File

@@ -0,0 +1,57 @@
package fr.cnd.compositor.blocks.services;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests unitaires pour {@link BlockService}.
* <p>
* Cette classe vérifie le comportement du service de résolution des templates
* qui parcourt les différents {@link BlockTemplateResolver}
* pour trouver le premier template correspondant.
*/
@QuarkusTest
class BlockServiceTest {
@Inject
BlockService blockService;
/**
* Vérifie que le service retourne null lorsque aucun resolver ne trouve le template.
*/
@Test
void shouldReturnNullWhenNoResolverFindsTemplate() {
BlockTemplate result = blockService.resolveTemplate("unknown-template").await().indefinitely();
assertNull(result);
}
/**
* Vérifie que le service trouve un template existant via les resolvers.
*/
@Test
void shouldResolveTemplateExistingTemplate() {
BlockTemplate result = blockService.resolveTemplate("test-block").await().indefinitely();
assertNotNull(result);
}
/**
* Vérifie que le template résolu peut effectuer un rendu correct.
*/
@Test
void shouldResolveTemplateWorkingTemplate() {
BlockTemplate result = blockService.resolveTemplate("test-block").await().indefinitely();
assertNotNull(result);
String rendered = result.render(BlockRenderDefinition.builder().build()).await().indefinitely();
assertEquals("<div>Test Block</div>", rendered);
}
}

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>core-integration-tests</artifactId>
<name>Core - Integration Tests</name>
<properties>
<skipITs>true</skipITs>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
package fr.cnd.compositor.core.it;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/core")
@ApplicationScoped
public class CoreResource {
// add some rest methods here
@GET
public String hello() {
return "Hello core";
}
}

View File

@@ -0,0 +1,7 @@
package fr.cnd.compositor.core.it;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class CoreResourceIT extends CoreResourceTest {
}

View File

@@ -0,0 +1,21 @@
package fr.cnd.compositor.core.it;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class CoreResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/core")
.then()
.statusCode(200)
.body(is("Hello core"));
}
}

21
modules/core/pom.xml Normal file
View File

@@ -0,0 +1,21 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>builder</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../builder/pom.xml</relativePath>
</parent>
<packaging>pom</packaging>
<artifactId>core-parent</artifactId>
<name>Core - Parent</name>
<modules>
<module>deployment</module>
<module>runtime</module>
</modules>
</project>

View File

@@ -0,0 +1,80 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>core</artifactId>
<name>Core - Runtime</name>
<dependencies>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>compositor</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
<annotationProcessorPathsUseDepMgmt>true</annotationProcessorPathsUseDepMgmt>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,60 @@
package fr.cnd.compositor.blocks;
import fr.cnd.compositor.models.BlockRenderRequest;
import fr.cnd.compositor.models.BlockRenderResult;
import fr.cnd.compositor.blocks.services.BlockService;
import io.smallrye.mutiny.Uni;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Path("/blocks")
public class BlockResource {
@Inject
BlockService blockService;
@POST
@Path("template/render")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<BlockRenderResult> render(BlockRenderRequest request) {
final Map<String, List<String>> result = new HashMap<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (final String definitionId : request.getDefinitions().keySet()) {
final List<String> definitionResults = new ArrayList<>();
result.put(definitionId, definitionResults);
chain = chain.chain(() -> blockService.renderTemplate(request.getDefinitions().get(definitionId))
.invoke(definitionResults::add))
.replaceWithVoid();
}
return chain.map(x -> BlockRenderResult.builder().result(result).build());
}
@POST
@Path("configuration/render")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<BlockRenderResult> renderConfiguration(BlockRenderRequest request) {
final Map<String, List<String>> result = new HashMap<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (final String definitionId : request.getDefinitions().keySet()) {
final List<String> definitionResults = new ArrayList<>();
result.put(definitionId, definitionResults);
chain = chain.chain(() -> blockService.renderBlockConfiguration(request.getDefinitions().get(definitionId))
.invoke(definitionResults::add))
.replaceWithVoid();
}
return chain.map(x -> BlockRenderResult.builder().result(result).build());
}
}

View File

@@ -1,6 +1,6 @@
package fr.cnd.compositor.blocks.pebble; package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.blocks.models.BlockRenderDefinition; import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate; import fr.cnd.compositor.blocks.specs.BlockTemplate;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject; import jakarta.inject.Inject;

View File

@@ -0,0 +1,33 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.blocks.specs.BlockEngine;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.StringWriter;
import java.util.Map;
@ApplicationScoped
public class PebbleBlockEngine implements BlockEngine {
@Inject
PebbleEngine pebbleEngine;
@Override
public Uni<String> render(String content, Map<String, Object> context) {
return Uni.createFrom().item(Unchecked.supplier(() -> {
try {
PebbleTemplate template = pebbleEngine.getTemplate(content);
StringWriter writer = new StringWriter();
template.evaluate(writer, context);
return writer.toString();
} catch (Exception e) {
throw new RuntimeException("Failed to render template content", e);
}
}));
}
}

View File

@@ -0,0 +1,19 @@
package fr.cnd.compositor.blocks.pebble;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.loader.StringLoader;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
public class PebbleProvider {
@Inject
PebbleBlocksExtension pebbleBlocksExtension;
@ApplicationScoped
PebbleEngine createPebbleEngine() {
return new PebbleEngine.Builder()
.loader(new StringLoader())
.extension(pebbleBlocksExtension)
.build();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.BlockConfiguration;
import fr.cnd.compositor.blocks.specs.BlockConfigurationResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIBlockConfigurationResolver implements BlockConfigurationResolver {
@Inject
@Any
Instance<BlockConfiguration> blockConfigurations;
@Override
public Uni<BlockConfiguration> resolveConfiguration(String blockIdentifier) {
Instance<BlockConfiguration> selected = blockConfigurations.select(Identifier.Literal.of(blockIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIBlockTemplateResolver implements BlockTemplateResolver {
@Inject
@Any
Instance<BlockTemplate> blockTemplates;
@Override
public Uni<BlockTemplate> resolveTemplate(String blockIdentifier) {
Instance<BlockTemplate> selected = blockTemplates.select(Identifier.Literal.of(blockIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.ContextMapping;
import fr.cnd.compositor.blocks.specs.ContextMappingResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIContextMappingResolver implements ContextMappingResolver {
@Inject
@Any
Instance<ContextMapping> contextMappings;
@Override
public Uni<ContextMapping> resolveContextMapping(String contextMappingIdentifier) {
Instance<ContextMapping> selected = contextMappings.select(Identifier.Literal.of(contextMappingIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,161 @@
package fr.cnd.compositor.blocks.services;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockConfiguration;
import fr.cnd.compositor.blocks.specs.BlockConfigurationResolver;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
import io.quarkus.arc.All;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.*;
/**
* Service responsible for resolving block templates by name.
* <p>
* This service aggregates all available {@link BlockTemplateResolver} implementations
* and queries them sequentially until one returns a non-null template. This allows
* multiple template sources (e.g., file-based, database-backed, remote) to coexist
* with a chain-of-responsibility pattern.
*/
@ApplicationScoped
public class BlockService {
@Inject
@All
List<BlockTemplateResolver> resolvers;
@Inject
@All
List<BlockConfigurationResolver> blockConfigurationResolvers;
/**
* Resolves a block template by its name.
* <p>
* Iterates through all registered {@link BlockTemplateResolver} instances and returns
* the first non-null {@link BlockTemplate} found. If no resolver can handle the template,
* returns {@code null}.
*
* @param templateName the identifier of the template to resolve
* @return a {@link Uni} emitting the resolved {@link BlockTemplate}, or {@code null} if not found
*/
public Uni<BlockTemplate> resolveTemplate(String templateName) {
Uni<BlockTemplate> result = Uni.createFrom().nullItem();
for (BlockTemplateResolver resolver : resolvers) {
result = result.chain(template -> {
if (template != null) {
return Uni.createFrom().item(template);
}
return resolver.resolveTemplate(templateName);
});
}
return result;
}
/**
* Resolves a block configuration by its identifier.
* <p>
* Iterates through all registered {@link BlockConfigurationResolver} instances and returns
* the first non-null {@link BlockConfiguration} found. If no resolver can handle the identifier,
* returns {@code null}.
*
* @param blockIdentifier the identifier of the block configuration to resolve
* @return a {@link Uni} emitting the resolved {@link BlockConfiguration}, or {@code null} if not found
*/
public Uni<BlockConfiguration> resolveBlockConfiguration(String blockIdentifier) {
Uni<BlockConfiguration> result = Uni.createFrom().nullItem();
for (BlockConfigurationResolver resolver : blockConfigurationResolvers) {
result = result.chain(configuration -> {
if (configuration != null) {
return Uni.createFrom().item(configuration);
}
return resolver.resolveConfiguration(blockIdentifier);
});
}
return result;
}
public Uni<String> renderTemplate(List<BlockRenderDefinition> definitions) {
final List<String> results = new ArrayList<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (BlockRenderDefinition definition : definitions) {
chain = chain.chain(() -> renderTemplate(definition))
.invoke(results::add)
.replaceWithVoid();
}
return chain.map(x -> String.join(" ", results));
}
public Uni<String> renderTemplate(BlockRenderDefinition definition) {
final Map<String, String> slotsContent = new HashMap<>();
Uni<Void> slotChain = Uni.createFrom().voidItem();
for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) {
slotChain = slotChain.chain(() -> renderTemplate(definition.getSlots().get(slotName)))
.invoke(slotContent -> slotsContent.put(slotName, slotContent))
.replaceWithVoid();
}
return slotChain.chain(() -> resolveTemplate(definition.getName()))
.chain(blockTemplate -> {
final Map<String, Object> context = new HashMap<>(Optional.ofNullable(definition.getInputs()).orElse(new HashMap<>()));
context.put("__slots__", slotsContent);
return blockTemplate.render(BlockRenderDefinition.builder()
.name(definition.getName())
.inputs(context)
.build());
});
}
public Uni<String> renderBlockConfiguration(List<BlockRenderDefinition> definitions) {
return mapDefinition(definitions)
.chain(this::renderTemplate);
}
private Uni<List<BlockRenderDefinition>> mapDefinition(List<BlockRenderDefinition> definitions) {
final List<BlockRenderDefinition> results = new ArrayList<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (BlockRenderDefinition definition : definitions) {
chain = chain.chain(() -> mapDefinition(definition))
.invoke(results::add)
.replaceWithVoid();
}
return chain.map(x -> results);
}
private Uni<BlockRenderDefinition> mapDefinition(BlockRenderDefinition definition) {
final Map<String, List<BlockRenderDefinition>> slotMapping = new HashMap<>();
Uni<Void> slotChain = Uni.createFrom().voidItem();
for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) {
final List<BlockRenderDefinition> slotDefinitions = new ArrayList<>();
slotMapping.put(slotName, slotDefinitions);
for (BlockRenderDefinition blockDefinition : definition.getSlots().get(slotName)) {
slotChain = slotChain.chain(() -> mapDefinition(blockDefinition))
.invoke(slotDefinitions::add)
.replaceWithVoid();
}
}
final BlockRenderDefinition mappedDefinition = BlockRenderDefinition.builder()
.slots(slotMapping)
.build();
return slotChain.chain(() -> resolveBlockConfiguration(definition.getName())
.call(blockConfiguration -> blockConfiguration
.getBlockTemplateName()
.invoke(mappedDefinition::setName))
.call(blockConfiguration -> blockConfiguration
.mapInputs(definition.getInputs())
.invoke(mappedDefinition::setInputs))
.map(x -> mappedDefinition));
}
}

View File

@@ -0,0 +1,7 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
public interface BlockConfigurationResolver {
Uni<BlockConfiguration> resolveConfiguration(String blockIdentifier);
}

View File

@@ -0,0 +1,52 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
import fr.cnd.compositor.blocks.pebble.PebbleBlockEngine;
import java.util.Map;
/**
* Interface définissant un moteur de rendu de templates pour les blocs.
* <p>
* Un {@code BlockEngine} est responsable de transformer un contenu template
* en chaîne de caractères finale, en utilisant un contexte de données fourni.
* Le rendu est effectué de manière asynchrone via {@link Uni}.
* <p>
* Les implémentations peuvent utiliser différents moteurs de templates
* (Pebble, Freemarker, Thymeleaf, etc.) selon les besoins.
*
* <h2>Exemple d'implémentation</h2>
* <pre>{@code
* @ApplicationScoped
* public class MyBlockEngine implements BlockEngine {
*
* @Override
* public Uni<String> render(String content, Map<String, Object> context) {
* return Uni.createFrom().item(() -> {
* // Logique de rendu du template
* return renderedContent;
* });
* }
* }
* }</pre>
*
* @see PebbleBlockEngine
*/
public interface BlockEngine {
/**
* Effectue le rendu d'un contenu template avec le contexte fourni.
* <p>
* Cette méthode prend un template sous forme de chaîne de caractères
* et le combine avec les données du contexte pour produire le résultat final.
*
* @param content le contenu du template à rendre, utilisant la syntaxe
* spécifique au moteur de template implémenté
* @param context une Map contenant les variables disponibles pour le template,
* où les clés sont les noms des variables et les valeurs leurs données
* @return un {@link Uni} émettant la chaîne de caractères résultant du rendu,
* ou une erreur si le rendu échoue (template invalide, erreur d'évaluation, etc.)
*/
Uni<String> render(String content, Map<String, Object> context);
}

View File

@@ -0,0 +1,8 @@
package fr.cnd.compositor.blocks.specs;
import fr.cnd.compositor.models.BlockRenderDefinition;
import io.smallrye.mutiny.Uni;
public interface BlockTemplate {
Uni<String> render(BlockRenderDefinition definition);
}

View File

@@ -0,0 +1,8 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
public interface BlockTemplateResolver {
Uni<BlockTemplate> resolveTemplate(String blockIdentifier);
}

View File

@@ -0,0 +1,9 @@
name: Core
#description: Do something useful.
metadata:
# keywords:
# - core
# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension
# categories:
# - "miscellaneous"
# status: "preview"

295
mvnw vendored Executable file
View File

@@ -0,0 +1,295 @@
#!/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.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
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"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
mvnw.cmd vendored Normal file
View File

@@ -0,0 +1,189 @@
<# : batch portion
@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.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

19
pom.xml
View File

@@ -7,14 +7,31 @@
<artifactId>root</artifactId> <artifactId>root</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<name>Blocks</name> <name>Compositor Parent</name>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>bom</module> <module>bom</module>
<module>builder</module> <module>builder</module>
<module>domain</module>
<module>modules/core</module> <module>modules/core</module>
</modules> </modules>
<repositories>
<repository>
<id>gitea</id>
<url>https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitea</id>
<url>https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven</url>
</repository>
<snapshotRepository>
<id>gitea</id>
<url>https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven</url>
</snapshotRepository>
</distributionManagement>
</project> </project>

Binary file not shown.

564
tmp/generate_cdc.py Normal file
View File

@@ -0,0 +1,564 @@
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from datetime import date
doc = Document()
style = doc.styles['Normal']
font = style.font
font.name = 'Calibri'
font.size = Pt(11)
# ── Page de garde ──
for _ in range(6):
doc.add_paragraph()
title = doc.add_paragraph()
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = title.add_run("Cahier des Charges")
run.bold = True
run.font.size = Pt(28)
run.font.color.rgb = RGBColor(0x1A, 0x3C, 0x6E)
subtitle = doc.add_paragraph()
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = subtitle.add_run("Plateforme de Billetterie pour le Tourisme Parisien\nPortail B2B à destination des professionnels du tourisme")
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
doc.add_paragraph()
meta = doc.add_paragraph()
meta.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = meta.add_run(f"Version 1.0 — {date.today().strftime('%d/%m/%Y')}\nDocument confidentiel")
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
doc.add_page_break()
# ── Historique des révisions ──
doc.add_heading("Historique des révisions", level=1)
table = doc.add_table(rows=2, cols=4)
table.style = 'Light Grid Accent 1'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
headers = ["Version", "Date", "Auteur", "Description"]
for i, h in enumerate(headers):
table.rows[0].cells[i].text = h
row = table.rows[1]
row.cells[0].text = "1.0"
row.cells[1].text = date.today().strftime("%d/%m/%Y")
row.cells[2].text = "[À compléter]"
row.cells[3].text = "Rédaction initiale du cahier des charges"
doc.add_page_break()
# ── Table des matières (placeholder) ──
doc.add_heading("Table des matières", level=1)
p = doc.add_paragraph("[Insérer la table des matières automatique depuis Word : Références > Table des matières]")
p.italic = True
doc.add_page_break()
# ═══════════════════════════════════════
# 1. CONTEXTE ET OBJECTIFS
# ═══════════════════════════════════════
doc.add_heading("1. Contexte et objectifs", level=1)
doc.add_heading("1.1 Contexte du projet", level=2)
doc.add_paragraph(
"Le présent cahier des charges décrit les spécifications fonctionnelles et techniques "
"d'une plateforme de billetterie en ligne dédiée au tourisme parisien. Cette plateforme "
"s'adresse exclusivement aux professionnels du secteur touristique : agences de voyages, "
"tour-opérateurs, réceptifs, autocaristes, comités d'entreprise et organisateurs d'événements."
)
doc.add_paragraph(
"La plateforme doit permettre à ces professionnels de rechercher, réserver et gérer "
"des billets pour l'ensemble des sites touristiques, musées, monuments, croisières, "
"spectacles et expériences disponibles sur la région parisienne."
)
doc.add_heading("1.2 Objectifs stratégiques", level=2)
items = [
"Centraliser l'offre touristique parisienne sur un portail B2B unique",
"Simplifier le processus de réservation pour les professionnels",
"Offrir des tarifs négociés et des conditions commerciales adaptées au B2B",
"Fournir des outils d'aide à la vente (contenus, médias, suggestions IA)",
"Assurer une visibilité optimale via une stratégie SEO avancée",
"Garantir une expérience utilisateur fluide sur tous les supports (responsive design)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("1.3 Périmètre", level=2)
doc.add_paragraph(
"Le périmètre couvre la conception, le développement, le déploiement et la maintenance "
"corrective de la plateforme (application web responsive, back-office d'administration, "
"APIs d'intégration). Les applications mobiles natives sont exclues de cette première version "
"mais pourront faire l'objet d'une évolution ultérieure."
)
# ═══════════════════════════════════════
# 2. PUBLIC CIBLE
# ═══════════════════════════════════════
doc.add_heading("2. Public cible et personas", level=1)
doc.add_heading("2.1 Utilisateurs principaux", level=2)
table = doc.add_table(rows=5, cols=3)
table.style = 'Light Grid Accent 1'
headers = ["Persona", "Profil", "Besoins clés"]
for i, h in enumerate(headers):
table.rows[0].cells[i].text = h
data = [
("Agent de voyage", "Employé d'agence, réserve pour le compte de ses clients", "Recherche rapide, devis, réservation groupée"),
("Tour-opérateur", "Conçoit des packages touristiques incluant Paris", "API d'intégration, tarifs volume, allotements"),
("Réceptif / DMC", "Gère l'accueil de groupes sur Paris", "Planning, gestion de groupes, billetterie multi-sites"),
("Administrateur", "Gestionnaire de la plateforme", "Back-office, statistiques, gestion des contenus"),
]
for idx, (persona, profil, besoins) in enumerate(data):
row = table.rows[idx + 1]
row.cells[0].text = persona
row.cells[1].text = profil
row.cells[2].text = besoins
# ═══════════════════════════════════════
# 3. ESPACE PUBLIC
# ═══════════════════════════════════════
doc.add_heading("3. Espace public", level=1)
doc.add_heading("3.1 Page d'accueil", level=2)
items = [
"Présentation de la plateforme et de sa proposition de valeur",
"Mise en avant des offres phares et des nouveautés (carrousel, bannières)",
"Moteur de recherche principal (par type d'activité, date, lieu, thématique)",
"Accès aux catégories principales (musées, monuments, croisières, spectacles, etc.)",
"Témoignages et références clients professionnels",
"Call-to-action vers l'inscription / la demande de compte professionnel",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("3.2 Catalogue des offres", level=2)
items = [
"Navigation par catégorie, thématique, arrondissement ou popularité",
"Fiches produit détaillées : description, photos, informations pratiques, conditions tarifaires",
"Système de filtres avancés (prix, disponibilité, accessibilité PMR, langue)",
"Affichage des disponibilités en temps réel (sous réserve de connexion fournisseur)",
"Suggestions de produits complémentaires (cross-selling assisté par IA)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("3.3 Pages institutionnelles", level=2)
items = [
"À propos / Qui sommes-nous",
"Conditions générales de vente (CGV) et mentions légales",
"Politique de confidentialité (RGPD)",
"FAQ et centre d'aide",
"Page contact avec formulaire",
"Blog / Actualités du tourisme parisien (levier SEO)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 4. ESPACE PRIVÉ
# ═══════════════════════════════════════
doc.add_heading("4. Espace privé (utilisateurs authentifiés)", level=1)
doc.add_heading("4.1 Inscription et authentification", level=2)
items = [
"Formulaire d'inscription avec validation manuelle ou automatique (numéro SIRET, licence d'agence)",
"Authentification sécurisée (email/mot de passe, SSO, 2FA optionnel)",
"Gestion des rôles : administrateur agence, agent, comptable (lecture seule)",
"Récupération et réinitialisation de mot de passe",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("4.2 Dashboard", level=2)
items = [
"Vue d'ensemble de l'activité : réservations récentes, chiffre d'affaires, alertes",
"Indicateurs clés de performance (KPI) personnalisables",
"Raccourcis vers les actions fréquentes (nouvelle réservation, devis en cours)",
"Notifications et messages de la plateforme",
"Calendrier des réservations à venir",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("4.3 Gestion des réservations", level=2)
items = [
"Panier multi-produits avec récapitulatif avant validation",
"Réservation instantanée ou sur demande (selon le fournisseur)",
"Historique complet des réservations avec statuts (confirmée, en attente, annulée)",
"Modification et annulation en ligne selon les conditions tarifaires",
"Téléchargement des billets (PDF, e-tickets, QR codes)",
"Génération de bons de commande et factures",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("4.4 Contenus exclusifs", level=2)
items = [
"Accès à des tarifs préférentiels et promotions réservées aux professionnels",
"Médiathèque professionnelle (photos HD, vidéos, descriptifs éditoriaux libres de droits)",
"Guides et supports de vente téléchargeables (PDF, présentations)",
"Webinaires et formations en ligne sur les produits touristiques",
"Alertes personnalisées sur les nouvelles offres et les disponibilités",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("4.5 Gestion du compte", level=2)
items = [
"Modification des informations de la société et des utilisateurs",
"Gestion des moyens de paiement (prélèvement, virement, carte bancaire, crédit)",
"Consultation du relevé de compte et de l'encours",
"Paramétrage des préférences de notification",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 5. BACK-OFFICE
# ═══════════════════════════════════════
doc.add_heading("5. Back-office d'administration", level=1)
doc.add_heading("5.1 Gestion des utilisateurs", level=2)
items = [
"CRUD complet sur les comptes professionnels (création, validation, suspension, suppression)",
"Affectation de rôles et permissions granulaires",
"Historique des connexions et journal d'audit",
"Système de validation des inscriptions (workflow d'approbation)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("5.2 Gestion du catalogue", level=2)
items = [
"Création et édition des fiches produit (WYSIWYG, médias, métadonnées SEO)",
"Gestion des catégories, tags et thématiques",
"Paramétrage des tarifs, allotements et règles de disponibilité",
"Import/export en masse (CSV, XML, API fournisseurs)",
"Gestion des fournisseurs et des contrats associés",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("5.3 Gestion des commandes et de la facturation", level=2)
items = [
"Tableau de bord des commandes avec filtres avancés",
"Validation, modification et annulation de commandes",
"Génération automatique de factures et avoirs",
"Suivi des paiements et relances automatiques",
"Export comptable (formats standards : FEC, CSV)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("5.4 Gestion des contenus éditoriaux (CMS)", level=2)
items = [
"Éditeur de pages (WYSIWYG) pour les contenus institutionnels et le blog",
"Gestion des bannières, carrousels et mises en avant",
"Planification de publication (programmation à date)",
"Gestion multilingue (français, anglais a minima ; extensible)",
"Prévisualisation avant publication",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("5.5 Statistiques et reporting", level=2)
items = [
"Tableaux de bord analytiques (ventes, CA, taux de conversion, panier moyen)",
"Rapports exportables (PDF, Excel)",
"Suivi du trafic et du comportement utilisateur (intégration analytics)",
"Rapports par fournisseur, par produit, par client",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 6. DESIGN RESPONSIVE
# ═══════════════════════════════════════
doc.add_heading("6. Prérequis de design responsive", level=1)
doc.add_heading("6.1 Approche Mobile-First", level=2)
doc.add_paragraph(
"La plateforme doit être conçue selon une approche Mobile-First, garantissant une "
"expérience optimale sur tous les terminaux. L'interface doit s'adapter de manière fluide "
"aux résolutions suivantes :"
)
table = doc.add_table(rows=5, cols=3)
table.style = 'Light Grid Accent 1'
headers = ["Breakpoint", "Résolution", "Cible"]
for i, h in enumerate(headers):
table.rows[0].cells[i].text = h
data = [
("Mobile", "320px — 767px", "Smartphones"),
("Tablette", "768px — 1023px", "Tablettes, iPad"),
("Desktop", "1024px — 1439px", "Ordinateurs portables"),
("Large Desktop", "≥ 1440px", "Écrans larges, moniteurs"),
]
for idx, (bp, res, cible) in enumerate(data):
row = table.rows[idx + 1]
row.cells[0].text = bp
row.cells[1].text = res
row.cells[2].text = cible
doc.add_heading("6.2 Exigences d'accessibilité", level=2)
items = [
"Conformité RGAA (Référentiel Général d'Amélioration de l'Accessibilité) niveau AA",
"Navigation au clavier complète",
"Contrastes de couleur conformes aux normes WCAG 2.1",
"Textes alternatifs sur tous les médias",
"Structure sémantique HTML5 (balises header, nav, main, article, etc.)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("6.3 Performance d'affichage", level=2)
items = [
"Temps de chargement initial (LCP) inférieur à 2,5 secondes sur mobile 4G",
"Score Lighthouse performance ≥ 90",
"Optimisation des images (formats WebP/AVIF, lazy loading, srcset responsive)",
"Mise en cache front-end (Service Worker optionnel pour le mode hors-ligne partiel)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("6.4 Charte graphique et UI/UX", level=2)
items = [
"Design system documenté (composants, typographie, couleurs, iconographie)",
"Maquettes à fournir pour les 4 breakpoints définis",
"Prototypage interactif (Figma ou équivalent) pour validation avant développement",
"Tests utilisateurs à prévoir sur un panel de professionnels du tourisme",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 7. OPTIMISATION IA
# ═══════════════════════════════════════
doc.add_heading("7. Outillage pour l'optimisation IA", level=1)
doc.add_heading("7.1 Recommandations intelligentes", level=2)
items = [
"Moteur de recommandation basé sur le comportement utilisateur (historique de réservations, recherches)",
"Suggestions de cross-selling et up-selling contextualisées",
"Personnalisation dynamique de la page d'accueil selon le profil du professionnel",
"Scoring de pertinence des offres en fonction de la saisonnalité et des tendances",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("7.2 Assistance à la rédaction et au contenu", level=2)
items = [
"Génération assistée de descriptions produit (fiches, accroches commerciales)",
"Traduction automatique des contenus avec relecture humaine",
"Suggestion de mots-clés et optimisation sémantique des fiches produit",
"Résumé automatique des avis et retours clients",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("7.3 Chatbot et assistance conversationnelle", level=2)
items = [
"Chatbot intelligent pour l'aide à la recherche et à la réservation",
"Support multilingue (français, anglais, espagnol, allemand a minima)",
"Escalade vers un agent humain si le chatbot ne peut pas répondre",
"Base de connaissances alimentée par les FAQ et la documentation produit",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("7.4 Analyse prédictive", level=2)
items = [
"Prévision de la demande par produit et par période",
"Détection d'anomalies (pics ou creux inhabituels)",
"Optimisation tarifaire dynamique (yield management assisté)",
"Segmentation automatique des clients professionnels",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 8. OPTIMISATION SEO
# ═══════════════════════════════════════
doc.add_heading("8. Outillage pour l'optimisation SEO", level=1)
doc.add_heading("8.1 SEO technique", level=2)
items = [
"Rendu côté serveur (SSR) ou pré-rendu statique (SSG) pour l'indexabilité des pages",
"URLs propres, lisibles et canoniques (ex: /musees/louvre-billets-coupe-file)",
"Balisage sémantique HTML5 et données structurées Schema.org (Product, Offer, Event, BreadcrumbList)",
"Sitemap XML dynamique et fichier robots.txt configurables depuis le back-office",
"Gestion automatique des redirections 301 lors de modifications d'URL",
"Temps de réponse serveur (TTFB) inférieur à 200ms",
"Support natif du protocole HTTPS et HTTP/2",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("8.2 SEO on-page", level=2)
items = [
"Balises title et meta description éditables par page depuis le back-office",
"Gestion des balises Hn (H1 unique par page, hiérarchie respectée)",
"Attributs alt éditables sur toutes les images",
"Fil d'Ariane (breadcrumb) sur toutes les pages avec balisage structuré",
"Pagination SEO-friendly (rel=prev/next, canonical)",
"Gestion du contenu dupliqué (canonical, hreflang pour le multilingue)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("8.3 SEO éditorial", level=2)
items = [
"Module de blog intégré avec catégorisation et maillage interne",
"Outils d'analyse sémantique intégrés (densité de mots-clés, lisibilité)",
"Suggestions IA de mots-clés longue traîne liés au tourisme parisien",
"Gestion du maillage interne assistée (suggestions de liens contextuels)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("8.4 Suivi et monitoring SEO", level=2)
items = [
"Intégration Google Search Console et Google Analytics 4",
"Tableau de bord SEO dans le back-office (positions, impressions, CTR)",
"Alertes automatiques en cas de dégradation (erreurs 404, chute de trafic, pages non indexées)",
"Audit SEO automatisé périodique avec recommandations",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 9. EXIGENCES TECHNIQUES
# ═══════════════════════════════════════
doc.add_heading("9. Exigences techniques", level=1)
doc.add_heading("9.1 Architecture", level=2)
items = [
"Architecture microservices ou modulaire découplée",
"API REST et/ou GraphQL documentée (OpenAPI / Swagger)",
"Séparation front-end / back-end (headless CMS recommandé)",
"Conteneurisation (Docker) et orchestration (Kubernetes ou équivalent)",
"Infrastructure cloud (AWS, GCP ou Azure) avec auto-scaling",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("9.2 Sécurité", level=2)
items = [
"Chiffrement des données en transit (TLS 1.3) et au repos",
"Conformité RGPD : consentement, droit à l'oubli, portabilité des données",
"Protection OWASP Top 10 (injection SQL, XSS, CSRF, etc.)",
"Authentification OAuth 2.0 / OpenID Connect",
"Audits de sécurité annuels et tests de pénétration",
"Journalisation des accès et des actions sensibles",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("9.3 Performance et disponibilité", level=2)
items = [
"Disponibilité cible : 99,9% (hors maintenance planifiée)",
"Capacité de montée en charge : supporter 10 000 utilisateurs simultanés",
"CDN pour les ressources statiques",
"Stratégie de cache multi-niveaux (CDN, reverse proxy, applicatif)",
"Plan de reprise d'activité (PRA) et plan de continuité d'activité (PCA)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_heading("9.4 Intégrations tierces", level=2)
items = [
"Passerelle de paiement (Stripe, Adyen ou équivalent) — CB, SEPA, virement",
"Connecteurs fournisseurs (APIs billetterie des sites touristiques)",
"Intégration CRM (Salesforce, HubSpot ou équivalent)",
"Outils d'emailing transactionnel et marketing (SendGrid, Brevo, etc.)",
"Intégration comptable (export FEC, connecteur ERP optionnel)",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 10. LIVRABLES ET PLANNING
# ═══════════════════════════════════════
doc.add_heading("10. Livrables attendus", level=1)
table = doc.add_table(rows=9, cols=3)
table.style = 'Light Grid Accent 1'
headers = ["Livrable", "Format", "Phase"]
for i, h in enumerate(headers):
table.rows[0].cells[i].text = h
data = [
("Spécifications techniques détaillées", "Document (PDF/DOCX)", "Conception"),
("Maquettes UI/UX", "Figma", "Conception"),
("Prototype interactif", "Figma / URL de staging", "Conception"),
("Code source", "Dépôt Git", "Développement"),
("Documentation technique (API, architecture)", "Markdown / Swagger", "Développement"),
("Jeux de tests (unitaires, intégration, E2E)", "Code source", "Développement"),
("Guide d'administration du back-office", "PDF / Wiki", "Livraison"),
("Plan de mise en production", "Document", "Livraison"),
]
for idx, (livrable, fmt, phase) in enumerate(data):
row = table.rows[idx + 1]
row.cells[0].text = livrable
row.cells[1].text = fmt
row.cells[2].text = phase
# ═══════════════════════════════════════
# 11. CRITÈRES D'ACCEPTATION
# ═══════════════════════════════════════
doc.add_heading("11. Critères d'acceptation", level=1)
items = [
"L'ensemble des fonctionnalités décrites dans ce cahier des charges est opérationnel",
"Les tests automatisés couvrent au minimum 80% du code métier",
"Le score Lighthouse est supérieur ou égal à 90 sur les 4 métriques (Performance, Accessibilité, SEO, Best Practices)",
"Les temps de réponse sont conformes aux seuils définis (TTFB < 200ms, LCP < 2,5s)",
"La plateforme est conforme au RGAA niveau AA",
"Un audit de sécurité a été réalisé sans vulnérabilité critique ou haute non corrigée",
"La documentation technique et utilisateur est complète et à jour",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
# ═══════════════════════════════════════
# 12. ANNEXES
# ═══════════════════════════════════════
doc.add_heading("12. Annexes", level=1)
items = [
"Annexe A — Glossaire des termes métier",
"Annexe B — Liste des sites touristiques et fournisseurs cibles",
"Annexe C — Benchmark concurrentiel",
"Annexe D — Wireframes préliminaires",
"Annexe E — Matrice des rôles et permissions",
]
for item in items:
doc.add_paragraph(item, style='List Bullet')
doc.add_paragraph()
p = doc.add_paragraph("[Contenus des annexes à compléter]")
p.italic = True
# ── Sauvegarde ──
output_path = "/Users/guillaume/workspace/codeanddata.fr/projects/uBlock/tmp/CDC_Billetterie_Tourisme_Paris.docx"
doc.save(output_path)
print(f"Document généré : {output_path}")