diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index abd47ed..6eb2d98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,11 @@ jobs: java-version: '21' distribution: 'temurin' + - uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + - name: Set versions run: | VERSION_BASE=$RELEASE_VERSION_BASE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91a800a --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.mvn/settings.xml b/.mvn/settings.xml new file mode 100644 index 0000000..e69de29 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/Blocks.http b/Blocks.http new file mode 100644 index 0000000..2f82224 --- /dev/null +++ b/Blocks.http @@ -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": {} + } + ] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..95b40d0 --- /dev/null +++ b/README.md @@ -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: . + +## 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 . + +## 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 it’s 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 . + +## 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) diff --git a/bom/pom.xml b/bom/pom.xml index 5f9fe2a..57512c3 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -11,9 +11,9 @@ ../pom.xml - bom - Bom pom + bom + Compositor Bom 3.14.1 @@ -29,12 +29,30 @@ 3.5.4 + ${project.version} 1.18.42 4.1.0 + + + fr.cnd.compositor + bom + ${compositor.version} + + + fr.cnd.compositor + compositor + ${compositor.version} + + + fr.cnd.compositor + core + ${compositor.version} + + io.pebbletemplates pebble diff --git a/domain/pom.xml b/domain/pom.xml new file mode 100644 index 0000000..3dd7026 --- /dev/null +++ b/domain/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + fr.cnd.compositor + bom + 1.0-SNAPSHOT + ../bom/pom.xml + + + + 8 + 8 + 8 + + + compositor + Compositor Domain + + + + org.projectlombok + lombok + + + + diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java similarity index 86% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java index 8cd5265..e5324ae 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java similarity index 84% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java index ed32c14..89e14f9 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java similarity index 82% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java index 085a1a7..5bce0da 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/deployment/pom.xml b/modules/core/deployment/pom.xml new file mode 100644 index 0000000..5416b62 --- /dev/null +++ b/modules/core/deployment/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + fr.cnd.compositor + core-parent + 1.0-SNAPSHOT + ../pom.xml + + core-deployment + Core - Deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-rest-jackson-deployment + + + + fr.cnd.compositor + core + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit-internal + test + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + + diff --git a/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java b/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java new file mode 100644 index 0000000..f8bc893 --- /dev/null +++ b/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java @@ -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); + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java new file mode 100644 index 0000000..3072065 --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java @@ -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 render(BlockRenderDefinition definition) { + return Uni.createFrom().item("
Test Block
"); + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java new file mode 100644 index 0000000..4386549 --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java @@ -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("
Hello
"); + + String result = template.render(BlockRenderDefinition.builder().build()).await().indefinitely(); + + assertEquals("
Hello
", result); + } + + @Test + void shouldRenderTemplateWithContext() { + AbstractPebbleBlockTemplate template = createTemplate("
Hello {{ name }}
"); + Map context = Map.of("name", "World"); + + String result = template.render(BlockRenderDefinition.builder() + .inputs(context) + .build()).await().indefinitely(); + + assertEquals("
Hello World
", result); + } + + @Test + void shouldRenderTemplateWithEmptyContext() { + AbstractPebbleBlockTemplate template = createTemplate("Static content"); + final Map context = new HashMap<>(); + + String result = template.render(BlockRenderDefinition.builder() + .inputs(context) + .build()).await().indefinitely(); + + assertEquals("Static content", result); + } + + private AbstractPebbleBlockTemplate createTemplate(String templateContent) { + AbstractPebbleBlockTemplate template = new AbstractPebbleBlockTemplate() { + @Override + public Uni getTemplate() { + return Uni.createFrom().item(templateContent); + } + }; + template.setEngine(engine); + return template; + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java new file mode 100644 index 0000000..e4dfa5b --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java @@ -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}. + *

+ * 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 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 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 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")); + } + +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java new file mode 100644 index 0000000..b1b4799 --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java @@ -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}. + *

