diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d10325..4e6b23c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,4 +20,10 @@ jobs: key: maven-${{ hashFiles('pom.xml') }} restore-keys: maven- - name: Build and test - run: ./mvnw clean verify + # -Pfull-build is the release-equivalent build: besides tests it generates + # the Javadoc jar, sources jar and CycloneDX SBOM, so CI catches Javadoc/ + # doclint problems (and a broken source/SBOM attachment) instead of only + # test failures — the tag-triggered release runs the same -Pfull-build goal. + # failOnWarnings turns any Javadoc warning into a build failure so they + # cannot regress silently. + run: ./mvnw -Pfull-build clean verify -Dmaven.javadoc.failOnWarnings=true diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java index 5a7c3ce..d84461b 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java @@ -50,4 +50,7 @@ TranslationConfig.class }) public class FullSpringServiceConfig { + + /** Creates the aggregate configuration; all feature configurations are wired via {@code @Import}. */ + public FullSpringServiceConfig() {} } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java b/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java index 4717cf7..af9cdb1 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java @@ -50,6 +50,11 @@ @Import({FullSpringServiceConfig.class, SpringServicesCoreAutoConfiguration.PackageRegistrar.class}) public class SpringServicesCoreAutoConfiguration { + /** + * Creates the auto-configuration; all beans are contributed via the imported configurations. + */ + public SpringServicesCoreAutoConfiguration() {} + /** Root package of the library — scanned for both entities and Spring Data repositories. */ static final String LIBRARY_ROOT_PACKAGE = "com.openelements.spring.base"; diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/AbstractDbBackedDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/AbstractDbBackedDataService.java index 22fe5e1..dc3b31c 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/AbstractDbBackedDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/AbstractDbBackedDataService.java @@ -89,6 +89,8 @@ public abstract class AbstractDbBackedDataService { */ @NonNull Optional findById(@NonNull final UUID id); + /** + * Looks up a DTO by its id, failing if none exists. + * + * @param id the id to look up + * @return the matching DTO + * @throws IllegalArgumentException if no entity with that id exists + */ default @NonNull D findByIdOrThrow(@NonNull final UUID id) { Objects.requireNonNull(id, "id must not be null"); return findById(id).orElseThrow(() -> new IllegalArgumentException("No such id " + id)); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/EntityRepository.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/EntityRepository.java index bb047ea..cb537eb 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/EntityRepository.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/EntityRepository.java @@ -27,12 +27,25 @@ @NoRepositoryBean public interface EntityRepository extends JpaRepository { + /** + * Returns the entities matching the given ids as a stream. + * + * @param ids the ids to look up + * @return a stream of the matching entities, in no particular order + */ @NonNull default Stream findAllByIds(@NonNull final Iterable ids) { Objects.requireNonNull(ids, "ids must not be null"); return findAllById(ids).stream(); } + /** + * Returns the entity with the given id, failing if none exists. + * + * @param id the id to look up + * @return the matching entity + * @throws IllegalArgumentException if no entity with that id exists + */ @NonNull default E findByIdOrThrow(@NonNull final UUID id) { Objects.requireNonNull(id, "ids must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/WithId.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/WithId.java index 9ff49f2..719ffaa 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/WithId.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/WithId.java @@ -29,16 +29,37 @@ */ public interface WithId { + /** + * Collects the ids of the given objects into an unmodifiable set. + * + * @param the type of the objects + * @param data the objects whose ids to collect + * @return an unmodifiable set of the ids + */ @NonNull static Set toIdSet(@NonNull final Iterable data) { return toIdStream(data).collect(Collectors.toUnmodifiableSet()); } + /** + * Collects the ids of the given objects into a list, preserving iteration order. + * + * @param the type of the objects + * @param data the objects whose ids to collect + * @return a list of the ids + */ @NonNull static List toIdList(@NonNull final Iterable data) { return toIdStream(data).toList(); } + /** + * Streams the ids of the given objects, preserving iteration order. + * + * @param the type of the objects + * @param data the objects whose ids to stream + * @return a stream of the ids + */ @NonNull static Stream toIdStream(@NonNull final Iterable data) { Objects.requireNonNull(data, "data must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/EntityWithImage.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/EntityWithImage.java index bd67b26..358d443 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/EntityWithImage.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/EntityWithImage.java @@ -3,16 +3,46 @@ import com.openelements.spring.base.data.DbEntity; import java.util.Optional; +/** + * Mixin for entities that carry a single embedded image, stored as raw bytes plus a MIME content + * type. Provides default methods that bridge between the raw storage columns and the richer {@link + * ImageData} and {@link ImageType} value types. + */ public interface EntityWithImage extends DbEntity { + /** + * Returns the raw bytes of the stored image. + * + * @return the image bytes, or {@code null} if no image is stored + */ byte[] getRawImageData(); + /** + * Sets the raw bytes of the stored image. + * + * @param rawImageData the image bytes, or {@code null} to clear the image + */ void setRawImageData(byte[] rawImageData); + /** + * Returns the MIME content type of the stored image. + * + * @return the content type, or {@code null} if no image is stored + */ String getContentType(); + /** + * Sets the MIME content type of the stored image. + * + * @param contentType the content type, or {@code null} to clear it + */ void setContentType(String contentType); + /** + * Returns the stored image as an {@link ImageData} value, if both bytes and type are present. + * + * @return the image data, or an empty {@link Optional} if no complete image is stored + */ default Optional imageData() { final byte[] rawImageData = getRawImageData(); if (rawImageData == null) { @@ -26,18 +56,33 @@ default Optional imageData() { return Optional.of(imageData); } + /** + * Returns the {@link ImageType} derived from the stored content type. + * + * @return the image type, or {@code null} if no content type is stored + */ default ImageType getImageType() { return Optional.ofNullable(getContentType()) .map(c -> ImageType.fromContentType(c)) .orElse(null); } + /** + * Sets the stored content type from the given {@link ImageType}. + * + * @param imageType the image type, or {@code null} to clear the content type + */ default void setImageType(ImageType imageType) { final String contentType = Optional.ofNullable(imageType).map(t -> t.getContentType()).orElse(null); setContentType(contentType); } + /** + * Stores the given image, updating both the raw bytes and the content type. + * + * @param imageData the image to store, or {@code null} to clear the image + */ default void setImageData(ImageData imageData) { if (imageData == null) { setRawImageData(null); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageData.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageData.java index 67c0362..0c24802 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageData.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageData.java @@ -22,6 +22,11 @@ public record ImageData(@NonNull byte[] data, @NonNull ImageType imageType) { */ public static final int MAX_IMAGE_SIZE = 20 * 1024 * 1024; + /** + * Validates the components, rejecting {@code null}, empty, and oversized image data. + * + * @throws IllegalArgumentException if {@code data} is empty or larger than {@link #MAX_IMAGE_SIZE} + */ public ImageData { Objects.requireNonNull(data, "data must not be null"); Objects.requireNonNull(imageType, "imageType must not be null"); @@ -33,32 +38,73 @@ public record ImageData(@NonNull byte[] data, @NonNull ImageType imageType) { } } + /** + * Creates an instance from raw bytes and a MIME content type string. + * + * @param data the image bytes + * @param contentType the MIME content type (e.g. "image/png") + * @return the image data + */ public static ImageData of(@NonNull byte[] data, @NonNull String contentType) { Objects.requireNonNull(contentType, "contentType must not be null"); return new ImageData(data, ImageType.fromContentType(contentType)); } + /** + * Creates an instance from an uploaded multipart file. + * + * @param file the uploaded file + * @return the image data + * @throws IOException if the file bytes cannot be read + */ public static ImageData of(@NonNull MultipartFile file) throws IOException { Objects.requireNonNull(file, "file must not be null"); return of(file.getBytes(), file.getContentType()); } + /** + * Returns the MIME content type of this image. + * + * @return the content type (e.g. "image/png") + */ public String contentType() { return imageType.getContentType(); } + /** + * Returns the content type of this image as a Spring {@link MediaType}. + * + * @return the media type of this image + */ public MediaType mediaType() { return imageType.toMediaType(); } + /** + * Wraps this image in a {@code 200 OK} HTTP response with the appropriate content type. + * + * @return an HTTP response carrying the image bytes + */ public ResponseEntity toHttpResponse() { return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType())).body(data()); } + /** + * Converts this image to JPEG. + * + * @return a new instance holding the JPEG-encoded image + */ public ImageData asJpeg() { return ImageUtilities.toJpeg(this); } + /** + * Converts this image to JPEG, scaling it to fit within the given bounds. + * + * @param maxWidth the maximum width in pixels + * @param maxHeight the maximum height in pixels + * @return a new instance holding the scaled JPEG-encoded image + */ public ImageData asJpeg(int maxWidth, int maxHeight) { return ImageUtilities.toJpeg(this, maxWidth, maxHeight); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageType.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageType.java index cef927f..6101bf2 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageType.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/ImageType.java @@ -2,12 +2,19 @@ import org.springframework.http.MediaType; +/** Supported image formats, each paired with its MIME content type. */ public enum ImageType { + /** Portable Network Graphics ({@code image/png}). */ PNG("image/png"), + /** JPEG ({@code image/jpeg}). */ JPEG("image/jpeg"), + /** WebP ({@code image/webp}). */ WEBP("image/webp"), + /** High Efficiency Image Coding ({@code image/heic}). */ HEIC("image/heic"), + /** High Efficiency Image File format ({@code image/heif}). */ HEIF("image/heif"), + /** Scalable Vector Graphics ({@code image/svg+xml}). */ SVG("image/svg+xml"); private final String contentType; @@ -16,6 +23,13 @@ public enum ImageType { this.contentType = contentType; } + /** + * Resolves the image type matching the given MIME content type. + * + * @param contentType the MIME content type to resolve + * @return the matching image type + * @throws IllegalArgumentException if no image type matches the content type + */ public static ImageType fromContentType(String contentType) { for (ImageType type : ImageType.values()) { if (type.contentType.equals(contentType)) { @@ -25,10 +39,20 @@ public static ImageType fromContentType(String contentType) { throw new IllegalArgumentException(contentType + " is not a valid image content type"); } + /** + * Returns the MIME content type associated with this image type. + * + * @return the MIME content type (e.g. "image/png") + */ public String getContentType() { return contentType; } + /** + * Returns this image type's content type as a Spring {@link MediaType}. + * + * @return the media type for this image type + */ public MediaType toMediaType() { return MediaType.parseMediaType(contentType); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/HeicSupportCheck.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/HeicSupportCheck.java index d2afd63..9f3acbe 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/HeicSupportCheck.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/HeicSupportCheck.java @@ -7,11 +7,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Utility that verifies whether the running JVM can decode HEIC/HEIF images. + * + *

