Project init
All checks were successful
Maven build / build (push) Successful in 1m47s

This commit is contained in:
Guillaume Dugas
2025-09-09 10:00:01 +02:00
commit de339f9554
102 changed files with 3781 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<?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.codeanddata.semrack</groupId>
<artifactId>semrack-core-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>semrack-core</artifactId>
<name>Semrack Core - Runtime</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</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>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,8 @@
package fr.codeanddata.semrack.core;
import fr.codeanddata.semrack.core.models.SemdocReadContext;
import io.smallrye.mutiny.Uni;
public interface SemdocReadInterceptor {
Uni<Void> interceptSemdocRead(SemdocReadContext context);
}

View File

@@ -0,0 +1,8 @@
package fr.codeanddata.semrack.core;
import fr.codeanddata.semrack.core.models.SemdocSearchContext;
import io.smallrye.mutiny.Uni;
public interface SemdocSearchInterceptor {
Uni<Void> interceptSemdocSearch(SemdocSearchContext context);
}

View File

@@ -0,0 +1,14 @@
package fr.codeanddata.semrack.core;
import fr.codeanddata.semrack.core.models.SearchRequest;
import fr.codeanddata.semrack.core.models.SearchResult;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import io.smallrye.mutiny.Uni;
public interface SemdocStorage {
Uni<SemrackDocument> readDocument(String uid);
Uni<SearchResult> searchDocument(SearchRequest query);
Uni<Long> countDocuments(SearchRequest request);
Uni<Boolean> documentsExist(SearchRequest query);
Uni<SemrackDocument> storeDocument(SemrackDocument document);
}

View File

@@ -0,0 +1,8 @@
package fr.codeanddata.semrack.core;
import fr.codeanddata.semrack.core.models.SemdocWriteContext;
import io.smallrye.mutiny.Uni;
public interface SemdocWriteInterceptor {
Uni<Void> interceptSemdocWrite(SemdocWriteContext context);
}

View File

@@ -0,0 +1,17 @@
package fr.codeanddata.semrack.core;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.inject.spi.CDI;
public interface SemrackLookupExpression<T> {
String apply(Object params);
default T convert(Object params) {
return CDI.current().select(ObjectMapper.class).get().convertValue(params, new TypeReference<T>() {});
}
default T convert(Object params, Class<T> clazz) {
return CDI.current().select(ObjectMapper.class).get().convertValue(params, clazz);
}
}

View File

@@ -0,0 +1,5 @@
package fr.codeanddata.semrack.core.enums;
public enum SemrackSortDirection {
asc, desc
}

View File