+ * 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("

Test Block
", rendered); + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java new file mode 100644 index 0000000..5e8d18c --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java @@ -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}. + *

+ * 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("

Test Block
", rendered); + } + +} diff --git a/modules/core/integration-tests/pom.xml b/modules/core/integration-tests/pom.xml new file mode 100644 index 0000000..885bcdd --- /dev/null +++ b/modules/core/integration-tests/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + + fr.cnd.compositor + core-parent + 1.0.0-SNAPSHOT + + core-integration-tests + Core - Integration Tests + + + true + + + + + io.quarkus + quarkus-rest + + + fr.cnd.compositor + core + ${project.version} + + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + false + true + + + + diff --git a/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java b/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java new file mode 100644 index 0000000..3352c66 --- /dev/null +++ b/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java @@ -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"; + } +} diff --git a/modules/core/integration-tests/src/main/resources/application.properties b/modules/core/integration-tests/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java new file mode 100644 index 0000000..13bbe66 --- /dev/null +++ b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java @@ -0,0 +1,7 @@ +package fr.cnd.compositor.core.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class CoreResourceIT extends CoreResourceTest { +} diff --git a/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java new file mode 100644 index 0000000..bfa3f75 --- /dev/null +++ b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java @@ -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")); + } +} diff --git a/modules/core/pom.xml b/modules/core/pom.xml new file mode 100644 index 0000000..5804cca --- /dev/null +++ b/modules/core/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + fr.cnd.compositor + builder + 1.0-SNAPSHOT + ../../builder/pom.xml + + + pom + core-parent + Core - Parent + + + deployment + runtime + + + diff --git a/modules/core/runtime/pom.xml b/modules/core/runtime/pom.xml new file mode 100644 index 0000000..a8b09ae --- /dev/null +++ b/modules/core/runtime/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + + fr.cnd.compositor + core-parent + 1.0-SNAPSHOT + ../pom.xml + + core + Core - Runtime + + + + fr.cnd.compositor + compositor + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest-jackson + + + io.pebbletemplates + pebble + + + org.projectlombok + lombok + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + default-compile + + + + org.projectlombok + lombok + ${lombok.version} + + + io.quarkus + quarkus-extension-processor + + + true + + + + + + + diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java new file mode 100644 index 0000000..62f82b5 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java @@ -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 render(BlockRenderRequest request) { + final Map> result = new HashMap<>(); + Uni chain = Uni.createFrom().voidItem(); + for (final String definitionId : request.getDefinitions().keySet()) { + final List 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 renderConfiguration(BlockRenderRequest request) { + final Map> result = new HashMap<>(); + Uni chain = Uni.createFrom().voidItem(); + for (final String definitionId : request.getDefinitions().keySet()) { + final List 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()); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java index ea394b4..0e5bb2c 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java @@ -1,6 +1,6 @@ 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 io.smallrye.mutiny.Uni; import jakarta.inject.Inject; diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java new file mode 100644 index 0000000..aab04b4 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java @@ -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 render(String content, Map 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); + } + })); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java new file mode 100644 index 0000000..558fb1c --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java @@ -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(); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java new file mode 100644 index 0000000..2769b93 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java @@ -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 blockConfigurations; + + @Override + public Uni resolveConfiguration(String blockIdentifier) { + Instance selected = blockConfigurations.select(Identifier.Literal.of(blockIdentifier)); + if (selected.isResolvable()) { + return Uni.createFrom().item(selected.get()); + } + return Uni.createFrom().nullItem(); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java new file mode 100644 index 0000000..0a34cbf --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java @@ -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 blockTemplates; + + @Override + public Uni resolveTemplate(String blockIdentifier) { + Instance selected = blockTemplates.select(Identifier.Literal.of(blockIdentifier)); + if (selected.isResolvable()) { + return Uni.createFrom().item(selected.get()); + } + return Uni.createFrom().nullItem(); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java new file mode 100644 index 0000000..e88a1d8 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java @@ -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 contextMappings; + + @Override + public Uni resolveContextMapping(String contextMappingIdentifier) { + Instance selected = contextMappings.select(Identifier.Literal.of(contextMappingIdentifier)); + if (selected.isResolvable()) { + return Uni.createFrom().item(selected.get()); + } + return Uni.createFrom().nullItem(); + } +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java new file mode 100644 index 0000000..f4fa447 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java @@ -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. + *