Checks that an {@link ImageIO} reader for the format is registered and that a bundled probe + * image can actually be decoded, so that missing native libraries are detected at startup rather + * than on the first upload. + */ public class HeicSupportCheck { private static final Logger log = LoggerFactory.getLogger(HeicSupportCheck.class); private static final String PROBE_RESOURCE = "/heic-probe/sample.heic"; + /** Creates a new instance; all functionality is exposed through static methods. */ + public HeicSupportCheck() {} + + /** + * Verifies that HEIC/HEIF images can be decoded by the current runtime. + * + * @return {@code true} if a reader is registered and the bundled probe image decodes, + * {@code false} otherwise + */ public static boolean verifyHeicSupport() { final boolean readerRegistered = Arrays.stream(ImageIO.getReaderFormatNames()) diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/ImageUtilities.java b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/ImageUtilities.java index 603aa7d..9eb74f6 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/ImageUtilities.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/data/image/util/ImageUtilities.java @@ -7,12 +7,29 @@ import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; +/** Utility methods for converting and resizing images into JPEG form. */ public class ImageUtilities { + /** Utility class; not meant to be instantiated. */ + private ImageUtilities() {} + + /** + * Converts the given image to JPEG. + * + * @param data the source image + * @return a new image holding the JPEG-encoded data + */ public static ImageData toJpeg(@NonNull ImageData data) { return toJpeg(data.data(), data.contentType()); } + /** + * Converts the given raw image bytes to JPEG. + * + * @param data the source image bytes + * @param contentType the MIME content type of the source image + * @return a new image holding the JPEG-encoded data + */ public static ImageData toJpeg(@NonNull byte[] data, @NonNull String contentType) { Objects.requireNonNull(data, "data must not be null"); Objects.requireNonNull(contentType, "contentType must not be null"); @@ -26,10 +43,27 @@ public static ImageData toJpeg(@NonNull byte[] data, @NonNull String contentType }; } + /** + * Converts the given image to JPEG and scales it to fit within the given bounds. + * + * @param data the source image + * @param maxWidth the maximum width in pixels + * @param maxHeight the maximum height in pixels + * @return a new image holding the scaled JPEG-encoded data + */ public static ImageData toJpeg(@NonNull ImageData data, int maxWidth, int maxHeight) { return toJpeg(data.data(), data.contentType(), maxWidth, maxHeight); } + /** + * Converts the given raw image bytes to JPEG and scales the result to fit within the given bounds. + * + * @param data the source image bytes + * @param contentType the MIME content type of the source image + * @param maxWidth the maximum width in pixels + * @param maxHeight the maximum height in pixels + * @return a new image holding the scaled JPEG-encoded data + */ public static ImageData toJpeg( @NonNull byte[] data, @NonNull String contentType, int maxWidth, int maxHeight) { Objects.requireNonNull(data, "data must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/events/GenericDataEvent.java b/spring-services-core/src/main/java/com/openelements/spring/base/events/GenericDataEvent.java index c5a04e2..6f48b31 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/events/GenericDataEvent.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/events/GenericDataEvent.java @@ -24,6 +24,7 @@ */ public abstract class GenericDataEvent extends ApplicationEvent { + /** The runtime DTO type this event refers to, captured to survive generic type erasure. */ private final Class type; /** diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectCreate.java b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectCreate.java index 882045f..bcc0073 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectCreate.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectCreate.java @@ -14,6 +14,8 @@ public class OnObjectCreate extends GenericDataEvent { /** + * Creates a new creation event for the given DTO. + * * @param source the newly created DTO */ public OnObjectCreate(@NonNull T source) { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectDelete.java b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectDelete.java index c21d155..9221606 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectDelete.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectDelete.java @@ -15,6 +15,8 @@ public class OnObjectDelete extends GenericDataEvent { /** + * Creates a new deletion event for the given DTO. + * * @param source the DTO snapshot taken before the entity was removed */ public OnObjectDelete(@NonNull T source) { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectUpdate.java b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectUpdate.java index 7169526..c57d616 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectUpdate.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/events/OnObjectUpdate.java @@ -15,6 +15,8 @@ public class OnObjectUpdate extends GenericDataEvent { /** + * Creates a new update event for the given DTO. + * * @param source the updated DTO in its post-update state */ public OnObjectUpdate(@NonNull T source) { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/AuthService.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/AuthService.java index 4ab5b54..4693e2d 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/AuthService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/AuthService.java @@ -49,6 +49,9 @@ public class AuthService { private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy(); + /** Creates a new service that reads authentication state from the Spring Security context. */ + public AuthService() {} + /** * Returns the {@link Authentication} of the current request. * diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/JsonAuthenticationEntryPoint.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/JsonAuthenticationEntryPoint.java index 2642f14..daa65cd 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/JsonAuthenticationEntryPoint.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/JsonAuthenticationEntryPoint.java @@ -38,6 +38,8 @@ public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint { private final String wwwAuthenticateHeader; /** + * Creates a new entry point that writes authentication failures as a JSON response. + * * @param objectMapper the application's Jackson mapper, used to write the JSON body * @param wwwAuthenticateHeader the value to send in the {@code WWW-Authenticate} response * header — typically {@code "Bearer"} for the JWT chain or diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/SecurityConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/SecurityConfig.java index 2e2765e..20981f5 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/SecurityConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/SecurityConfig.java @@ -59,6 +59,8 @@ public class SecurityConfig { private final ApiKeyDataService apiKeyDataService; /** + * Creates the security configuration. + * * @param apiKeyDataService the data service used to validate API keys; injected into the {@link * ApiKeyAuthenticationFilter} that is registered on the external API chain */ @@ -154,6 +156,8 @@ public JwtAuthenticationConverter jwtAuthenticationConverter() { * @param http the HTTP security builder * @param apiKeyAuthenticationFilter the filter that extracts and validates the {@code X-API-Key} * header + * @param apiKeyAuthenticationEntryPoint the entry point that renders authentication failures on + * this chain as a JSON {@code 401} response * @return the configured external API filter chain * @throws Exception if Spring Security fails to build the chain */ diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/UserInformation.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/UserInformation.java index b0630b8..3b9f68f 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/UserInformation.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/UserInformation.java @@ -26,6 +26,14 @@ * {@code externalId} — which covers Authentik, Okta, Entra ID, and Keycloak in default * configurations. * + * + * @param id the JWT {@code sub} claim, the stable subject identifier of the user + * @param name the {@code name} claim, typically the user's display name, or {@code null} if absent + * @param email the {@code email} claim, or {@code null} if not asserted by the identity provider + * @param avatarUrl an absolute URL to the identity provider's avatar image, or {@code null} if absent + * @param userName the resolved preferred username, guaranteed non-null and non-blank for + * {@code AuthService}-produced records + * @param externalId the stable IdP-side identifier, mirroring {@code id} for common IdPs */ public record UserInformation( String id, diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/apikey/ApiKeyAuthenticationFilter.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/apikey/ApiKeyAuthenticationFilter.java index 2163573..80ed27f 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/apikey/ApiKeyAuthenticationFilter.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/apikey/ApiKeyAuthenticationFilter.java @@ -66,6 +66,8 @@ public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { SecurityContextHolder.getContextHolderStrategy(); /** + * Creates a new filter that authenticates requests using their API key. + * * @param apiKeyService the data service used to validate API keys * @param authenticationEntryPoint the entry point invoked on authentication failure — produces * the uniform JSON {@code 401} response and the {@code WWW-Authenticate} header diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/security/roles/Roles.java b/spring-services-core/src/main/java/com/openelements/spring/base/security/roles/Roles.java index 424fb45..932b4ee 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/security/roles/Roles.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/security/roles/Roles.java @@ -14,11 +14,15 @@ private Roles() {} /** Predefined role for IT administrators. */ public static final String ROLE_IT_ADMIN = "IT-ADMIN"; + /** Predefined role for internal employees. */ public static final String ROLE_EMPLOYEE = "EMPLOYEE"; + /** Predefined role for external, non-employee users. */ public static final String ROLE_EXTERNAL = "EXTERNAL"; + /** Predefined role for back-office staff. */ public static final String ROLE_BACKOFFICE = "BACKOFFICE"; + /** Predefined role for management users. */ public static final String ROLE_MANAGEMENT = "MANAGEMENT"; } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyConfig.java index 5c5a763..7d4c03b 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyConfig.java @@ -13,4 +13,8 @@ */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = ApiKeyConfig.class) -public class ApiKeyConfig {} +public class ApiKeyConfig { + + /** Creates the configuration; the API key beans are registered via component scanning. */ + public ApiKeyConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyCreatedDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyCreatedDto.java index 4981ff0..eedd93a 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyCreatedDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyCreatedDto.java @@ -7,6 +7,13 @@ /** * One-time response DTO for a newly created API key. Contains the raw key which is shown exactly * once and never stored or retrievable again. + * + * @param id the unique identifier of the API key + * @param name the user-given name of the API key + * @param keyPrefix the non-secret display prefix of the key + * @param key the raw API key value, shown only once and never retrievable again + * @param createdBy the name of the user who created the key + * @param createdAt the timestamp at which the key was created */ @Schema(description = "Newly created API key (raw key shown only once)") public record ApiKeyCreatedDto( diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDataService.java index 8c913e1..65071ba 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDataService.java @@ -30,6 +30,12 @@ public class ApiKeyDataService { private final ApiKeyRepository apiKeyRepository; private final UserService userService; + /** + * Creates a new service for managing API keys. + * + * @param apiKeyRepository the repository used to persist and look up API keys + * @param userService the service used to resolve the user an operation is performed by + */ public ApiKeyDataService( @NonNull final ApiKeyRepository apiKeyRepository, @NonNull final UserService userService) { this.apiKeyRepository = diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDto.java index 35b9827..aa6e2b5 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyDto.java @@ -4,7 +4,15 @@ import java.time.Instant; import java.util.UUID; -/** DTO for listing API keys. Does not contain the raw key. */ +/** + * DTO for listing API keys. Does not contain the raw key. + * + * @param id the unique identifier of the API key + * @param name the user-given name of the API key + * @param keyPrefix the non-secret display prefix of the key + * @param createdBy the name of the user who created the key + * @param createdAt the timestamp at which the key was created + */ @Schema(description = "API Key") public record ApiKeyDto( @Schema(description = "API key ID", requiredMode = Schema.RequiredMode.REQUIRED) UUID id, @@ -21,6 +29,12 @@ public record ApiKeyDto( @Schema(description = "Creation timestamp", requiredMode = Schema.RequiredMode.REQUIRED) Instant createdAt) { + /** + * Builds a DTO from the given API key entity, omitting the raw key value. + * + * @param entity the persisted API key entity + * @return the corresponding DTO without the raw key + */ public static ApiKeyDto fromEntity(final ApiKeyEntity entity) { return new ApiKeyDto( entity.getId(), diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyEntity.java index d90178c..ddf1be9 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/apikey/ApiKeyEntity.java @@ -16,49 +16,94 @@ @Table(name = "api_keys", schema = DbSchema.NAME) public class ApiKeyEntity extends AbstractEntity { + /** The user-given name of the API key. */ @Column(name = "name", nullable = false, length = 255) private String name; + /** The SHA-256 hash of the raw key; the raw key itself is never stored. */ @Column(name = "key_hash", nullable = false, length = 64, unique = true) private String keyHash; + /** The non-secret display prefix of the key. */ @Column(name = "key_prefix", nullable = false, length = 20) private String keyPrefix; + /** The name of the user who created the key. */ @Column(name = "created_by", nullable = false, length = 255) private String createdBy; + /** Creates a new, empty API key entity for use by JPA and callers building a key to persist. */ public ApiKeyEntity() {} + /** + * Returns the user-given name of this API key. + * + * @return the name of the key + */ @NameSupplier public String getName() { return name; } + /** + * Sets the user-given name of this API key. + * + * @param name the name of the key + */ public void setName(final String name) { this.name = Objects.requireNonNull(name, "name must not be null"); } + /** + * Returns the SHA-256 hash of the raw key, which is the only representation stored. + * + * @return the hex-encoded hash of the key + */ public String getKeyHash() { return keyHash; } + /** + * Sets the SHA-256 hash of the raw key. + * + * @param keyHash the hex-encoded hash of the key + */ public void setKeyHash(final String keyHash) { this.keyHash = Objects.requireNonNull(keyHash, "keyHash must not be null"); } + /** + * Returns the non-secret display prefix of the key. + * + * @return the display prefix of the key + */ public String getKeyPrefix() { return keyPrefix; } + /** + * Sets the non-secret display prefix of the key. + * + * @param keyPrefix the display prefix of the key + */ public void setKeyPrefix(final String keyPrefix) { this.keyPrefix = Objects.requireNonNull(keyPrefix, "keyPrefix must not be null"); } + /** + * Returns the name of the user who created this key. + * + * @return the name of the creating user + */ public String getCreatedBy() { return createdBy; } + /** + * Sets the name of the user who created this key. + * + * @param createdBy the name of the creating user + */ public void setCreatedBy(final String createdBy) { this.createdBy = Objects.requireNonNull(createdBy, "createdBy must not be null"); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditCleanupJob.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditCleanupJob.java index c73db9f..6cff967 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditCleanupJob.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditCleanupJob.java @@ -19,6 +19,12 @@ public class AuditCleanupJob { private final AuditProperties auditProperties; + /** + * Creates a new cleanup job. + * + * @param auditLogRepository the repository from which expired audit rows are deleted + * @param auditProperties the configuration providing the retention window + */ public AuditCleanupJob( final AuditLogRepository auditLogRepository, final AuditProperties auditProperties) { this.auditLogRepository = diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditConfig.java index 091c805..abdfbfd 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditConfig.java @@ -17,4 +17,8 @@ @ComponentScan(basePackageClasses = AuditConfig.class) @EnableScheduling @Import(UserConfig.class) -public class AuditConfig {} +public class AuditConfig { + + /** Creates the configuration; the audit-log beans are registered via component scanning. */ + public AuditConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDataService.java index 006c4ba..a949345 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDataService.java @@ -32,6 +32,14 @@ public class AuditLogDataService extends AbstractDbBackedDataService findAllEntityTypes() { * * @param entityType the simple class name of the audited DTO * @param entityId the id of the audited entity + * @param name the human readable name of the audited entity * @param action the kind of lifecycle event * @param user the user that performed the action * @return the persisted DTO diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDto.java index e2ff20e..bff99a0 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogDto.java @@ -12,6 +12,7 @@ * @param id the audit entry id * @param entityType the simple class name of the audited DTO type * @param entityId the id of the audited entity + * @param name the human readable name of the audited entity * @param action the kind of lifecycle event that produced this entry * @param user the user that performed the action (references the System User row for * unauthenticated operations) diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEntity.java index c55a903..5081635 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEntity.java @@ -20,15 +20,19 @@ @Table(name = "audit_log", schema = DbSchema.NAME) public class AuditLogEntity extends AbstractEntity { + /** The simple class name of the audited DTO type. */ @Column(name = "entity_type", nullable = false, length = 255) private String entityType; + /** The id of the audited entity. */ @Column(name = "entity_id", nullable = false) private UUID entityId; + /** The human readable name of the audited entity at the time of the action. */ @Column(name = "entity_name", nullable = false) private String name; + /** The kind of lifecycle event this entry records. */ @Enumerated(EnumType.STRING) @Column(name = "action", nullable = false, length = 20) private AuditAction action; @@ -44,42 +48,92 @@ public class AuditLogEntity extends AbstractEntity { /** Default constructor required by JPA. */ public AuditLogEntity() {} + /** + * Returns the simple class name of the audited DTO type. + * + * @return the audited entity type + */ public String getEntityType() { return entityType; } + /** + * Sets the simple class name of the audited DTO type. + * + * @param entityType the audited entity type + */ public void setEntityType(final String entityType) { this.entityType = Objects.requireNonNull(entityType, "entityType must not be null"); } + /** + * Returns the id of the audited entity. + * + * @return the id of the audited entity + */ public UUID getEntityId() { return entityId; } + /** + * Sets the id of the audited entity. + * + * @param entityId the id of the audited entity + */ public void setEntityId(final UUID entityId) { this.entityId = Objects.requireNonNull(entityId, "entityId must not be null"); } + /** + * Returns the kind of lifecycle event this entry records. + * + * @return the audited action + */ public AuditAction getAction() { return action; } + /** + * Sets the kind of lifecycle event this entry records. + * + * @param action the audited action + */ public void setAction(final AuditAction action) { this.action = Objects.requireNonNull(action, "action must not be null"); } + /** + * Returns the user that performed the audited action. + * + * @return the acting user, the System User for unauthenticated operations + */ public UserEntity getUser() { return user; } + /** + * Sets the user that performed the audited action. + * + * @param user the acting user, the System User for unauthenticated operations + */ public void setUser(final UserEntity user) { this.user = Objects.requireNonNull(user, "user must not be null"); } + /** + * Returns the human readable name of the audited entity at the time of the action. + * + * @return the human readable name of the audited entity + */ public String getName() { return name; } + /** + * Sets the human readable name of the audited entity. + * + * @param name the human readable name of the audited entity + */ public void setName(String name) { this.name = name; } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEventListener.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEventListener.java index 3893ecf..49f7c04 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEventListener.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditLogEventListener.java @@ -38,6 +38,13 @@ public class AuditLogEventListener { private final UserRepository userRepository; + /** + * Creates a new listener that turns data-lifecycle events into audit entries. + * + * @param auditLogDataService the service used to persist audit entries + * @param authService the service used to resolve the current authenticated user + * @param userRepository the repository used to resolve the acting user entity + */ public AuditLogEventListener( final AuditLogDataService auditLogDataService, final AuthService authService, diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditProperties.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditProperties.java index 8ad35c2..a15e45f 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditProperties.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/audit/AuditProperties.java @@ -21,6 +21,8 @@ public record AuditProperties(int retentionDays) { public static final int DEFAULT_RETENTION_DAYS = 1460; /** + * Creates the properties, binding the retention window from configuration and validating it. + * * @param retentionDays bound from the {@code audit.retention-days} property */ public AuditProperties(@Value("${audit.retention-days:1460}") final int retentionDays) { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentConfig.java index 69b28a1..7bd434b 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentConfig.java @@ -5,7 +5,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +/** + * Spring configuration for the comment feature. + * + *

Component-scans this package to register the comment service and repository, and imports the + * user configuration on which the comment authorship depends. + */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = CommentConfig.class) @Import(UserConfig.class) -public class CommentConfig {} +public class CommentConfig { + + /** Creates the configuration; the comment beans are registered via component scanning. */ + public CommentConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentCreateDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentCreateDto.java index 03d9d61..37f5996 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentCreateDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentCreateDto.java @@ -2,4 +2,9 @@ import jakarta.validation.constraints.NotBlank; +/** + * Request DTO carrying the text of a comment to be created. + * + * @param text the comment text; must not be blank + */ public record CommentCreateDto(@NotBlank String text) {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentDto.java index 67df2e8..0ea01af 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentDto.java @@ -5,5 +5,14 @@ import java.time.Instant; import java.util.UUID; +/** + * Public projection of a comment. + * + * @param id the unique identifier of the comment + * @param text the comment text + * @param author the user who wrote the comment + * @param createdAt the timestamp at which the comment was created + * @param updatedAt the timestamp at which the comment was last updated + */ public record CommentDto(UUID id, String text, UserDto author, Instant createdAt, Instant updatedAt) implements WithId {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentEntity.java index 44c036d..e489bfd 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentEntity.java @@ -7,13 +7,16 @@ import java.util.Objects; import org.jspecify.annotations.NonNull; +/** JPA entity representing a single comment together with its author. */ @Entity @Table(name = "comments", schema = DbSchema.NAME) public class CommentEntity extends AbstractEntity { + /** The text content of the comment. */ @Column(name = "text", nullable = false, columnDefinition = "TEXT") private String text; + /** The user who wrote the comment. */ @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn( name = "author_id", @@ -21,20 +24,41 @@ public class CommentEntity extends AbstractEntity { foreignKey = @ForeignKey(name = "fk_comments_author")) private UserEntity author; + /** Creates a new, empty comment entity for use by JPA and the owning service. */ protected CommentEntity() {} + /** + * Returns the text of this comment. + * + * @return the comment text + */ public String getText() { return text; } + /** + * Sets the text of this comment. + * + * @param text the comment text + */ public void setText(@NonNull final String text) { this.text = Objects.requireNonNull(text, "text must not be null"); } + /** + * Returns the author of this comment. + * + * @return the user who wrote the comment + */ public UserEntity getAuthor() { return author; } + /** + * Sets the author of this comment. + * + * @param author the user who wrote the comment + */ public void setAuthor(@NonNull final UserEntity author) { this.author = Objects.requireNonNull(author, "author must not be null"); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentRepository.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentRepository.java index 911e2d2..38ba67d 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentRepository.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentRepository.java @@ -2,4 +2,5 @@ import com.openelements.spring.base.data.EntityRepository; +/** Spring Data repository for {@link CommentEntity} instances. */ public interface CommentRepository extends EntityRepository {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentService.java index de48b18..2e08e3e 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/comment/CommentService.java @@ -11,6 +11,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/** + * Data service managing comments, setting the current user as the author of newly created comments. + */ @Service @Transactional public class CommentService extends AbstractDbBackedDataService { @@ -19,6 +22,13 @@ public class CommentService extends AbstractDbBackedDataServiceComponent-scans this package to register the settings service and repository. + */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = SettingsConfig.class) -public class SettingsConfig {} +public class SettingsConfig { + + /** Creates the configuration; the settings beans are registered via component scanning. */ + public SettingsConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsDataService.java index 21c616c..e553afb 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsDataService.java @@ -12,6 +12,11 @@ public class SettingsDataService { private final SettingsRepository settingsRepository; + /** + * Creates a new settings service. + * + * @param settingsRepository the repository used to persist and look up settings + */ public SettingsDataService(final SettingsRepository settingsRepository) { this.settingsRepository = Objects.requireNonNull(settingsRepository, "settingsRepository must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsEntity.java index ff9994e..d4ed24b 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsEntity.java @@ -13,27 +13,50 @@ @Table(name = "settings", schema = DbSchema.NAME) public class SettingsEntity extends AbstractEntity { + /** The unique key of the setting. */ @Column(name = "`key`", length = 100, nullable = false, unique = true) private String key; + /** The value of the setting. */ @Column(name = "`value`", nullable = false, columnDefinition = "TEXT") private String value; + /** Creates a new, empty settings entity for use by JPA and the owning service. */ protected SettingsEntity() {} + /** + * Returns the key of this setting. + * + * @return the setting key + */ @NameSupplier public String getKey() { return key; } + /** + * Sets the key of this setting. + * + * @param key the setting key + */ public void setKey(final String key) { this.key = Objects.requireNonNull(key, "key must not be null"); } + /** + * Returns the value of this setting. + * + * @return the setting value + */ public String getValue() { return value; } + /** + * Sets the value of this setting. + * + * @param value the setting value + */ public void setValue(final String value) { this.value = Objects.requireNonNull(value, "value must not be null"); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsRepository.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsRepository.java index 35d3f18..6d26337 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsRepository.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/settings/SettingsRepository.java @@ -14,7 +14,18 @@ */ public interface SettingsRepository extends JpaRepository { + /** + * Looks up a setting by its key. + * + * @param key the setting key + * @return the matching setting, or an empty {@link Optional} if none exists + */ Optional findByKey(String key); + /** + * Deletes the setting with the given key, if present. + * + * @param key the setting key + */ void deleteByKey(String key); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/system/SystemDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/system/SystemDto.java index c811d1b..b70215c 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/system/SystemDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/system/SystemDto.java @@ -3,5 +3,14 @@ import com.openelements.spring.base.data.WithId; import java.util.UUID; +/** + * DTO describing an external system integration. + * + * @param id the unique identifier of the system + * @param name the display name of the system + * @param description a human-readable description of the system + * @param apiKey the API key used to authenticate against the system + * @param baseUrl the base URL of the system + */ public record SystemDto(UUID id, String name, String description, String apiKey, String baseUrl) implements WithId {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/PreTagDeleteEvent.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/PreTagDeleteEvent.java index 25a9a08..c53a90e 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/PreTagDeleteEvent.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/PreTagDeleteEvent.java @@ -21,6 +21,8 @@ public class PreTagDeleteEvent extends ApplicationEvent { /** + * Creates a new pre-delete event for the given tag. + * * @param tag the tag that is about to be deleted * @throws NullPointerException if {@code tag} is {@code null} */ diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagConfig.java index 6e41b2f..065c020 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagConfig.java @@ -12,4 +12,8 @@ */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = TagConfig.class) -public class TagConfig {} +public class TagConfig { + + /** Creates the configuration; the tag beans are registered via component scanning. */ + public TagConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagDataService.java index ce48a8e..c57f44b 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/tag/TagDataService.java @@ -23,6 +23,12 @@ public class TagDataService extends AbstractDbBackedDataServiceComponent-scans this package to register the translation-related beans. + */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = TranslationConfig.class) public class TranslationConfig { + + /** Creates the configuration; the translation beans are registered via component scanning. */ + public TranslationConfig() {} } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/SystemUserInitializer.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/SystemUserInitializer.java index 1494a57..a1b9114 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/SystemUserInitializer.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/SystemUserInitializer.java @@ -42,6 +42,12 @@ public class SystemUserInitializer implements ApplicationRunner { private final JdbcTemplate jdbcTemplate; + /** + * Creates a new initializer that ensures the System User row exists at startup. + * + * @param userRepository the repository used to check for the existing System User + * @param jdbcTemplate the template used to insert the System User row + */ public SystemUserInitializer( final UserRepository userRepository, final JdbcTemplate jdbcTemplate) { this.userRepository = Objects.requireNonNull(userRepository, "userRepository must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserConfig.java index 25ca0e5..0d13aa1 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserConfig.java @@ -3,6 +3,15 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +/** + * Spring configuration for the user feature. + * + *

Component-scans this package to register the user service, repository and provisioning beans. + */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = UserConfig.class) -public class UserConfig {} +public class UserConfig { + + /** Creates the configuration; the user beans are registered via component scanning. */ + public UserConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserDto.java index c7b1418..ac981f9 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserDto.java @@ -12,6 +12,14 @@ * {@link #avatarUrl()} points directly at the identity provider's avatar image — clients render it * in an {@code } tag without any proxying or caching by this library. {@code avatarUrl} is * {@code null} when the identity provider does not assert an avatar for the user. + * + * @param id the unique identifier of the user + * @param name the user's display name + * @param email the user's email address + * @param avatarUrl the avatar image URL synchronized from the identity provider, or {@code null} if + * none is asserted + * @param createdAt the timestamp at which the user record was created + * @param updatedAt the timestamp at which the user record was last updated */ @Schema(description = "User") public record UserDto( diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntity.java index 5d2baf3..0835f99 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntity.java @@ -28,31 +28,40 @@ @BatchSize(size = 50) public class UserEntity extends AbstractEntity { + /** The OAuth2 subject identifier ({@code sub} claim); may be {@code null} for SCIM-only rows. */ @Column(name = "sub", nullable = true, unique = true, length = 255) private String sub; + /** The stable IdP-side identifier; may be {@code null} for older rows without one. */ @Column(name = "external_id", nullable = true, unique = true, length = 255) private String externalId; + /** The unique business-key handle of the user. */ @Column(name = "user_name", nullable = false, unique = true, length = 255) private String userName; + /** The cached display name of the user. */ @Column(name = "name", nullable = false, length = 255) private String name; + /** The cached email address of the user; may be {@code null} if not asserted. */ @Column(name = "email", length = 255) private String email; + /** The cached avatar URL of the user; may be {@code null} if not asserted. */ @Column(name = "avatar_url", length = 2048) private String avatarUrl; + /** Whether the user is active; inactive users are rejected by every authentication path. */ @Column(name = "active", nullable = false) private boolean active = true; + /** The last-update timestamp of the user row. */ @UpdateTimestamp @Column(name = "updated_at", nullable = false) private Instant updatedAt; + /** Creates a new, empty user entity for use by JPA and the provisioning code. */ public UserEntity() {} /** @@ -70,6 +79,11 @@ public String getSub() { return sub; } + /** + * Sets the OAuth2 subject identifier ({@code sub} claim) of this user. + * + * @param sub the subject identifier, or {@code null} if not yet known + */ public void setSub(final String sub) { this.sub = sub; } @@ -89,6 +103,11 @@ public String getExternalId() { return externalId; } + /** + * Sets the stable IdP-side identifier of this user. + * + * @param externalId the IdP-side identifier, or {@code null} if not known + */ public void setExternalId(final String externalId) { this.externalId = externalId; } @@ -104,6 +123,11 @@ public String getUserName() { return userName; } + /** + * Sets the unique business-key handle of this user. + * + * @param userName the user-name handle + */ public void setUserName(final String userName) { this.userName = userName; } @@ -119,6 +143,11 @@ public boolean isActive() { return active; } + /** + * Sets whether this user is active. + * + * @param active {@code true} to activate the user, {@code false} to deactivate + */ public void setActive(final boolean active) { this.active = active; } @@ -126,12 +155,19 @@ public void setActive(final boolean active) { /** * Returns the cached display name of the user, kept in sync with the JWT {@code name} claim by * {@link UserService}. + * + * @return the display name of the user */ @NameSupplier public String getName() { return name; } + /** + * Sets the cached display name of the user. + * + * @param name the display name of the user + */ public void setName(final String name) { Objects.requireNonNull(name, "name must not be null"); this.name = name; @@ -140,11 +176,18 @@ public void setName(final String name) { /** * Returns the cached email address of the user, kept in sync with the JWT {@code email} claim by * {@link UserService}. May be {@code null} if the identity provider does not assert it. + * + * @return the email address of the user, or {@code null} if not asserted */ public String getEmail() { return email; } + /** + * Sets the cached email address of the user. + * + * @param email the email address of the user, or {@code null} if not asserted + */ public void setEmail(final String email) { this.email = email; } @@ -152,15 +195,27 @@ public void setEmail(final String email) { /** * Returns the avatar URL of the user, kept in sync with the JWT {@code avatar} claim by {@link * UserService}. May be {@code null} if the identity provider does not assert it. + * + * @return the avatar URL of the user, or {@code null} if not asserted */ public String getAvatarUrl() { return avatarUrl; } + /** + * Sets the avatar URL of the user. + * + * @param avatarUrl the avatar URL of the user, or {@code null} if not asserted + */ public void setAvatarUrl(final String avatarUrl) { this.avatarUrl = avatarUrl; } + /** + * Returns the timestamp at which this user was last updated. + * + * @return the last update timestamp + */ public Instant getUpdatedAt() { return updatedAt; } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntityPrincipalDirectory.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntityPrincipalDirectory.java index 5248bfc..8be5ac2 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntityPrincipalDirectory.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserEntityPrincipalDirectory.java @@ -37,6 +37,11 @@ public class UserEntityPrincipalDirectory implements PrincipalDirectory { private final UserRepository userRepository; + /** + * Creates a new directory backed by the user repository. + * + * @param userRepository the repository used to look up and live-check users + */ public UserEntityPrincipalDirectory(final UserRepository userRepository) { this.userRepository = Objects.requireNonNull(userRepository, "userRepository must not be null"); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserProvisioner.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserProvisioner.java index 8010c32..a980532 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserProvisioner.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserProvisioner.java @@ -40,6 +40,11 @@ public class UserProvisioner { private final UserRepository userRepository; + /** + * Creates a new provisioner backed by the user repository. + * + * @param userRepository the repository used to insert newly provisioned users + */ public UserProvisioner(final UserRepository userRepository) { this.userRepository = Objects.requireNonNull(userRepository, "userRepository must not be null"); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserService.java index 730dff0..29d842e 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/user/UserService.java @@ -41,6 +41,14 @@ public class UserService extends AbstractDbBackedDataService the DTO type that was created + * @param event the creation event + */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleOnObjectCreate(final OnObjectCreate event) { Objects.requireNonNull(event, "event must not be null"); handle(WebhookDataEventType.CREATED, event.getType(), event.getData()); } + /** + * Handles an entity-update event by notifying the registered webhooks after commit. + * + * @param the DTO type that was updated + * @param event the update event + */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleOnObjectUpdate(final OnObjectUpdate event) { Objects.requireNonNull(event, "event must not be null"); handle(WebhookDataEventType.UPDATED, event.getType(), event.getData()); } + /** + * Handles an entity-deletion event by notifying the registered webhooks after commit. + * + * @param the DTO type that was deleted + * @param event the deletion event + */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleOnObjectDelete(final OnObjectDelete event) { Objects.requireNonNull(event, "event must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/WebhookSender.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/WebhookSender.java index 7c6fceb..58fe7c5 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/WebhookSender.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/WebhookSender.java @@ -33,6 +33,12 @@ public class WebhookSender { private final WebhookDataService webhookDataService; + /** + * Creates a new webhook sender. + * + * @param webhookRestClient the pre-configured {@link RestClient} used to deliver webhook calls + * @param webhookDataService the service used to look up webhooks and record delivery results + */ public WebhookSender( final RestClient webhookRestClient, final WebhookDataService webhookDataService) { this.restClient = @@ -41,6 +47,12 @@ public WebhookSender( Objects.requireNonNull(webhookDataService, "webhookDataService must not be null"); } + /** + * Delivers a payload to a single webhook and records the resulting HTTP status asynchronously. + * + * @param webhook the target webhook + * @param payload the payload to deliver + */ @Async protected void sendAndTrack(final WebhookDto webhook, final WebhookEventPayload payload) { int status; @@ -79,10 +91,21 @@ private boolean isTimeout(final ResourceAccessException e) { return e.getCause() instanceof SocketTimeoutException; } + /** + * Delivers a data-event payload to every currently active webhook. + * + * @param payload the payload to deliver + */ public void sendAndTrack(WebhookDataEventPayload payload) { webhookDataService.findAllActive().forEach(webhook -> sendAndTrack(webhook, payload)); } + /** + * Sends a ping payload to a single webhook to verify that its endpoint is reachable. + * + * @param id the id of the webhook to ping + * @throws IllegalArgumentException if no webhook with that id exists + */ public void ping(final UUID id) { Objects.requireNonNull(id, "id must not be null"); final WebhookDto dto = diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDataService.java index cece7f4..935e603 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDataService.java @@ -12,6 +12,12 @@ public class WebhookDataService extends AbstractDbBackedDataService findAllActive() { return getRepository().findAllByActiveTrue().stream().map(this::toData).toList(); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDto.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDto.java index fdd8f0d..b5dc8a5 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDto.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookDto.java @@ -5,7 +5,17 @@ import java.time.Instant; import java.util.UUID; -/** DTO representing a webhook registration. */ +/** + * DTO representing a webhook registration. + * + * @param id the unique identifier of the webhook registration + * @param url the target URL that receives the webhook callbacks + * @param active whether the webhook is currently enabled and will be called + * @param lastStatus the HTTP status of the last delivery attempt ({@code null} if never called, + * {@code 0} for a connection error, {@code -1} for a timeout) + * @param lastCalledAt the timestamp of the most recent delivery attempt, or {@code null} if never + * called + */ @Schema(description = "Webhook") public record WebhookDto( @Schema(description = "Webhook ID", requiredMode = Schema.RequiredMode.REQUIRED) UUID id, @@ -21,6 +31,12 @@ public record WebhookDto( @Schema(description = "Timestamp of the last webhook call") Instant lastCalledAt) implements WithId { + /** + * Builds a DTO from the given webhook entity. + * + * @param entity the source entity + * @return the corresponding DTO + */ public static WebhookDto fromEntity(final WebhookEntity entity) { return new WebhookDto( entity.getId(), diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookEntity.java index 621f7ba..da4d295 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/data/WebhookEntity.java @@ -13,49 +13,95 @@ @Table(name = "webhooks", schema = DbSchema.NAME) public class WebhookEntity extends AbstractEntity { + /** The target URL that receives the webhook callbacks. */ @Column(name = "url", nullable = false, length = 2048) private String url; + /** Whether the webhook is currently enabled. */ @Column(name = "active", nullable = false) private boolean active = true; + /** The HTTP status of the last delivery attempt; {@code null} if never called. */ @Column(name = "last_status") private Integer lastStatus; + /** The timestamp of the most recent delivery attempt; {@code null} if never called. */ @Column(name = "last_called_at") private Instant lastCalledAt; /** Default constructor required by JPA. */ public WebhookEntity() {} + /** + * Returns the target URL that receives the webhook callbacks. + * + * @return the target URL + */ public String getUrl() { return url; } + /** + * Sets the target URL that receives the webhook callbacks. + * + * @param url the target URL + */ public void setUrl(final String url) { this.url = Objects.requireNonNull(url, "url must not be null"); } + /** + * Returns whether this webhook is currently active. + * + * @return {@code true} if the webhook is active, {@code false} otherwise + */ public boolean isActive() { return active; } + /** + * Sets whether this webhook is currently active. + * + * @param active {@code true} to enable the webhook, {@code false} to disable it + */ public void setActive(final boolean active) { this.active = active; } + /** + * Returns the HTTP status of the last delivery attempt. + * + * @return the last status ({@code null} if never called, {@code 0} for a connection error, + * {@code -1} for a timeout) + */ public Integer getLastStatus() { return lastStatus; } + /** + * Sets the HTTP status of the last delivery attempt. + * + * @param lastStatus the last status ({@code null} if never called, {@code 0} for a connection + * error, {@code -1} for a timeout) + */ public void setLastStatus(final Integer lastStatus) { this.lastStatus = lastStatus; } + /** + * Returns the timestamp of the most recent delivery attempt. + * + * @return the last call timestamp, or {@code null} if never called + */ public Instant getLastCalledAt() { return lastCalledAt; } + /** + * Sets the timestamp of the most recent delivery attempt. + * + * @param lastCalledAt the last call timestamp, or {@code null} if never called + */ public void setLastCalledAt(final Instant lastCalledAt) { this.lastCalledAt = lastCalledAt; } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookDataEventPayload.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookDataEventPayload.java index c4f8e05..5c1bdda 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookDataEventPayload.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookDataEventPayload.java @@ -9,6 +9,7 @@ /** * JSON payload sent to webhook URLs when a domain event occurs. * + * @param the type of the entity DTO carried by this payload * @param eventId unique identifier for idempotency * @param eventType the type of domain event * @param dataClass the entity class @@ -19,6 +20,7 @@ public record WebhookDataEventPayload( UUID eventId, WebhookDataEventType eventType, Class dataClass, T data, Instant timestamp) implements WebhookEventPayload { + /** Validates the components, rejecting {@code null} values and a {@code null} data id. */ public WebhookDataEventPayload { Objects.requireNonNull(eventId, "eventId must not be null"); Objects.requireNonNull(eventType, "eventType must not be null"); @@ -27,6 +29,15 @@ public record WebhookDataEventPayload( Objects.requireNonNull(timestamp, "timestamp must not be null"); } + /** + * Creates a payload with a fresh event id and the current timestamp for the given entity. + * + * @param the type of the entity DTO + * @param eventType the type of domain event + * @param dataclass the entity class + * @param data the entity DTO + * @return a new payload describing the event + */ public static WebhookDataEventPayload of( WebhookDataEventType eventType, Class dataclass, T data) { Objects.requireNonNull(data, "data must not be null"); @@ -34,6 +45,14 @@ public static WebhookDataEventPayload of( UUID.randomUUID(), eventType, dataclass, data, Instant.now()); } + /** + * Creates a payload for the given entity, inferring the entity class from its runtime type. + * + * @param the type of the entity DTO + * @param eventType the type of domain event + * @param data the entity DTO + * @return a new payload describing the event + */ public static WebhookDataEventPayload of( WebhookDataEventType eventType, T data) { Objects.requireNonNull(data, "data must not be null"); diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookEventPayload.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookEventPayload.java index 8f50ed2..76e7e4f 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookEventPayload.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookEventPayload.java @@ -3,9 +3,20 @@ import java.time.Instant; import java.util.UUID; +/** Common contract for all payloads delivered to webhook endpoints. */ public interface WebhookEventPayload { + /** + * Returns the unique identifier of this event, used by receivers for idempotency. + * + * @return the event id + */ UUID eventId(); + /** + * Returns the moment at which the event occurred. + * + * @return the event timestamp + */ Instant timestamp(); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookPingEventPayload.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookPingEventPayload.java index 9dcfdff..49b0871 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookPingEventPayload.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/webhook/payload/WebhookPingEventPayload.java @@ -3,9 +3,20 @@ import java.time.Instant; import java.util.UUID; +/** + * Payload for a webhook ping, used to verify that an endpoint is reachable. + * + * @param eventId the unique identifier of this ping event + * @param timestamp the moment at which the ping was created + */ public record WebhookPingEventPayload(UUID eventId, Instant timestamp) implements WebhookEventPayload { + /** + * Creates a new ping payload with a fresh event id and the current timestamp. + * + * @return a new ping payload + */ public static WebhookEventPayload create() { return new WebhookPingEventPayload(UUID.randomUUID(), Instant.now()); } diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantDbBackedDataService.java b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantDbBackedDataService.java index 0fa8640..e6dfbb2 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantDbBackedDataService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantDbBackedDataService.java @@ -42,6 +42,8 @@ public abstract class AbstractMultitenantDbBackedDataService< private final ApplicationEventPublisher eventPublisher; /** + * Constructs a new tenant-scoped service that broadcasts lifecycle events via the given publisher. + * * @param eventPublisher Spring's event publisher, used to broadcast lifecycle events */ protected AbstractMultitenantDbBackedDataService(final ApplicationEventPublisher eventPublisher) { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantEntity.java b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantEntity.java index 5b2115a..3c88f08 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantEntity.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/AbstractMultitenantEntity.java @@ -22,9 +22,13 @@ public class AbstractMultitenantEntity extends AbstractEntity implements EntityWithMultitenantSupport { + /** The id of the tenant that owns this row; assigned by the owning data service on insert. */ @Column(nullable = false) private String tenantId; + /** Creates a new tenant-scoped entity; the tenant id is assigned by the owning data service. */ + public AbstractMultitenantEntity() {} + @Override public String getTenantId() { return tenantId; diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantConfig.java index c9cac18..a72ff01 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantConfig.java @@ -12,4 +12,8 @@ */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = TenantConfig.class) -public class TenantConfig {} +public class TenantConfig { + + /** Creates the configuration; the tenant beans are registered via component scanning. */ + public TenantConfig() {} +} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantService.java b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantService.java index c734cf2..609f88d 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantService.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/tenant/TenantService.java @@ -17,6 +17,11 @@ public class TenantService { private final AuthService authService; + /** + * Creates a new tenant service. + * + * @param authService the service used to resolve the authenticated principal + */ @Autowired public TenantService(final AuthService authService) { this.authService = Objects.requireNonNull(authService, "authService must not be null"); diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java index ee8930c..9a333b4 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java @@ -20,4 +20,8 @@ */ @AutoConfiguration @Import(DbBackupConfig.class) -public class DbBackupAutoConfiguration {} +public class DbBackupAutoConfiguration { + + /** Creates the auto-configuration. */ + public DbBackupAutoConfiguration() {} +} diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java index bbf8ddc..087db14 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java @@ -33,17 +33,29 @@ public final class BackupDownload implements AutoCloseable { this.httpResponse = Objects.requireNonNull(httpResponse, "httpResponse must not be null"); } - /** Identifier of the backup being downloaded (e.g. {@code backup_20260510T010000Z.sql.gz}). */ + /** + * Identifier of the backup being downloaded (e.g. {@code backup_20260510T010000Z.sql.gz}). + * + * @return the backup identifier + */ public String id() { return id; } - /** Content length advertised by the sidecar, or {@code -1} if not known. */ + /** + * Content length advertised by the sidecar, or {@code -1} if not known. + * + * @return the size of the download in bytes, or {@code -1} if unknown + */ public long sizeBytes() { return sizeBytes; } - /** Gzipped SQL dump as a stream. Caller is responsible for closing via {@link #close()}. */ + /** + * Gzipped SQL dump as a stream. Caller is responsible for closing via {@link #close()}. + * + * @return the gzipped SQL dump stream + */ public InputStream stream() { return stream; } diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java index b743987..ea34f7a 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java @@ -8,6 +8,15 @@ * Snapshot of a backup job. Fields are {@code null} until the corresponding lifecycle event has * occurred — e.g. {@code startedAt}, {@code finishedAt}, {@code durationMs} and {@code backupId} * remain {@code null} while the job is {@link BackupJobStatus#QUEUED queued}. + * + * @param jobId the sidecar-assigned identifier of the backup job + * @param status the current lifecycle state of the job + * @param triggeredBy who or what initiated the job (API call or scheduler) + * @param startedAt the instant the job started running, or {@code null} while still queued + * @param finishedAt the instant the job finished, or {@code null} while not yet finished + * @param durationMs how long the job ran in milliseconds, or {@code null} while not yet finished + * @param errorMessage the failure message when the job failed, or {@code null} otherwise + * @param backupId the identifier of the produced backup artefact, or {@code null} until one exists */ @JsonIgnoreProperties(ignoreUnknown = true) public record BackupJob( diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java index 0c32e91..34e9c0e 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java @@ -7,12 +7,16 @@ * in its JSON payloads. */ public enum BackupJobStatus { + /** The job has been accepted and is waiting to start. */ @JsonProperty("queued") QUEUED, + /** The job is currently producing a backup. */ @JsonProperty("running") RUNNING, + /** The job completed and a backup artefact was produced. */ @JsonProperty("succeeded") SUCCEEDED, + /** The job ended with an error and no usable backup was produced. */ @JsonProperty("failed") FAILED } diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java index 8d6b341..d1d89a1 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java @@ -3,7 +3,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.time.Instant; -/** Metadata for a stored backup artefact. */ +/** + * Metadata for a stored backup artefact. + * + * @param id the unique identifier of the backup artefact + * @param createdAt the instant the backup was created + * @param sizeBytes the size of the backup file in bytes + * @param sha256 the SHA-256 checksum of the backup file + * @param pgVersion the PostgreSQL server version the backup was taken from + * @param durationMs how long the backup took to produce, in milliseconds + * @param triggeredBy who or what initiated the backup (API call or scheduler) + */ @JsonIgnoreProperties(ignoreUnknown = true) public record BackupMetadata( String id, diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java index a8057b2..5dd5c9a 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java @@ -2,7 +2,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -/** Service-level metadata returned by the sidecar's {@code GET /info} probe. */ +/** + * Service-level metadata returned by the sidecar's {@code GET /info} probe. + * + * @param version the sidecar's own software version + * @param pgDumpVersion the version of the {@code pg_dump} tool the sidecar uses + * @param retention how long backups are kept before being purged + * @param backupInterval how often the sidecar's scheduler triggers a backup + * @param backup runtime status of the most recent backup + */ @JsonIgnoreProperties(ignoreUnknown = true) public record BackupServiceInfo( String version, @@ -11,12 +19,28 @@ public record BackupServiceInfo( BackupInterval backupInterval, Backup backup) { + /** + * Retention policy for stored backups. + * + * @param days the number of days backups are retained + */ @JsonIgnoreProperties(ignoreUnknown = true) public record Retention(int days) {} + /** + * Schedule on which the sidecar automatically triggers backups. + * + * @param iso8601 the interval as an ISO-8601 duration string + * @param seconds the same interval expressed in seconds + */ @JsonIgnoreProperties(ignoreUnknown = true) public record BackupInterval(String iso8601, long seconds) {} + /** + * Runtime status of the most recent backup. + * + * @param lastSuccessfulBackupAgeSeconds age in seconds of the last successful backup + */ @JsonIgnoreProperties(ignoreUnknown = true) public record Backup(long lastSuccessfulBackupAgeSeconds) {} } diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java index a647307..4f19d74 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java @@ -4,8 +4,10 @@ /** Who or what initiated a backup. */ public enum BackupTrigger { + /** The backup was initiated by an explicit API call. */ @JsonProperty("api") API, + /** The backup was initiated automatically by the sidecar's scheduler. */ @JsonProperty("scheduler") SCHEDULER } diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java index 087709e..cf7c77f 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java @@ -7,5 +7,8 @@ * trigger request with {@code 409 Conflict} and the still-running job's details. This wrapper makes * that distinction explicit: {@link #alreadyRunning()} is {@code true} when the returned {@link * #job()} refers to a pre-existing job rather than a freshly enqueued one. + * + * @param job the backup job returned by the sidecar, either freshly enqueued or already running + * @param alreadyRunning {@code true} when {@code job} refers to a backup that was already in flight */ public record BackupTriggerResult(BackupJob job, boolean alreadyRunning) {} diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java index 0af249a..1441850 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java @@ -30,6 +30,11 @@ public class DbBackupClient { private final RestClient restClient; private final DbBackupProperties props; + /** + * Creates a client bound to the sidecar described by the given properties. + * + * @param props the db-backup-service connection settings; must not be {@code null} + */ public DbBackupClient(final DbBackupProperties props) { this.props = Objects.requireNonNull(props, "props must not be null"); this.restClient = @@ -39,7 +44,11 @@ public DbBackupClient(final DbBackupProperties props) { .build(); } - /** Returns {@code true} when the sidecar's {@code /health} probe answers {@code 2xx}. */ + /** + * Returns {@code true} when the sidecar's {@code /health} probe answers {@code 2xx}. + * + * @return {@code true} if the sidecar reports healthy, {@code false} otherwise + */ public boolean isHealthy() { try { return restClient @@ -51,7 +60,11 @@ public boolean isHealthy() { } } - /** Reads the sidecar's {@code /info} metadata. Unauthenticated. */ + /** + * Reads the sidecar's {@code /info} metadata. Unauthenticated. + * + * @return the service metadata reported by the sidecar + */ public BackupServiceInfo getInfo() { return restClient .get() @@ -71,6 +84,8 @@ public BackupServiceInfo getInfo() { * to run at a time; if a backup is already in flight the returned * {@link BackupTriggerResult#alreadyRunning()} is {@code true} and {@link * BackupTriggerResult#job()} refers to the still-running job. + * + * @return the result describing the enqueued or already-running backup job */ public BackupTriggerResult triggerBackup() { return restClient @@ -91,7 +106,12 @@ public BackupTriggerResult triggerBackup() { }); } - /** Returns the current state of a backup job, or empty if the sidecar doesn't know it (404). */ + /** + * Returns the current state of a backup job, or empty if the sidecar doesn't know it (404). + * + * @param jobId the identifier of the backup job to look up + * @return the job snapshot, or empty if no job with that id exists + */ public Optional getJob(final String jobId) { Objects.requireNonNull(jobId, "jobId must not be null"); return restClient @@ -113,7 +133,11 @@ public Optional getJob(final String jobId) { }); } - /** Lists every available backup, newest first. */ + /** + * Lists every available backup, newest first. + * + * @return the metadata of all available backups, ordered newest first + */ public List listBackups() { return restClient .get() @@ -132,7 +156,11 @@ public List listBackups() { }); } - /** Returns metadata for the newest successful backup, or empty if there isn't one yet (404). */ + /** + * Returns metadata for the newest successful backup, or empty if there isn't one yet (404). + * + * @return the metadata of the latest backup, or empty if none exists yet + */ public Optional getLatestBackup() { return restClient .get() @@ -156,6 +184,9 @@ public Optional getLatestBackup() { /** * Streams the gzip body of a specific backup. The returned {@link BackupDownload} owns the HTTP * connection — always consume it in a try-with-resources. + * + * @param id the identifier of the backup to download + * @return a handle over the open download stream */ public BackupDownload downloadBackup(final String id) { Objects.requireNonNull(id, "id must not be null"); @@ -166,7 +197,11 @@ public BackupDownload downloadBackup(final String id) { .exchange((req, res) -> openDownload(id, res), false); } - /** Streams the gzip body of the newest successful backup. See {@link #downloadBackup(String)}. */ + /** + * Streams the gzip body of the newest successful backup. See {@link #downloadBackup(String)}. + * + * @return a handle over the open download stream for the latest backup + */ public BackupDownload downloadLatestBackup() { return restClient .get() diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java index 6239702..4c4d6bd 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java @@ -27,4 +27,8 @@ @ConditionalOnProperty(prefix = "openelements.db-backup", name = "enabled", havingValue = "true") @ComponentScan(basePackageClasses = DbBackupConfig.class) @EnableConfigurationProperties(DbBackupProperties.class) -public class DbBackupConfig {} +public class DbBackupConfig { + + /** Creates the configuration. */ + public DbBackupConfig() {} +} diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java index db5e540..29a848a 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java @@ -3,10 +3,21 @@ /** Thin runtime exception so callers can distinguish db-backup-service errors. */ public final class DbBackupException extends RuntimeException { + /** + * Creates a new exception with the given detail message. + * + * @param message the detail message describing the db-backup-service error + */ public DbBackupException(final String message) { super(message); } + /** + * Creates a new exception with the given detail message and cause. + * + * @param message the detail message describing the db-backup-service error + * @param cause the underlying cause of this error + */ public DbBackupException(final String message, final Throwable cause) { super(message, cause); } diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java index 88c9c1c..8536e18 100644 --- a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java @@ -34,6 +34,7 @@ public record DbBackupProperties( @NotBlank String baseUrl, String apiToken, Duration requestTimeout) { + /** Applies the default request timeout of 30 seconds when none is configured. */ public DbBackupProperties { if (requestTimeout == null) { requestTimeout = Duration.ofSeconds(30); diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java b/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java index 6140c71..64e700d 100644 --- a/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java +++ b/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java @@ -24,4 +24,8 @@ @AutoConfiguration @ConditionalOnClass(MimeMessage.class) @Import(EmailConfig.class) -public class EmailAutoConfiguration {} +public class EmailAutoConfiguration { + + /** Creates the auto-configuration. */ + public EmailAutoConfiguration() {} +} diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java index b5e2dd2..989c075 100644 --- a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java +++ b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java @@ -6,4 +6,8 @@ /** Configuration for email sending beans. */ @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = EmailConfig.class) -public class EmailConfig {} +public class EmailConfig { + + /** Creates the configuration. */ + public EmailConfig() {} +} diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java index 1214212..9985343 100644 --- a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java +++ b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java @@ -3,10 +3,21 @@ /** Unchecked exception thrown by {@link EmailService} for any email-related failure. */ public class EmailException extends RuntimeException { + /** + * Creates a new exception with the given detail message. + * + * @param message the detail message describing the email failure + */ public EmailException(final String message) { super(message); } + /** + * Creates a new exception with the given detail message and cause. + * + * @param message the detail message describing the email failure + * @param cause the underlying cause of the failure + */ public EmailException(final String message, final Throwable cause) { super(message, cause); } diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java index 36b6a10..309dd3c 100644 --- a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java +++ b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java @@ -15,18 +15,41 @@ public class EmailProperties { private String fromName; + /** Creates the properties holder with unset values. */ + public EmailProperties() {} + + /** + * Returns the configured sender email address. + * + * @return the sender email address, or {@code null} if not configured + */ public String getFrom() { return from; } + /** + * Sets the sender email address. + * + * @param from the sender email address + */ public void setFrom(final String from) { this.from = from; } + /** + * Returns the optional display name used for the sender address. + * + * @return the sender display name, or {@code null} if not configured + */ public String getFromName() { return fromName; } + /** + * Sets the optional display name used for the sender address. + * + * @param fromName the sender display name + */ public void setFromName(final String fromName) { this.fromName = fromName; } diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java index be3d5d7..6ad0575 100644 --- a/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java +++ b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java @@ -43,6 +43,13 @@ public class EmailService { private final EmailProperties properties; + /** + * Creates the service. + * + * @param mailSenderProvider provider for the optional {@link JavaMailSender}; if none is available + * the service is still created but every send fails fast + * @param properties the email configuration properties + */ public EmailService( final ObjectProvider mailSenderProvider, final EmailProperties properties) { this.mailSender = mailSenderProvider.getIfAvailable(); @@ -73,6 +80,9 @@ void warnIfNotConfigured() { /** * Sends a plain-text email to the given address. * + * @param to the recipient email address + * @param subject the email subject line + * @param body the plain-text email body * @throws EmailException if mail sending is not configured, the sender address is missing, or * SMTP delivery fails * @throws NullPointerException if any argument is {@code null} @@ -106,6 +116,9 @@ public void sendEmail( /** * Sends a plain-text email to the address stored on {@code user}. * + * @param user the recipient whose stored email address is used + * @param subject the email subject line + * @param body the plain-text email body * @throws IllegalArgumentException if the user has no email address * @throws EmailException if mail sending is not configured or SMTP delivery fails * @throws NullPointerException if any argument is {@code null} diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java index f825e4f..8a91f9f 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java @@ -24,4 +24,9 @@ @AutoConfiguration @ConditionalOnClass(McpServer.class) @Import(McpConfiguration.class) -public class McpAutoConfiguration {} +public class McpAutoConfiguration { + + /** Creates the auto-configuration. */ + public McpAutoConfiguration() { + } +} diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java index 7e6ed74..48728c5 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java @@ -24,4 +24,8 @@ @EnableConfigurationProperties(McpProperties.class) @Import({McpPaging.class, McpToolSupport.class, McpSecurityConfig.class, McpServerConfig.class}) public class McpConfiguration { + + /** Creates the configuration. */ + public McpConfiguration() { + } } diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java index 0e4ab02..d110822 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java @@ -21,6 +21,11 @@ public class McpPaging { private final McpProperties properties; + /** + * Creates the paging helper. + * + * @param properties the MCP properties supplying the default and maximum page sizes + */ public McpPaging(final McpProperties properties) { this.properties = Objects.requireNonNull(properties, "properties must not be null"); } diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java index eb1f1f3..9b07027 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java @@ -40,6 +40,10 @@ @ConditionalOnProperty(prefix = "openelements.mcp", name = "enabled", havingValue = "true") public class McpSecurityConfig { + /** Creates the configuration. */ + public McpSecurityConfig() { + } + /** Path patterns covered by the MCP security chain: the endpoint itself and any sub-path. */ static final String[] MCP_PATHS = {"/mcp", "/mcp/**"}; diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java index b6217e3..c958b10 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java @@ -35,6 +35,10 @@ @ConditionalOnProperty(prefix = "openelements.mcp", name = "enabled", havingValue = "true") public class McpServerConfig { + /** Creates the configuration. */ + public McpServerConfig() { + } + /** * The MCP endpoint path; kept in sync with {@link McpSecurityConfig#MCP_PATHS}. */ @@ -44,6 +48,11 @@ public class McpServerConfig { * The Streamable HTTP transport provider. Uses the application's Jackson * {@link ObjectMapper} (so date/time and other modules match the REST API) * wrapped as the SDK's {@code McpJsonMapper}. + * + * @param objectMapper the application's Jackson mapper used to (de)serialize MCP payloads + * @param apiKeyDataService the key store used to resolve the authenticated actor from the + * {@code X-API-Key} header for the transport context + * @return the configured Streamable HTTP transport provider serving {@code /mcp} */ @Bean public WebMvcStreamableServerTransportProvider mcpTransportProvider( diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java index d38d24b..58e0c06 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java @@ -19,6 +19,8 @@ public interface McpToolProvider { /** + * Returns the MCP tool specifications contributed by this provider. + * * @return the tool specifications contributed by this provider */ List toolSpecifications(); diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java index 2b1dc57..6d25a98 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java @@ -34,6 +34,12 @@ public class McpToolSupport { private final McpPaging paging; private final ObjectMapper objectMapper; + /** + * Creates the tool support. + * + * @param paging the paging helper used to resolve and clamp page requests + * @param objectMapper the Jackson mapper used to serialize tool payloads to JSON + */ public McpToolSupport(final McpPaging paging, final ObjectMapper objectMapper) { this.paging = Objects.requireNonNull(paging, "paging must not be null"); this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null"); diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java index 47bb13a..fb53bf9 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java @@ -40,6 +40,8 @@ public static McpSchema.Tool tool(final String name, final String description, } /** + * Builds the standard pagination input properties. + * * @return a mutable property map pre-populated with the standard {@code page} * and {@code size} pagination properties */ @@ -51,6 +53,8 @@ public static Map paginationProps() { } /** + * Builds a schema fragment for a single typed property. + * * @param type the JSON-schema type (e.g. {@code "string"}, {@code "integer"}) * @param description the property description * @return a single-property schema fragment @@ -60,6 +64,8 @@ public static Map prop(final String type, final String descripti } /** + * Builds a schema fragment for a UUID-formatted string property. + * * @param description the property description * @return a schema fragment for a UUID-formatted string property */ @@ -68,6 +74,8 @@ public static Map uuidProp(final String description) { } /** + * Builds a schema fragment for an array of UUID-formatted strings. + * * @param description the property description * @return a schema fragment for an array of UUID-formatted strings */ @@ -79,6 +87,10 @@ public static Map uuidArray(final String description) { // -- Argument parsing --------------------------------------------------- /** + * Reads an optional string argument. + * + * @param args the tool argument map + * @param key the argument name * @return the argument as a string, or {@code null} if absent */ public static String string(final Map args, final String key) { @@ -87,6 +99,10 @@ public static String string(final Map args, final String key) { } /** + * Reads a required, non-blank string argument. + * + * @param args the tool argument map + * @param key the argument name * @return the non-blank argument as a string * @throws IllegalArgumentException if absent or blank */ @@ -99,6 +115,10 @@ public static String requiredString(final Map args, final String } /** + * Reads an optional integer argument. + * + * @param args the tool argument map + * @param key the argument name * @return the argument as an integer, or {@code null} if absent * @throws IllegalArgumentException if present but not a valid integer */ @@ -118,6 +138,10 @@ public static Integer integer(final Map args, final String key) } /** + * Reads an optional boolean argument. + * + * @param args the tool argument map + * @param key the argument name * @return the argument as a boolean, or {@code null} if absent */ public static Boolean bool(final Map args, final String key) { @@ -132,6 +156,10 @@ public static Boolean bool(final Map args, final String key) { } /** + * Reads an optional UUID argument. + * + * @param args the tool argument map + * @param key the argument name * @return the argument as a UUID, or {@code null} if absent or blank * @throws IllegalArgumentException if present but not a valid UUID */ @@ -148,6 +176,10 @@ public static UUID uuid(final Map args, final String key) { } /** + * Reads a required UUID argument. + * + * @param args the tool argument map + * @param key the argument name * @return the required argument as a UUID * @throws IllegalArgumentException if absent, blank, or not a valid UUID */ @@ -160,6 +192,10 @@ public static UUID requiredUuid(final Map args, final String key } /** + * Reads an optional array-of-UUIDs argument. + * + * @param args the tool argument map + * @param key the argument name * @return the argument as a list of UUIDs, or {@code null} if absent * @throws IllegalArgumentException if present but not an array of valid UUIDs */ diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java index 7c44eaa..9f0f8af 100644 --- a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java @@ -7,6 +7,11 @@ */ public class McpUnavailableException extends RuntimeException { + /** + * Creates the exception. + * + * @param message the detail message describing why the subsystem is unavailable + */ public McpUnavailableException(final String message) { super(message); } diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java b/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java index 92b98a7..1a47163 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java @@ -20,4 +20,8 @@ */ @AutoConfiguration @Import(SearchConfig.class) -public class SearchAutoConfiguration {} +public class SearchAutoConfiguration { + + /** Creates the auto-configuration. */ + public SearchAutoConfiguration() {} +} diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java index a8e61ed..e5212d1 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java @@ -15,6 +15,15 @@ @Component public class BootstrapInvoker { + /** Creates the bootstrap invoker. */ + public BootstrapInvoker() {} + + /** + * Runs the given task on the {@code searchIndexExecutor} pool via Spring's {@code @Async} proxy, + * so the caller (the startup runner) is not blocked while the reindex executes. + * + * @param task the reindex work to run asynchronously + */ @Async("searchIndexExecutor") public void run(final Runnable task) { task.run(); diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java index 5ad62a8..211c857 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java @@ -13,9 +13,16 @@ */ public final class Highlighter { - // Private-use Unicode marks so we can detect Meilisearch's highlight - // boundary inside otherwise HTML-escaped text. + /** + * Private-use Unicode marker that Meilisearch places before a matched fragment, detected inside + * otherwise HTML-escaped text and later replaced with an opening {@code } tag. + */ public static final String PRE_MARK = ""; + + /** + * Private-use Unicode marker that Meilisearch places after a matched fragment, detected inside + * otherwise HTML-escaped text and later replaced with a closing {@code } tag. + */ public static final String POST_MARK = ""; private Highlighter() {} @@ -24,6 +31,11 @@ private Highlighter() {} * Escapes HTML special characters in user-typed text, then turns the Meilisearch boundary markers * back into {@code } / {@code }. The result is safe to render with {@code * dangerouslySetInnerHTML}. + * + * @param raw the {@code _formatted} value from Meilisearch, containing the private-use highlight + * markers; may be {@code null} + * @return an HTML-safe string with user content escaped and highlight markers converted to {@code + * } tags, or an empty string when {@code raw} is {@code null} */ public static String safeHighlight(final String raw) { if (raw == null) { diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java index 59a884d..97108d5 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java @@ -36,6 +36,14 @@ public class MeilisearchBootstrapRunner implements ApplicationRunner { private final SearchReadinessState state; private final BootstrapInvoker invoker; + /** + * Creates the bootstrap runner. + * + * @param steps all registered reindex steps, in {@code @Order} sequence + * @param client the Meilisearch client used to check health and write documents + * @param state the shared readiness flag flipped around the reindex + * @param invoker the indirection that runs the reindex on the async executor pool + */ public MeilisearchBootstrapRunner( final List steps, final MeilisearchClient client, diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java index b5fb11c..46c1a50 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java @@ -30,6 +30,13 @@ public class MeilisearchClient { private final ObjectMapper objectMapper; private final AtomicReference apiKey; + /** + * Creates the client, seeding the initial API key from the configured master key and building a + * {@link RestClient} pointed at the configured Meilisearch host. + * + * @param props the bound Meilisearch connection settings + * @param objectMapper the Jackson mapper used to serialize request bodies + */ public MeilisearchClient(final MeilisearchProperties props, final ObjectMapper objectMapper) { Objects.requireNonNull(props, "props must not be null"); this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null"); @@ -41,13 +48,22 @@ public MeilisearchClient(final MeilisearchProperties props, final ObjectMapper o .build(); } - /** Swap the master key out for a scoped runtime key. */ + /** + * Swap the master key out for a scoped runtime key. + * + * @param key the scoped API key to use for all subsequent requests + */ public void useApiKey(final String key) { Objects.requireNonNull(key, "key must not be null"); apiKey.set(key); } - /** Returns true if Meilisearch reports {@code status == "available"}. */ + /** + * Returns true if Meilisearch reports {@code status == "available"}. + * + * @return {@code true} if the {@code /health} endpoint reports the instance as available, + * {@code false} if it is unhealthy or unreachable + */ public boolean isHealthy() { try { final JsonNode body = exchange(restClient.get().uri("/health"), JsonNode.class); @@ -60,6 +76,10 @@ public boolean isHealthy() { /** * Mints a scoped API key tied to the given index patterns via {@code POST /keys}. Caller must * hold the master key. + * + * @param indexPatterns the index name patterns the new key is allowed to access + * @param actions the Meilisearch actions the new key is permitted to perform + * @return the newly minted scoped API key */ public String createScopedKey(final List indexPatterns, final List actions) { try { @@ -78,7 +98,13 @@ public String createScopedKey(final List indexPatterns, final List> docs) { try { final String body = objectMapper.writeValueAsString(docs); @@ -91,7 +117,13 @@ public long addDocuments(final String indexUid, final List> } } - /** Deletes a single document by primary key. */ + /** + * Deletes a single document by primary key. + * + * @param indexUid the index UID the document lives in + * @param id the primary-key value of the document to delete + * @return the UID of the asynchronous Meilisearch task created for the delete + */ public long deleteDocument(final String indexUid, final String id) { final JsonNode response = exchange( @@ -99,7 +131,13 @@ public long deleteDocument(final String indexUid, final String id) { return response.path("taskUid").asLong(); } - /** Writes index settings (searchable / filterable / sortable / etc). Idempotent. */ + /** + * Writes index settings (searchable / filterable / sortable / etc). Idempotent. + * + * @param indexUid the index UID whose settings are updated + * @param settings the settings payload to apply, keyed by Meilisearch setting name + * @return the UID of the asynchronous Meilisearch task created for the settings update + */ public long updateSettings(final String indexUid, final Map settings) { try { final String body = objectMapper.writeValueAsString(settings); @@ -116,6 +154,9 @@ public long updateSettings(final String indexUid, final Map sett * Ensures the named index exists. Idempotent: Meilisearch returns 202 on first creation and 4xx * ({@code index_already_exists}) on duplicate; the latter is swallowed so this is safe to call on * every startup. + * + * @param indexUid the UID of the index to create if absent + * @param primaryKey the primary-key field name for the index */ public void ensureIndex(final String indexUid, final String primaryKey) { try { @@ -136,6 +177,10 @@ public void ensureIndex(final String indexUid, final String primaryKey) { /** * Polls {@code GET /tasks/{id}} until the task reaches a terminal state or the timeout elapses. + * + * @param taskUid the UID of the Meilisearch task to poll + * @param timeout the maximum time to wait for the task to reach a terminal state + * @return the observed terminal outcome, or {@link TaskOutcome#TIMED_OUT} if the timeout elapsed */ public TaskOutcome waitForTask(final long taskUid, final Duration timeout) { final long deadline = System.nanoTime() + timeout.toNanos(); @@ -164,6 +209,9 @@ public TaskOutcome waitForTask(final long taskUid, final Duration timeout) { * Polls {@code GET /tasks?limit=1} until the most recent task globally reaches a terminal state * (succeeded / failed / canceled), or the timeout elapses. Used by integration tests to wait for * the indexer to settle after an event-driven write. + * + * @param timeout the maximum time to wait for the latest task to reach a terminal state + * @return the observed terminal outcome, or {@link TaskOutcome#TIMED_OUT} if the timeout elapsed */ public TaskOutcome waitForLatestTask(final Duration timeout) { final long deadline = System.nanoTime() + timeout.toNanos(); @@ -191,6 +239,9 @@ public TaskOutcome waitForLatestTask(final Duration timeout) { /** * Issues a {@code POST /multi-search}; the {@code queries} body is opaque (caller-built JSON). + * + * @param queries the caller-built multi-search request body, keyed by request field name + * @return the raw JSON response from Meilisearch */ public JsonNode multiSearch(final Map queries) { try { diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java index 73d1c05..ad66677 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java @@ -2,6 +2,11 @@ /** Thin runtime exception so callers can distinguish Meilisearch errors. */ public final class MeilisearchException extends RuntimeException { + /** + * Creates the exception with the given detail message. + * + * @param message the detail message describing the Meilisearch error + */ public MeilisearchException(final String message) { super(message); } diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java index 775a610..7e567af 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java @@ -26,6 +26,12 @@ public class MeilisearchIndexSettingsInitializer implements ApplicationRunner { private final MeilisearchClient client; private final List settings; + /** + * Creates the index-settings initializer. + * + * @param client the Meilisearch client used to ensure indexes and write settings + * @param settings all registered per-index settings beans + */ public MeilisearchIndexSettingsInitializer( final MeilisearchClient client, final List settings) { this.client = client; diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java index 5ed3fb7..ad1cc7b 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java @@ -19,12 +19,21 @@ * helper. By default no prefix is applied; an application that hosts several logical datasets in * one Meilisearch instance can set {@code openelements.meilisearch.index-prefix} to namespace its * indexes. The concrete index names live in the application, not here. + * + * @param host base URL of the Meilisearch instance; required and must not be blank + * @param masterKey master API key used at startup before it is exchanged for a scoped key; may be + * {@code null} if the instance runs without authentication + * @param indexPrefix prefix prepended to every index name to namespace this application's indexes; + * defaults to an empty string (no prefix) when not configured + * @param requestTimeout timeout applied to HTTP calls against Meilisearch; defaults to 5 seconds + * when not configured */ @Validated @ConfigurationProperties("openelements.meilisearch") public record MeilisearchProperties( @NotBlank String host, String masterKey, String indexPrefix, Duration requestTimeout) { + /** Applies defaults for optional components: empty index prefix and a 5-second request timeout. */ public MeilisearchProperties { if (indexPrefix == null) { indexPrefix = ""; @@ -34,7 +43,12 @@ public record MeilisearchProperties( } } - /** Prepends the configured index prefix to {@code suffix}. */ + /** + * Prepends the configured index prefix to {@code suffix}. + * + * @param suffix the base index name to namespace + * @return the fully resolved index name, i.e. the configured prefix followed by {@code suffix} + */ public String resolveIndex(final String suffix) { return indexPrefix + suffix; } diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java index 94dee1d..c3cdd50 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java @@ -26,6 +26,12 @@ public class MeilisearchScopedKeyInitializer implements ApplicationRunner { private final MeilisearchClient client; private final Optional scopedKey; + /** + * Creates the scoped-key initializer. + * + * @param client the Meilisearch client whose API key is swapped on success + * @param scopedKey the optional scoped-key specification supplied by the application + */ public MeilisearchScopedKeyInitializer( final MeilisearchClient client, final Optional scopedKey) { this.client = client; diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java index 592b27f..7de665d 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java @@ -37,4 +37,8 @@ @ConditionalOnProperty(prefix = "openelements.meilisearch", name = "enabled", havingValue = "true") @ComponentScan(basePackageClasses = SearchConfig.class) @EnableConfigurationProperties(MeilisearchProperties.class) -public class SearchConfig {} +public class SearchConfig { + + /** Creates the configuration. */ + public SearchConfig() {} +} diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java index f6c2967..44559bb 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java @@ -13,13 +13,19 @@ */ public interface SearchIndexBootstrapStep { - /** Target index UID (already prefixed). Used for the Meilisearch write and for log identity. */ + /** + * Target index UID (already prefixed). Used for the Meilisearch write and for log identity. + * + * @return the fully resolved target index UID for this step + */ String indexUid(); /** * Lazy stream of documents to push. The lib batches into groups of {@link * MeilisearchBootstrapRunner#BATCH_SIZE} and flushes via {@link MeilisearchClient#addDocuments}. * The lib closes the stream via try-with-resources. + * + * @return a lazy, closeable stream of already-mapped documents to index for this step */ Stream> documents(); } diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java index 1f0b023..13f3ad8 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java @@ -13,7 +13,14 @@ public class SearchReadinessState { private final AtomicBoolean bootstrapping = new AtomicBoolean(true); - /** Returns {@code true} while the initial reindex is still running. */ + /** Creates the readiness state, initially marked as bootstrapping. */ + public SearchReadinessState() {} + + /** + * Returns whether the initial reindex is still running. + * + * @return {@code true} while the initial reindex is in progress, {@code false} once it finished + */ public boolean isBootstrapping() { return bootstrapping.get(); } diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java index a96ccc1..84eb59e 100644 --- a/spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java +++ b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java @@ -2,7 +2,13 @@ /** Terminal state of a Meilisearch async task as observed by polling. */ public enum TaskOutcome { + + /** The task completed successfully. */ SUCCEEDED, + + /** The task ended in a failed or canceled state, or polling was interrupted. */ FAILED, + + /** The task did not reach a terminal state before the polling timeout elapsed. */ TIMED_OUT } diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java index 767c981..4c968c6 100644 --- a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java @@ -14,6 +14,9 @@ @EnableConfigurationProperties(SlackProperties.class) public class SlackConfig { + /** Creates the configuration. */ + public SlackConfig() {} + @Bean @Nullable MethodsClient slackMethodsClient(final SlackProperties properties) { final String token = properties.getToken(); diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java index f4fabe8..2eaa60e 100644 --- a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java @@ -3,10 +3,21 @@ /** Unchecked exception thrown by {@link SlackService} for any Slack-related failure. */ public class SlackException extends RuntimeException { + /** + * Creates a new exception with the given detail message. + * + * @param message the detail message describing the Slack-related failure + */ public SlackException(final String message) { super(message); } + /** + * Creates a new exception with the given detail message and underlying cause. + * + * @param message the detail message describing the Slack-related failure + * @param cause the underlying cause of the failure + */ public SlackException(final String message, final Throwable cause) { super(message, cause); } diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java index b8185f9..7e36283 100644 --- a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java @@ -12,10 +12,23 @@ public class SlackProperties { private String token; + /** Creates the properties with no token configured. */ + public SlackProperties() {} + + /** + * Returns the configured Slack bot token. + * + * @return the Slack bot token, or {@code null} if none is configured + */ public String getToken() { return token; } + /** + * Sets the Slack bot token used to authenticate against the Slack API. + * + * @param token the Slack bot token + */ public void setToken(final String token) { this.token = token; } diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java index 8fe2623..00c9a85 100644 --- a/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java @@ -30,6 +30,12 @@ public class SlackService { @Nullable private final MethodsClient methodsClient; + /** + * Creates the service, resolving the Slack client if one is available. + * + * @param slackMethodsClientProvider provider for the Slack {@link MethodsClient}; the client is + * absent when no Slack token is configured + */ public SlackService(final ObjectProvider slackMethodsClientProvider) { this.methodsClient = slackMethodsClientProvider.getIfAvailable(); } diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java b/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java index cac1c98..29020e5 100644 --- a/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java @@ -25,4 +25,8 @@ @AutoConfiguration @ConditionalOnClass(Slack.class) @Import(SlackConfig.class) -public class SlackAutoConfiguration {} +public class SlackAutoConfiguration { + + /** Creates the auto-configuration. */ + public SlackAutoConfiguration() {} +}