@@ -0,0 +1,18 @@
package fr.codeanddata.semrack.core.exceptions;
public class SemrackException extends Exception {
public SemrackException() {
}
public SemrackException(String message) {
super(message);
}
public SemrackException(Throwable cause) {
super(cause);
}
public SemrackException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,18 @@
package fr.codeanddata.semrack.core.exceptions;
public class SemrackRuntimeException extends RuntimeException {
public SemrackRuntimeException() {
}
public SemrackRuntimeException(String message) {
super(message);
}
public SemrackRuntimeException(Throwable cause) {
super(cause);
}
public SemrackRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,33 @@
package fr.codeanddata.semrack.core.interceptors;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.codeanddata.semrack.core.SemdocWriteInterceptor;
import fr.codeanddata.semrack.core.models.SemdocWriteContext;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
@ApplicationScoped
public class AnnotateWriteInterceptor implements SemdocWriteInterceptor {
public static final String ANNOTATE_KEY = "annotate";
public static final String CUSTOM_PREFIX = "custom/";
@Inject
ObjectMapper objectMapper;
@Override
public Uni<Void> interceptSemdocWrite(SemdocWriteContext context) {
final Map<String, Object> directives = context.getDirectives();
if (directives.containsKey(ANNOTATE_KEY)) {
final Map<String, Object> annotate = objectMapper.convertValue(directives.get(ANNOTATE_KEY), new TypeReference<Map<String, Object>>() {});
annotate.keySet().forEach(key -> context.getNextDocument().getAnnotations().put(CUSTOM_PREFIX + key, annotate.get(key)));
}
return Uni.createFrom().voidItem();
}
}

View File

@@ -0,0 +1,51 @@
package fr.codeanddata.semrack.core.interceptors;
import fr.codeanddata.semrack.core.SemdocWriteInterceptor;
import fr.codeanddata.semrack.core.models.SemdocWriteContext;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.Optional;
@ApplicationScoped
public class PublishWriteInterceptor implements SemdocWriteInterceptor {
final String PUBLICATION_PREFIX = "publication/";
@Inject
UIDGenerator uidGenerator;
@Override
public Uni<Void> interceptSemdocWrite(SemdocWriteContext context) {
final Map<String, Object> directives = context.getDirectives();
final SemrackDocument current = context.getCurrentDocument();
final SemrackDocument next = context.getNextDocument();
int version = (int) Optional.ofNullable(current)
.map(SemrackDocument::getAnnotations)
.map(annotations -> annotations.getOrDefault(PUBLICATION_PREFIX + "version", 0))
.orElse(0);
next.getAnnotations().put(PUBLICATION_PREFIX + "reference", Optional.ofNullable(current)
.map(SemrackDocument::getAnnotations)
.map(annotations -> annotations.getOrDefault(PUBLICATION_PREFIX + "reference", next.getUid()))
.orElse(next.getUid()));
final boolean publish = directives.getOrDefault("publish", false).equals(true);
final boolean snapshot = directives.getOrDefault("snapshot", publish).equals(true);
next.getAnnotations().put(PUBLICATION_PREFIX + "publish", publish);
next.getAnnotations().put(PUBLICATION_PREFIX + "snapshot", snapshot);
next.getAnnotations().put(PUBLICATION_PREFIX + "version", snapshot ? version + 1 : version);
if (snapshot && current != null) {
next.setUid(uidGenerator.apply(next));
}
return Uni.createFrom().voidItem();
}
}

View File

@@ -0,0 +1,14 @@
package fr.codeanddata.semrack.core.mappers;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import fr.codeanddata.semrack.core.models.PushDocumentRequest;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
@Mapper(componentModel = MappingConstants.ComponentModel.CDI)
public abstract class SemrackDocumentMapper {
@Mapping(source = "uid", target = "uid")
@Mapping(source = "metadata", target = "metadata")
abstract SemrackDocument toDocument(PushDocumentRequest document);
}

View File

@@ -0,0 +1,14 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaginationInfo {
long page;
long size;
long total;
}

View File

@@ -0,0 +1,29 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PushDocumentRequest {
/**
* L'identifiant unique du document.
*/
String uid;
/**
* Les directives sont les paramètres d'extensions.
* Elles ont pour vocation de générer des actions sur les documents, comme le publish, etc...
*/
Map<String, Object> directives;
/**
* Le contenu du document.
*/
Map<String, Object> metadata;
}

View File

@@ -0,0 +1,18 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchRequest {
Map<String, Object> filter;
List<SemrackSort> sort;
SemrackPagination paginate;
List<String> fields;
}

View File

@@ -0,0 +1,15 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchResult {
List<SemrackDocument> documents;
PaginationInfo pagination;
}

View File

@@ -0,0 +1,16 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SemdocReadContext {
SemrackDocument currentDocument;
}

View File

@@ -0,0 +1,15 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SemdocSearchContext {
Map<String, Object> search;
}

View File

@@ -0,0 +1,30 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SemdocWriteContext {
/**
* Previous stored document
*/
SemrackDocument currentDocument;
/**
* Document to be store. It initialized with :
* - the currentDocument uid if exists, or a new one
* - the metadata to be persisted
*/
SemrackDocument nextDocument;
/**
* The directives to be applied
*/
Map<String, Object> directives;
}

View File

@@ -0,0 +1,32 @@
package fr.codeanddata.semrack.core.models;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection(serialization = true)
public class SemrackDocument {
/**
* L'identifiant unique du document.
*/
String uid;
/**
* Les annotations sont créées par le système, au travers des extensions. Elles sont en lecture seule.
* Elles ont pour vocation d'aider au classement et à la recherche du document (tags, catégories, date de création, etc.).
*/
Map<String, Object> annotations;
/**
* Le contenu du document.
*/
Map<String, Object> metadata;
}

View File

@@ -0,0 +1,13 @@
package fr.codeanddata.semrack.core.models;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SemrackPagination {
Integer page;
Integer size;
}

View File

@@ -0,0 +1,14 @@
package fr.codeanddata.semrack.core.models;
import fr.codeanddata.semrack.core.enums.SemrackSortDirection;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SemrackSort {
String field;
SemrackSortDirection direction;
}

View File

@@ -0,0 +1,68 @@
package fr.codeanddata.semrack.core.repositories;
import fr.codeanddata.semrack.core.SemdocStorage;
import fr.codeanddata.semrack.core.SemdocWriteInterceptor;
import fr.codeanddata.semrack.core.exceptions.SemrackRuntimeException;
import fr.codeanddata.semrack.core.models.*;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
@ApplicationScoped
public class SemdocRepository {
@Inject
SemdocStorage semdocStorage;
@Inject
@Any
Instance<SemdocWriteInterceptor> writeInterceptors;
@Inject
UIDGenerator uidGenerator;
Uni<SearchResult> searchDocument(SearchRequest query) {
return semdocStorage.searchDocument(query);
}
public Uni<SemrackDocument> pushDocument(PushDocumentRequest pushDocument) {
return Optional.ofNullable(pushDocument.getUid()).map(semdocStorage::readDocument).orElse(Uni.createFrom().nullItem())
.chain(currentDocument -> {
final SemdocWriteContext context = SemdocWriteContext.builder()
.directives(pushDocument.getDirectives())
.currentDocument(currentDocument)
.nextDocument(SemrackDocument.builder()
.uid(Optional.ofNullable(currentDocument).map(SemrackDocument::getUid).orElse(uidGenerator.apply(null)))
.metadata(pushDocument.getMetadata())
.annotations(new HashMap<>())
.build())
.build();
final AtomicReference<Uni<Void>> documentUni = new AtomicReference<>(Uni.createFrom().voidItem());
writeInterceptors.stream().forEach(filter -> documentUni.set(documentUni.get().chain(() -> filter.interceptSemdocWrite(context))));
return documentUni.get().chain(() -> semdocStorage.storeDocument(context.getNextDocument()));
});
}
public Uni<SemrackDocument> getOrCreate(SearchRequest query, PushDocumentRequest toCreate) {
return searchDocument(query)
.chain(searchResult -> {
if (searchResult.getDocuments().isEmpty()) {
return pushDocument(toCreate);
} else if (searchResult.getDocuments().size() > 1) {
throw new SemrackRuntimeException("Multiple documents found");
} else {
return Uni.createFrom().item(searchResult.getDocuments().getFirst());
}
});
}
}

View File

@@ -0,0 +1,52 @@
package fr.codeanddata.semrack.core.services;
import fr.codeanddata.semrack.core.SemrackLookupExpression;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ApplicationScoped
public class SemrackLookupService {
@Inject
@Any
Instance<SemrackLookupExpression<?>> operators;
final Map<String, SemrackLookupExpression<?>> operatorsIndex = new HashMap<>();
void startup(@Observes StartupEvent event) {
operators.stream()
.filter(s -> s.getClass().getSuperclass().isAnnotationPresent(Identifier.class))
.forEach(s -> {
Identifier identifier = s.getClass().getSuperclass().getAnnotation(Identifier.class);
operatorsIndex.put(identifier.value(), s);
});
}
public String lookup(Map<String, Object> lookupExpression) {
final Set<String> lookupKeys = lookupExpression.keySet();
if (lookupKeys.isEmpty()) {
return "";
} else if (lookupKeys.size() > 1) {
throw new RuntimeException("Only one lookup expression is allowed");
} else {
final String lookupKey = lookupKeys.stream().findFirst().get();
if (! operatorsIndex.containsKey(lookupKey)) {
throw new RuntimeException("Unknown lookup expression '" + lookupKey + "'");
} else {
final SemrackLookupExpression<?> lookup = operatorsIndex.get(lookupKey);
final Object lookupParams = lookupExpression.get(lookupKey);
return lookup.apply(lookupParams);
}
}
}
}

View File

@@ -0,0 +1,27 @@
package fr.codeanddata.semrack.core.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
@ApplicationScoped
public class SemdocUtils {
@Inject
ObjectMapper objectMapper;
public <T> T map(Object map, Class<T> clazz) {
return objectMapper.convertValue(map, clazz);
}
public <T> T map(Object map, TypeReference<T> typeRef) {
return objectMapper.convertValue(map, typeRef);
}
public Map<String, Object> remap(Object map) {
return objectMapper.convertValue(map, new TypeReference<>() {});
}
}

View File

@@ -0,0 +1,23 @@
package fr.codeanddata.semrack.core.utils;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.commons.codec.digest.DigestUtils;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
import java.util.function.Function;
@ApplicationScoped
public class UIDGenerator implements Function<SemrackDocument, String> {
@Override
public String apply(SemrackDocument semrackDocument) {
final String uuid = UUID.randomUUID().toString();
final LocalDateTime now = LocalDateTime.now();
final long timestamp = now.toInstant(ZoneOffset.UTC).toEpochMilli();
return DigestUtils.sha256Hex(uuid + timestamp);
}
}

View File

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