+ * 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 resolvers; + + @Inject + @All + List blockConfigurationResolvers; + + /** + * Resolves a block template by its name. + *

+ * 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 resolveTemplate(String templateName) { + Uni 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. + *

+ * 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 resolveBlockConfiguration(String blockIdentifier) { + Uni 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 renderTemplate(List definitions) { + final List results = new ArrayList<>(); + Uni 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 renderTemplate(BlockRenderDefinition definition) { + final Map slotsContent = new HashMap<>(); + Uni 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 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 renderBlockConfiguration(List definitions) { + return mapDefinition(definitions) + .chain(this::renderTemplate); + } + + private Uni> mapDefinition(List definitions) { + final List results = new ArrayList<>(); + Uni chain = Uni.createFrom().voidItem(); + for (BlockRenderDefinition definition : definitions) { + chain = chain.chain(() -> mapDefinition(definition)) + .invoke(results::add) + .replaceWithVoid(); + } + + return chain.map(x -> results); + } + + private Uni mapDefinition(BlockRenderDefinition definition) { + final Map> slotMapping = new HashMap<>(); + Uni slotChain = Uni.createFrom().voidItem(); + for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) { + final List 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)); + } + +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java new file mode 100644 index 0000000..138011d --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java @@ -0,0 +1,7 @@ +package fr.cnd.compositor.blocks.specs; + +import io.smallrye.mutiny.Uni; + +public interface BlockConfigurationResolver { + Uni resolveConfiguration(String blockIdentifier); +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java new file mode 100644 index 0000000..6a3c59e --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java @@ -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. + *

+ * 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}. + *

+ * Les implémentations peuvent utiliser différents moteurs de templates + * (Pebble, Freemarker, Thymeleaf, etc.) selon les besoins. + * + *

Exemple d'implémentation

+ *
{@code
+ * @ApplicationScoped
+ * public class MyBlockEngine implements BlockEngine {
+ *
+ *   @Override
+ *   public Uni render(String content, Map context) {
+ *     return Uni.createFrom().item(() -> {
+ *       // Logique de rendu du template
+ *       return renderedContent;
+ *     });
+ *   }
+ * }
+ * }
+ * + * @see PebbleBlockEngine + */ +public interface BlockEngine { + + /** + * Effectue le rendu d'un contenu template avec le contexte fourni. + *

+ * 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 render(String content, Map context); +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java new file mode 100644 index 0000000..5cb1227 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java @@ -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 render(BlockRenderDefinition definition); +} diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java new file mode 100644 index 0000000..99a3d84 --- /dev/null +++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java @@ -0,0 +1,8 @@ +package fr.cnd.compositor.blocks.specs; + +import io.smallrye.mutiny.Uni; + +public interface BlockTemplateResolver { + Uni resolveTemplate(String blockIdentifier); +} + diff --git a/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..e01f36a --- /dev/null +++ b/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -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" diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -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-,maven-mvnd--}/ +[ -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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -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-,maven-mvnd--}/ +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" diff --git a/pom.xml b/pom.xml index cf23d7e..dce6b49 100644 --- a/pom.xml +++ b/pom.xml @@ -7,14 +7,31 @@ root 1.0-SNAPSHOT - Blocks + Compositor Parent pom bom builder + domain modules/core + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + diff --git a/tmp/CDC_Billetterie_Tourisme_Paris.docx b/tmp/CDC_Billetterie_Tourisme_Paris.docx new file mode 100644 index 0000000..d935939 Binary files /dev/null and b/tmp/CDC_Billetterie_Tourisme_Paris.docx differ diff --git a/tmp/generate_cdc.py b/tmp/generate_cdc.py new file mode 100644 index 0000000..d2371ba --- /dev/null +++ b/tmp/generate_cdc.py @@ -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}") \ No newline at end of file